diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index cf640d5..0000000 --- a/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89e38dd..a50fa82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,10 @@ jobs: - name: Install Dependencies run: npm install + + - name: build-linux + if: matrix.os == 'ubuntu-latest' + run: npm run build:linux - name: build-mac if: matrix.os == 'macos-latest' diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 9c6b791..0000000 --- a/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -out -dist -pnpm-lock.yaml -LICENSE.md -tsconfig.json -tsconfig.*.json diff --git a/.prettierrc.yaml b/.prettierrc.yaml deleted file mode 100644 index f99263a..0000000 --- a/.prettierrc.yaml +++ /dev/null @@ -1,4 +0,0 @@ -singleQuote: false -semi: false -printWidth: 100 -trailingComma: none diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 940260d..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["dbaeumer.vscode-eslint"] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 0b6b9a6..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Debug Main Process", - "type": "node", - "request": "launch", - "cwd": "${workspaceRoot}", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", - "windows": { - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" - }, - "runtimeArgs": ["--sourcemap"], - "env": { - "REMOTE_DEBUGGING_PORT": "9222" - } - }, - { - "name": "Debug Renderer Process", - "port": 9222, - "request": "attach", - "type": "chrome", - "webRoot": "${workspaceFolder}/src/renderer", - "timeout": 60000, - "presentation": { - "hidden": true - } - } - ], - "compounds": [ - { - "name": "Debug All", - "configurations": ["Debug Main Process", "Debug Renderer Process"], - "presentation": { - "order": 1 - } - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e879dfd..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - }, - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } -} diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 2bf4f29..0000000 --- a/TODO.md +++ /dev/null @@ -1,9 +0,0 @@ -[] auto install java on setup -[x] support install ask configs -[] DEVLOGS -[] improve package last task view (statusText) -[] show git clone status - -[] fix app update modal -[] fix update removes "options.txt" -[] improve child process on management \ No newline at end of file diff --git a/dev-app-update.yml b/dev-app-update.yml deleted file mode 100644 index c8c0c0f..0000000 --- a/dev-app-update.yml +++ /dev/null @@ -1,3 +0,0 @@ -provider: generic -url: https://example.com/auto-updates -updaterCacheDirName: rs-bundler-updater diff --git a/electron-builder.yml b/electron-builder.yml deleted file mode 100644 index 6677137..0000000 --- a/electron-builder.yml +++ /dev/null @@ -1,43 +0,0 @@ -appId: com.ragestudio.bundler -productName: rs-bundler -directories: - buildResources: build -files: - - '!**/.vscode/*' - - '!src/*' - - '!electron.vite.config.{js,ts,mjs,cjs}' - - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' - - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' -asarUnpack: - - resources/** -win: - executableName: rs-bundler - icon: resources/icon.ico -nsis: - artifactName: ${name}-${version}-setup.${ext} - shortcutName: ${productName} - uninstallDisplayName: ${productName} - createDesktopShortcut: always -mac: - entitlementsInherit: build/entitlements.mac.plist - extendInfo: - - NSCameraUsageDescription: Application requests access to the device's camera. - - NSMicrophoneUsageDescription: Application requests access to the device's microphone. - - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. - - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. - notarize: false -dmg: - artifactName: ${name}-${version}.${ext} -linux: - target: - - AppImage - - snap - - deb - maintainer: electronjs.org - category: Utility -appImage: - artifactName: ${name}-${version}.${ext} -npmRebuild: false -publish: - provider: generic - url: https://storage.ragestudio.net/rs-bundler/release diff --git a/package.json b/package.json index b32085a..4d468ff 100644 --- a/package.json +++ b/package.json @@ -1,75 +1,13 @@ { - "name": "rs-bundler", - "version": "0.15.0", - "description": "RageStudio Bundler Utility GUI", - "main": "./out/main/index.js", - "author": "RageStudio", + "name": "@ragestudio/relic-core", + "private": true, + "workspaces": [ + "packages/*" + ], + "repository": "https://github.com/srgooglo/rs_bundler", + "author": "SrGooglo ", "license": "MIT", "scripts": { - "format": "prettier --write .", - "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", - "start": "electron-vite preview", - "dev": "electron-vite dev", - "build": "electron-vite build", - "postinstall": "electron-builder install-app-deps", - "pack:win": "electron-builder --win --config", - "pack:mac": "electron-builder --mac --config", - "pack:linux": "electron-builder --linux --config", - "build:win": "npm run build && npm run pack:win", - "build:mac": "npm run build && npm run pack:mac", - "build:linux": "npm run build && npm run pack:linux" - }, - "dependencies": { - "@electron-toolkit/preload": "^2.0.0", - "@electron-toolkit/utils": "^2.0.0", - "@getstation/electron-google-oauth2": "^14.0.0", - "@imjs/electron-differential-updater": "^5.1.7", - "@loadable/component": "^5.16.3", - "@ragestudio/hermes": "^0.1.1", - "adm-zip": "^0.5.10", - "antd": "^5.13.2", - "checksum": "^1.0.0", - "classnames": "^2.3.2", - "electron-differential-updater": "^4.3.2", - "electron-is-dev": "^2.0.0", - "electron-store": "^8.1.0", - "electron-updater": "^6.1.1", - "googleapis": "^105.0.0", - "got": "11.8.3", - "human-format": "^1.2.0", - "less": "^4.2.0", - "lodash": "^4.17.21", - "merge-stream": "^2.0.0", - "node-7z": "^3.0.0", - "open": "8.4.2", - "progress-stream": "^2.0.0", - "protocol-registry": "^1.4.1", - "react-icons": "^4.11.0", - "react-router-dom": "6.6.2", - "react-spinners": "^0.13.8", - "react-spring": "^9.7.3", - "react-motion": "0.5.2", - "request": "^2.88.2", - "rimraf": "^5.0.5", - "signal-exit": "^4.1.0", - "unzipper": "^0.10.14", - "upath": "^2.0.1", - "uuid": "^9.0.1", - "which": "^4.0.0", - "winreg": "^1.2.5" - }, - "devDependencies": { - "@electron-toolkit/eslint-config": "^1.0.1", - "@electron-toolkit/eslint-config-prettier": "^1.0.1", - "@vitejs/plugin-react": "^4.0.4", - "electron": "^25.6.0", - "electron-builder": "^24.6.3", - "electron-vite": "^1.0.27", - "eslint": "^8.47.0", - "eslint-plugin-react": "^7.33.2", - "prettier": "^3.0.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "vite": "^4.4.9" + "postinstall": "node scripts/postinstall.js" } } diff --git a/packages/cli/bin b/packages/cli/bin new file mode 100644 index 0000000..e1b3d5a --- /dev/null +++ b/packages/cli/bin @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require("./dist/index.js") \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..0103959 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,21 @@ +{ + "name": "@ragestudio/relic-cli", + "version": "0.17.0", + "license": "MIT", + "author": "RageStudio", + "description": "RageStudio Relic, yet another package manager.", + "main": "./dist/index.js", + "bin": { + "relic": "./bin.js" + }, + "scripts": { + "dev": "hermes-node ./src/index.js", + "build": "hermes build" + }, + "dependencies": { + "commander": "^12.0.0" + }, + "devDependencies": { + "@ragestudio/hermes": "^0.1.1" + } +} diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js new file mode 100644 index 0000000..b507daf --- /dev/null +++ b/packages/cli/src/index.js @@ -0,0 +1,169 @@ +import RelicCore from "@ragestudio/relic-core" +import { program, Command, Argument } from "commander" + +import pkg from "../package.json" + +const commands = [ + { + cmd: "install", + description: "Install a package manifest from a path or URL", + arguments: [ + { + name: "package_manifest", + description: "Path or URL to a package manifest", + } + ], + fn: async (package_manifest, options) => { + await core.initialize() + await core.setup() + + return await core.package.install(package_manifest, options) + } + }, + { + cmd: "run", + description: "Execute a package", + arguments: [ + { + name: "id", + description: "The id of the package to execute", + } + ], + fn: async (pkg_id, options) => { + await core.initialize() + await core.setup() + + return await core.package.execute(pkg_id, options) + } + }, + { + cmd: "update", + description: "Update a package", + arguments: [ + { + name: "id", + description: "The id of the package to update", + } + ], + fn: async (pkg_id, options) => { + await core.initialize() + await core.setup() + + return await core.package.update(pkg_id, options) + } + }, + { + cmd: "uninstall", + description: "Uninstall a package", + arguments: [ + { + name: "id", + description: "The id of the package to uninstall", + } + ], + fn: async (pkg_id, options) => { + await core.initialize() + + return await core.package.uninstall(pkg_id, options) + } + }, + { + cmd: "apply", + description: "Apply changes to a installed package", + arguments: [ + { + name: "id", + description: "The id of the package to apply changes to", + }, + ], + options: [ + { + name: "add_patches", + description: "Add patches to the package", + }, + { + name: "remove_patches", + description: "Remove patches from the package", + }, + ], + fn: async (pkg_id, options) => { + await core.initialize() + + return await core.package.apply(pkg_id, options) + } + }, + { + cmd: "list", + description: "List installed package manifests", + fn: async () => { + await core.initialize() + + return console.log(await core.package.list()) + } + }, + { + cmd: "open-path", + description: "Open the base path or a package path", + options: [ + { + name: "pkg_id", + description: "Path to open", + } + ], + fn: async (options) => { + await core.initialize() + + await core.openPath(options.pkg_id) + } + } +] + +async function main() { + global.core = new RelicCore() + + program + .name(pkg.name) + .description(pkg.description) + .version(pkg.version) + + for await (const command of commands) { + const cmd = new Command(command.cmd).action(command.fn) + + if (command.description) { + cmd.description(command.description) + } + + if (Array.isArray(command.arguments)) { + for await (const argument of command.arguments) { + if (typeof argument === "string") { + cmd.addArgument(new Argument(argument)) + } else { + const arg = new Argument(argument.name, argument.description) + + if (argument.default) { + arg.default(argument.default) + } + + cmd.addArgument(arg) + } + } + } + + if (Array.isArray(command.options)) { + for await (const option of command.options) { + if (typeof option === "string") { + cmd.option(option) + } else { + cmd.option(option.name, option.description, option.default) + } + } + } + + program.addCommand(cmd) + } + + program.parse() +} + + +main() \ No newline at end of file diff --git a/packages/core/.swcrc b/packages/core/.swcrc new file mode 100644 index 0000000..04c57a0 --- /dev/null +++ b/packages/core/.swcrc @@ -0,0 +1,11 @@ +{ + "$schema": "http://json.schemastore.org/swcrc", + "module": { + "type": "commonjs", + // These are defaults. + "strict": false, + "strictMode": true, + "lazy": false, + "noInterop": false + } +} \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..e3258af --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,43 @@ +{ + "name": "@ragestudio/relic-core", + "version": "0.17.0", + "license": "MIT", + "author": "RageStudio", + "description": "RageStudio Relic, yet another package manager.", + "main": "./dist/index.js", + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "hermes build", + "build:swc": "npx swc ./src --out-dir ./dist --strip-leading-paths" + }, + "dependencies": { + "@foxify/events": "^2.1.0", + "adm-zip": "^0.5.12", + "axios": "^1.6.8", + "checksum": "^1.0.0", + "cli-color": "^2.0.4", + "cli-progress": "^3.12.0", + "deep-object-diff": "^1.1.9", + "extends-classes": "^1.0.5", + "googleapis": "^134.0.0", + "human-format": "^1.2.0", + "merge-stream": "^2.0.0", + "module-alias": "^2.2.3", + "node-7z": "^3.0.0", + "open": "8.4.2", + "request": "^2.88.2", + "rimraf": "^5.0.5", + "signal-exit": "^4.1.0", + "unzipper": "^0.10.14", + "upath": "^2.0.1", + "uuid": "^9.0.1", + "winston": "^3.13.0" + }, + "devDependencies": { + "@swc/cli": "^0.3.12", + "@swc/core": "^1.4.11" + } +} diff --git a/packages/core/src/classes/ManifestAuthDB.js b/packages/core/src/classes/ManifestAuthDB.js new file mode 100644 index 0000000..fe32684 --- /dev/null +++ b/packages/core/src/classes/ManifestAuthDB.js @@ -0,0 +1,36 @@ +import path from "path" +import { JSONFilePreset } from "../libraries/lowdb/presets/node" + +import Vars from "../vars" + +//! WARNING: Please DO NOT storage any password or sensitive data here, +// cause its not use any encryption method, and it will be stored in plain text. +// This is intended to store session tokens among other vars. + +export default class ManifestAuthService { + static vaultPath = path.resolve(Vars.runtime_path, "auth.json") + + static async withDB() { + return await JSONFilePreset(ManifestAuthService.vaultPath, {}) + } + + static has = async (pkg_id) => { + const db = await this.withDB() + + return !!db.data[pkg_id] + } + + static set = async (pkg_id, value) => { + const db = await this.withDB() + + return await db.update((data) => { + data[pkg_id] = value + }) + } + + static get = async (pkg_id) => { + const db = await this.withDB() + + return await db.data[pkg_id] + } +} \ No newline at end of file diff --git a/packages/core/src/classes/ManifestConfig.js b/packages/core/src/classes/ManifestConfig.js new file mode 100644 index 0000000..7bc379b --- /dev/null +++ b/packages/core/src/classes/ManifestConfig.js @@ -0,0 +1,34 @@ +import DB from "../db" + +export default class ManifestConfigManager { + constructor(pkg_id) { + this.pkg_id = pkg_id + this.config = null + } + + async initialize() { + const pkg = await DB.getPackages(this.pkg_id) ?? {} + + this.config = pkg.config + } + + set(key, value) { + this.config[key] = value + + DB.updatePackageById(pkg_id, { config: this.config }) + + return this.config + } + + get(key) { + return this.config[key] + } + + delete(key) { + delete this.config[key] + + DB.updatePackageById(pkg_id, { config: this.config }) + + return this.config + } +} \ No newline at end of file diff --git a/packages/core/src/classes/PatchManager.js b/packages/core/src/classes/PatchManager.js new file mode 100644 index 0000000..870d224 --- /dev/null +++ b/packages/core/src/classes/PatchManager.js @@ -0,0 +1,149 @@ +import Logger from "../logger" + +import DB from "../db" +import fs from "node:fs" + +import GenericSteps from "../generic_steps" +import parseStringVars from "../utils/parseStringVars" + +export default class PatchManager { + constructor(pkg, manifest) { + this.pkg = pkg + this.manifest = manifest + + this.log = Logger.child({ service: `PATCH-MANAGER|${pkg.id}` }) + } + + async get(select) { + if (!this.manifest.patches) { + return [] + } + + let list = [] + + if (typeof select === "undefined") { + list = this.manifest.patches + } + + if (Array.isArray(select)) { + for await (let id of select) { + const patch = this.manifest.patches.find((patch) => patch.id === id) + + if (patch) { + list.push(patch) + } + } + } + + return list + } + + async reapply() { + if (Array.isArray(this.pkg.applied_patches)) { + return await this.patch(this.pkg.applied_patches) + } + + return true + } + + async patch(select) { + const list = await this.get(select) + + for await (let patch of list) { + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `Applying patch [${patch.id}]...`, + }) + + this.log.info(`Applying patch [${patch.id}]...`) + + if (Array.isArray(patch.additions)) { + this.log.info(`Applying ${patch.additions.length} Additions...`) + + for await (let addition of patch.additions) { + // resolve patch file + addition.file = await parseStringVars(addition.file, this.pkg) + + if (fs.existsSync(addition.file)) { + this.log.info(`Addition [${addition.file}] already exists. Skipping...`) + continue + } + + this.log.info(`Applying addition [${addition.file}]`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `Applying addition [${addition.file}]`, + }) + + await GenericSteps(this.pkg, addition.steps, this.log) + } + } + + if (!this.pkg.applied_patches.includes(patch.id)) { + this.pkg.applied_patches.push(patch.id) + } + } + + await DB.updatePackageById(this.pkg.id, { applied_patches: this.pkg.applied_patches }) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `${list.length} Patches applied`, + }) + + this.log.info(`${list.length} Patches applied`) + + return this.pkg + } + + async remove(select) { + const list = await this.get(select) + + for await (let patch of list) { + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `Removing patch [${patch.id}]...`, + }) + + this.log.info(`Removing patch [${patch.id}]...`) + + if (Array.isArray(patch.additions)) { + this.log.info(`Removing ${patch.additions.length} Additions...`) + + for await (let addition of patch.additions) { + addition.file = await parseStringVars(addition.file, this.pkg) + + if (!fs.existsSync(addition.file)) { + this.log.info(`Addition [${addition.file}] does not exist. Skipping...`) + continue + } + + this.log.info(`Removing addition [${addition.file}]`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `Removing addition [${addition.file}]`, + }) + + await fs.promises.unlink(addition.file) + } + } + + this.pkg.applied_patches = this.pkg.applied_patches.filter((p) => { + return p !== patch.id + }) + } + + await DB.updatePackageById(this.pkg.id, { applied_patches: this.pkg.applied_patches }) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `${list.length} Patches removed`, + }) + + this.log.info(`${list.length} Patches removed`) + + return this.pkg + } +} \ No newline at end of file diff --git a/packages/core/src/db.js b/packages/core/src/db.js new file mode 100644 index 0000000..4d99499 --- /dev/null +++ b/packages/core/src/db.js @@ -0,0 +1,115 @@ +import { JSONFilePreset } from "./libraries/lowdb/presets/node" +import Vars from "./vars" +import pkg from "../package.json" +import fs from "node:fs" +import lodash from "lodash" + +export default class DB { + static get defaultRoot() { + return { + created_at_version: pkg.version, + packages: [], + } + } + + static defaultPackageState({ + id, + name, + icon, + version, + author, + install_path, + description, + license, + last_status, + remote_manifest, + local_manifest, + config, + executable, + }) { + return { + id: id, + name: name, + version: version, + icon: icon, + install_path: install_path, + description: description, + author: author, + license: license ?? "unlicensed", + local_manifest: local_manifest ?? null, + remote_manifest: remote_manifest ?? null, + applied_patches: [], + config: typeof config === "object" ? config : {}, + last_status: last_status ?? "installing", + last_update: null, + installed_at: null, + executable: executable ?? false, + } + } + + static async withDB() { + return await JSONFilePreset(Vars.db_path, DB.defaultRoot) + } + + static async initialize() { + await this.cleanOrphans() + } + + static async cleanOrphans() { + const list = await this.getPackages() + + for (const pkg of list) { + if (!fs.existsSync(pkg.install_path)) { + await this.deletePackage(pkg.id) + } + } + } + + static async getPackages(pkg_id) { + const db = await this.withDB() + + if (pkg_id) { + return db.data["packages"].find((i) => i.id === pkg_id) + } + + return db.data["packages"] + } + + static async writePackage(pkg) { + const db = await this.withDB() + + const prevIndex = db.data["packages"].findIndex((i) => i.id === pkg.id) + + if (prevIndex !== -1) { + db.data["packages"][prevIndex] = pkg + } else { + db.data["packages"].push(pkg) + } + + await db.write() + + return db.data + } + + static async updatePackageById(pkg_id, obj) { + let pkg = await this.getPackages(pkg_id) + + if (!pkg) { + throw new Error("Package not found") + } + + return await this.writePackage(lodash.merge({ ...pkg }, obj)) + } + + static async deletePackage(pkg_id) { + const db = await this.withDB() + + await db.update((data) => { + data["packages"] = data["packages"].filter((i) => i.id !== pkg_id) + + return data + }) + + return pkg_id + } +} \ No newline at end of file diff --git a/src/main/generic_steps/git_clone.js b/packages/core/src/generic_steps/git_clone.js similarity index 56% rename from src/main/generic_steps/git_clone.js rename to packages/core/src/generic_steps/git_clone.js index e93d3f7..1b857c8 100644 --- a/src/main/generic_steps/git_clone.js +++ b/packages/core/src/generic_steps/git_clone.js @@ -1,33 +1,39 @@ +import Logger from "../logger" + import path from "node:path" import fs from "node:fs" import upath from "upath" -import { execa } from "../lib/execa" +import { execa } from "../libraries/execa" -import sendToRender from "../utils/sendToRender" import Vars from "../vars" -export default async (manifest, step) => { +export default async (pkg, step) => { + if (!step.path) { + step.path = `.` + } + + const Log = Logger.child({ service: `GIT|${pkg.id}` }) + const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" - const final_path = upath.normalizeSafe(path.resolve(manifest.install_path, step.path)) + const final_path = upath.normalizeSafe(path.resolve(pkg.install_path, step.path)) if (!fs.existsSync(final_path)) { fs.mkdirSync(final_path, { recursive: true }) } - sendToRender(`pkg:update:status`, { - id: manifest.id, - statusText: `Cloning ${step.url}`, + Log.info(`Cloning from [${step.url}]`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Cloning from [${step.url}]`, }) - console.log(`USING GIT BIN >`, gitCMD) - - console.log(`[${manifest.id}] steps.git_clone() | Cloning ${step.url}...`) - const args = [ "clone", //`--depth ${step.depth ?? 1}`, //"--filter=blob:none", //"--filter=tree:0", + "--progress", "--recurse-submodules", "--remote-submodules", step.url, @@ -40,5 +46,5 @@ export default async (manifest, step) => { stderr: "inherit", }) - return manifest + return pkg } \ No newline at end of file diff --git a/packages/core/src/generic_steps/git_pull.js b/packages/core/src/generic_steps/git_pull.js new file mode 100644 index 0000000..f60db44 --- /dev/null +++ b/packages/core/src/generic_steps/git_pull.js @@ -0,0 +1,33 @@ +import Logger from "../logger" + +import path from "node:path" +import fs from "node:fs" +import { execa } from "../libraries/execa" + +import Vars from "../vars" + +export default async (pkg, step) => { + if (!step.path) { + step.path = `.` + } + + const Log = Logger.child({ service: `GIT|${pkg.id}` }) + + const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" + const _path = path.resolve(pkg.install_path, step.path) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Pulling...`, + }) + + Log.info(`Pulling from HEAD...`) + + await execa(gitCMD, ["pull", "--rebase"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + return pkg +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/git_reset.js b/packages/core/src/generic_steps/git_reset.js new file mode 100644 index 0000000..60c31f4 --- /dev/null +++ b/packages/core/src/generic_steps/git_reset.js @@ -0,0 +1,83 @@ +import Logger from "../logger" + +import path from "node:path" +import fs from "node:fs" +import { execa } from "../libraries/execa" + +import git_pull from "./git_pull" +import Vars from "../vars" + +export default async (pkg, step) => { + if (!step.path) { + step.path = `.` + } + + const Log = Logger.child({ service: `GIT|${pkg.id}` }) + + const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" + + const _path = path.resolve(pkg.install_path, step.path) + const from = step.from ?? "HEAD" + + if (!fs.existsSync(_path)) { + fs.mkdirSync(_path, { recursive: true }) + } + + Log.info(`Fetching from origin`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Fetching from origin...`, + }) + + // fetch from origin + await execa(gitCMD, ["fetch", "origin"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + Log.info(`Cleaning untracked files...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Cleaning untracked files...`, + }) + + await execa(gitCMD, ["clean", "-df"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + Log.info(`Resetting to ${from}`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Resetting to ${from}`, + }) + + await execa(gitCMD, ["reset", "--hard", from], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + // pull the latest + await git_pull(pkg, step) + + Log.info(`Checkout to HEAD`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Checkout to HEAD`, + }) + + await execa(gitCMD, ["checkout", "HEAD"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + return pkg +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/http.js b/packages/core/src/generic_steps/http.js new file mode 100644 index 0000000..57f614c --- /dev/null +++ b/packages/core/src/generic_steps/http.js @@ -0,0 +1,66 @@ +import path from "node:path" +import fs from "node:fs" +import os from "node:os" + +import downloadHttpFile from "../helpers/downloadHttpFile" +import parseStringVars from "../utils/parseStringVars" +import extractFile from "../utils/extractFile" + +export default async (pkg, step, logger) => { + if (!step.path) { + step.path = `./${path.basename(step.url)}` + } + + step.path = await parseStringVars(step.path, pkg) + + let _path = path.resolve(pkg.install_path, step.path) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Downloading [${step.url}]`, + }) + + logger.info(`Downloading [${step.url} to ${_path}]`) + + if (step.tmp) { + _path = path.resolve(os.tmpdir(), String(new Date().getTime()), path.basename(step.url)) + } + + fs.mkdirSync(path.resolve(_path, ".."), { recursive: true }) + + await downloadHttpFile(step.url, _path, (progress) => { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + use_id_only: true, + status_text: `Downloaded ${progress.transferredString} / ${progress.totalString} | ${progress.speedString}/s`, + }) + }) + + logger.info(`Downloaded finished.`) + + if (step.extract) { + if (typeof step.extract === "string") { + step.extract = path.resolve(pkg.install_path, step.extract) + } else { + step.extract = path.resolve(pkg.install_path, ".") + } + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Extracting bundle...`, + }) + + await extractFile(_path, step.extract) + + if (step.deleteAfterExtract !== false) { + logger.info(`Deleting temporal file [${_path}]...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Deleting temporal files...`, + }) + + await fs.promises.rm(_path, { recursive: true }) + } + } +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/index.js b/packages/core/src/generic_steps/index.js new file mode 100644 index 0000000..96e2017 --- /dev/null +++ b/packages/core/src/generic_steps/index.js @@ -0,0 +1,48 @@ +import Logger from "../logger" + +import ISM_GIT_CLONE from "./git_clone" +import ISM_GIT_PULL from "./git_pull" +import ISM_GIT_RESET from "./git_reset" +import ISM_HTTP from "./http" + +const InstallationStepsMethods = { + git_clone: ISM_GIT_CLONE, + git_pull: ISM_GIT_PULL, + git_reset: ISM_GIT_RESET, + http_file: ISM_HTTP, +} + +const StepsOrders = [ + "git_clones", + "git_pull", + "git_reset", + "http_file", +] + +export default async function processGenericSteps(pkg, steps, logger = Logger) { + logger.info(`Processing generic steps...`) + + if (!Array.isArray(steps)) { + throw new Error(`Steps must be an array`) + } + + if (steps.length === 0) { + return pkg + } + + steps = steps.sort((a, b) => { + return StepsOrders.indexOf(a.type) - StepsOrders.indexOf(b.type) + }) + + for await (let step of steps) { + step.type = step.type.toLowerCase() + + if (!InstallationStepsMethods[step.type]) { + throw new Error(`Unknown step: ${step.type}`) + } + + await InstallationStepsMethods[step.type](pkg, step, logger) + } + + return pkg +} diff --git a/packages/core/src/handlers/apply.js b/packages/core/src/handlers/apply.js new file mode 100644 index 0000000..1270b84 --- /dev/null +++ b/packages/core/src/handlers/apply.js @@ -0,0 +1,96 @@ +import Logger from "../logger" + +import PatchManager from "../classes/PatchManager" +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" +import DB from "../db" + +const BaseLog = Logger.child({ service: "APPLIER" }) + +function findPatch(patches, applied_patches, changes, mustBeInstalled) { + return patches.filter((patch) => { + const patchID = patch.id + + if (typeof changes.patches[patchID] === "undefined") { + return false + } + + if (mustBeInstalled === true && !applied_patches.includes(patch.id) && changes.patches[patchID] === true) { + return true + } + + if (mustBeInstalled === false && applied_patches.includes(patch.id) && changes.patches[patchID] === false) { + return true + } + + return false + }).map((patch) => patch.id) +} + +export default async function apply(pkg_id, changes = {}) { + try { + let pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.error(`Package not found [${pkg_id}]`) + return null + } + + let manifest = await ManifestReader(pkg.local_manifest) + manifest = await ManifestVM(manifest.code) + + const Log = Logger.child({ service: `APPLIER|${pkg.id}` }) + + Log.info(`Applying changes to package...`) + Log.info(`Changes: ${JSON.stringify(changes)}`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Applying changes to package...`, + last_status: "loading", + }) + + if (changes.patches) { + if (!Array.isArray(pkg.applied_patches)) { + pkg.applied_patches = [] + } + + const patches = new PatchManager(pkg, manifest) + + await patches.remove(findPatch(manifest.patches, pkg.applied_patches, changes, false)) + await patches.patch(findPatch(manifest.patches, pkg.applied_patches, changes, true)) + } + + if (changes.config) { + Log.info(`Applying config to package...`) + + if (Object.keys(changes.config).length !== 0) { + Object.entries(changes.config).forEach(([key, value]) => { + pkg.config[key] = value + }) + } + } + + await DB.writePackage(pkg) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: "All changes applied", + }) + + Log.info(`All changes applied to package.`) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:error`, { + event: "apply", + id: pkg_id, + error + }) + + BaseLog.error(`Failed to apply changes to package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/authorize.js b/packages/core/src/handlers/authorize.js new file mode 100644 index 0000000..e1d19ab --- /dev/null +++ b/packages/core/src/handlers/authorize.js @@ -0,0 +1,33 @@ +import ManifestAuthDB from "../classes/ManifestAuthDB" +import DB from "../db" + +import Logger from "../logger" + +const Log = Logger.child({ service: "AUTH" }) + +export default async (pkg_id, value) => { + if (!pkg_id) { + Log.error("pkg_id is required") + return false + } + + if (!value) { + Log.error("value is required") + return false + } + + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + Log.error("Package not found") + return false + } + + Log.info(`Setting auth for [${pkg_id}]`) + + await ManifestAuthDB.set(pkg_id, value) + + global._relic_eventBus.emit("pkg:authorized", pkg) + + return true +} \ No newline at end of file diff --git a/packages/core/src/handlers/checkUpdate.js b/packages/core/src/handlers/checkUpdate.js new file mode 100644 index 0000000..48fe56b --- /dev/null +++ b/packages/core/src/handlers/checkUpdate.js @@ -0,0 +1,43 @@ +import Logger from "../logger" +import DB from "../db" + +import softRead from "./read" + +const Log = Logger.child({ service: "CHECK_UPDATE" }) + +export default async function checkUpdate(pkg_id) { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + Log.error("Package not found") + return false + } + + Log.info(`Checking update for [${pkg_id}]`) + + const remoteSoftManifest = await softRead(pkg.remote_manifest, { + soft: true + }) + + if (!remoteSoftManifest) { + Log.error("Cannot read remote manifest") + return false + } + + if (pkg.version === remoteSoftManifest.version) { + Log.info("No update available") + return false + } + + Log.info("Update available") + Log.info("Local:", pkg.version) + Log.info("Remote:", remoteSoftManifest.version) + Log.info("Changelog:", remoteSoftManifest.changelog_url) + + return { + id: pkg.id, + local: pkg.version, + remote: remoteSoftManifest.version, + changelog: remoteSoftManifest.changelog_url, + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/execute.js b/packages/core/src/handlers/execute.js new file mode 100644 index 0000000..b22684b --- /dev/null +++ b/packages/core/src/handlers/execute.js @@ -0,0 +1,94 @@ +import Logger from "../logger" + +import fs from "node:fs" + +import DB from "../db" +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" +import parseStringVars from "../utils/parseStringVars" +import { execa } from "../libraries/execa" + +const BaseLog = Logger.child({ service: "EXECUTER" }) + +export default async function execute(pkg_id, { useRemote = false, force = false } = {}) { + try { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.info(`Package not found [${pkg_id}]`) + return false + } + + if (pkg.last_status !== "installed") { + if (!force) { + BaseLog.info(`Package not installed [${pkg_id}], aborting execution`) + + global._relic_eventBus.emit(`pkg:error`, { + id: pkg_id, + event: "execute", + error: new Error("Package not valid or not installed"), + }) + + return false + } + } + + const manifestPath = useRemote ? pkg.remote_manifest : pkg.local_manifest + + if (!fs.existsSync(manifestPath)) { + BaseLog.error(`Manifest not found in expected path [${manifestPath}] + \nMaybe the package installation has not been completed yet or corrupted. + `) + + return false + } + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "loading", + status_text: null, + }) + + const ManifestRead = await ManifestReader(manifestPath) + + const manifest = await ManifestVM(ManifestRead.code) + + if (typeof manifest.execute === "function") { + await manifest.execute(pkg) + } + + if (typeof manifest.execute === "string") { + manifest.execute = parseStringVars(manifest.execute, pkg) + + BaseLog.info(`Executing binary > [${manifest.execute}]`) + + const args = Array.isArray(manifest.execute_args) ? manifest.execute_args : [] + + await execa(manifest.execute, args, { + cwd: pkg.install_path, + stdout: "inherit", + stderr: "inherit", + }) + } + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "installed", + status_text: null, + }) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:error`, { + id: pkg_id, + event: "execute", + last_status: "installed", + error, + }) + + BaseLog.error(`Failed to execute package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} diff --git a/packages/core/src/handlers/install.js b/packages/core/src/handlers/install.js new file mode 100644 index 0000000..e94274c --- /dev/null +++ b/packages/core/src/handlers/install.js @@ -0,0 +1,183 @@ +import Logger from "../logger" + +import fs from "node:fs" + +import DB from "../db" +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" +import GenericSteps from "../generic_steps" +import Apply from "../handlers/apply" + +const BaseLog = Logger.child({ service: "INSTALLER" }) + +export default async function install(manifest) { + let id = null + + try { + BaseLog.info(`Invoking new installation...`) + BaseLog.info(`Fetching manifest [${manifest}]`) + + const ManifestRead = await ManifestReader(manifest) + + manifest = await ManifestVM(ManifestRead.code) + + id = manifest.constructor.id + + const Log = BaseLog.child({ service: `INSTALLER|${id}` }) + + Log.info(`Creating install path [${manifest.install_path}]`) + + if (fs.existsSync(manifest.install_path)) { + Log.info(`Package already exists, removing...`) + await fs.rmSync(manifest.install_path, { recursive: true }) + } + + await fs.mkdirSync(manifest.install_path, { recursive: true }) + + Log.info(`Initializing manifest...`) + + if (typeof manifest.initialize === "function") { + await manifest.initialize() + } + + Log.info(`Appending to db...`) + + const pkg = DB.defaultPackageState({ + ...manifest.constructor, + id: id, + name: manifest.constructor.pkg_name, + version: manifest.constructor.version, + install_path: manifest.install_path, + description: manifest.constructor.description, + license: manifest.constructor.license, + last_status: "installing", + remote_manifest: ManifestRead.remote_manifest, + local_manifest: ManifestRead.local_manifest, + executable: !!manifest.execute + }) + + await DB.writePackage(pkg) + + global._relic_eventBus.emit("pkg:new", pkg) + + if (manifest.configuration) { + Log.info(`Applying default config to package...`) + + pkg.config = Object.entries(manifest.configuration).reduce((acc, [key, value]) => { + acc[key] = value.default + + return acc + }, {}) + } + + if (typeof manifest.beforeInstall === "function") { + Log.info(`Executing beforeInstall hook...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing beforeInstall hook...`, + }) + + await manifest.beforeInstall(pkg) + } + + if (Array.isArray(manifest.installSteps)) { + Log.info(`Executing generic install steps...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing generic install steps...`, + }) + + await GenericSteps(pkg, manifest.installSteps, Log) + } + + if (typeof manifest.afterInstall === "function") { + Log.info(`Executing afterInstall hook...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing afterInstall hook...`, + }) + + await manifest.afterInstall(pkg) + } + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Finishing up...`, + }) + + Log.info(`Copying manifest to the final location...`) + + const finalPath = `${manifest.install_path}/.rmanifest` + + if (fs.existsSync(finalPath)) { + await fs.promises.unlink(finalPath) + } + + await fs.promises.copyFile(ManifestRead.local_manifest, finalPath) + + if (ManifestRead.is_catched) { + Log.info(`Removing cache manifest...`) + await fs.promises.unlink(ManifestRead.local_manifest) + } + + pkg.local_manifest = finalPath + pkg.last_status = "loading" + pkg.installed_at = Date.now() + + await DB.writePackage(pkg) + + if (manifest.patches) { + const defaultPatches = manifest.patches.filter((patch) => patch.default) + + if (defaultPatches.length > 0) { + Log.info(`Applying default patches...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Applying default patches...`, + }) + + await Apply(id, { + patches: Object.fromEntries(defaultPatches.map((patch) => [patch.id, true])), + }) + } + } + + pkg.last_status = "installed" + + await DB.writePackage(pkg) + + global._relic_eventBus.emit(`pkg:update:state`, { + ...pkg, + id: pkg.id, + last_status: "installed", + status_text: `Installation completed successfully`, + }) + + global._relic_eventBus.emit(`pkg:new:done`, pkg) + + Log.info(`Package installed successfully!`) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:error`, { + id: id ?? manifest.constructor.id, + event: "install", + error, + }) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: id ?? manifest.constructor.id, + last_status: "failed", + status_text: `Installation failed`, + }) + + BaseLog.error(`Error during installation of package [${id}] >`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/lastOperationRetry.js b/packages/core/src/handlers/lastOperationRetry.js new file mode 100644 index 0000000..9da6b9b --- /dev/null +++ b/packages/core/src/handlers/lastOperationRetry.js @@ -0,0 +1,76 @@ +import fs from "node:fs" +import path from "node:path" + +import Logger from "../logger" +import DB from "../db" + +import PackageInstall from "./install" +import PackageUpdate from "./update" +import PackageUninstall from "./uninstall" + +import Vars from "../vars" + +export default async function lastOperationRetry(pkg_id) { + try { + const Log = Logger.child({ service: `OPERATION_RETRY|${pkg_id}` }) + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + Log.error(`This package doesn't exist`) + return null + } + + Log.info(`Try performing last operation retry...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing last operation retry...`, + }) + + switch (pkg.last_status) { + case "installing": + await PackageInstall(pkg.local_manifest) + break + case "updating": + await PackageUpdate(pkg_id) + break + case "uninstalling": + await PackageUninstall(pkg_id) + break + case "failed": { + // copy pkg.local_manifest to cache after uninstall + const cachedManifest = path.join(Vars.cache_path, path.basename(pkg.local_manifest)) + + await fs.promises.copyFile(pkg.local_manifest, cachedManifest) + + await PackageUninstall(pkg_id) + await PackageInstall(cachedManifest) + break + } + default: { + Log.error(`Invalid last status: ${pkg.last_status}`) + + global._relic_eventBus.emit(`pkg:error`, { + id: pkg.id, + event: "retrying last operation", + status_text: `Performing last operation retry...`, + }) + + return null + } + } + + return pkg + } catch (error) { + Logger.error(`Failed to perform last operation retry of [${pkg_id}]`) + Logger.error(error) + + global._relic_eventBus.emit(`pkg:error`, { + event: "retrying last operation", + id: pkg_id, + error: error, + }) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/list.js b/packages/core/src/handlers/list.js new file mode 100644 index 0000000..eb51f5a --- /dev/null +++ b/packages/core/src/handlers/list.js @@ -0,0 +1,5 @@ +import DB from "../db" + +export default async function list() { + return await DB.getPackages() +} \ No newline at end of file diff --git a/packages/core/src/handlers/read.js b/packages/core/src/handlers/read.js new file mode 100644 index 0000000..225842f --- /dev/null +++ b/packages/core/src/handlers/read.js @@ -0,0 +1,9 @@ +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" + +export default async function softRead(manifest, options = {}) { + const Reader = await ManifestReader(manifest) + const VM = await ManifestVM(Reader.code, options) + + return VM +} \ No newline at end of file diff --git a/packages/core/src/handlers/uninstall.js b/packages/core/src/handlers/uninstall.js new file mode 100644 index 0000000..f2c0984 --- /dev/null +++ b/packages/core/src/handlers/uninstall.js @@ -0,0 +1,87 @@ +import Logger from "../logger" + +import DB from "../db" +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" + +import { rimraf } from "rimraf" + +const BaseLog = Logger.child({ service: "UNINSTALLER" }) + +export default async function uninstall(pkg_id) { + try { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.info(`Package not found [${pkg_id}]`) + return null + } + + const Log = Logger.child({ service: `UNINSTALLER|${pkg.id}` }) + + Log.info(`Uninstalling package...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Uninstalling package...`, + }) + + try { + const ManifestRead = await ManifestReader(pkg.local_manifest) + const manifest = await ManifestVM(ManifestRead.code) + + if (typeof manifest.uninstall === "function") { + Log.info(`Performing uninstall hook...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing uninstall hook...`, + }) + + await manifest.uninstall(pkg) + } + } catch (error) { + Log.error(`Failed to perform uninstall hook`, error) + global._relic_eventBus.emit(`pkg:error`, { + event: "uninstall", + id: pkg.id, + error + }) + } + + Log.info(`Deleting package directory...`) + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Deleting package directory...`, + }) + await rimraf(pkg.install_path) + + Log.info(`Removing package from database...`) + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Removing package from database...`, + }) + await DB.deletePackage(pkg.id) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "deleted", + status_text: `Uninstalling package...`, + }) + global._relic_eventBus.emit(`pkg:remove`, pkg) + Log.info(`Package uninstalled successfully!`) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:error`, { + event: "uninstall", + id: pkg_id, + error + }) + + BaseLog.error(`Failed to uninstall package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/update.js b/packages/core/src/handlers/update.js new file mode 100644 index 0000000..ac7ca9d --- /dev/null +++ b/packages/core/src/handlers/update.js @@ -0,0 +1,139 @@ +import Logger from "../logger" + +import DB from "../db" + +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" + +import GenericSteps from "../generic_steps" +import PatchManager from "../classes/PatchManager" + +const BaseLog = Logger.child({ service: "UPDATER" }) + +const AllowedPkgChanges = [ + "id", + "name", + "version", + "description", + "author", + "license", + "icon", + "core_minimum_version", + "remote_manifest", +] + +const ManifestKeysMap = { + "name": "pkg_name", +} + +export default async function update(pkg_id) { + try { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.error(`Package not found [${pkg_id}]`) + + return null + } + + const Log = BaseLog.child({ service: `UPDATER|${pkg.id}` }) + + let ManifestRead = await ManifestReader(pkg.local_manifest) + let manifest = await ManifestVM(ManifestRead.code) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "updating", + status_text: `Updating package...`, + }) + + pkg.last_status = "updating" + + await DB.writePackage(pkg) + + if (typeof manifest.update === "function") { + Log.info(`Performing update hook...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing update hook...`, + }) + + await manifest.update(pkg) + } + + if (manifest.updateSteps) { + Log.info(`Performing update steps...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing update steps...`, + }) + + await GenericSteps(pkg, manifest.updateSteps, Log) + } + + if (Array.isArray(pkg.applied_patches)) { + const patchManager = new PatchManager(pkg, manifest) + + await patchManager.reapply() + } + + if (typeof manifest.afterUpdate === "function") { + Log.info(`Performing after update hook...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing after update hook...`, + }) + + await manifest.afterUpdate(pkg) + } + + ManifestRead = await ManifestReader(pkg.local_manifest) + manifest = await ManifestVM(ManifestRead.code) + + // override public static values + for await (const key of AllowedPkgChanges) { + if (key in manifest.constructor) { + const mapKey = ManifestKeysMap[key] || key + pkg[key] = manifest.constructor[mapKey] + } + } + + pkg.last_status = "installed" + pkg.last_update = Date.now() + + await DB.writePackage(pkg) + + Log.info(`Package updated successfully`) + + global._relic_eventBus.emit(`pkg:update:state`, { + ...pkg, + id: pkg.id, + }) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:error`, { + event: "update", + id: pkg_id, + error, + last_status: "failed" + }) + + try { + await DB.updatePackageById(pkg_id, { + last_status: "failed", + }) + } catch (error) { + BaseLog.error(`Failed to update status of pkg [${pkg_id}]`) + BaseLog.error(error.stack) + } + + BaseLog.error(`Failed to update package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/helpers/downloadHttpFile.js b/packages/core/src/helpers/downloadHttpFile.js new file mode 100644 index 0000000..d347b81 --- /dev/null +++ b/packages/core/src/helpers/downloadHttpFile.js @@ -0,0 +1,73 @@ +import fs from "node:fs" +import axios from "axios" +import humanFormat from "human-format" +import cliProgress from "cli-progress" + +function convertSize(size) { + return `${humanFormat(size, { + decimals: 2, + })}B` +} + +export default async (url, destination, progressCallback) => { + const progressBar = new cliProgress.SingleBar({ + format: "[{bar}] {percentage}% | {total_formatted} | {speed}/s | {eta_formatted}", + barCompleteChar: "\u2588", + barIncompleteChar: "\u2591", + hideCursor: true + }, cliProgress.Presets.shades_classic) + + const { data: remoteStream, headers } = await axios.get(url, { + responseType: "stream", + }) + + const localStream = fs.createWriteStream(destination) + + let progress = { + total: Number(headers["content-length"] ?? 0), + transferred: 0, + speed: 0, + } + + let lastTickTransferred = 0 + + progressBar.start(progress.total, 0, { + speed: "0B/s", + total_formatted: convertSize(progress.total), + }) + + remoteStream.pipe(localStream) + + remoteStream.on("data", (data) => { + progress.transferred = progress.transferred + Buffer.byteLength(data) + }) + + const progressInterval = setInterval(() => { + progress.speed = ((progress.transferred ?? 0) - lastTickTransferred) / 1 + + lastTickTransferred = progress.transferred ?? 0 + + progress.transferredString = convertSize(progress.transferred ?? 0) + progress.totalString = convertSize(progress.total) + progress.speedString = convertSize(progress.speed) + + progressBar.update(progress.transferred, { + speed: progress.speedString, + }) + + if (typeof progressCallback === "function") { + progressCallback(progress) + } + }, 1000) + + await new Promise((resolve, reject) => { + localStream.on("finish", resolve) + localStream.on("error", reject) + }) + + progressBar.stop() + + clearInterval(progressInterval) + + return destination +} \ No newline at end of file diff --git a/src/main/utils/sendToRender.js b/packages/core/src/helpers/sendToRender.js similarity index 94% rename from src/main/utils/sendToRender.js rename to packages/core/src/helpers/sendToRender.js index 6fc3784..8a534a5 100644 --- a/src/main/utils/sendToRender.js +++ b/packages/core/src/helpers/sendToRender.js @@ -5,6 +5,10 @@ const forbidden = [ ] export default (event, data) => { + if (!global.win) { + return false + } + try { function serializeIpc(data) { if (!data) { diff --git a/packages/core/src/helpers/setup.js b/packages/core/src/helpers/setup.js new file mode 100644 index 0000000..826c4c6 --- /dev/null +++ b/packages/core/src/helpers/setup.js @@ -0,0 +1,201 @@ +import Logger from "../logger" + +const Log = Logger.child({ service: "SETUP" }) + +import path from "node:path" +import fs from "node:fs" +import os from "node:os" +import admzip from "adm-zip" +import resolveOs from "../utils/resolveOs" +import chmodRecursive from "../utils/chmodRecursive" + +import downloadFile from "../helpers/downloadHttpFile" + +import Vars from "../vars" +import Prerequisites from "../prerequisites" + +export default async () => { + if (!fs.existsSync(Vars.binaries_path)) { + Log.info(`Creating binaries directory: ${Vars.binaries_path}...`) + await fs.promises.mkdir(Vars.binaries_path, { recursive: true }) + } + + for await (let prerequisite of Prerequisites) { + try { + Log.info(`Checking prerequisite: ${prerequisite.id}...`) + + if (Array.isArray(prerequisite.requireOs) && !prerequisite.requireOs.includes(os.platform())) { + Log.info(`Prerequisite: ${prerequisite.id} is not required for this os.`) + continue + } + + if (!fs.existsSync(prerequisite.finalBin)) { + Log.info(`Missing prerequisite: ${prerequisite.id}, installing...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Installing ${prerequisite.id}`, + }) + + if (fs.existsSync(prerequisite.destination)) { + Log.info(`Deleting temporal file [${prerequisite.destination}]`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Deleting temporal file [${prerequisite.destination}]`, + }) + + await fs.promises.rm(prerequisite.destination) + } + + if (fs.existsSync(prerequisite.extract)) { + Log.info(`Deleting temporal directory [${prerequisite.extract}]`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Deleting temporal directory [${prerequisite.extract}]`, + }) + + await fs.promises.rm(prerequisite.extract, { recursive: true }) + } + + Log.info(`Creating base directory: ${Vars.binaries_path}/${prerequisite.id}...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Creating base directory: ${Vars.binaries_path}/${prerequisite.id}`, + }) + + await fs.promises.mkdir(path.resolve(Vars.binaries_path, prerequisite.id), { recursive: true }) + + if (typeof prerequisite.url === "function") { + prerequisite.url = await prerequisite.url(resolveOs(), os.arch()) + Log.info(`Resolved url: ${prerequisite.url}`) + } + + Log.info(`Downloading ${prerequisite.id} from [${prerequisite.url}] to destination [${prerequisite.destination}]...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Starting download ${prerequisite.id} from [${prerequisite.url}] to destination [${prerequisite.destination}]`, + }) + + try { + await downloadFile( + prerequisite.url, + prerequisite.destination, + (progress) => { + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Downloaded ${progress.transferredString} / ${progress.totalString} | ${progress.speedString}/s`, + }) + } + ) + } catch (error) { + if (fs.existsSync(prerequisite.destination)) { + await fs.promises.rm(prerequisite.destination) + } + + throw error + } + + if (typeof prerequisite.extract === "string") { + Log.info(`Extracting ${prerequisite.id} to destination [${prerequisite.extract}]...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Extracting ${prerequisite.id} to destination [${prerequisite.extract}]`, + }) + + const zip = new admzip(prerequisite.destination) + + await zip.extractAllTo(prerequisite.extract, true) + + Log.info(`Extraction ok...`) + } + + if (prerequisite.extractTargetFromName === true) { + let name = path.basename(prerequisite.url) + const ext = path.extname(name) + + name = name.replace(ext, "") + + if (fs.existsSync(path.resolve(prerequisite.extract, name))) { + await fs.promises.rename(path.resolve(prerequisite.extract, name), `${prerequisite.extract}_old`) + await fs.promises.rm(prerequisite.extract, { recursive: true }) + await fs.promises.rename(`${prerequisite.extract}_old`, prerequisite.extract) + } + } + + if (prerequisite.deleteBeforeExtract === true) { + Log.info(`Deleting temporal file [${prerequisite.destination}]`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Deleting temporal file [${prerequisite.destination}]`, + }) + + await fs.promises.unlink(prerequisite.destination) + } + + if (typeof prerequisite.rewriteExecutionPermission !== "undefined") { + const to = typeof prerequisite.rewriteExecutionPermission === "string" ? + prerequisite.rewriteExecutionPermission : + prerequisite.finalBin + + Log.info(`Rewriting permissions to ${to}...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Rewriting permissions to ${to}`, + }) + + await chmodRecursive(to, 0o755) + } + + if (Array.isArray(prerequisite.moveDirs)) { + for (const dir of prerequisite.moveDirs) { + if (Array.isArray(dir.requireOs)) { + if (!dir.requireOs.includes(resolveOs())) { + continue + } + } + + Log.info(`Moving ${dir.from} to ${dir.to}...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Moving ${dir.from} to ${dir.to}`, + }) + + await fs.promises.rename(dir.from, dir.to) + + if (dir.deleteParentBefore === true) { + await fs.promises.rm(path.dirname(dir.from), { recursive: true }) + } + } + } + } + + global._relic_eventBus.emit("app:setup", { + installed: true, + message: null, + }) + + Log.info(`Prerequisite: ${prerequisite.id} is ready!`) + } catch (error) { + global._relic_eventBus.emit("app:setup", { + installed: false, + error: error, + message: error.message, + }) + + Log.error("Aborting setup due to an error...") + Log.error(error) + + throw error + } + + Log.info(`All prerequisites are ready!`) + } +} \ No newline at end of file diff --git a/packages/core/src/index.js b/packages/core/src/index.js new file mode 100644 index 0000000..5440832 --- /dev/null +++ b/packages/core/src/index.js @@ -0,0 +1,70 @@ +import fs from "node:fs" +import { EventEmitter } from "@foxify/events" +import { onExit } from "signal-exit" +import open from "open" + +import SetupHelper from "./helpers/setup" +import Logger from "./logger" + +import Vars from "./vars" +import DB from "./db" + +import PackageInstall from "./handlers/install" +import PackageExecute from "./handlers/execute" +import PackageUninstall from "./handlers/uninstall" +import PackageUpdate from "./handlers/update" +import PackageApply from "./handlers/apply" +import PackageList from "./handlers/list" +import PackageRead from "./handlers/read" +import PackageAuthorize from "./handlers/authorize" +import PackageCheckUpdate from "./handlers/checkUpdate" +import PackageLastOperationRetry from "./handlers/lastOperationRetry" + +export default class RelicCore { + constructor(params) { + this.params = params + } + + eventBus = global._relic_eventBus = new EventEmitter() + + logger = Logger + + db = DB + + async initialize() { + await DB.initialize() + + onExit(this.onExit) + } + + onExit = () => { + if (fs.existsSync(Vars.cache_path)) { + fs.rmSync(Vars.cache_path, { recursive: true, force: true }) + } + } + + async setup() { + return await SetupHelper() + } + + package = { + install: PackageInstall, + execute: PackageExecute, + uninstall: PackageUninstall, + update: PackageUpdate, + apply: PackageApply, + list: PackageList, + read: PackageRead, + authorize: PackageAuthorize, + checkUpdate: PackageCheckUpdate, + lastOperationRetry: PackageLastOperationRetry, + } + + openPath(pkg_id) { + if (!pkg_id) { + return open(Vars.runtime_path) + } + + return open(Vars.packages_path + "/" + pkg_id) + } +} \ No newline at end of file diff --git a/src/main/lib/execa/index.js b/packages/core/src/libraries/execa/index.js old mode 100755 new mode 100644 similarity index 100% rename from src/main/lib/execa/index.js rename to packages/core/src/libraries/execa/index.js diff --git a/src/main/lib/execa/lib/command.js b/packages/core/src/libraries/execa/lib/command.js old mode 100755 new mode 100644 similarity index 100% rename from src/main/lib/execa/lib/command.js rename to packages/core/src/libraries/execa/lib/command.js diff --git a/src/main/lib/execa/lib/error.js b/packages/core/src/libraries/execa/lib/error.js old mode 100755 new mode 100644 similarity index 100% rename from src/main/lib/execa/lib/error.js rename to packages/core/src/libraries/execa/lib/error.js diff --git a/src/main/lib/execa/lib/kill.js b/packages/core/src/libraries/execa/lib/kill.js old mode 100755 new mode 100644 similarity index 100% rename from src/main/lib/execa/lib/kill.js rename to packages/core/src/libraries/execa/lib/kill.js diff --git a/src/main/lib/execa/lib/pipe.js b/packages/core/src/libraries/execa/lib/pipe.js old mode 100755 new mode 100644 similarity index 100% rename from src/main/lib/execa/lib/pipe.js rename to packages/core/src/libraries/execa/lib/pipe.js diff --git a/src/main/lib/execa/lib/promise.js b/packages/core/src/libraries/execa/lib/promise.js old mode 100755 new mode 100644 similarity index 100% rename from src/main/lib/execa/lib/promise.js rename to packages/core/src/libraries/execa/lib/promise.js diff --git a/src/main/lib/execa/lib/stdio.js b/packages/core/src/libraries/execa/lib/stdio.js old mode 100755 new mode 100644 similarity index 100% rename from src/main/lib/execa/lib/stdio.js rename to packages/core/src/libraries/execa/lib/stdio.js diff --git a/src/main/lib/execa/lib/stream.js b/packages/core/src/libraries/execa/lib/stream.js old mode 100755 new mode 100644 similarity index 100% rename from src/main/lib/execa/lib/stream.js rename to packages/core/src/libraries/execa/lib/stream.js diff --git a/src/main/lib/execa/lib/verbose.js b/packages/core/src/libraries/execa/lib/verbose.js old mode 100755 new mode 100644 similarity index 100% rename from src/main/lib/execa/lib/verbose.js rename to packages/core/src/libraries/execa/lib/verbose.js diff --git a/src/main/lib/get-stream/array-buffer.js b/packages/core/src/libraries/get-stream/array-buffer.js similarity index 100% rename from src/main/lib/get-stream/array-buffer.js rename to packages/core/src/libraries/get-stream/array-buffer.js diff --git a/src/main/lib/get-stream/array.js b/packages/core/src/libraries/get-stream/array.js similarity index 100% rename from src/main/lib/get-stream/array.js rename to packages/core/src/libraries/get-stream/array.js diff --git a/src/main/lib/get-stream/buffer.js b/packages/core/src/libraries/get-stream/buffer.js similarity index 100% rename from src/main/lib/get-stream/buffer.js rename to packages/core/src/libraries/get-stream/buffer.js diff --git a/src/main/lib/get-stream/contents.js b/packages/core/src/libraries/get-stream/contents.js similarity index 100% rename from src/main/lib/get-stream/contents.js rename to packages/core/src/libraries/get-stream/contents.js diff --git a/src/main/lib/get-stream/index.js b/packages/core/src/libraries/get-stream/index.js similarity index 100% rename from src/main/lib/get-stream/index.js rename to packages/core/src/libraries/get-stream/index.js diff --git a/src/main/lib/get-stream/string.js b/packages/core/src/libraries/get-stream/string.js similarity index 100% rename from src/main/lib/get-stream/string.js rename to packages/core/src/libraries/get-stream/string.js diff --git a/src/main/lib/get-stream/utils.js b/packages/core/src/libraries/get-stream/utils.js similarity index 100% rename from src/main/lib/get-stream/utils.js rename to packages/core/src/libraries/get-stream/utils.js diff --git a/src/main/lib/human-signals/core.js b/packages/core/src/libraries/human-signals/core.js similarity index 100% rename from src/main/lib/human-signals/core.js rename to packages/core/src/libraries/human-signals/core.js diff --git a/src/main/lib/human-signals/index.js b/packages/core/src/libraries/human-signals/index.js similarity index 100% rename from src/main/lib/human-signals/index.js rename to packages/core/src/libraries/human-signals/index.js diff --git a/src/main/lib/human-signals/realtime.js b/packages/core/src/libraries/human-signals/realtime.js similarity index 100% rename from src/main/lib/human-signals/realtime.js rename to packages/core/src/libraries/human-signals/realtime.js diff --git a/src/main/lib/human-signals/signals.js b/packages/core/src/libraries/human-signals/signals.js similarity index 100% rename from src/main/lib/human-signals/signals.js rename to packages/core/src/libraries/human-signals/signals.js diff --git a/src/main/lib/is-stream/index.js b/packages/core/src/libraries/is-stream/index.js similarity index 100% rename from src/main/lib/is-stream/index.js rename to packages/core/src/libraries/is-stream/index.js diff --git a/packages/core/src/libraries/lowdb/adapters/Memory.js b/packages/core/src/libraries/lowdb/adapters/Memory.js new file mode 100644 index 0000000..798cd36 --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/Memory.js @@ -0,0 +1,24 @@ +export class Memory { + #data = null + + read() { + return Promise.resolve(this.#data) + } + + write(obj) { + this.#data = obj + return Promise.resolve() + } +} + +export class MemorySync { + #data = null + + read() { + return this.#data || null + } + + write(obj) { + this.#data = obj + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/adapters/node/DataFile.js b/packages/core/src/libraries/lowdb/adapters/node/DataFile.js new file mode 100644 index 0000000..0506e0c --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/node/DataFile.js @@ -0,0 +1,51 @@ +import { TextFile, TextFileSync } from "./TextFile.js" + +export class DataFile { + #adapter + #parse + #stringify + + constructor(filename, { parse, stringify }) { + this.#adapter = new TextFile(filename) + this.#parse = parse + this.#stringify = stringify + } + + async read() { + const data = await this.#adapter.read() + if (data === null) { + return null + } else { + return this.#parse(data) + } + } + + write(obj) { + return this.#adapter.write(this.#stringify(obj)) + } +} + +export class DataFileSync { + #adapter + #parse + #stringify + + constructor(filename, { parse, stringify }) { + this.#adapter = new TextFileSync(filename) + this.#parse = parse + this.#stringify = stringify + } + + read() { + const data = this.#adapter.read() + if (data === null) { + return null + } else { + return this.#parse(data) + } + } + + write(obj) { + this.#adapter.write(this.#stringify(obj)) + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js b/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js new file mode 100644 index 0000000..8811a87 --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js @@ -0,0 +1,19 @@ +import { DataFile, DataFileSync } from "./DataFile.js"; + +export class JSONFile extends DataFile { + constructor(filename) { + super(filename, { + parse: JSON.parse, + stringify: (data) => JSON.stringify(data, null, 2), + }); + } +} + +export class JSONFileSync extends DataFileSync { + constructor(filename) { + super(filename, { + parse: JSON.parse, + stringify: (data) => JSON.stringify(data, null, 2), + }); + } +} diff --git a/packages/core/src/libraries/lowdb/adapters/node/TextFile.js b/packages/core/src/libraries/lowdb/adapters/node/TextFile.js new file mode 100644 index 0000000..b1f3321 --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/node/TextFile.js @@ -0,0 +1,65 @@ +import { readFileSync, renameSync, writeFileSync } from "node:fs" +import { readFile } from "node:fs/promises" +import path from "node:path" + +import { Writer } from "../../steno" + +export class TextFile { + #filename + #writer + + constructor(filename) { + this.#filename = filename + this.#writer = new Writer(filename) + } + + async read() { + let data + + try { + data = await readFile(this.#filename, "utf-8") + } catch (e) { + if (e.code === "ENOENT") { + return null + } + throw e + } + + return data + } + + write(str) { + return this.#writer.write(str) + } +} + +export class TextFileSync { + #tempFilename + #filename + + constructor(filename) { + this.#filename = filename + const f = filename.toString() + this.#tempFilename = path.join(path.dirname(f), `.${path.basename(f)}.tmp`) + } + + read() { + let data + + try { + data = readFileSync(this.#filename, "utf-8") + } catch (e) { + if (e.code === "ENOENT") { + return null + } + throw e + } + + return data + } + + write(str) { + writeFileSync(this.#tempFilename, str) + renameSync(this.#tempFilename, this.#filename) + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/core/Low.js b/packages/core/src/libraries/lowdb/core/Low.js new file mode 100644 index 0000000..f0b76e5 --- /dev/null +++ b/packages/core/src/libraries/lowdb/core/Low.js @@ -0,0 +1,48 @@ +function checkArgs(adapter, defaultData) { + if (adapter === undefined) throw new Error("lowdb: missing adapter") + if (defaultData === undefined) throw new Error("lowdb: missing default data") +} + +export class Low { + constructor(adapter, defaultData) { + checkArgs(adapter, defaultData) + this.adapter = adapter + this.data = defaultData + } + + async read() { + const data = await this.adapter.read() + if (data) this.data = data + } + + async write() { + if (this.data) await this.adapter.write(this.data) + } + + async update(fn) { + fn(this.data) + await this.write() + } +} + +export class LowSync { + constructor(adapter, defaultData) { + checkArgs(adapter, defaultData) + this.adapter = adapter + this.data = defaultData + } + + read() { + const data = this.adapter.read() + if (data) this.data = data + } + + write() { + if (this.data) this.adapter.write(this.data) + } + + update(fn) { + fn(this.data) + this.write() + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/presets/node.js b/packages/core/src/libraries/lowdb/presets/node.js new file mode 100644 index 0000000..e8526fb --- /dev/null +++ b/packages/core/src/libraries/lowdb/presets/node.js @@ -0,0 +1,23 @@ +import { Memory, MemorySync } from "../adapters/Memory.js" +import { JSONFile, JSONFileSync } from "../adapters/node/JSONFile.js" +import { Low, LowSync } from "../core/Low.js" + +export async function JSONFilePreset(filename, defaultData) { + const adapter = process.env.NODE_ENV === "test" ? new Memory() : new JSONFile(filename) + + const db = new Low(adapter, defaultData) + + await db.read() + + return db +} + +export function JSONFileSyncPreset(filename, defaultData) { + const adapter = process.env.NODE_ENV === "test" ? new MemorySync() : new JSONFileSync(filename) + + const db = new LowSync(adapter, defaultData) + + db.read() + + return db +} \ No newline at end of file diff --git a/src/main/lib/steno/index.ts b/packages/core/src/libraries/lowdb/steno/index.js similarity index 66% rename from src/main/lib/steno/index.ts rename to packages/core/src/libraries/lowdb/steno/index.js index ef8bfa0..d0c5558 100644 --- a/src/main/lib/steno/index.ts +++ b/packages/core/src/libraries/lowdb/steno/index.js @@ -1,27 +1,22 @@ -import { PathLike } from 'node:fs' -import { rename, writeFile } from 'node:fs/promises' -import { basename, dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' +import { rename, writeFile } from "node:fs/promises" +import { basename, dirname, join } from "node:path" +import { fileURLToPath } from "node:url" // Returns a temporary file // Example: for /some/file will return /some/.file.tmp -function getTempFilename(file: PathLike): string { +function getTempFilename(file) { const f = file instanceof URL ? fileURLToPath(file) : file.toString() return join(dirname(f), `.${basename(f)}.tmp`) } // Retries an asynchronous operation with a delay between retries and a maximum retry count -async function retryAsyncOperation( - fn: () => Promise, - maxRetries: number, - delayMs: number, -): Promise { +async function retryAsyncOperation(fn, maxRetries, delayMs) { for (let i = 0; i < maxRetries; i++) { try { return await fn() } catch (error) { if (i < maxRetries - 1) { - await new Promise((resolve) => setTimeout(resolve, delayMs)) + await new Promise(resolve => setTimeout(resolve, delayMs)) } else { throw error // Rethrow the error if max retries reached } @@ -29,21 +24,17 @@ async function retryAsyncOperation( } } -type Resolve = () => void -type Reject = (error: Error) => void -type Data = Parameters[1] - export class Writer { - #filename: PathLike - #tempFilename: PathLike + #filename + #tempFilename #locked = false - #prev: [Resolve, Reject] | null = null - #next: [Resolve, Reject] | null = null - #nextPromise: Promise | null = null - #nextData: Data | null = null + #prev = null + #next = null + #nextPromise = null + #nextData = null // File is locked, add data for later - #add(data: Data): Promise { + #add(data) { // Only keep most recent data this.#nextData = data @@ -59,18 +50,18 @@ export class Writer { } // File isn't locked, write data - async #write(data: Data): Promise { + async #write(data) { // Lock file this.#locked = true try { // Atomic write - await writeFile(this.#tempFilename, data, 'utf-8') + await writeFile(this.#tempFilename, data, "utf-8") await retryAsyncOperation( async () => { await rename(this.#tempFilename, this.#filename) }, 10, - 100, + 100 ) // Call resolve @@ -96,12 +87,12 @@ export class Writer { } } - constructor(filename: PathLike) { + constructor(filename) { this.#filename = filename this.#tempFilename = getTempFilename(filename) } - async write(data: Data): Promise { + async write(data) { return this.#locked ? this.#add(data) : this.#write(data) } -} +} \ No newline at end of file diff --git a/src/main/lib/mimic-function/index.js b/packages/core/src/libraries/mimic-function/index.js similarity index 100% rename from src/main/lib/mimic-function/index.js rename to packages/core/src/libraries/mimic-function/index.js diff --git a/src/main/lib/npm-run-path/index.js b/packages/core/src/libraries/npm-run-path/index.js similarity index 100% rename from src/main/lib/npm-run-path/index.js rename to packages/core/src/libraries/npm-run-path/index.js diff --git a/src/main/lib/onetime/index.js b/packages/core/src/libraries/onetime/index.js similarity index 100% rename from src/main/lib/onetime/index.js rename to packages/core/src/libraries/onetime/index.js diff --git a/src/main/lib/strip-final-newline/index.js b/packages/core/src/libraries/strip-final-newline/index.js similarity index 100% rename from src/main/lib/strip-final-newline/index.js rename to packages/core/src/libraries/strip-final-newline/index.js diff --git a/packages/core/src/logger.js b/packages/core/src/logger.js new file mode 100644 index 0000000..12bbc1a --- /dev/null +++ b/packages/core/src/logger.js @@ -0,0 +1,45 @@ +import winston from "winston" +import WinstonTransport from "winston-transport" +import colors from "cli-color" + +const servicesToColor = { + "CORE": { + color: "whiteBright", + background: "bgBlackBright", + }, +} + +const paintText = (level, service, ...args) => { + let { color, background } = servicesToColor[service ?? "CORE"] ?? servicesToColor["CORE"] + + if (level === "error") { + color = "whiteBright" + background = "bgRedBright" + } + + return colors[background][color](...args) +} + +const format = winston.format.printf(({ timestamp, service = "CORE", level, message, }) => { + return `${paintText(level, service, `(${level}) [${service}]`)} > ${message}` +}) + +class EventBusTransport extends WinstonTransport { + log(info, next) { + global._relic_eventBus.emit(`logger:new`, info) + next() + } +} + +export default winston.createLogger({ + format: winston.format.combine( + winston.format.timestamp(), + format + ), + transports: [ + new winston.transports.Console(), + new EventBusTransport(), + //new winston.transports.File({ filename: "error.log", level: "error" }), + //new winston.transports.File({ filename: "combined.log" }), + ], +}) \ No newline at end of file diff --git a/packages/core/src/manifest/libraries.js b/packages/core/src/manifest/libraries.js new file mode 100644 index 0000000..edab737 --- /dev/null +++ b/packages/core/src/manifest/libraries.js @@ -0,0 +1,23 @@ +import PublicInternalLibraries from "./libs" + +const isAClass = (x) => x && typeof x === "function" && x.prototype && typeof x.prototype.constructor === "function" + +export default async (dependencies, bindCtx) => { + const libraries = {} + + for await (const lib of dependencies) { + if (PublicInternalLibraries[lib]) { + if (typeof PublicInternalLibraries[lib] === "function" && isAClass(PublicInternalLibraries[lib])) { + libraries[lib] = new PublicInternalLibraries[lib](bindCtx) + + if (libraries[lib].initialize) { + await libraries[lib].initialize() + } + } else { + libraries[lib] = PublicInternalLibraries[lib] + } + } + } + + return libraries +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/auth/index.js b/packages/core/src/manifest/libs/auth/index.js new file mode 100644 index 0000000..2f1371a --- /dev/null +++ b/packages/core/src/manifest/libs/auth/index.js @@ -0,0 +1,54 @@ +import open from "open" +import axios from "axios" +import ManifestAuthDB from "../../../classes/ManifestAuthDB" + +export default class Auth { + constructor(ctx) { + this.manifest = ctx.manifest + } + + async get() { + const storagedData = await ManifestAuthDB.get(this.manifest.id) + + if (storagedData && this.manifest.authService) { + if (!this.manifest.authService.getter) { + return storagedData + } + + const result = await axios({ + method: "POST", + url: this.manifest.authService.getter, + headers: { + "Content-Type": "application/json", + }, + data: { + auth_data: storagedData, + } + }).catch((err) => { + global._relic_eventBus.emit("auth:getter:error", err) + + return err + }) + + if (result instanceof Error) { + throw result + } + + console.log(result.data) + + return result.data + } + + return storagedData + } + + request() { + if (!this.manifest.authService || !this.manifest.authService.fetcher) { + return false + } + + const authURL = this.manifest.authService.fetcher + + open(authURL) + } +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/fs/index.js b/packages/core/src/manifest/libs/fs/index.js new file mode 100644 index 0000000..a025bc1 --- /dev/null +++ b/packages/core/src/manifest/libs/fs/index.js @@ -0,0 +1,39 @@ +import fs from "node:fs" +import path from "node:path" + +// Protect from reading or write operations outside of the package directory +export default class SecureFileSystem { + constructor(ctx) { + this.jailPath = ctx.manifest.install_path + } + + checkOutsideJail(target) { + // if (!path.resolve(target).startsWith(this.jailPath)) { + // throw new Error("Cannot access resource outside of package directory") + // } + } + + readFileSync(destination, options) { + this.checkOutsideJail(destination) + + return fs.readFileSync(finalPath, options) + } + + copyFileSync(from, to) { + this.checkOutsideJail(from) + this.checkOutsideJail(to) + + return fs.copyFileSync(from, to) + } + + writeFileSync(destination, data, options) { + this.checkOutsideJail(destination) + + return fs.writeFileSync(finalPath, data, options) + } + + // don't need to check finalPath + existsSync(...args) { + return fs.existsSync(...args) + } +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/index.js b/packages/core/src/manifest/libs/index.js new file mode 100644 index 0000000..3a33e39 --- /dev/null +++ b/packages/core/src/manifest/libs/index.js @@ -0,0 +1,15 @@ +import Open from "./open" +import Path from "./path" +import Fs from "./fs" +import Auth from "./auth" + +// Third party libraries +import Mcl from "./mcl" + +export default { + fs: Fs, + path: Path, + open: Open, + auth: Auth, + mcl: Mcl +} \ No newline at end of file diff --git a/src/main/lib/mcl/authenticator.js b/packages/core/src/manifest/libs/mcl/authenticator.js similarity index 100% rename from src/main/lib/mcl/authenticator.js rename to packages/core/src/manifest/libs/mcl/authenticator.js diff --git a/src/main/lib/mcl/handler.js b/packages/core/src/manifest/libs/mcl/handler.js similarity index 100% rename from src/main/lib/mcl/handler.js rename to packages/core/src/manifest/libs/mcl/handler.js diff --git a/src/main/lib/mcl/index.js b/packages/core/src/manifest/libs/mcl/index.js similarity index 72% rename from src/main/lib/mcl/index.js rename to packages/core/src/manifest/libs/mcl/index.js index a280b5a..1386624 100644 --- a/src/main/lib/mcl/index.js +++ b/packages/core/src/manifest/libs/mcl/index.js @@ -1,6 +1,10 @@ +import Logger from "../../../logger" + import Client from "./launcher" import Authenticator from "./authenticator" +const Log = Logger.child({ service: "MCL" }) + export default class MCL { /** * Asynchronously authenticate the user using the provided username and password. @@ -27,6 +31,17 @@ export default class MCL { launcher.on("close", (e) => console.log(e)) launcher.on("error", (e) => console.log(e)) + if (typeof callbacks === "undefined") { + callbacks = { + install: () => { + Log.info("Downloading Minecraft assets...") + }, + init_assets: () => { + Log.info("Initializing Minecraft assets...") + } + } + } + await launcher.launch(opts, callbacks) return launcher diff --git a/src/main/lib/mcl/launcher.js b/packages/core/src/manifest/libs/mcl/launcher.js similarity index 100% rename from src/main/lib/mcl/launcher.js rename to packages/core/src/manifest/libs/mcl/launcher.js diff --git a/packages/core/src/manifest/libs/open/index.js b/packages/core/src/manifest/libs/open/index.js new file mode 100644 index 0000000..d696826 --- /dev/null +++ b/packages/core/src/manifest/libs/open/index.js @@ -0,0 +1,15 @@ +import Logger from "../../../logger" + +import open, { apps } from "open" + +const Log = Logger.child({ service: "OPEN-LIB" }) + +export default { + spawn: async (...args) => { + Log.info("Open spawned with args >") + console.log(...args) + + return await open(...args) + }, + apps: apps, +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/path/index.js b/packages/core/src/manifest/libs/path/index.js new file mode 100644 index 0000000..1b4bd73 --- /dev/null +++ b/packages/core/src/manifest/libs/path/index.js @@ -0,0 +1,3 @@ +import path from "node:path" + +export default path \ No newline at end of file diff --git a/packages/core/src/manifest/reader.js b/packages/core/src/manifest/reader.js new file mode 100644 index 0000000..a34915f --- /dev/null +++ b/packages/core/src/manifest/reader.js @@ -0,0 +1,60 @@ +import fs from "node:fs" +import path from "node:path" +import axios from "axios" +import checksum from "checksum" + +import Vars from "../vars" + +export async function readManifest(manifest) { + // check if manifest is a directory or a url + const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi + + const target = manifest?.remote_url ?? manifest + + if (urlRegex.test(target)) { + if (!fs.existsSync(Vars.cache_path)) { + fs.mkdirSync(Vars.cache_path, { recursive: true }) + } + + const { data: code } = await axios.get(target) + + const manifestChecksum = checksum(code, { algorithm: "md5" }) + + const cachedManifest = path.join(Vars.cache_path, `${manifestChecksum}.rmanifest`) + + await fs.promises.writeFile(cachedManifest, code) + + return { + remote_manifest: manifest, + local_manifest: cachedManifest, + is_catched: true, + code: code, + } + } else { + if (!fs.existsSync(target)) { + throw new Error(`Manifest not found: ${target}`) + } + + if (!fs.statSync(target).isFile()) { + throw new Error(`Manifest is not a file: ${target}`) + } + + // copy to cache + const cachedManifest = path.join(Vars.cache_path, path.basename(target)) + + await fs.promises.copyFile(target, cachedManifest) + + if (!fs.existsSync(cachedManifest)) { + throw new Error(`Manifest copy failed: ${target}`) + } + + return { + remote_manifest: undefined, + local_manifest: target, + is_catched: false, + code: fs.readFileSync(target, "utf8"), + } + } +} + +export default readManifest \ No newline at end of file diff --git a/packages/core/src/manifest/vm.js b/packages/core/src/manifest/vm.js new file mode 100644 index 0000000..65bcbe3 --- /dev/null +++ b/packages/core/src/manifest/vm.js @@ -0,0 +1,83 @@ +import Logger from "../logger" + +import os from "node:os" +import vm from "node:vm" +import path from "node:path" +import ManifestConfigManager from "../classes/ManifestConfig" + +import resolveOs from "../utils/resolveOs" +import FetchLibraries from "./libraries" + +import Vars from "../vars" + +async function BuildManifest(baseClass, context, { soft = false } = {}) { + // inject install_path + context.install_path = path.resolve(Vars.packages_path, baseClass.id) + baseClass.install_path = context.install_path + + if (soft === true) { + return baseClass + } + + const configManager = new ManifestConfigManager(baseClass.id) + + await configManager.initialize() + + let dependencies = [] + + if (Array.isArray(baseClass.useLib)) { + dependencies = [ + ...dependencies, + ...baseClass.useLib + ] + } + + // modify context + context.Log = Logger.child({ service: `VM|${baseClass.id}` }) + context.Lib = await FetchLibraries(dependencies, { + manifest: baseClass, + install_path: context.install_path, + }) + context.Config = configManager + + // Construct the instance + const instance = new baseClass() + + instance.install_path = context.install_path + + return instance +} + +function injectUseManifest(code) { + return code + "\n\nuse(Manifest);" +} + +export default async (code, { soft = false } = {}) => { + return await new Promise(async (resolve, reject) => { + try { + code = injectUseManifest(code) + + const context = { + Vars: Vars, + Log: Logger.child({ service: "MANIFEST_VM" }), + use: (baseClass) => { + return BuildManifest( + baseClass, + context, + { + soft: soft, + } + ).then(resolve) + }, + os_string: resolveOs(), + arch: os.arch(), + } + + vm.createContext(context) + + await vm.runInContext(code, context) + } catch (error) { + reject(error) + } + }) +} \ No newline at end of file diff --git a/packages/core/src/prerequisites.js b/packages/core/src/prerequisites.js new file mode 100644 index 0000000..9fb7df7 --- /dev/null +++ b/packages/core/src/prerequisites.js @@ -0,0 +1,70 @@ +import resolveRemoteBinPath from "./utils/resolveRemoteBinPath" +import Vars from "./vars" +import path from "node:path" +import axios from "axios" + +const baseURL = "https://storage.ragestudio.net/rstudio/binaries" + +export default [ + { + id: "7z-bin", + finalBin: Vars.sevenzip_bin, + url: resolveRemoteBinPath(`${baseURL}/7zip-bin`, process.platform === "win32" ? "7za.exe" : "7za"), + destination: Vars.sevenzip_bin, + rewriteExecutionPermission: true, + }, + { + id: "git-bin", + finalBin: Vars.git_bin, + url: resolveRemoteBinPath(`${baseURL}/git`, "git-bundle-2.4.0.zip"), + destination: path.resolve(Vars.binaries_path, "git-bundle.zip"), + extract: path.resolve(Vars.binaries_path, "git-bin"), + requireOs: ["win32"], + rewriteExecutionPermission: true, + deleteBeforeExtract: true, + }, + { + id: "rclone-bin", + finalBin: Vars.rclone_bin, + url: resolveRemoteBinPath(`${baseURL}/rclone`, "rclone-bin.zip"), + destination: path.resolve(Vars.binaries_path, "rclone-bin.zip"), + extract: path.resolve(Vars.binaries_path, "rclone-bin"), + requireOs: ["win32"], + rewriteExecutionPermission: true, + deleteBeforeExtract: true, + }, + { + id: "java_jre_bin", + finalBin: Vars.java_jre_bin, + url: async (os, arch) => { + const { data } = await axios({ + method: "GET", + url: "https://api.azul.com/metadata/v1/zulu/packages", + params: { + arch: arch, + java_version: "JAVA_22", + os: os, + archive_type: "zip", + javafx_bundled: "false", + java_package_type: "jre", + page_size: "1", + } + }) + + return data[0].download_url + }, + destination: path.resolve(Vars.binaries_path, "java-jre.zip"), + extract: path.resolve(Vars.binaries_path, "java_jre_bin"), + extractTargetFromName: true, + moveDirs: [ + { + requireOs: ["macos"], + from: path.resolve(Vars.binaries_path, "java_jre_bin", "zulu-22.jre", "Contents"), + to: path.resolve(Vars.binaries_path, "java_jre_bin", "Contents"), + deleteParentBefore: true + } + ], + rewriteExecutionPermission: path.resolve(Vars.binaries_path, "java_jre_bin"), + deleteBeforeExtract: true, + }, +] \ No newline at end of file diff --git a/packages/core/src/utils/chmodRecursive.js b/packages/core/src/utils/chmodRecursive.js new file mode 100644 index 0000000..8a1d7a1 --- /dev/null +++ b/packages/core/src/utils/chmodRecursive.js @@ -0,0 +1,16 @@ +import fs from "node:fs" +import path from "node:path" + +async function chmodRecursive(target, mode) { + if (fs.lstatSync(target).isDirectory()) { + const files = await fs.promises.readdir(target, { withFileTypes: true }) + + for (const file of files) { + await chmodRecursive(path.join(target, file.name), mode) + } + } else { + await fs.promises.chmod(target, mode) + } +} + +export default chmodRecursive diff --git a/src/main/utils/extractFile.js b/packages/core/src/utils/extractFile.js similarity index 81% rename from src/main/utils/extractFile.js rename to packages/core/src/utils/extractFile.js index 8cead9d..19040a7 100644 --- a/src/main/utils/extractFile.js +++ b/packages/core/src/utils/extractFile.js @@ -1,3 +1,5 @@ +import Logger from "../logger" + import fs from "node:fs" import path from "node:path" import { pipeline as streamPipeline } from "node:stream/promises" @@ -7,10 +9,12 @@ import unzipper from "unzipper" import Vars from "../vars" +const Log = Logger.child({ service: "EXTRACTOR" }) + export async function extractFile(file, dest) { const ext = path.extname(file) - console.log(`extractFile() | Extracting ${file} to ${dest}`) + Log.info(`Extracting ${file} to ${dest}`) switch (ext) { case ".zip": { @@ -24,13 +28,13 @@ export async function extractFile(file, dest) { } case ".7z": { await extractFull(file, dest, { - $bin: Vars.sevenzip_path, + $bin: Vars.sevenzip_bin, }) break } case ".gz": { await extractFull(file, dest, { - $bin: Vars.sevenzip_path + $bin: Vars.sevenzip_bin }) break } diff --git a/src/main/utils/parseStringVars.js b/packages/core/src/utils/parseStringVars.js similarity index 91% rename from src/main/utils/parseStringVars.js rename to packages/core/src/utils/parseStringVars.js index 6ae8687..9042d92 100644 --- a/src/main/utils/parseStringVars.js +++ b/packages/core/src/utils/parseStringVars.js @@ -8,7 +8,7 @@ export default function parseStringVars(str, pkg) { name: pkg.name, version: pkg.version, install_path: pkg.install_path, - remote_url: pkg.remote_url, + remote: pkg.remote, } const regex = /%([^%]+)%/g diff --git a/src/main/utils/readDirRecurse.js b/packages/core/src/utils/readDirRecurse.js similarity index 100% rename from src/main/utils/readDirRecurse.js rename to packages/core/src/utils/readDirRecurse.js diff --git a/packages/core/src/utils/resolveOs.js b/packages/core/src/utils/resolveOs.js new file mode 100644 index 0000000..1bf58de --- /dev/null +++ b/packages/core/src/utils/resolveOs.js @@ -0,0 +1,17 @@ +import os from "node:os" + +export default () => { + if (os.platform() === "win32") { + return "windows" + } + + if (os.platform() === "darwin") { + return "macos" + } + + if (os.platform() === "linux") { + return "linux" + } + + return os.platform() +} \ No newline at end of file diff --git a/packages/core/src/utils/resolveRemoteBinPath.js b/packages/core/src/utils/resolveRemoteBinPath.js new file mode 100644 index 0000000..acc8926 --- /dev/null +++ b/packages/core/src/utils/resolveRemoteBinPath.js @@ -0,0 +1,15 @@ +export default (pre, post) => { + let url = null + + if (process.platform === "darwin") { + url = `${pre}/mac/${process.arch}/${post}` + } + else if (process.platform === "win32") { + url = `${pre}/win/${process.arch}/${post}` + } + else { + url = `${pre}/linux/${process.arch}/${post}` + } + + return url +} \ No newline at end of file diff --git a/packages/core/src/vars.js b/packages/core/src/vars.js new file mode 100644 index 0000000..3fc23fc --- /dev/null +++ b/packages/core/src/vars.js @@ -0,0 +1,35 @@ +import path from "node:path" +import upath from "upath" + +const isWin = process.platform.includes("win") +const isMac = process.platform.includes("darwin") + +const runtimeName = "rs-relic" + +const userdata_path = upath.normalizeSafe(path.resolve( + process.env.APPDATA || + (process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"), +)) +const runtime_path = upath.normalizeSafe(path.join(userdata_path, runtimeName)) +const cache_path = upath.normalizeSafe(path.join(runtime_path, "cache")) +const packages_path = upath.normalizeSafe(path.join(runtime_path, "packages")) +const binaries_path = upath.normalizeSafe(path.resolve(runtime_path, "binaries")) +const db_path = upath.normalizeSafe(path.resolve(runtime_path, "db.json")) + +const binaries = { + sevenzip_bin: upath.normalizeSafe(path.resolve(binaries_path, "7z-bin", isWin ? "7za.exe" : "7za")), + git_bin: upath.normalizeSafe(path.resolve(binaries_path, "git-bin", "bin", isWin ? "git.exe" : "git")), + rclone_bin: upath.normalizeSafe(path.resolve(binaries_path, "rclone-bin", isWin ? "rclone.exe" : "rclone")), + java_jre_bin: upath.normalizeSafe(path.resolve(binaries_path, "java_jre_bin", (isMac ? "Contents/Home/bin/java" : (isWin ? "bin/java.exe" : "bin/java")))), +} + +export default { + runtimeName, + db_path, + userdata_path, + runtime_path, + cache_path, + packages_path, + binaries_path, + ...binaries, +} \ No newline at end of file diff --git a/packages/gui/electron-builder.yml b/packages/gui/electron-builder.yml new file mode 100644 index 0000000..3a18d50 --- /dev/null +++ b/packages/gui/electron-builder.yml @@ -0,0 +1,38 @@ +appId: com.ragestudio.relic +productName: Relic +directories: + buildResources: build +files: + - '!**/.vscode/*' + - '!src/*' + - '!electron.vite.config.{js,ts,mjs,cjs}' + - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' + - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' +asarUnpack: + - resources/** +win: + executableName: Relic + icon: resources/icon.ico +nsis: + artifactName: ${productName}-${version}-setup.${ext} + shortcutName: ${productName} + uninstallDisplayName: ${productName} + createDesktopShortcut: always +mac: + icon: resources/icon.icns + notarize: false +dmg: + artifactName: ${productName}-${version}.${ext} +linux: + target: + - AppImage + - snap + - deb + maintainer: electronjs.org + category: Utility +appImage: + artifactName: ${productName}-${version}.${ext} +npmRebuild: false +publish: + provider: generic + url: https://storage.ragestudio.net/relic/release diff --git a/electron.vite.config.js b/packages/gui/electron.vite.config.js similarity index 81% rename from electron.vite.config.js rename to packages/gui/electron.vite.config.js index 9fad50e..f1c9f06 100644 --- a/electron.vite.config.js +++ b/packages/gui/electron.vite.config.js @@ -5,23 +5,9 @@ import react from "@vitejs/plugin-react" export default defineConfig({ main: { plugins: [externalizeDepsPlugin()], - // build: { - // rollupOptions: { - // output: { - // format: "es" - // } - // } - // }, }, preload: { plugins: [externalizeDepsPlugin()], - // build: { - // rollupOptions: { - // output: { - // format: "es" - // } - // } - // }, }, renderer: { server: { diff --git a/packages/gui/package.json b/packages/gui/package.json new file mode 100644 index 0000000..1297121 --- /dev/null +++ b/packages/gui/package.json @@ -0,0 +1,55 @@ +{ + "name": "@ragestudio/relic-gui", + "version": "0.17.0", + "description": "RageStudio Relic, yet another package manager.", + "main": "./out/main/index.js", + "author": "RageStudio", + "license": "MIT", + "scripts": { + "start": "electron-vite preview", + "dev": "npm run build:core && electron-vite dev", + "build": "npm run build:core && electron-vite build", + "postinstall": "electron-builder install-app-deps", + "pack:win": "electron-builder --win --config", + "pack:mac": "electron-builder --mac --config", + "pack:linux": "electron-builder --linux --config", + "build:win": "npm run build && npm run pack:win", + "build:mac": "npm run build && npm run pack:mac", + "build:linux": "npm run build && npm run pack:linux", + "build:core": "cd ../core && npm run build:swc" + }, + "dependencies": { + "@electron-toolkit/preload": "^2.0.0", + "@electron-toolkit/utils": "^2.0.0", + "@getstation/electron-google-oauth2": "^14.0.0", + "@imjs/electron-differential-updater": "^5.1.7", + "@loadable/component": "^5.16.3", + "@ragestudio/relic-core": "^0.17.0", + "antd": "^5.13.2", + "classnames": "^2.3.2", + "electron-differential-updater": "^4.3.2", + "electron-is-dev": "^2.0.0", + "electron-store": "^8.1.0", + "electron-updater": "^6.1.1", + "got": "11.8.3", + "human-format": "^1.2.0", + "protocol-registry": "^1.4.1", + "less": "^4.2.0", + "lodash": "^4.17.21", + "react-icons": "^4.11.0", + "react-motion": "0.5.2", + "react-router-dom": "6.6.2", + "react-spinners": "^0.13.8", + "react-spring": "^9.7.3" + }, + "devDependencies": { + "@ragestudio/hermes": "^0.1.1", + "@vitejs/plugin-react": "^4.0.4", + "electron": "25.6.0", + "electron-builder": "24.6.3", + "electron-vite": "^2.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vite": "^4.4.9" + } +} diff --git a/resources/icon.ico b/packages/gui/resources/icon.ico similarity index 100% rename from resources/icon.ico rename to packages/gui/resources/icon.ico diff --git a/resources/icon.png b/packages/gui/resources/icon.png similarity index 100% rename from resources/icon.png rename to packages/gui/resources/icon.png diff --git a/resources/icon.svg b/packages/gui/resources/icon.svg similarity index 100% rename from resources/icon.svg rename to packages/gui/resources/icon.svg diff --git a/packages/gui/src/main/classes/CoreAdapter.js b/packages/gui/src/main/classes/CoreAdapter.js new file mode 100644 index 0000000..6d11690 --- /dev/null +++ b/packages/gui/src/main/classes/CoreAdapter.js @@ -0,0 +1,169 @@ +import sendToRender from "../utils/sendToRender" +import { ipcMain } from "electron" + +export default class CoreAdapter { + constructor(electronApp, RelicCore) { + this.app = electronApp + this.core = RelicCore + this.initialized = false + } + + loggerWindow = null + + ipcEvents = { + "pkg:list": async () => { + return await this.core.package.list() + }, + "pkg:get": async (event, pkg_id) => { + return await this.core.db.getPackages(pkg_id) + }, + "pkg:read": async (event, manifest_path, options = {}) => { + const manifest = await this.core.package.read(manifest_path, options) + + return JSON.stringify({ + ...this.core.db.defaultPackageState({ ...manifest }), + ...manifest, + name: manifest.pkg_name, + }) + }, + "pkg:install": async (event, manifest_path) => { + return await this.core.package.install(manifest_path) + }, + "pkg:update": async (event, pkg_id, { execOnFinish = false } = {}) => { + await this.core.package.update(pkg_id) + + if (execOnFinish) { + await this.core.package.execute(pkg_id) + } + + return true + }, + "pkg:apply": async (event, pkg_id, changes) => { + return await this.core.package.apply(pkg_id, changes) + }, + "pkg:uninstall": async (event, pkg_id) => { + return await this.core.package.uninstall(pkg_id) + }, + "pkg:execute": async (event, pkg_id, { force = false } = {}) => { + // check for updates first + if (!force) { + const update = await this.core.package.checkUpdate(pkg_id) + + if (update) { + return sendToRender("pkg:update_available", update) + } + } + + return await this.core.package.execute(pkg_id) + }, + "pkg:open": async (event, pkg_id) => { + return await this.core.openPath(pkg_id) + }, + "pkg:last_operation_retry": async (event, pkg_id) => { + return await this.core.package.lastOperationRetry(pkg_id) + }, + "pkg:cancel_current_operation": async (event, pkg_id) => { + return await this.core.package.cancelCurrentOperation(pkg_id) + }, + "core:open-path": async (event, pkg_id) => { + return await this.core.openPath(pkg_id) + }, + } + + coreEvents = { + "pkg:new": (pkg) => { + sendToRender("pkg:new", pkg) + }, + "pkg:remove": (pkg) => { + sendToRender("pkg:remove", pkg) + }, + "pkg:update:state": (data = {}) => { + if (!data.id) { + return false + } + + if (data.use_id_only === true) { + return sendToRender(`pkg:update:state:${data.id}`, data) + } + + return sendToRender("pkg:update:state", data) + }, + "pkg:new:done": (pkg) => { + sendToRender("pkg:new:done", pkg) + }, + "app:setup": (data) => { + sendToRender("app:setup", data) + }, + "auth:getter:error": (err) => { + sendToRender(`new:notification`, { + type: "error", + message: "Failed to authorize", + description: err.response.data.message ?? err.response.data.error ?? err.message, + duration: 10 + }) + }, + "pkg:authorized": (pkg) => { + sendToRender(`new:notification`, { + type: "success", + message: "Package authorized", + description: `${pkg.name} has been authorized! You can start the package now.`, + }) + }, + "pkg:error": (data) => { + sendToRender(`new:notification`, { + type: "error", + message: `An error occurred`, + description: `Something failed to ${data.event} package ${data.id}`, + }) + + sendToRender(`pkg:update:state`, data) + }, + "logger:new": (data) => { + if (this.loggerWindow) { + this.loggerWindow.webContents.send("logger:new", data) + } + } + } + + attachLogger = (window) => { + this.loggerWindow = window + + window.webContents.send("logger:new", { + timestamp: new Date().getTime(), + message: "Core adapter attached...", + }) + } + + detachLogger = () => { + this.loggerWindow = null + } + + initialize = async () => { + if (this.initialized) { + return + } + + for (const [key, handler] of Object.entries(this.coreEvents)) { + global._relic_eventBus.on(key, handler) + } + + for (const [key, handler] of Object.entries(this.ipcEvents)) { + ipcMain.handle(key, handler) + } + + await this.core.initialize() + await this.core.setup() + + this.initialized = true + } + + deinitialize = () => { + for (const [key, handler] of Object.entries(this.coreEvents)) { + global._relic_eventBus.off(key, handler) + } + + for (const [key, handler] of Object.entries(this.ipcEvents)) { + ipcMain.removeHandler(key, handler) + } + } +} \ No newline at end of file diff --git a/src/main/index.js b/packages/gui/src/main/index.js similarity index 55% rename from src/main/index.js rename to packages/gui/src/main/index.js index 2a55508..7af50cb 100644 --- a/src/main/index.js +++ b/packages/gui/src/main/index.js @@ -1,10 +1,7 @@ -import sendToRender from "./utils/sendToRender" - global.SettingsStore = new Store({ name: "settings", watch: true, }) - import path from "node:path" import { app, shell, BrowserWindow, ipcMain } from "electron" @@ -12,77 +9,73 @@ import { electronApp, optimizer, is } from "@electron-toolkit/utils" import isDev from "electron-is-dev" import Store from "electron-store" +let RelicCore = null + +if (isDev) { + RelicCore = require("../../../core").default +} else { + RelicCore = require("@ragestudio/relic-core").default +} + +import CoreAdapter from "./classes/CoreAdapter" +import sendToRender from "./utils/sendToRender" import pkg from "../../package.json" -import setup from "./setup" - -import PkgManager from "./manager" -import { readManifest } from "./utils/readManifest" - -import GoogleDriveAPI from "./lib/google_drive" - -import AuthService from "./auth" - const { autoUpdater } = require("electron-differential-updater") const ProtocolRegistry = require("protocol-registry") -const protocolRegistryNamespace = "rsbundle" +const protocolRegistryNamespace = "relic" + +class LogsViewer { + window = null + + async createWindow() { + this.window = new BrowserWindow({ + width: 800, + height: 600, + show: false, + resizable: true, + autoHideMenuBar: true, + icon: "../../resources/icon.png", + webPreferences: { + preload: path.join(__dirname, "../preload/index.js"), + sandbox: false, + }, + }) + + if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { + this.window.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}/#logs`) + } else { + this.window.loadFile(path.join(__dirname, "../renderer/index.html"), { + hash: "#logs", + }) + } + + await new Promise((resolve) => this.window.once("ready-to-show", resolve)) + + this.window.show() + + return this.window + } + + closeWindow() { + if (this.window) { + this.window.close() + } + } +} class ElectronApp { constructor() { - this.pkgManager = new PkgManager() - this.win = null + this.core = new RelicCore() + this.adapter = new CoreAdapter(this, this.core) } - authService = global.authService = new AuthService() + window = null + + logsViewer = new LogsViewer() handlers = { - "pkg:list": async () => { - return await this.pkgManager.getInstalledPackages() - }, - "pkg:get": async (event, manifest_id) => { - return await this.pkgManager.getInstalledPackages(manifest_id) - }, - "pkg:read": async (event, manifest_url) => { - return JSON.stringify(await readManifest(manifest_url)) - }, - "pkg:install": async (event, manifest) => { - this.pkgManager.install(manifest) - }, - "pkg:update": async (event, manifest_id, { execOnFinish = false } = {}) => { - await this.pkgManager.update(manifest_id) - - if (execOnFinish) { - await this.pkgManager.execute(manifest_id) - } - }, - "pkg:apply": async (event, manifest_id, changes) => { - return await this.pkgManager.applyChanges(manifest_id, changes) - }, - "pkg:retry_install": async (event, manifest_id) => { - const pkg = await this.pkgManager.getInstalledPackages(manifest_id) - - if (!pkg) { - return false - } - - await this.pkgManager.install(pkg) - }, - "pkg:cancel_install": async (event, manifest_id) => { - return await this.pkgManager.uninstall(manifest_id) - }, - "pkg:delete_auth": async (event, manifest_id) => { - return this.authService.unauthorize(manifest_id) - }, - "pkg:uninstall": async (event, ...args) => { - return await this.pkgManager.uninstall(...args) - }, - "pkg:execute": async (event, ...args) => { - return await this.pkgManager.execute(...args) - }, - "pkg:open": async (event, manifest_id) => { - return await this.pkgManager.open(manifest_id) - }, "updater:check": () => { autoUpdater.checkForUpdates() }, @@ -91,55 +84,57 @@ class ElectronApp { autoUpdater.quitAndInstall() }, 3000) }, - "settings:get": (e, key) => { + "settings:get": (event, key) => { return global.SettingsStore.get(key) }, - "settings:set": (e, key, value) => { + "settings:set": (event, key, value) => { return global.SettingsStore.set(key, value) }, - "settings:delete": (e, key) => { + "settings:delete": (event, key) => { return global.SettingsStore.delete(key) }, - "settings:has": (e, key) => { + "settings:has": (event, key) => { return global.SettingsStore.has(key) }, + "app:open-logs": async (event) => { + const loggerWindow = await this.logsViewer.createWindow() + + this.adapter.attachLogger(loggerWindow) + + loggerWindow.on("closed", () => { + this.adapter.detachLogger() + }) + + loggerWindow.webContents.send("logger:new", { + timestamp: new Date().getTime(), + message: "Logger opened, starting watching logs", + }) + }, "app:init": async (event, data) => { try { - await setup() - } catch (err) { - console.error(err) + await this.adapter.initialize() - sendToRender("new:notification", { - message: "Setup failed", - description: err.message + return { + pkg: pkg, + authorizedServices: {} + } + } catch (error) { + console.error(error) + + sendToRender("app:init:failed", { + message: "Initalization failed", + error: error, }) - } - // check if can decode google drive token - const googleDrive_enabled = !!(await GoogleDriveAPI.readCredentials()) - - return { - pkg: pkg, - authorizedServices: { - drive: googleDrive_enabled + return { + error } } } } - events = { - "open-runtime-path": () => { - return this.pkgManager.openRuntimePath() - }, - "open-dev-logs": () => { - return sendToRender("new:message", { - message: "Not implemented yet", - }) - } - } - createWindow() { - this.win = global.win = new BrowserWindow({ + this.window = global.mainWindow = new BrowserWindow({ width: 450, height: 670, show: false, @@ -152,28 +147,26 @@ class ElectronApp { } }) - this.win.on("ready-to-show", () => { - this.win.show() + this.window.on("ready-to-show", () => { + this.window.show() }) - this.win.webContents.setWindowOpenHandler((details) => { + this.window.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: "deny" } }) if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { - this.win.loadURL(process.env["ELECTRON_RENDERER_URL"]) + this.window.loadURL(process.env["ELECTRON_RENDERER_URL"]) } else { - this.win.loadFile(path.join(__dirname, "../renderer/index.html")) + this.window.loadFile(path.join(__dirname, "../renderer/index.html")) } } handleURLProtocol(url) { const urlStarter = `${protocolRegistryNamespace}://` - console.log(url) - if (url.startsWith(urlStarter)) { const urlValue = url.split(urlStarter)[1] @@ -184,25 +177,23 @@ class ElectronApp { explicitAction[0] = explicitAction[0].slice(0, -1) } - console.log(explicitAction) - if (explicitAction.length > 0) { switch (explicitAction[0]) { case "authorize": { if (!explicitAction[2]) { - const [pkgid, token] = explicitAction[1].split("%23") - return this.authService.authorize(pkgid, token) + const [pkg_id, token] = explicitAction[1].split("%23") + return this.core.package.authorize(pkg_id, token) } else { - return this.authService.authorize(explicitAction[1], explicitAction[2]) + return this.core.package.authorize(explicitAction[1], explicitAction[2]) } } default: { - return sendToRender("installation:invoked", explicitAction[0]) + return sendToRender("pkg:installation:invoked", explicitAction[0]) } } } else { // by default if no action is specified, assume is a install action - return sendToRender("installation:invoked", urlValue) + return sendToRender("pkg:installation:invoked", urlValue) } } } @@ -211,12 +202,12 @@ class ElectronApp { event.preventDefault() // Someone tried to run a second instance, we should focus our window. - if (this.win) { - if (this.win.isMinimized()) { - this.win.restore() + if (this.window) { + if (this.window.isMinimized()) { + this.window.restore() } - this.win.focus() + this.window.focus() } console.log(`Second instance >`, commandLine) @@ -240,10 +231,6 @@ class ElectronApp { ipcMain.handle(key, this.handlers[key]) } - for (const key in this.events) { - ipcMain.on(key, this.events[key]) - } - app.on("second-instance", this.handleOnSecondInstance) app.on("open-url", (event, url) => { @@ -288,8 +275,6 @@ class ElectronApp { } } - await GoogleDriveAPI.init() - await this.createWindow() if (!isDev) { @@ -315,4 +300,4 @@ class ElectronApp { } } -new ElectronApp().initialize() +new ElectronApp().initialize() \ No newline at end of file diff --git a/packages/gui/src/main/utils/sendToRender.js b/packages/gui/src/main/utils/sendToRender.js new file mode 100644 index 0000000..df4266f --- /dev/null +++ b/packages/gui/src/main/utils/sendToRender.js @@ -0,0 +1,39 @@ +import lodash from "lodash" + +const forbidden = [ + "libraries" +] + +export default (event, data) => { + try { + function serializeIpc(data) { + if (!data) { + return undefined + } + + data = JSON.stringify(data) + + data = JSON.parse(data) + + const copy = lodash.cloneDeep(data) + + if (!Array.isArray(copy)) { + Object.keys(copy).forEach((key) => { + if (forbidden.includes(key)) { + delete copy[key] + } + + if (typeof copy[key] === "function") { + delete copy[key] + } + }) + } + + return copy + } + + global.mainWindow.webContents.send(event, serializeIpc(data)) + } catch (error) { + console.error(error) + } +} \ No newline at end of file diff --git a/src/preload/index.js b/packages/gui/src/preload/index.js similarity index 100% rename from src/preload/index.js rename to packages/gui/src/preload/index.js diff --git a/packages/gui/src/renderer/assets/bruh_fox.jpg b/packages/gui/src/renderer/assets/bruh_fox.jpg new file mode 100644 index 0000000..bc3399e Binary files /dev/null and b/packages/gui/src/renderer/assets/bruh_fox.jpg differ diff --git a/src/renderer/assets/icon.jsx b/packages/gui/src/renderer/assets/icon.jsx similarity index 100% rename from src/renderer/assets/icon.jsx rename to packages/gui/src/renderer/assets/icon.jsx diff --git a/src/renderer/config/paths_decorators.js b/packages/gui/src/renderer/config/paths_decorators.js similarity index 100% rename from src/renderer/config/paths_decorators.js rename to packages/gui/src/renderer/config/paths_decorators.js diff --git a/packages/gui/src/renderer/index.html b/packages/gui/src/renderer/index.html new file mode 100644 index 0000000..3155c2f --- /dev/null +++ b/packages/gui/src/renderer/index.html @@ -0,0 +1,15 @@ + + + + + + Relic + + + + +
+ + + + \ No newline at end of file diff --git a/packages/gui/src/renderer/src/App.jsx b/packages/gui/src/renderer/src/App.jsx new file mode 100644 index 0000000..ee7464a --- /dev/null +++ b/packages/gui/src/renderer/src/App.jsx @@ -0,0 +1,181 @@ +import GlobalApp from "./GlobalApp.jsx" + +import React from "react" +import * as antd from "antd" + +import versions from "utils/getVersions" +import GlobalStateContext from "contexts/global" + +import AppLayout from "layout" +import AppModalDialog from "layout/components/ModalDialog" +import AppDrawer from "layout/components/Drawer" + +import { InternalRouter, PageRender } from "./router.jsx" + +import CrashError from "components/Crash" +import LogsViewer from "./pages/logs" + +// create a global app context +window.app = GlobalApp + +class App extends React.Component { + state = { + pkg: null, + + crash: null, + initializing: true, + + appSetup: { + error: false, + installed: false, + message: null, + }, + + appUpdate: { + changelog: null, + available: false, + }, + + authorizedServices: [], + } + + ipcEvents = { + "new:notification": (event, data) => { + app.notification[data.type || "info"]({ + message: data.message, + description: data.description, + loading: data.loading, + duration: data.duration, + icon: data.icon, + placement: "bottomLeft" + }) + }, + "new:message": (event, data) => { + antd.message[data.type || "info"](data.message) + }, + "app:setup": (event, data) => { + this.setState({ + appSetup: data, + }) + }, + "app:update_available": (event, data) => { + if (this.state.initializing) { + return false + } + + this.setState({ + appUpdate: { + available: true, + }, + }) + + app.appUpdateAvailable(data) + }, + "pkg:install:ask": (event, data) => { + if (this.state.initializing) { + return false + } + + app.pkgInstallWizard(data) + }, + "pkg:update_available": (event, data) => { + if (this.state.initializing) { + return false + } + + app.pkgUpdateAvailable(data) + }, + "pkg:installation:invoked": (event, data) => { + if (this.state.initializing) { + return false + } + + app.invokeInstall(data) + }, + "app:init:failed": (event, data) => { + this.setState({ + crash: data, + }) + } + } + + componentDidMount = async () => { + console.log(`React version > ${versions["react"]}`) + console.log(`DOMRouter version > ${versions["react-router-dom"]}`) + + if (window.location.hash === "#logs") { + return await this.setState({ + initializing: false, + no_layout: true, + log_viewer_mode: true, + }) + } + + window.app.style.appendClassname("initializing") + + for (const event in this.ipcEvents) { + ipc.exclusiveListen(event, this.ipcEvents[event]) + } + + const mainInitialization = await ipc.exec("app:init") + + console.log(`app:init() | Result >`, mainInitialization) + + if (mainInitialization.error) { + return false + } + + await this.setState({ + initializing: false, + pkg: mainInitialization.pkg, + }) + + app.location.push("/") + + window.app.style.removeClassname("initializing") + } + + render() { + return + { + this.state.log_viewer_mode && + } + + { + !this.state.log_viewer_mode && <> + + + { + !this.state.crash && <> + + + + + + + + } + + { + this.state.crash && + } + + + + } + + } +} + +export default App diff --git a/src/renderer/src/GlobalApp.jsx b/packages/gui/src/renderer/src/GlobalApp.jsx similarity index 77% rename from src/renderer/src/GlobalApp.jsx rename to packages/gui/src/renderer/src/GlobalApp.jsx index 054bd05..4a50a20 100644 --- a/src/renderer/src/GlobalApp.jsx +++ b/packages/gui/src/renderer/src/GlobalApp.jsx @@ -10,13 +10,25 @@ globalThis.getRootCssVar = getRootCssVar globalThis.notification = notification globalThis.message = message -export default class GlobalCTXApp { - static applyUpdate = () => { - message.loading("Updating, please wait...") +class GlobalStyleController { + static root = document.getElementById("root") - ipc.exec("updater:apply") + static appendClassname = (classname) => { + console.log(`appending classname >`, classname) + GlobalStyleController.root.classList.add(classname) } + static removeClassname = (classname) => { + console.log(`removing classname >`, classname) + GlobalStyleController.root.classList.remove(classname) + } + + static getRootCssVar = getRootCssVar +} + +export default class GlobalCTXApp { + static style = GlobalStyleController + static invokeInstall = (manifest) => { console.log(`installation invoked >`, manifest) @@ -62,6 +74,12 @@ export default class GlobalCTXApp { }) } + static applyUpdate = () => { + message.loading("Updating, please wait...") + + ipc.exec("updater:apply") + } + static checkUpdates = () => { ipc.exec("updater:check") } diff --git a/packages/gui/src/renderer/src/components/Crash/index.jsx b/packages/gui/src/renderer/src/components/Crash/index.jsx new file mode 100644 index 0000000..dfad2bc --- /dev/null +++ b/packages/gui/src/renderer/src/components/Crash/index.jsx @@ -0,0 +1,27 @@ +import React from "react" +import "./index.less" + +const Crash = (props) => { + const { crash } = props + + return
+
+ +
+ +

Crash

+

The application has encontered a critical error that cannot handle it, so must be terminated.

+ +
+

Detailed error:

+ + + {JSON.stringify(crash, null, 2)} + +
+
+} + +export default Crash \ No newline at end of file diff --git a/packages/gui/src/renderer/src/components/Crash/index.less b/packages/gui/src/renderer/src/components/Crash/index.less new file mode 100644 index 0000000..587ef18 --- /dev/null +++ b/packages/gui/src/renderer/src/components/Crash/index.less @@ -0,0 +1,58 @@ +.app-crash { + display: flex; + flex-direction: column; + + height: 100%; + + gap: 20px; + + padding: 20px; + + h1 { + font-size: 1.5rem; + font-weight: bold; + } + + .crash-icon { + display: flex; + flex-direction: row; + + justify-content: center; + align-items: center; + + width: 100%; + + img { + width: 200px; + height: 200px; + + object-fit: contain; + + border-radius: 12px; + } + } + + .crash-details { + display: flex; + flex-direction: column; + + width: 100%; + + gap: 7px; + + code { + background-color: var(--background-color-secondary); + + padding: 10px; + + border-radius: 12px; + + font-family: "DM Mono", monospace; + + font-size: 0.8rem; + + word-break: break-all; + white-space: pre-wrap; + } + } +} \ No newline at end of file diff --git a/src/renderer/src/components/Icons/index.jsx b/packages/gui/src/renderer/src/components/Icons/index.jsx similarity index 100% rename from src/renderer/src/components/Icons/index.jsx rename to packages/gui/src/renderer/src/components/Icons/index.jsx diff --git a/src/renderer/src/components/InstallConfigAsk/index.jsx b/packages/gui/src/renderer/src/components/InstallConfigAsk/index.jsx similarity index 100% rename from src/renderer/src/components/InstallConfigAsk/index.jsx rename to packages/gui/src/renderer/src/components/InstallConfigAsk/index.jsx diff --git a/src/renderer/src/components/InstallConfigAsk/index.less b/packages/gui/src/renderer/src/components/InstallConfigAsk/index.less similarity index 100% rename from src/renderer/src/components/InstallConfigAsk/index.less rename to packages/gui/src/renderer/src/components/InstallConfigAsk/index.less diff --git a/src/renderer/src/components/ManifestInfo/index.jsx b/packages/gui/src/renderer/src/components/ManifestInfo/index.jsx similarity index 85% rename from src/renderer/src/components/ManifestInfo/index.jsx rename to packages/gui/src/renderer/src/components/ManifestInfo/index.jsx index d3aa520..e1d6517 100644 --- a/src/renderer/src/components/ManifestInfo/index.jsx +++ b/packages/gui/src/renderer/src/components/ManifestInfo/index.jsx @@ -10,7 +10,7 @@ const ManifestInfo = (props) => { const [error, setError] = React.useState(null) async function handleInstall() { - await ipc.exec("pkg:install", props.manifest) + ipc.exec("pkg:install", props.manifest) if (typeof props.close === "function") { props.close() @@ -21,7 +21,7 @@ const ManifestInfo = (props) => { setLoading(true) try { - const result = await ipc.exec("pkg:read", url) + const result = await ipc.exec("pkg:read", url, { soft: true }) setManifest(JSON.parse(result)) @@ -40,7 +40,12 @@ const ManifestInfo = (props) => { }, [props.manifest]) if (error) { - return + console.error(error) + return } if (loading) { @@ -58,7 +63,7 @@ const ManifestInfo = (props) => {

- {manifest.name} + {manifest.pkg_name ?? manifest.name}

diff --git a/src/renderer/src/components/ManifestInfo/index.less b/packages/gui/src/renderer/src/components/ManifestInfo/index.less similarity index 100% rename from src/renderer/src/components/ManifestInfo/index.less rename to packages/gui/src/renderer/src/components/ManifestInfo/index.less diff --git a/src/renderer/src/components/NewInstallation/index.jsx b/packages/gui/src/renderer/src/components/NewInstallation/index.jsx similarity index 100% rename from src/renderer/src/components/NewInstallation/index.jsx rename to packages/gui/src/renderer/src/components/NewInstallation/index.jsx diff --git a/src/renderer/src/components/NewInstallation/index.less b/packages/gui/src/renderer/src/components/NewInstallation/index.less similarity index 100% rename from src/renderer/src/components/NewInstallation/index.less rename to packages/gui/src/renderer/src/components/NewInstallation/index.less diff --git a/src/renderer/src/components/PackageConfigItem/index.jsx b/packages/gui/src/renderer/src/components/PackageConfigItem/index.jsx similarity index 100% rename from src/renderer/src/components/PackageConfigItem/index.jsx rename to packages/gui/src/renderer/src/components/PackageConfigItem/index.jsx diff --git a/src/renderer/src/components/PackageItem/index.jsx b/packages/gui/src/renderer/src/components/PackageItem/index.jsx similarity index 66% rename from src/renderer/src/components/PackageItem/index.jsx rename to packages/gui/src/renderer/src/components/PackageItem/index.jsx index 7bb3a5f..3986322 100644 --- a/src/renderer/src/components/PackageItem/index.jsx +++ b/packages/gui/src/renderer/src/components/PackageItem/index.jsx @@ -4,17 +4,24 @@ import classnames from "classnames" import BarLoader from "react-spinners/BarLoader" -import { MdFolder, MdDelete, MdPlayArrow, MdUpdate, MdOutlineMoreVert, MdSettings, MdInfoOutline } from "react-icons/md" +import { MdCode, MdFolder, MdDelete, MdPlayArrow, MdUpdate, MdOutlineMoreVert, MdSettings, MdInfoOutline } from "react-icons/md" import "./index.less" const PackageItem = (props) => { const [manifest, setManifest] = React.useState(props.manifest) - const isLoading = manifest.status === "installing" || manifest.status === "uninstalling" || manifest.status === "loading" - const isInstalled = manifest.status === "installed" - const isInstalling = manifest.status === "installing" - const isFailed = manifest.status === "error" + const isLoading = manifest.last_status === "loading" || manifest.last_status === "installing" || manifest.last_status === "updating" + const isInstalling = manifest.last_status === "installing" + const isInstalled = !!manifest.installed_at + const isFailed = manifest.last_status === "failed" + + console.log(manifest, { + isLoading, + isInstalling, + isInstalled, + isFailed + }) const onClickUpdate = () => { antd.Modal.confirm({ @@ -31,7 +38,7 @@ const PackageItem = (props) => { } const onClickFolder = () => { - ipc.exec("pkg:open", manifest.id) + ipc.exec("core:open-path", manifest.id) } const onClickDelete = () => { @@ -53,7 +60,7 @@ const PackageItem = (props) => { } const onClickRetryInstall = () => { - ipc.exec("pkg:retry_install", manifest.id) + ipc.exec("pkg:last_operation_retry", manifest.id) } function handleUpdate(event, data) { @@ -65,10 +72,10 @@ const PackageItem = (props) => { function renderStatusLine(manifest) { if (isLoading) { - return manifest.status + return manifest.last_status } - return `v${manifest.version}` ?? "N/A" + return `${isFailed ? "failed |" : ""} v${manifest.version}` ?? "N/A" } const MenuProps = { @@ -111,10 +118,10 @@ const PackageItem = (props) => { } React.useEffect(() => { - ipc.on(`pkg:update:status:${manifest.id}`, handleUpdate) + ipc.on(`pkg:update:state:${manifest.id}`, handleUpdate) return () => { - ipc.off(`pkg:update:status:${manifest.id}`, handleUpdate) + ipc.off(`pkg:update:state:${manifest.id}`, handleUpdate) } }, []) @@ -126,12 +133,20 @@ const PackageItem = (props) => { className={classnames( "installation_item_wrapper", { - ["status_visible"]: !isInstalled + ["status_visible"]: isLoading, + ["loading"]: isLoading, + ["installing"]: isInstalling, } )} >
- + { + !manifest.icon && + } + + { + manifest.icon && + }

@@ -148,27 +163,37 @@ const PackageItem = (props) => {
{ - isFailed && - Retry - + isFailed && <> + + Retry + + + } + type="primary" + onClick={onClickDelete} + /> + } { - isInstalled && manifest.executable && - + } { - isInstalled && !manifest.executable && @@ -181,7 +206,7 @@ const PackageItem = (props) => { } { - isInstalling && @@ -198,7 +223,9 @@ const PackageItem = (props) => { isLoading && } -

{manifest.statusText ?? "Unknown status"}

+ { + manifest.status_text &&

{manifest.status_text}

+ }

} diff --git a/src/renderer/src/components/PackageItem/index.less b/packages/gui/src/renderer/src/components/PackageItem/index.less similarity index 100% rename from src/renderer/src/components/PackageItem/index.less rename to packages/gui/src/renderer/src/components/PackageItem/index.less diff --git a/src/renderer/src/components/PackageUpdateAvailable/index.jsx b/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx similarity index 87% rename from src/renderer/src/components/PackageUpdateAvailable/index.jsx rename to packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx index 03c9e15..99a6eb9 100644 --- a/src/renderer/src/components/PackageUpdateAvailable/index.jsx +++ b/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx @@ -16,7 +16,7 @@ const PackageUpdateAvailable = ({ update, close }) => { } function handleUpdate() { - ipc.exec("pkg:update", update.manifest.id, { + ipc.exec("pkg:update", update.id, { execOnFinish: true }) @@ -24,7 +24,7 @@ const PackageUpdateAvailable = ({ update, close }) => { } function handleContinue() { - ipc.exec("pkg:execute", update.manifest.id, { + ipc.exec("pkg:execute", update.id, { force: true }) @@ -38,7 +38,7 @@ const PackageUpdateAvailable = ({ update, close }) => {

- {update.current_version} {`->`} {update.new_version} + {update.local} {`->`} {update.remote}

diff --git a/src/renderer/src/components/PackageUpdateAvailable/index.less b/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.less similarity index 100% rename from src/renderer/src/components/PackageUpdateAvailable/index.less rename to packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.less diff --git a/packages/gui/src/renderer/src/components/Splash/index.jsx b/packages/gui/src/renderer/src/components/Splash/index.jsx new file mode 100644 index 0000000..5ab9e95 --- /dev/null +++ b/packages/gui/src/renderer/src/components/Splash/index.jsx @@ -0,0 +1,49 @@ +import React from "react" +import * as antd from "antd" +import { BarLoader } from "react-spinners" +import GlobalStateContext from "contexts/global" + +import "./index.less" + +const Splash = (props) => { + const globalState = React.useContext(GlobalStateContext) + + return
+ { + !!globalState.appSetup.message &&
+

+ Setting up... +

+

+ Please wait while the application is being set up. +

+
+ } + + { + globalState.appSetup.message && <> +
+
+ + {globalState.appSetup.message} + +
+
+ + + + } + + { + !globalState.appSetup.message && + } +
+} + +export default Splash \ No newline at end of file diff --git a/packages/gui/src/renderer/src/components/Splash/index.less b/packages/gui/src/renderer/src/components/Splash/index.less new file mode 100644 index 0000000..4cfc654 --- /dev/null +++ b/packages/gui/src/renderer/src/components/Splash/index.less @@ -0,0 +1,46 @@ +.splash { + display: flex; + flex-direction: column; + + justify-content: center; + + height: 100%; + + gap: 20px; + + .app-setup_header { + display: flex; + flex-direction: column; + + padding-top: 30px; + + gap: 10px; + + h1 { + font-size: 1.7rem; + } + } + + .app-setup_message-wrapper { + display: flex; + flex-direction: column; + + height: 100%; + + justify-content: center; + + .app-setup_message { + padding: 15px 10px; + border-radius: 12px; + + transition: all 150ms ease-in-out; + + background-color: var(--background-color-secondary); + + font-family: "DM Mono", monospace; + + word-break: break-all; + white-space: pre-wrap; + } + } +} \ No newline at end of file diff --git a/src/renderer/src/contexts/global.js b/packages/gui/src/renderer/src/contexts/global.js similarity index 100% rename from src/renderer/src/contexts/global.js rename to packages/gui/src/renderer/src/contexts/global.js diff --git a/src/renderer/src/contexts/installations.jsx b/packages/gui/src/renderer/src/contexts/packages.jsx similarity index 84% rename from src/renderer/src/contexts/installations.jsx rename to packages/gui/src/renderer/src/contexts/packages.jsx index 4e8ae1a..a4bf844 100644 --- a/src/renderer/src/contexts/installations.jsx +++ b/packages/gui/src/renderer/src/contexts/packages.jsx @@ -5,11 +5,14 @@ export const Context = React.createContext([]) export class WithContext extends React.Component { state = { + loading: true, packages: [], - pendingInstallation: false, } ipcEvents = { + "pkg:new:done": (event, data) => { + antd.message.success(`Successfully installed ${data.name}`) + }, "pkg:new": (event, data) => { antd.message.loading(`Installing ${data.id}`) @@ -42,7 +45,7 @@ export class WithContext extends React.Component { }) } }, - "pkg:update:status": (event, data) => { + "pkg:update:state": (event, data) => { const { id } = data let newData = this.state.packages @@ -59,24 +62,27 @@ export class WithContext extends React.Component { packages: newData }) } - - console.log(`[ipc] pkg:update:status >`, data) } } - componentDidMount = async () => { + loadPackages = async () => { + await this.setState({ + loading: true, + }) + const packages = await ipc.exec("pkg:list") + await this.setState({ + packages: packages, + }) + } + + componentDidMount = async () => { for (const event in this.ipcEvents) { ipc.exclusiveListen(event, this.ipcEvents[event]) } - this.setState({ - packages: [ - ...this.state.packages, - ...packages, - ] - }) + await this.loadPackages() } render() { diff --git a/src/renderer/src/layout/components/Drawer/index.jsx b/packages/gui/src/renderer/src/layout/components/Drawer/index.jsx similarity index 100% rename from src/renderer/src/layout/components/Drawer/index.jsx rename to packages/gui/src/renderer/src/layout/components/Drawer/index.jsx diff --git a/src/renderer/src/layout/components/Header/index.jsx b/packages/gui/src/renderer/src/layout/components/Header/index.jsx similarity index 96% rename from src/renderer/src/layout/components/Header/index.jsx rename to packages/gui/src/renderer/src/layout/components/Header/index.jsx index 3af492f..e558e9b 100644 --- a/src/renderer/src/layout/components/Header/index.jsx +++ b/packages/gui/src/renderer/src/layout/components/Header/index.jsx @@ -62,13 +62,13 @@ const Header = (props) => { } { - ctx.updateAvailable && } onClick={app.applyUpdate} type="primary" > - Update now + Update app } diff --git a/src/renderer/src/layout/components/Header/index.less b/packages/gui/src/renderer/src/layout/components/Header/index.less similarity index 91% rename from src/renderer/src/layout/components/Header/index.less rename to packages/gui/src/renderer/src/layout/components/Header/index.less index 78ac904..61f04f3 100644 --- a/src/renderer/src/layout/components/Header/index.less +++ b/packages/gui/src/renderer/src/layout/components/Header/index.less @@ -12,19 +12,22 @@ view-transition-name: main-header; + height: var(--app_header_height); + display: inline-flex; flex-direction: row; align-items: center; - background-color: darken(@var-background-color-primary, 10%); - gap: 30px; - - height: var(--app_header_height); - padding: 0 20px; + overflow: hidden; + + transition: 150ms ease-in-out; + + background-color: darken(@var-background-color-primary, 10%); + .branding { display: inline-flex; flex-direction: row; @@ -69,9 +72,12 @@ justify-content: center; border-radius: 50%; + padding: 3px; background-color: var(--primary-color); - font-size: 1.4rem; + font-size: 1.6rem; + + color: #3b3b3b; } .app_header_nav_title { diff --git a/src/renderer/src/layout/components/ModalDialog/index.jsx b/packages/gui/src/renderer/src/layout/components/ModalDialog/index.jsx similarity index 100% rename from src/renderer/src/layout/components/ModalDialog/index.jsx rename to packages/gui/src/renderer/src/layout/components/ModalDialog/index.jsx diff --git a/src/renderer/src/layout/index.jsx b/packages/gui/src/renderer/src/layout/index.jsx similarity index 100% rename from src/renderer/src/layout/index.jsx rename to packages/gui/src/renderer/src/layout/index.jsx diff --git a/src/renderer/src/main.jsx b/packages/gui/src/renderer/src/main.jsx similarity index 100% rename from src/renderer/src/main.jsx rename to packages/gui/src/renderer/src/main.jsx diff --git a/src/renderer/src/pages/index.jsx b/packages/gui/src/renderer/src/pages/index.jsx similarity index 61% rename from src/renderer/src/pages/index.jsx rename to packages/gui/src/renderer/src/pages/index.jsx index b667593..992ae26 100644 --- a/src/renderer/src/pages/index.jsx +++ b/packages/gui/src/renderer/src/pages/index.jsx @@ -3,32 +3,33 @@ import * as antd from "antd" import { MdAdd } from "react-icons/md" -import { Context as InstallationsContext, WithContext } from "contexts/installations" +import { Context as InstallationsContext, WithContext } from "contexts/packages" import PackageItem from "components/PackageItem" import NewInstallation from "components/NewInstallation" import "./index.less" -class InstallationsManager extends React.Component { +class Packages extends React.Component { static contextType = InstallationsContext render() { - const { packages } = this.context + const { packages, loading } = this.context const empty = packages.length == 0 - return
-
+ return
+
} onClick={() => app.drawer.open(NewInstallation, { - title: "Add new installation", + title: "Install new package", height: "200px", })} + className="add-btn" > - Add new installation + Add new
-
+
{ - empty && + loading && } { - packages.map((manifest) => { + !loading && empty && + } + + { + !loading && packages.map((manifest) => { return }) } @@ -54,10 +59,10 @@ class InstallationsManager extends React.Component { } } -const InstallationsManagerPage = (props) => { +const PackagesPage = (props) => { return - + } -export default InstallationsManagerPage \ No newline at end of file +export default PackagesPage \ No newline at end of file diff --git a/packages/gui/src/renderer/src/pages/index.less b/packages/gui/src/renderer/src/pages/index.less new file mode 100644 index 0000000..5249677 --- /dev/null +++ b/packages/gui/src/renderer/src/pages/index.less @@ -0,0 +1,71 @@ +.packages { + display: flex; + flex-direction: column; + + height: 100%; + + gap: 10px; + + .packages-header { + display: flex; + flex-direction: row; + gap: 10px; + + .ant-btn-default { + background-color: var(--background-color-secondary); + } + + .add-btn { + display: flex; + flex-direction: row; + + padding: 5px; + border-radius: 12px; + gap: 0px; + + span:not(.ant-btn-icon) { + opacity: 0; + + transition: all 150ms ease; + + max-width: 0px; + } + + &:hover { + padding: 5px 10px; + border-radius: 8px; + + gap: 7px; + + span:not(.ant-btn-icon) { + opacity: 1; + max-width: 200px; + } + } + + .ant-btn-icon { + margin: 0; + + font-size: 1.2rem; + } + } + } + + .packages-list { + display: flex; + flex-direction: column; + + height: 100%; + padding: 10px; + + gap: 10px; + + background-color: var(--background-color-secondary); + border-radius: 12px; + + &.empty { + align-items: center; + justify-content: center; + } + } +} \ No newline at end of file diff --git a/packages/gui/src/renderer/src/pages/logs/index.jsx b/packages/gui/src/renderer/src/pages/logs/index.jsx new file mode 100644 index 0000000..c494cea --- /dev/null +++ b/packages/gui/src/renderer/src/pages/logs/index.jsx @@ -0,0 +1,67 @@ +import React from "react" + +import "./index.less" + +const Timestamp = ({ timestamp }) => { + if (isNaN(timestamp)) { + return {timestamp} + } + + return + { + new Date(timestamp).toLocaleString().split(", ").join("|") + } + +} + +const LogEntry = ({ log }) => { + return
+ + {">"} + + + {log.timestamp && } + + {!log.timestamp && - no timestamp -} + +

+ {log.message ?? "No message"} +

+
+} + +const LogsViewer = () => { + const listRef = React.useRef() + const [timeline, setTimeline] = React.useState([]) + + const events = { + "logger:new": (event, log) => { + setTimeline((timeline) => [...timeline, log]) + + listRef.current.scrollTop = listRef.current.scrollHeight + } + } + + React.useEffect(() => { + for (const event in events) { + ipc.exclusiveListen(event, events[event]) + } + }, []) + + return
+ { + timeline.length === 0 &&

No logs

+ } + + { + timeline.map((log) => ) + } +
+} + +export default LogsViewer \ No newline at end of file diff --git a/packages/gui/src/renderer/src/pages/logs/index.less b/packages/gui/src/renderer/src/pages/logs/index.less new file mode 100644 index 0000000..74b6c71 --- /dev/null +++ b/packages/gui/src/renderer/src/pages/logs/index.less @@ -0,0 +1,47 @@ +.app-logs { + display: flex; + flex-direction: column; + + padding: 10px; + + font-family: "DM Mono", monospace; + + overflow-x: hidden; + overflow-y: scroll; + + height: 100vh; + + .log-entry { + display: flex; + flex-direction: row; + + align-items: flex-start; + + gap: 7px; + font-size: 0.8rem; + line-height: 0.8rem; + + border-radius: 8px; + + padding: 8px; + + color: var(--text-color); + + span { + color: var(--text-color); + + white-space: nowrap; + word-break: break-all; + } + + .timestamp { + opacity: 0.9; + + font-size: 0.7rem; + } + + &:nth-child(odd) { + background-color: var(--background-color-secondary); + } + } +} \ No newline at end of file diff --git a/src/renderer/src/pages/pkg/[pkg_id].jsx b/packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx similarity index 90% rename from src/renderer/src/pages/pkg/[pkg_id].jsx rename to packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx index df90cd0..565325b 100644 --- a/src/renderer/src/pages/pkg/[pkg_id].jsx +++ b/packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx @@ -7,23 +7,23 @@ import PKGConfigItem from "components/PackageConfigItem" import "./index.less" const PKGConfigs = (props) => { - const { defaultConfigs = {}, configs = {} } = props + const { config = {}, items = {} } = props - if (Object.keys(defaultConfigs).length === 0) { + if (Object.keys(items).length === 0) { return

No configuration available

} - return Object.keys(defaultConfigs).map((key, index) => { - const config = defaultConfigs[key] + return Object.keys(items).map((key, index) => { + const itemConfig = items[key] - config.id = key + itemConfig.id = key return }) @@ -62,12 +62,8 @@ const PackageOptions = (props) => { const { manifest } = props - if (!Array.isArray(manifest.applied_patches)) { - manifest.applied_patches = Array() - } - const [changes, setChanges] = React.useState({ - configs: {}, + config: manifest.config ?? {}, patches: manifest.patches ? Object.fromEntries(manifest.patches.map((p) => { return [p.id, manifest.applied_patches.includes(p.id)] })) : null @@ -139,7 +135,7 @@ const PackageOptions = (props) => { return
{ - manifest.configs &&
+ manifest.configuration &&

{

{ - handleChanges("configs", key, value) + handleChanges("config", key, value) }} />
@@ -293,12 +289,24 @@ const PackageOptionsLoader = (props) => { const [manifest, setManifest] = React.useState(null) React.useEffect(() => { - ipc.exec("pkg:get", pkg_id).then((manifest) => { - console.log(manifest) - setManifest(manifest) - }) + loadManifest() }, [pkg_id]) + async function loadManifest() { + let pkg = await ipc.exec("pkg:get", pkg_id) + + if (!pkg) { + return + } + + const manifestInstance = await ipc.exec("pkg:read", pkg.local_manifest) + + setManifest({ + ...JSON.parse(manifestInstance), + ...pkg, + }) + } + if (!manifest) { return } diff --git a/src/renderer/src/pages/pkg/index.less b/packages/gui/src/renderer/src/pages/pkg/index.less similarity index 100% rename from src/renderer/src/pages/pkg/index.less rename to packages/gui/src/renderer/src/pages/pkg/index.less diff --git a/src/renderer/src/pages/settings/index.jsx b/packages/gui/src/renderer/src/pages/settings/index.jsx similarity index 98% rename from src/renderer/src/pages/settings/index.jsx rename to packages/gui/src/renderer/src/pages/settings/index.jsx index 67cc177..9b9c294 100644 --- a/src/renderer/src/pages/settings/index.jsx +++ b/packages/gui/src/renderer/src/pages/settings/index.jsx @@ -1,6 +1,6 @@ import React from "react" import * as antd from "antd" -import { Icons, Icon } from "components/Icons" +import { Icon } from "components/Icons" import settingsList from "@renderer/settings_list" diff --git a/src/renderer/src/pages/settings/index.less b/packages/gui/src/renderer/src/pages/settings/index.less similarity index 100% rename from src/renderer/src/pages/settings/index.less rename to packages/gui/src/renderer/src/pages/settings/index.less diff --git a/src/renderer/src/router.jsx b/packages/gui/src/renderer/src/router.jsx similarity index 90% rename from src/renderer/src/router.jsx rename to packages/gui/src/renderer/src/router.jsx index 68f1867..599c16e 100644 --- a/src/renderer/src/router.jsx +++ b/packages/gui/src/renderer/src/router.jsx @@ -1,5 +1,4 @@ import React from "react" -import BarLoader from "react-spinners/BarLoader" import { Skeleton } from "antd" import { HashRouter, Route, Routes, useNavigate, useParams } from "react-router-dom" @@ -7,12 +6,14 @@ import loadable from "@loadable/component" import GlobalStateContext from "contexts/global" +import SplashScreen from "components/Splash" + const DefaultNotFoundRender = () => { return
Not found
} const DefaultLoadingRender = () => { - return + return } const BuildPageController = (route, element, bindProps) => { @@ -155,19 +156,8 @@ export const PageRender = (props) => { const globalState = React.useContext(GlobalStateContext) - if (globalState.setup_step || globalState.loading) { - return
- - -

Setting up...

- - -
{globalState.setup_step}
-
-
+ if (globalState.initializing) { + return } return diff --git a/src/renderer/src/settings_list.jsx b/packages/gui/src/renderer/src/settings_list.jsx similarity index 93% rename from src/renderer/src/settings_list.jsx rename to packages/gui/src/renderer/src/settings_list.jsx index 1478796..809b799 100644 --- a/src/renderer/src/settings_list.jsx +++ b/packages/gui/src/renderer/src/settings_list.jsx @@ -20,6 +20,7 @@ export default [ render: (props) => { return (