merge from local

This commit is contained in:
SrGooglo 2024-04-02 22:04:44 +02:00
parent 86a6effeb1
commit e187a49947
21 changed files with 508 additions and 147 deletions

View File

@ -33,6 +33,7 @@ export default async (pkg, step) => {
//`--depth ${step.depth ?? 1}`,
//"--filter=blob:none",
//"--filter=tree:0",
"--progress",
"--recurse-submodules",
"--remote-submodules",
step.url,

View File

@ -83,6 +83,7 @@ export default async function apply(pkg_id, changes = {}) {
return pkg
} catch (error) {
global._relic_eventBus.emit(`pkg:error`, {
event: "apply",
id: pkg_id,
error
})

View File

@ -19,6 +19,20 @@ export default async function execute(pkg_id, { useRemote = false, force = false
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)) {

View File

@ -164,12 +164,13 @@ export default async function install(manifest) {
return pkg
} catch (error) {
global._relic_eventBus.emit(`pkg:error`, {
id: id,
error
id: id ?? manifest.constructor.id,
event: "install",
error,
})
global._relic_eventBus.emit(`pkg:update:state`, {
id: id,
id: id ?? manifest.constructor.id,
last_status: "failed",
status_text: `Installation failed`,
})

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

@ -20,21 +20,33 @@ export default async function uninstall(pkg_id) {
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...`,
})
const ManifestRead = await ManifestReader(pkg.local_manifest)
const manifest = await ManifestVM(ManifestRead.code)
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`, {
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,
status_text: `Performing uninstall hook...`,
error
})
await manifest.uninstall(pkg)
}
Log.info(`Deleting package directory...`)
@ -62,6 +74,7 @@ export default async function uninstall(pkg_id) {
return pkg
} catch (error) {
global._relic_eventBus.emit(`pkg:error`, {
event: "uninstall",
id: pkg_id,
error
})

View File

@ -116,10 +116,21 @@ export default async function update(pkg_id) {
return pkg
} catch (error) {
global._relic_eventBus.emit(`pkg:error`, {
event: "update",
id: pkg_id,
error
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)

View File

@ -18,6 +18,7 @@ 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) {
@ -55,7 +56,8 @@ export default class RelicCore {
list: PackageList,
read: PackageRead,
authorize: PackageAuthorize,
checkUpdate: PackageCheckUpdate
checkUpdate: PackageCheckUpdate,
lastOperationRetry: PackageLastOperationRetry,
}
openPath(pkg_id) {

View File

@ -1,4 +1,5 @@
import winston from "winston"
import WinstonTransport from "winston-transport"
import colors from "cli-color"
const servicesToColor = {
@ -6,10 +7,6 @@ const servicesToColor = {
color: "whiteBright",
background: "bgBlackBright",
},
"INSTALL": {
color: "whiteBright",
background: "bgBlueBright",
},
}
const paintText = (level, service, ...args) => {
@ -27,6 +24,13 @@ const format = winston.format.printf(({ timestamp, service = "CORE", level, mess
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(),
@ -34,6 +38,7 @@ export default winston.createLogger({
),
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

@ -39,6 +39,15 @@ export async function readManifest(manifest) {
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,

View File

@ -1,14 +1,76 @@
import sendToRender from "../utils/sendToRender"
import { ipcMain } from "electron"
export default class CoreAdapter {
constructor(electronApp, RelicCore) {
this.app = electronApp
this.core = RelicCore
this.initialize()
this.initialized = false
}
events = {
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)
},
@ -51,22 +113,53 @@ export default class CoreAdapter {
sendToRender(`new:notification`, {
type: "error",
message: `An error occurred`,
description: `Something failed to ${data.event} package ${data.pkg_id}`,
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)
}
}
}
initialize = () => {
for (const [key, handler] of Object.entries(this.events)) {
attachLogger = (window) => {
this.loggerWindow = window
window.webContents.send("logger:new", {
timestamp: new Date().getTime(),
message: "Core adapter Logger attached",
})
}
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.events)) {
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

@ -26,62 +26,54 @@ const ProtocolRegistry = require("protocol-registry")
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"))
}
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.win = null
this.core = new RelicCore()
this.adapter = new CoreAdapter(this, this.core)
}
window = null
logsViewer = new LogsViewer()
handlers = {
"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)
},
"updater:check": () => {
autoUpdater.checkForUpdates()
},
@ -90,22 +82,31 @@ 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.webContents.send("logger:new", {
timestamp: new Date().getTime(),
message: "Logger opened, starting watching logs",
})
},
"app:init": async (event, data) => {
try {
await this.core.initialize()
await this.core.setup()
await this.adapter.initialize()
return {
pkg: pkg,
@ -126,19 +127,8 @@ class ElectronApp {
}
}
events = {
"open-runtime-path": () => {
return this.core.openPath()
},
"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,
@ -151,20 +141,20 @@ 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"))
}
}
@ -206,12 +196,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)
@ -235,10 +225,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) => {
@ -308,4 +294,4 @@ class ElectronApp {
}
}
new ElectronApp().initialize()
new ElectronApp().initialize()

View File

@ -32,7 +32,7 @@ export default (event, data) => {
return copy
}
global.win.webContents.send(event, serializeIpc(data))
global.mainWindow.webContents.send(event, serializeIpc(data))
} catch (error) {
console.error(error)
}

View File

@ -12,14 +12,19 @@ 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 = {
initializing: true,
pkg: null,
crash: null,
initializing: true,
appSetup: {
error: false,
installed: false,
@ -98,6 +103,15 @@ class App extends React.Component {
console.log(`React version > ${versions["react"]}`)
console.log(`DOMRouter version > ${versions["react-router-dom"]}`)
//check if path is /logs
if (window.location.pathname === "/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) {
@ -133,17 +147,34 @@ class App extends React.Component {
algorithm: antd.theme.darkAlgorithm
}}
>
<InternalRouter>
<GlobalStateContext.Provider value={this.state}>
{
this.state.log_viewer_mode && <LogsViewer />
}
<AppDrawer />
<AppModalDialog />
{
!this.state.log_viewer_mode && <>
<InternalRouter>
<GlobalStateContext.Provider value={this.state}>
{
!this.state.crash && <>
<AppDrawer />
<AppModalDialog />
<AppLayout>
<PageRender />
</AppLayout>
</GlobalStateContext.Provider>
</InternalRouter>
<AppLayout>
<PageRender />
</AppLayout>
</>
}
{
this.state.crash && <CrashError
crash={this.state.crash}
/>
}
</GlobalStateContext.Provider>
</InternalRouter>
</>
}
</antd.ConfigProvider>
}
}

View File

@ -6,6 +6,8 @@
gap: 20px;
padding: 20px;
h1 {
font-size: 1.5rem;
font-weight: bold;

View File

@ -14,7 +14,7 @@ const PackageItem = (props) => {
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 === "error"
const isFailed = manifest.last_status === "failed"
console.log(manifest, {
isLoading,
@ -38,7 +38,7 @@ const PackageItem = (props) => {
}
const onClickFolder = () => {
ipc.exec("pkg:open", manifest.id)
ipc.exec("core:open-path", manifest.id)
}
const onClickDelete = () => {
@ -60,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) {
@ -75,7 +75,7 @@ const PackageItem = (props) => {
return manifest.last_status
}
return `v${manifest.version}` ?? "N/A"
return `${isFailed ? "failed |" : ""} v${manifest.version}` ?? "N/A"
}
const MenuProps = {
@ -148,7 +148,6 @@ const PackageItem = (props) => {
manifest.icon && <img src={manifest.icon} className="installation_item_icon" />
}
<div className="installation_item_info">
<h2>
{
@ -164,16 +163,24 @@ const PackageItem = (props) => {
<div className="installation_item_actions">
{
isFailed && <antd.Button
type="primary"
onClick={onClickRetryInstall}
>
Retry
</antd.Button>
isFailed && <>
<antd.Button
type="primary"
onClick={onClickRetryInstall}
>
Retry
</antd.Button>
<antd.Button
icon={<MdDelete />}
type="primary"
onClick={onClickDelete}
/>
</>
}
{
isInstalled && manifest.executable && <antd.Dropdown.Button
!isFailed && isInstalled && manifest.executable && <antd.Dropdown.Button
menu={MenuProps}
onClick={onClickPlay}
type="primary"
@ -186,7 +193,7 @@ const PackageItem = (props) => {
}
{
isInstalled && !manifest.executable && <antd.Dropdown
isFailed && isInstalled && !manifest.executable && <antd.Dropdown
menu={MenuProps}
disabled={isLoading}
>
@ -199,7 +206,7 @@ const PackageItem = (props) => {
}
{
isInstalling && <antd.Button
isFailed && isInstalling && <antd.Button
type="primary"
onClick={onClickCancelInstall}
>

View File

@ -0,0 +1,67 @@
import React from "react"
import "./index.less"
const Timestamp = ({ timestamp }) => {
if (isNaN(timestamp)) {
return <span className="timestamp">{timestamp}</span>
}
return <span
className="timestamp"
>
{
new Date(timestamp).toLocaleString().split(", ").join("|")
}
</span>
}
const LogEntry = ({ log }) => {
return <div className="log-entry">
<span className="line_indicator">
{">"}
</span>
{log.timestamp && <Timestamp timestamp={log.timestamp} />}
{!log.timestamp && <span className="timestamp">- no timestamp -</span>}
<p>
{log.message ?? "No message"}
</p>
</div>
}
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 <div
className="app-logs"
ref={listRef}
>
{
timeline.length === 0 && <p>No logs</p>
}
{
timeline.map((log) => <LogEntry key={log.id} log={log} />)
}
</div>
}
export default LogsViewer

View File

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

View File

@ -307,8 +307,6 @@ const PackageOptionsLoader = (props) => {
})
}
console.log(manifest)
if (!manifest) {
return <antd.Skeleton active />
}

View File

@ -7,7 +7,6 @@ import loadable from "@loadable/component"
import GlobalStateContext from "contexts/global"
import SplashScreen from "components/Splash"
import CrashError from "components/Crash"
const DefaultNotFoundRender = () => {
return <div>Not found</div>
@ -131,18 +130,6 @@ export const InternalRouter = (props) => {
}
export const PageRender = (props) => {
const globalState = React.useContext(GlobalStateContext)
if (globalState.crash) {
return <CrashError
crash={globalState.crash}
/>
}
if (globalState.initializing) {
return <SplashScreen />
}
const routes = React.useMemo(() => {
let paths = {
...import.meta.glob("/src/pages/**/[a-z[]*.jsx"),
@ -167,6 +154,12 @@ export const PageRender = (props) => {
return paths
}, [])
const globalState = React.useContext(GlobalStateContext)
if (globalState.initializing) {
return <SplashScreen />
}
return <Routes>
{
routes.map((route, index) => {

View File

@ -20,6 +20,7 @@ export default [
render: (props) => {
return (
<Button
disabled
type={props.value ? "primary" : "default"}
onClick={() => {
if (!props.value) {
@ -65,7 +66,10 @@ export default [
icon: "MdUpdate",
type: "switch",
storaged: true,
defaultValue: false
defaultValue: false,
props: {
disabled: true
}
}
]
},
@ -83,7 +87,7 @@ export default [
props: {
children: "Open",
onClick: () => {
ipc.send("open-runtime-path")
ipc.exec("core:open-path")
}
},
storaged: false
@ -97,7 +101,7 @@ export default [
props: {
children: "Open",
onClick: () => {
ipc.send("open-dev-logs")
ipc.exec("app:open-logs")
}
},
storaged: false