Merge pull request #1 from ragestudio/devel

New core arch
This commit is contained in:
srgooglo 2024-04-02 22:17:44 +02:00 committed by GitHub
commit 079d644c83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
193 changed files with 3908 additions and 4458 deletions

View File

@ -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

View File

@ -25,6 +25,10 @@ jobs:
- name: Install Dependencies - name: Install Dependencies
run: npm install run: npm install
- name: build-linux
if: matrix.os == 'ubuntu-latest'
run: npm run build:linux
- name: build-mac - name: build-mac
if: matrix.os == 'macos-latest' if: matrix.os == 'macos-latest'
run: npm run build:mac run: npm run build:mac

View File

@ -1,6 +0,0 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json

View File

@ -1,4 +0,0 @@
singleQuote: false
semi: false
printWidth: 100
trailingComma: none

View File

@ -1,3 +0,0 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

39
.vscode/launch.json vendored
View File

@ -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
}
}
]
}

11
.vscode/settings.json vendored
View File

@ -1,11 +0,0 @@
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View File

@ -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

View File

@ -1,3 +0,0 @@
provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: rs-bundler-updater

View File

@ -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

View File

@ -1,75 +1,13 @@
{ {
"name": "rs-bundler", "name": "@ragestudio/relic-core",
"version": "0.15.0", "private": true,
"description": "RageStudio Bundler Utility GUI", "workspaces": [
"main": "./out/main/index.js", "packages/*"
"author": "RageStudio", ],
"repository": "https://github.com/srgooglo/rs_bundler",
"author": "SrGooglo <srgooglo@ragestudio.net>",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"format": "prettier --write .", "postinstall": "node scripts/postinstall.js"
"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"
} }
} }

2
packages/cli/bin Normal file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env node
require("./dist/index.js")

21
packages/cli/package.json Normal file
View File

@ -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"
}
}

169
packages/cli/src/index.js Normal file
View File

@ -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()

11
packages/core/.swcrc Normal file
View File

@ -0,0 +1,11 @@
{
"$schema": "http://json.schemastore.org/swcrc",
"module": {
"type": "commonjs",
// These are defaults.
"strict": false,
"strictMode": true,
"lazy": false,
"noInterop": false
}
}

View File

@ -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"
}
}

View File

@ -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]
}
}

View File

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

View File

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

115
packages/core/src/db.js Normal file
View File

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

View File

@ -1,33 +1,39 @@
import Logger from "../logger"
import path from "node:path" import path from "node:path"
import fs from "node:fs" import fs from "node:fs"
import upath from "upath" import upath from "upath"
import { execa } from "../lib/execa" import { execa } from "../libraries/execa"
import sendToRender from "../utils/sendToRender"
import Vars from "../vars" 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 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)) { if (!fs.existsSync(final_path)) {
fs.mkdirSync(final_path, { recursive: true }) fs.mkdirSync(final_path, { recursive: true })
} }
sendToRender(`pkg:update:status`, { Log.info(`Cloning from [${step.url}]`)
id: manifest.id,
statusText: `Cloning ${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 = [ const args = [
"clone", "clone",
//`--depth ${step.depth ?? 1}`, //`--depth ${step.depth ?? 1}`,
//"--filter=blob:none", //"--filter=blob:none",
//"--filter=tree:0", //"--filter=tree:0",
"--progress",
"--recurse-submodules", "--recurse-submodules",
"--remote-submodules", "--remote-submodules",
step.url, step.url,
@ -40,5 +46,5 @@ export default async (manifest, step) => {
stderr: "inherit", stderr: "inherit",
}) })
return manifest return pkg
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import DB from "../db"
export default async function list() {
return await DB.getPackages()
}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,10 @@ const forbidden = [
] ]
export default (event, data) => { export default (event, data) => {
if (!global.win) {
return false
}
try { try {
function serializeIpc(data) { function serializeIpc(data) {
if (!data) { if (!data) {

View File

@ -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!`)
}
}

View File

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

View File

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

View File

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

View File

@ -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),
});
}
}

View File

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

View File

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

View File

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

View File

@ -1,27 +1,22 @@
import { PathLike } from 'node:fs' import { rename, writeFile } from "node:fs/promises"
import { rename, writeFile } from 'node:fs/promises' import { basename, dirname, join } from "node:path"
import { basename, dirname, join } from 'node:path' import { fileURLToPath } from "node:url"
import { fileURLToPath } from 'node:url'
// Returns a temporary file // Returns a temporary file
// Example: for /some/file will return /some/.file.tmp // 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() const f = file instanceof URL ? fileURLToPath(file) : file.toString()
return join(dirname(f), `.${basename(f)}.tmp`) return join(dirname(f), `.${basename(f)}.tmp`)
} }
// Retries an asynchronous operation with a delay between retries and a maximum retry count // Retries an asynchronous operation with a delay between retries and a maximum retry count
async function retryAsyncOperation( async function retryAsyncOperation(fn, maxRetries, delayMs) {
fn: () => Promise<void>,
maxRetries: number,
delayMs: number,
): Promise<void> {
for (let i = 0; i < maxRetries; i++) { for (let i = 0; i < maxRetries; i++) {
try { try {
return await fn() return await fn()
} catch (error) { } catch (error) {
if (i < maxRetries - 1) { if (i < maxRetries - 1) {
await new Promise((resolve) => setTimeout(resolve, delayMs)) await new Promise(resolve => setTimeout(resolve, delayMs))
} else { } else {
throw error // Rethrow the error if max retries reached 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<typeof writeFile>[1]
export class Writer { export class Writer {
#filename: PathLike #filename
#tempFilename: PathLike #tempFilename
#locked = false #locked = false
#prev: [Resolve, Reject] | null = null #prev = null
#next: [Resolve, Reject] | null = null #next = null
#nextPromise: Promise<void> | null = null #nextPromise = null
#nextData: Data | null = null #nextData = null
// File is locked, add data for later // File is locked, add data for later
#add(data: Data): Promise<void> { #add(data) {
// Only keep most recent data // Only keep most recent data
this.#nextData = data this.#nextData = data
@ -59,18 +50,18 @@ export class Writer {
} }
// File isn't locked, write data // File isn't locked, write data
async #write(data: Data): Promise<void> { async #write(data) {
// Lock file // Lock file
this.#locked = true this.#locked = true
try { try {
// Atomic write // Atomic write
await writeFile(this.#tempFilename, data, 'utf-8') await writeFile(this.#tempFilename, data, "utf-8")
await retryAsyncOperation( await retryAsyncOperation(
async () => { async () => {
await rename(this.#tempFilename, this.#filename) await rename(this.#tempFilename, this.#filename)
}, },
10, 10,
100, 100
) )
// Call resolve // Call resolve
@ -96,12 +87,12 @@ export class Writer {
} }
} }
constructor(filename: PathLike) { constructor(filename) {
this.#filename = filename this.#filename = filename
this.#tempFilename = getTempFilename(filename) this.#tempFilename = getTempFilename(filename)
} }
async write(data: Data): Promise<void> { async write(data) {
return this.#locked ? this.#add(data) : this.#write(data) return this.#locked ? this.#add(data) : this.#write(data)
} }
} }

View File

@ -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" }),
],
})

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,10 @@
import Logger from "../../../logger"
import Client from "./launcher" import Client from "./launcher"
import Authenticator from "./authenticator" import Authenticator from "./authenticator"
const Log = Logger.child({ service: "MCL" })
export default class MCL { export default class MCL {
/** /**
* Asynchronously authenticate the user using the provided username and password. * 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("close", (e) => console.log(e))
launcher.on("error", (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) await launcher.launch(opts, callbacks)
return launcher return launcher

View File

@ -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,
}

View File

@ -0,0 +1,3 @@
import path from "node:path"
export default path

View File

@ -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

View File

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

View File

@ -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,
},
]

View File

@ -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

View File

@ -1,3 +1,5 @@
import Logger from "../logger"
import fs from "node:fs" import fs from "node:fs"
import path from "node:path" import path from "node:path"
import { pipeline as streamPipeline } from "node:stream/promises" import { pipeline as streamPipeline } from "node:stream/promises"
@ -7,10 +9,12 @@ import unzipper from "unzipper"
import Vars from "../vars" import Vars from "../vars"
const Log = Logger.child({ service: "EXTRACTOR" })
export async function extractFile(file, dest) { export async function extractFile(file, dest) {
const ext = path.extname(file) const ext = path.extname(file)
console.log(`extractFile() | Extracting ${file} to ${dest}`) Log.info(`Extracting ${file} to ${dest}`)
switch (ext) { switch (ext) {
case ".zip": { case ".zip": {
@ -24,13 +28,13 @@ export async function extractFile(file, dest) {
} }
case ".7z": { case ".7z": {
await extractFull(file, dest, { await extractFull(file, dest, {
$bin: Vars.sevenzip_path, $bin: Vars.sevenzip_bin,
}) })
break break
} }
case ".gz": { case ".gz": {
await extractFull(file, dest, { await extractFull(file, dest, {
$bin: Vars.sevenzip_path $bin: Vars.sevenzip_bin
}) })
break break
} }

View File

@ -8,7 +8,7 @@ export default function parseStringVars(str, pkg) {
name: pkg.name, name: pkg.name,
version: pkg.version, version: pkg.version,
install_path: pkg.install_path, install_path: pkg.install_path,
remote_url: pkg.remote_url, remote: pkg.remote,
} }
const regex = /%([^%]+)%/g const regex = /%([^%]+)%/g

View File

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

View File

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

35
packages/core/src/vars.js Normal file
View File

@ -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,
}

View File

@ -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

View File

@ -5,23 +5,9 @@ import react from "@vitejs/plugin-react"
export default defineConfig({ export default defineConfig({
main: { main: {
plugins: [externalizeDepsPlugin()], plugins: [externalizeDepsPlugin()],
// build: {
// rollupOptions: {
// output: {
// format: "es"
// }
// }
// },
}, },
preload: { preload: {
plugins: [externalizeDepsPlugin()], plugins: [externalizeDepsPlugin()],
// build: {
// rollupOptions: {
// output: {
// format: "es"
// }
// }
// },
}, },
renderer: { renderer: {
server: { server: {

55
packages/gui/package.json Normal file
View File

@ -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"
}
}

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

@ -1,10 +1,7 @@
import sendToRender from "./utils/sendToRender"
global.SettingsStore = new Store({ global.SettingsStore = new Store({
name: "settings", name: "settings",
watch: true, watch: true,
}) })
import path from "node:path" import path from "node:path"
import { app, shell, BrowserWindow, ipcMain } from "electron" 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 isDev from "electron-is-dev"
import Store from "electron-store" 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 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 { autoUpdater } = require("electron-differential-updater")
const ProtocolRegistry = require("protocol-registry") 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 { class ElectronApp {
constructor() { constructor() {
this.pkgManager = new PkgManager() this.core = new RelicCore()
this.win = null this.adapter = new CoreAdapter(this, this.core)
} }
authService = global.authService = new AuthService() window = null
logsViewer = new LogsViewer()
handlers = { 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": () => { "updater:check": () => {
autoUpdater.checkForUpdates() autoUpdater.checkForUpdates()
}, },
@ -91,55 +84,57 @@ class ElectronApp {
autoUpdater.quitAndInstall() autoUpdater.quitAndInstall()
}, 3000) }, 3000)
}, },
"settings:get": (e, key) => { "settings:get": (event, key) => {
return global.SettingsStore.get(key) return global.SettingsStore.get(key)
}, },
"settings:set": (e, key, value) => { "settings:set": (event, key, value) => {
return global.SettingsStore.set(key, value) return global.SettingsStore.set(key, value)
}, },
"settings:delete": (e, key) => { "settings:delete": (event, key) => {
return global.SettingsStore.delete(key) return global.SettingsStore.delete(key)
}, },
"settings:has": (e, key) => { "settings:has": (event, key) => {
return global.SettingsStore.has(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) => { "app:init": async (event, data) => {
try { try {
await setup() await this.adapter.initialize()
} catch (err) {
console.error(err)
sendToRender("new:notification", {
message: "Setup failed",
description: err.message
})
}
// check if can decode google drive token
const googleDrive_enabled = !!(await GoogleDriveAPI.readCredentials())
return { return {
pkg: pkg, pkg: pkg,
authorizedServices: { authorizedServices: {}
drive: googleDrive_enabled
}
}
}
} }
} catch (error) {
console.error(error)
events = { sendToRender("app:init:failed", {
"open-runtime-path": () => { message: "Initalization failed",
return this.pkgManager.openRuntimePath() error: error,
},
"open-dev-logs": () => {
return sendToRender("new:message", {
message: "Not implemented yet",
}) })
return {
error
}
}
} }
} }
createWindow() { createWindow() {
this.win = global.win = new BrowserWindow({ this.window = global.mainWindow = new BrowserWindow({
width: 450, width: 450,
height: 670, height: 670,
show: false, show: false,
@ -152,28 +147,26 @@ class ElectronApp {
} }
}) })
this.win.on("ready-to-show", () => { this.window.on("ready-to-show", () => {
this.win.show() this.window.show()
}) })
this.win.webContents.setWindowOpenHandler((details) => { this.window.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url) shell.openExternal(details.url)
return { action: "deny" } return { action: "deny" }
}) })
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { 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 { } else {
this.win.loadFile(path.join(__dirname, "../renderer/index.html")) this.window.loadFile(path.join(__dirname, "../renderer/index.html"))
} }
} }
handleURLProtocol(url) { handleURLProtocol(url) {
const urlStarter = `${protocolRegistryNamespace}://` const urlStarter = `${protocolRegistryNamespace}://`
console.log(url)
if (url.startsWith(urlStarter)) { if (url.startsWith(urlStarter)) {
const urlValue = url.split(urlStarter)[1] const urlValue = url.split(urlStarter)[1]
@ -184,25 +177,23 @@ class ElectronApp {
explicitAction[0] = explicitAction[0].slice(0, -1) explicitAction[0] = explicitAction[0].slice(0, -1)
} }
console.log(explicitAction)
if (explicitAction.length > 0) { if (explicitAction.length > 0) {
switch (explicitAction[0]) { switch (explicitAction[0]) {
case "authorize": { case "authorize": {
if (!explicitAction[2]) { if (!explicitAction[2]) {
const [pkgid, token] = explicitAction[1].split("%23") const [pkg_id, token] = explicitAction[1].split("%23")
return this.authService.authorize(pkgid, token) return this.core.package.authorize(pkg_id, token)
} else { } else {
return this.authService.authorize(explicitAction[1], explicitAction[2]) return this.core.package.authorize(explicitAction[1], explicitAction[2])
} }
} }
default: { default: {
return sendToRender("installation:invoked", explicitAction[0]) return sendToRender("pkg:installation:invoked", explicitAction[0])
} }
} }
} else { } else {
// by default if no action is specified, assume is a install action // 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() event.preventDefault()
// Someone tried to run a second instance, we should focus our window. // Someone tried to run a second instance, we should focus our window.
if (this.win) { if (this.window) {
if (this.win.isMinimized()) { if (this.window.isMinimized()) {
this.win.restore() this.window.restore()
} }
this.win.focus() this.window.focus()
} }
console.log(`Second instance >`, commandLine) console.log(`Second instance >`, commandLine)
@ -240,10 +231,6 @@ class ElectronApp {
ipcMain.handle(key, this.handlers[key]) 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("second-instance", this.handleOnSecondInstance)
app.on("open-url", (event, url) => { app.on("open-url", (event, url) => {
@ -288,8 +275,6 @@ class ElectronApp {
} }
} }
await GoogleDriveAPI.init()
await this.createWindow() await this.createWindow()
if (!isDev) { if (!isDev) {

Some files were not shown because too many files have changed in this diff Show More