diff --git a/.github/workflows/main-osx.yml b/.github/workflows/main-osx.yml index e230704..027f26e 100644 --- a/.github/workflows/main-osx.yml +++ b/.github/workflows/main-osx.yml @@ -52,4 +52,4 @@ jobs: uses: actions/upload-artifact@v2 with: name: sidenoder-darwin-x64-zip - path: /tmp/builds/darwin/zip/ \ No newline at end of file + path: /tmp/builds/darwin/zip/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..e69de29 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..48fcc3a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +.github/ +node_modules/ +syncedversion.txt +synced.txt +com.sample.android.apk +/dist/* +/out/* +**/*.min.css +**/*.min.css.map +**/*.min.js +**/*.min.js.map +**/*.custom.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c72176a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,14 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "always", + "requirePragma": false, + "insertPragma": false, + "proseWrap": "preserve", + "plugins": ["@destination/prettier-plugin-twig"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 4abca7b..ba6e70b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,17 +2,28 @@ "version": "0.2.0", "configurations": [ { - "name": "Launch", + "name": "Electron: Main", "type": "node", "request": "launch", - "program": "${workspaceFolder}/main.js", - "stopOnEntry": false, - "args": [], - "cwd": "${workspaceRoot}", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", - "runtimeArgs": [".", "--enable-logging"], - "env": {}, - "sourceMaps": false + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "start"], + "env": { + "MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223" + } + }, + { + "name": "Electron: Renderer", + "type": "chrome", + "request": "attach", + "port": 9223, + "webRoot": "${workspaceFolder}", + "timeout": 60000 + } + ], + "compounds": [ + { + "name": "Electron: All", + "configurations": ["Electron: Main", "Electron: Renderer"] } ] } diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 0f3a763..8fa4836 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -7,14 +7,14 @@ image: node:10.15.3 pipelines: default: - - step: - name: Build and Test - caches: - - node - script: - - npm install - - npm run-script pack - - npm run-script dist - artifacts: - - dist/SideNoder*.AppImage - - dist/SideNoder*.snap \ No newline at end of file + - step: + name: Build and Test + caches: + - node + script: + - npm install + - npm run-script pack + - npm run-script dist + artifacts: + - dist/SideNoder*.AppImage + - dist/SideNoder*.snap diff --git a/main.js b/main.js index bda6604..e75412d 100644 --- a/main.js +++ b/main.js @@ -1,26 +1,29 @@ -const { app, BrowserWindow, Notification, powerSaveBlocker, ipcMain, dialog } = require('electron'); +const { + app, + BrowserWindow, + Notification, + powerSaveBlocker, + ipcMain, + dialog, +} = require("electron"); // const { BrowserWindow } = require('@electron/remote') -const fs = require('fs'); -const path = require('path'); -global.twig = require('electron-twig'); +const fs = require("fs"); +const path = require("path"); +global.twig = require("electron-twig"); app.disableHardwareAcceleration(); -const { - EOL, - platform, - arch, - homedir, - tmpdir, -} = require('os'); +const { EOL, platform, arch, homedir, tmpdir } = require("os"); global.endOfLine = EOL; global.platform = platform(); // process.platform global.arch = arch(); -global.homedir = homedir().replace(/\\/g, '/'); -global.tmpdir = tmpdir().replace(/\\/g, '/'); -global.mountFolder = path.join(global.tmpdir, 'mnt').replace(/\\/g, '/'); -global.sidenoderHome = path.join(global.homedir, 'sidenoder').replace(/\\/g, '/'); +global.homedir = homedir().replace(/\\/g, "/"); +global.tmpdir = tmpdir().replace(/\\/g, "/"); +global.mountFolder = path.join(global.tmpdir, "mnt").replace(/\\/g, "/"); +global.sidenoderHome = path + .join(global.homedir, "sidenoder") + .replace(/\\/g, "/"); global.adbDevice = false; global.mounted = false; @@ -28,99 +31,96 @@ global.updateAvailable = false; global.currentConfiguration = {}; global.rcloneSections = []; global.installedApps = []; -global.hash_alg = 'sha256'; -global.locale = 'en-US'; +global.hash_alg = "sha256"; +global.locale = "en-US"; -global.platform = global.platform.replace('32', '').replace('64', ''); -if (global.platform == 'darwin') global.platform = 'mac'; +global.platform = global.platform.replace("32", "").replace("64", ""); +if (global.platform == "darwin") global.platform = "mac"; -app.on('ready', () => { +app.on("ready", () => { global.locale = app.getLocale(); }); -eval(fs.readFileSync(path.join(__dirname, 'versioncheck.js'), 'utf8')); - -const tools = require('./tools'); +eval(fs.readFileSync(path.join(__dirname, "versioncheck.js"), "utf8")); +const tools = require("./tools"); // const id = powerSaveBlocker.start('prevent-display-sleep'); // console.log(powerSaveBlocker.isStarted(id)); - - -ipcMain.on('get_installed', async (event, arg) => { - console.log('get_installed received'); +ipcMain.on("get_installed", async (event, arg) => { + console.log("get_installed received"); const apps = await tools.getInstalledApps(); - console.log('get_installed', apps.length); + console.log("get_installed", apps.length); - event.reply('get_installed', { success: true, apps }); + event.reply("get_installed", { success: true, apps }); return; }); -ipcMain.on('get_installed_with_updates', async (event, arg) => { - console.log('get_installed_with_updates received'); +ipcMain.on("get_installed_with_updates", async (event, arg) => { + console.log("get_installed_with_updates received"); const apps = await tools.getInstalledAppsWithUpdates(); - console.log('get_installed_with_updates', apps.length); + console.log("get_installed_with_updates", apps.length); - event.reply('get_installed_with_updates', { success: true, apps }); + event.reply("get_installed_with_updates", { success: true, apps }); }); -ipcMain.on('get_device_info', async (event, arg) => { +ipcMain.on("get_device_info", async (event, arg) => { getDeviceInfo(event); }); async function getDeviceInfo(event) { - console.log('get_device_info received'); + console.log("get_device_info received"); const res = await tools.getDeviceInfo(); - event.reply('get_device_info', res); + event.reply("get_device_info", res); } -ipcMain.on('connect_wireless', async (event, arg) => { - console.log('connect_wireless received'); +ipcMain.on("connect_wireless", async (event, arg) => { + console.log("connect_wireless received"); if (!global.adbDevice && !global.currentConfiguration.lastIp) { - console.log('Missing device, sending ask_device'); - event.reply('connect_wireless', { success: false }); - event.reply('ask_device', ''); + console.log("Missing device, sending ask_device"); + event.reply("connect_wireless", { success: false }); + event.reply("ask_device", ""); return; } const ip = await tools.connectWireless(); - event.reply('connect_wireless', { success: !!ip, ip }); + event.reply("connect_wireless", { success: !!ip, ip }); return; }); -ipcMain.on('disconnect_wireless', async (event, arg) => { - console.log('disconnect_wireless received'); +ipcMain.on("disconnect_wireless", async (event, arg) => { + console.log("disconnect_wireless received"); const res = await tools.disconnectWireless(); - event.reply('connect_wireless', { success: !res }); + event.reply("connect_wireless", { success: !res }); return; }); -ipcMain.on('check_deps', async (event, arg) => { - console.log('check_deps received', arg); +ipcMain.on("check_deps", async (event, arg) => { + console.log("check_deps received", arg); const res = await tools.checkDeps(arg); - event.reply('check_deps', res); + event.reply("check_deps", res); }); -ipcMain.on('mount', async (event, arg) => { +ipcMain.on("mount", async (event, arg) => { await tools.mount(); setTimeout(() => checkMount(event), 1000); return; }); -ipcMain.on('check_mount', async (event, arg) => { +ipcMain.on("check_mount", async (event, arg) => { checkMount(event); }); let rcloneProgress = false; async function checkMount(event) { await tools.checkMount(); - event.reply('check_mount', { success: global.mounted }); + event.reply("check_mount", { success: global.mounted }); if (global.mounted && !rcloneProgress) { tools.updateRcloneProgress(); rcloneProgress = true; @@ -129,68 +129,67 @@ async function checkMount(event) { return; } -ipcMain.on('start_sideload', async (event, arg) => { - console.log('start_sideload received'); +ipcMain.on("start_sideload", async (event, arg) => { + console.log("start_sideload received"); if (!global.adbDevice) { - console.log('Missing device, sending ask_device'); - event.reply('ask_device', ''); - return + console.log("Missing device, sending ask_device"); + event.reply("ask_device", ""); + return; } - event.reply('start_sideload', { success: true, path: arg.path }); + event.reply("start_sideload", { success: true, path: arg.path }); await tools.sideloadFolder(arg); getDeviceInfo(event); return; }); -ipcMain.on('folder_install', async (event, { path, update }) => { - console.log('folder_install received', path); +ipcMain.on("folder_install", async (event, { path, update }) => { + console.log("folder_install received", path); if (!global.adbDevice) { - console.log('Missing device, sending ask_device'); - event.reply('ask_device', ''); + console.log("Missing device, sending ask_device"); + event.reply("ask_device", ""); return; } const install = await tools.getApkFromFolder(path); console.log({ install }); - event.reply('ask_sideload', { success: true, install, update }); + event.reply("ask_sideload", { success: true, install, update }); return; }); -ipcMain.on('filedrop', async (event, path) => { - console.log('filedrop received'); +ipcMain.on("filedrop", async (event, path) => { + console.log("filedrop received"); if (!global.adbDevice) { - console.log('Missing device, sending ask_device'); - event.reply('ask_device', ''); + console.log("Missing device, sending ask_device"); + event.reply("ask_device", ""); return; } // TODO: check isApk - event.reply('ask_sideload', { success: true, install: { path } }); + event.reply("ask_sideload", { success: true, install: { path } }); return; }); - -ipcMain.on('reset_cache', async (event, arg) => { +ipcMain.on("reset_cache", async (event, arg) => { await tools.resetCache(arg); }); -ipcMain.on('get_dir', async (event, arg) => { - console.log('get_dir received', arg); - if ((typeof arg === 'string') && arg.endsWith('.apk')) { +ipcMain.on("get_dir", async (event, arg) => { + console.log("get_dir received", arg); + if (typeof arg === "string" && arg.endsWith(".apk")) { const install = { path: arg, - } - const lastslashindex = install.path.lastIndexOf('/'); + }; + const lastslashindex = install.path.lastIndexOf("/"); const folder = install.path.substring(0, lastslashindex); install.install_desc = await tools.detectInstallTxt(folder); install.notes = await tools.detectNoteTxt(folder); - event.reply('ask_sideload', { success: true, install }); // TODO: install_desc + event.reply("ask_sideload", { success: true, install }); // TODO: install_desc return; } @@ -203,275 +202,269 @@ ipcMain.on('get_dir', async (event, arg) => { dirList = []; incList = []; notSupported = []; - if (!list) incList = [{ name: 'ERROR: Browse failed' }]; - else for (const item of list) { - if (!item.isFile) { - dirList.push(item); - continue; - } - - if ((item.name.endsWith('.apk') || item.name.endsWith('.obb'))) { - incList.push(item); - continue; + if (!list) incList = [{ name: "ERROR: Browse failed" }]; + else + for (const item of list) { + if (!item.isFile) { + dirList.push(item); + continue; + } + + if (item.name.endsWith(".apk") || item.name.endsWith(".obb")) { + incList.push(item); + continue; + } + + notSupported.push(item); } - notSupported.push(item); - } - response = {}; response.success = true; response.list = dirList.concat(incList, notSupported); response.path = folder; // console.log(response.list, response.list.length, incList.length, notSupported.length); - win.webContents.send('get_dir',response); + win.webContents.send("get_dir", response); //event.reply('get_dir', response) }); -ipcMain.on('enable_mtp', async (event, arg) => { - console.log('enable_mtp received'); +ipcMain.on("enable_mtp", async (event, arg) => { + console.log("enable_mtp received"); if (!global.adbDevice) { - console.log('Missing device, sending ask_device'); - event.reply('ask_device', ''); + console.log("Missing device, sending ask_device"); + event.reply("ask_device", ""); return; } try { const res = await tools.enableMTP(); - event.reply('cmd_sended', { success: res }); - } - catch (err) { - event.reply('cmd_sended', { success: err }); + event.reply("cmd_sended", { success: res }); + } catch (err) { + event.reply("cmd_sended", { success: err }); } return; }); -ipcMain.on('scrcpy_start', async (event, arg) => { - console.log('scrcpy_start received'); +ipcMain.on("scrcpy_start", async (event, arg) => { + console.log("scrcpy_start received"); if (!global.adbDevice) { - console.log('Missing device, sending ask_device'); - event.reply('ask_device', ''); + console.log("Missing device, sending ask_device"); + event.reply("ask_device", ""); return; } const res = await tools.startSCRCPY(); - console.log('startSCRCPY', res); - event.reply('scrcpy_start', { success: !!res }); + console.log("startSCRCPY", res); + event.reply("scrcpy_start", { success: !!res }); return; }); -ipcMain.on('reboot_device', async (event, arg) => { - console.log('reboot_device received'); +ipcMain.on("reboot_device", async (event, arg) => { + console.log("reboot_device received"); if (!global.adbDevice) { - console.log('Missing device, sending ask_device'); - event.reply('ask_device', ''); + console.log("Missing device, sending ask_device"); + event.reply("ask_device", ""); return; } try { const res = await tools.rebootDevice(); - event.reply('cmd_sended', { success: res }); - } - catch (err) { - event.reply('cmd_sended', { success: err }); + event.reply("cmd_sended", { success: res }); + } catch (err) { + event.reply("cmd_sended", { success: err }); } return; }); -ipcMain.on('reboot_recovery', async (event, arg) => { - console.log('reboot_recovery received'); +ipcMain.on("reboot_recovery", async (event, arg) => { + console.log("reboot_recovery received"); if (!global.adbDevice) { - console.log('Missing device, sending ask_device'); - event.reply('ask_device', ''); + console.log("Missing device, sending ask_device"); + event.reply("ask_device", ""); return; } try { const res = await tools.rebootRecovery(); - event.reply('cmd_sended', { success: res }); - } - catch (err) { - event.reply('cmd_sended', { success: err }); + event.reply("cmd_sended", { success: res }); + } catch (err) { + event.reply("cmd_sended", { success: err }); } return; }); -ipcMain.on('reboot_bootloader', async (event, arg) => { - console.log('reboot_bootloader received'); +ipcMain.on("reboot_bootloader", async (event, arg) => { + console.log("reboot_bootloader received"); if (!global.adbDevice) { - console.log('Missing device, sending ask_device'); - event.reply('ask_device', ''); + console.log("Missing device, sending ask_device"); + event.reply("ask_device", ""); return; } try { const res = await tools.rebootBootloader(); - event.reply('cmd_sended', { success: res }); - } - catch (err) { - event.reply('cmd_sended', { success: err }); + event.reply("cmd_sended", { success: res }); + } catch (err) { + event.reply("cmd_sended", { success: err }); } return; }); -ipcMain.on('sideload_update', async (event, arg) => { - console.log('sideload_update received'); +ipcMain.on("sideload_update", async (event, arg) => { + console.log("sideload_update received"); if (!global.adbDevice) { - console.log('Missing device, sending ask_device'); - event.reply('ask_device', ''); + console.log("Missing device, sending ask_device"); + event.reply("ask_device", ""); return; } if (!arg) { - console.log('update.zip not defined'); - event.reply('cmd_sended', { success: 'Update.zip path not defined' }); + console.log("update.zip not defined"); + event.reply("cmd_sended", { success: "Update.zip path not defined" }); return; } try { const res = await tools.sideloadFile(arg); - event.reply('cmd_sended', { success: res }); - } - catch (err) { - event.reply('cmd_sended', { success: err }); + event.reply("cmd_sended", { success: res }); + } catch (err) { + event.reply("cmd_sended", { success: err }); } return; }); -ipcMain.on('device_tweaks', async (event, arg) => { - console.log('device_tweaks received', arg); +ipcMain.on("device_tweaks", async (event, arg) => { + console.log("device_tweaks received", arg); - if (arg.cmd == 'get') { + if (arg.cmd == "get") { const res = await tools.deviceTweaksGet(arg); - event.reply('device_tweaks', res); + event.reply("device_tweaks", res); } - if (arg.cmd == 'set') { + if (arg.cmd == "set") { if (!global.adbDevice) { - console.log('Missing device, sending ask_device'); - event.reply('ask_device', ''); + console.log("Missing device, sending ask_device"); + event.reply("ask_device", ""); return; } const res = await tools.deviceTweaksSet(arg); - event.reply('device_tweaks', arg); + event.reply("device_tweaks", arg); } return; }); -ipcMain.on('uninstall', async (event, arg) => { - console.log('uninstall received'); +ipcMain.on("uninstall", async (event, arg) => { + console.log("uninstall received"); resp = await tools.uninstall(arg); - event.reply('uninstall', { success: true }); + event.reply("uninstall", { success: true }); getDeviceInfo(event); return; }); -ipcMain.on('get_activities', async (event, arg) => { - console.log('get_activities received', arg); +ipcMain.on("get_activities", async (event, arg) => { + console.log("get_activities received", arg); const activities = await tools.getActivities(arg); - event.reply('get_activities', { success: !!activities, activities }); + event.reply("get_activities", { success: !!activities, activities }); return; }); -ipcMain.on('start_activity', async (event, arg) => { - console.log('start_activity received', arg); +ipcMain.on("start_activity", async (event, arg) => { + console.log("start_activity received", arg); const resp = await tools.startActivity(arg); - event.reply('start_activity', { success: !!resp }); + event.reply("start_activity", { success: !!resp }); return; }); -ipcMain.on('start_app', async (event, arg) => { - console.log('start_app received', arg); +ipcMain.on("start_app", async (event, arg) => { + console.log("start_app received", arg); const activity = await tools.getLaunchActivity(arg); const resp = await tools.startActivity(activity); - event.reply('start_app', { success: !!resp }); + event.reply("start_app", { success: !!resp }); return; }); -ipcMain.on('dev_open_url', async (event, arg) => { - console.log('dev_open_url received', arg); +ipcMain.on("dev_open_url", async (event, arg) => { + console.log("dev_open_url received", arg); const resp = await tools.devOpenUrl(arg); - event.reply('dev_open_url', { success: !!resp }); + event.reply("dev_open_url", { success: !!resp }); return; }); - -ipcMain.on('change_config', async (event, { key, val }) => { - console.log('change_config received', {key, val}); +ipcMain.on("change_config", async (event, { key, val }) => { + console.log("change_config received", { key, val }); val = await tools.changeConfig(key, val); - event.reply('change_config', { success: true, key, val }); + event.reply("change_config", { success: true, key, val }); return; }); -ipcMain.on('app_config_set', async (event, { pkg, key, val }) => { - console.log('change_config received', { pkg, key, val }); +ipcMain.on("app_config_set", async (event, { pkg, key, val }) => { + console.log("change_config received", { pkg, key, val }); const res = await tools.changeAppConfig(pkg, key, val); - event.reply('app_config_set', res); + event.reply("app_config_set", res); return; }); -ipcMain.on('app_info', async (event, arg) => { - console.log('app_info received', arg); +ipcMain.on("app_info", async (event, arg) => { + console.log("app_info received", arg); const res = await tools.appInfo(arg); // console.log({ res }); - event.reply('app_info', res); + event.reply("app_info", res); return; }); -ipcMain.on('app_events_info', async (event, arg) => { - console.log('app_events_info received', arg); +ipcMain.on("app_events_info", async (event, arg) => { + console.log("app_events_info received", arg); const res = await tools.appInfoEvents(arg); // console.log({ res }); - event.reply('app_events_info', res); + event.reply("app_events_info", res); return; }); -ipcMain.on('app_tools', async (event, arg) => { - console.log('app_tools received', arg); +ipcMain.on("app_tools", async (event, arg) => { + console.log("app_tools received", arg); const resp = await tools.checkAppTools(arg); - event.reply('app_tools', resp); + event.reply("app_tools", resp); return; }); -ipcMain.on('app_backup', async (event, arg) => { - console.log('app_backup received', arg); +ipcMain.on("app_backup", async (event, arg) => { + console.log("app_backup received", arg); const resp = await tools.backupApp(arg); - event.reply('app_backup', { success: resp }); + event.reply("app_backup", { success: resp }); return; }); -ipcMain.on('data_backup', async (event, arg) => { - console.log('data_backup received', arg); +ipcMain.on("data_backup", async (event, arg) => { + console.log("data_backup received", arg); const resp = await tools.backupAppData(arg); - event.reply('data_backup', { success: resp }); + event.reply("data_backup", { success: resp }); return; }); -ipcMain.on('data_restore', async (event, arg) => { - console.log('data_restore received', arg); +ipcMain.on("data_restore", async (event, arg) => { + console.log("data_restore received", arg); const resp = await tools.restoreAppData(arg); - event.reply('data_restore', { success: resp }); + event.reply("data_restore", { success: resp }); return; }); - global.close = false; -function createWindow () { +function createWindow() { global.win = new BrowserWindow({ width: 1000, minWidth: 920, height: 800, minHeight: 500, - title: 'Quest-Sidenoder', + title: "Quest-Sidenoder", //frame:false, webPreferences: { nodeIntegration: true, enableRemoteModule: true, contextIsolation: false, webView: true, - } + }, }); - require('@electron/remote/main').initialize(); + require("@electron/remote/main").initialize(); require("@electron/remote/main").enable(win.webContents); win.setMenu(null); win.maximize(true); @@ -484,10 +477,10 @@ function createWindow () { sidenoderHome: global.sidenoderHome, version: global.version, currentConfiguration: global.currentConfiguration, - } + }; win.loadURL(`file://${__dirname}/views/index.twig`); - if (process.argv[2] == '--dev') { + if (process.argv[2] == "--dev") { win.webContents.openDevTools(); } @@ -512,7 +505,7 @@ function createWindow () { });*/ } -global.notify = function (title, body, urgency = 'normal') { +global.notify = function (title, body, urgency = "normal") { const not = new Notification({ title, body, @@ -520,14 +513,13 @@ global.notify = function (title, body, urgency = 'normal') { urgency, // 'normal' | 'critical' | 'low' }); not.show(); -} +}; async function startApp() { try { await tools.reloadConfig(); - } - catch(e) { - console.error('reloadConfig', e); + } catch (e) { + console.error("reloadConfig", e); // tools.returnError('Could not (re)load config file.'); } @@ -535,14 +527,14 @@ async function startApp() { await app.whenReady(); createWindow(); - app.on('activate', () => { + app.on("activate", () => { if (BrowserWindow.getAllWindows().length != 0) return; createWindow(); }); - app.on('window-all-closed', (e) => { + app.on("window-all-closed", (e) => { // powerSaveBlocker.stop(id) - console.log('quit'); - if (global.platform !== 'mac') { + console.log("quit"); + if (global.platform !== "mac") { app.quit(); } }); diff --git a/package-lock.json b/package-lock.json index a3e9689..1aac756 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "dependencies": { "@devicefarmer/adbkit": "3.2.6", "@electron/remote": "2.1.2", + "@fortawesome/fontawesome-free": "^6.6.0", "adbkit-apkreader": "3.2.0", "command-exists": "1.2.9", "compare-versions": "6.1.1", @@ -19,13 +20,18 @@ "electron-twig": "1.1.1", "fix-path": "4.0.0", "jquery": "3.7.1", + "jquery-ui": "^1.14.0", "node-fetch": "3.3.2", "socks-proxy-agent": "8.0.4", "web-auto-extractor": "1.0.17" }, "devDependencies": { + "@destination/prettier-plugin-twig": "^1.5.0", "electron": "32.0.2", - "electron-builder": "25.0.5" + "electron-builder": "25.0.5", + "husky": "^9.1.5", + "lint-staged": "^15.2.10", + "prettier": "^3.3.3" } }, "node_modules/@babel/runtime": { @@ -39,6 +45,20 @@ "node": ">=6.9.0" } }, + "node_modules/@destination/prettier-plugin-twig": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@destination/prettier-plugin-twig/-/prettier-plugin-twig-1.5.0.tgz", + "integrity": "sha512-cTgy7lQOs0eTdNIqNQL9YDZ2ZFRyOzF5MfF07Lp8o1LLDMqjJgYfD382LIrXZ1E5s+ViYSgQJacR2U4Zx16XKw==", + "dev": true, + "dependencies": { + "html-styles": "^1.0.0", + "line-column": "^1.0.2", + "ohm-js": "^16.3.0" + }, + "peerDependencies": { + "prettier": "^2.0.0 || ^3.0.0" + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -415,6 +435,14 @@ "node": ">= 10.0.0" } }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", + "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==", + "engines": { + "node": ">=6" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -866,6 +894,21 @@ "ajv": "^6.9.1" } }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1257,6 +1300,18 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1545,15 +1600,18 @@ } }, "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "dependencies": { - "restore-cursor": "^3.1.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-spinners": { @@ -1646,6 +1704,12 @@ "color-support": "bin.js" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1860,11 +1924,11 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2412,6 +2476,18 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -2425,9 +2501,9 @@ "optional": true }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -2445,6 +2521,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2585,6 +2667,18 @@ "node": ">=10" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fix-path": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fix-path/-/fix-path-4.0.0.tgz", @@ -2733,6 +2827,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -2943,9 +3049,9 @@ "dev": true }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "optional": true, "dependencies": { "function-bind": "^1.1.2" @@ -2966,6 +3072,12 @@ "node": ">=10" } }, + "node_modules/html-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/html-styles/-/html-styles-1.0.0.tgz", + "integrity": "sha512-cDl5dcj73oI4Hy0DSUNh54CAwslNLJRCCoO+RNkVo+sBrjA/0+7E/xzvj3zH/GxbbBLGJhE0hBe1eg+0FINC6w==", + "dev": true + }, "node_modules/htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -3040,6 +3152,21 @@ "ms": "^2.0.0" } }, + "node_modules/husky": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.5.tgz", + "integrity": "sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag==", + "dev": true, + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -3177,6 +3304,15 @@ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "dev": true }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3204,8 +3340,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/isbinaryfile": { "version": "5.0.2", @@ -3224,6 +3359,18 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -3274,6 +3421,14 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, + "node_modules/jquery-ui": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.14.0.tgz", + "integrity": "sha512-mPfYKBoRCf0MzaT2cyW5i3IuZ7PfTITaasO5OFLAQxrHuI+ZxruPa+4/K1OMNT8oElLWGtIxc9aRbyw20BKr8g==", + "dependencies": { + "jquery": ">=1.12.0 <5.0.0" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3371,143 +3526,616 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/locutus": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.15.tgz", - "integrity": "sha512-2xWC4RkoAoCVXEb/stzEgG1TNgd+mrkLBj6TuEDNyUoKeQ2XzDTyJUC23sMiqbL6zJmJSP3w59OZo+zc4IBOmA==", + "node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, "engines": { - "node": ">= 10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.assignin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", - "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" - }, - "node_modules/lodash.bind": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", - "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "node_modules/line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha512-Ktrjk5noGYlHsVnYWh62FLVs4hTb8A3e+vucNZMgPeAOITdshMSgv4cCZQeRDjm7+goqmo6+liZwTXo+U3sVww==", "dev": true, - "peer": true - }, - "node_modules/lodash.filter": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, - "node_modules/lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" + "dependencies": { + "isarray": "^1.0.0", + "isobject": "^2.0.0" + } }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "node_modules/lint-staged": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", + "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", "dev": true, - "peer": true - }, - "node_modules/lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "node_modules/lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==" - }, - "node_modules/lodash.reduce": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" - }, - "node_modules/lodash.reject": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", - "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" + "dependencies": { + "chalk": "~5.3.0", + "commander": "~12.1.0", + "debug": "~4.3.6", + "execa": "~8.0.1", + "lilconfig": "~3.1.2", + "listr2": "~8.2.4", + "micromatch": "~4.0.8", + "pidtree": "~0.6.0", + "string-argv": "~0.3.2", + "yaml": "~2.5.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } }, - "node_modules/lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "node_modules/lint-staged/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, - "peer": true + "engines": { + "node": ">=18" + } }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/lint-staged/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" }, "engines": { - "node": ">=10" + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "node_modules/lint-staged/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, "engines": { - "node": ">=8" + "node": ">=16.17.0" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "devOptional": true, - "dependencies": { - "yallist": "^4.0.0" - }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-fetch-happen": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true, - "dependencies": { + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/listr2/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/locutus": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.15.tgz", + "integrity": "sha512-2xWC4RkoAoCVXEb/stzEgG1TNgd+mrkLBj6TuEDNyUoKeQ2XzDTyJUC23sMiqbL6zJmJSP3w59OZo+zc4IBOmA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" + }, + "node_modules/lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "peer": true + }, + "node_modules/lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "peer": true + }, + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==" + }, + "node_modules/lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, + "node_modules/lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" + }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "peer": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", @@ -3569,6 +4197,19 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -3610,6 +4251,18 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -3741,9 +4394,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/negotiator": { "version": "0.6.3", @@ -3966,6 +4619,15 @@ "node": ">= 0.4" } }, + "node_modules/ohm-js": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/ohm-js/-/ohm-js-16.6.0.tgz", + "integrity": "sha512-X9P4koSGa7swgVQ0gt71UCYtkAQGOjciJPJAz74kDxWt8nXbH5HrDOQG6qBDH7SR40ktNv4x61BwpTDE9q4lRA==", + "dev": true, + "engines": { + "node": ">=0.12.1" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4011,6 +4673,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -4122,6 +4809,30 @@ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -4136,6 +4847,21 @@ "node": ">=10.4.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -4322,16 +5048,46 @@ } }, "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/retry": { @@ -4343,6 +5099,12 @@ "node": ">= 4" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4668,6 +5430,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4864,6 +5635,18 @@ "tmp": "^0.2.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -5112,6 +5895,18 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "devOptional": true }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -5207,6 +6002,17 @@ "regenerator-runtime": "^0.13.4" } }, + "@destination/prettier-plugin-twig": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@destination/prettier-plugin-twig/-/prettier-plugin-twig-1.5.0.tgz", + "integrity": "sha512-cTgy7lQOs0eTdNIqNQL9YDZ2ZFRyOzF5MfF07Lp8o1LLDMqjJgYfD382LIrXZ1E5s+ViYSgQJacR2U4Zx16XKw==", + "dev": true, + "requires": { + "html-styles": "^1.0.0", + "line-column": "^1.0.2", + "ohm-js": "^16.3.0" + } + }, "@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -5489,6 +6295,11 @@ } } }, + "@fortawesome/fontawesome-free": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", + "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==" + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -5841,6 +6652,15 @@ "dev": true, "requires": {} }, + "ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "requires": { + "environment": "^1.0.0" + } + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -6164,6 +6984,15 @@ "concat-map": "0.0.1" } }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -6384,13 +7213,13 @@ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "requires": { - "restore-cursor": "^3.1.0" + "restore-cursor": "^5.0.0" } }, "cli-spinners": { @@ -6456,6 +7285,12 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6627,11 +7462,11 @@ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "decompress-response": { @@ -7059,6 +7894,12 @@ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" }, + "environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true + }, "err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -7072,9 +7913,9 @@ "optional": true }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, "escape-string-regexp": { @@ -7083,6 +7924,12 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "optional": true }, + "eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7188,6 +8035,15 @@ } } }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "fix-path": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fix-path/-/fix-path-4.0.0.tgz", @@ -7298,6 +8154,12 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true + }, "get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -7450,9 +8312,9 @@ "dev": true }, "hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "optional": true, "requires": { "function-bind": "^1.1.2" @@ -7467,6 +8329,12 @@ "lru-cache": "^6.0.0" } }, + "html-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/html-styles/-/html-styles-1.0.0.tgz", + "integrity": "sha512-cDl5dcj73oI4Hy0DSUNh54CAwslNLJRCCoO+RNkVo+sBrjA/0+7E/xzvj3zH/GxbbBLGJhE0hBe1eg+0FINC6w==", + "dev": true + }, "htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -7529,6 +8397,12 @@ "ms": "^2.0.0" } }, + "husky": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.5.tgz", + "integrity": "sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag==", + "dev": true + }, "iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -7624,6 +8498,12 @@ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "dev": true }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -7639,8 +8519,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "peer": true + "dev": true }, "isbinaryfile": { "version": "5.0.2", @@ -7653,6 +8532,15 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, "jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -7691,6 +8579,14 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, + "jquery-ui": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.14.0.tgz", + "integrity": "sha512-mPfYKBoRCf0MzaT2cyW5i3IuZ7PfTITaasO5OFLAQxrHuI+ZxruPa+4/K1OMNT8oElLWGtIxc9aRbyw20BKr8g==", + "requires": { + "jquery": ">=1.12.0 <5.0.0" + } + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -7778,6 +8674,222 @@ } } }, + "lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true + }, + "line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha512-Ktrjk5noGYlHsVnYWh62FLVs4hTb8A3e+vucNZMgPeAOITdshMSgv4cCZQeRDjm7+goqmo6+liZwTXo+U3sVww==", + "dev": true, + "requires": { + "isarray": "^1.0.0", + "isobject": "^2.0.0" + } + }, + "lint-staged": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", + "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", + "dev": true, + "requires": { + "chalk": "~5.3.0", + "commander": "~12.1.0", + "debug": "~4.3.6", + "execa": "~8.0.1", + "lilconfig": "~3.1.2", + "listr2": "~8.2.4", + "micromatch": "~4.0.8", + "pidtree": "~0.6.0", + "string-argv": "~0.3.2", + "yaml": "~2.5.0" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + }, + "commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true + }, + "execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + } + }, + "get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true + }, + "human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + } + } + }, + "listr2": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "dev": true, + "requires": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "requires": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + } + }, + "emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true + }, + "slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + } + }, + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + } + } + } + }, "locutus": { "version": "2.0.15", "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.15.tgz", @@ -7880,6 +8992,89 @@ "is-unicode-supported": "^0.1.0" } }, + "log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "requires": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "requires": { + "get-east-asian-width": "^1.0.0" + } + }, + "slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + } + }, + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + } + } + } + }, "lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -7951,6 +9146,16 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, "mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -7977,6 +9182,12 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -8070,9 +9281,9 @@ "dev": true }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "negotiator": { "version": "0.6.3", @@ -8223,6 +9434,12 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "optional": true }, + "ohm-js": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/ohm-js/-/ohm-js-16.6.0.tgz", + "integrity": "sha512-X9P4koSGa7swgVQ0gt71UCYtkAQGOjciJPJAz74kDxWt8nXbH5HrDOQG6qBDH7SR40ktNv4x61BwpTDE9q4lRA==", + "dev": true + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8254,6 +9471,27 @@ "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" + }, + "dependencies": { + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + } } }, "p-cancelable": { @@ -8331,6 +9569,18 @@ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true + }, "plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -8342,6 +9592,12 @@ "xmlbuilder": "^15.1.1" } }, + "prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -8489,13 +9745,30 @@ } }, "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "dependencies": { + "onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "requires": { + "mimic-function": "^5.0.0" + } + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } } }, "retry": { @@ -8504,6 +9777,12 @@ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true }, + "rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -8748,6 +10027,12 @@ "safe-buffer": "~5.1.0" } }, + "string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8905,6 +10190,15 @@ "tmp": "^0.2.0" } }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -9091,6 +10385,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "devOptional": true }, + "yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true + }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 1d73f3a..5525018 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,12 @@ "web-auto-extractor": "1.0.17" }, "devDependencies": { + "@destination/prettier-plugin-twig": "^1.5.0", "electron": "32.0.2", - "electron-builder": "25.0.5" + "electron-builder": "25.0.5", + "husky": "^9.1.5", + "lint-staged": "^15.2.10", + "prettier": "^3.3.3" }, "scripts": { "start": "electron ./ --dev", diff --git a/readme.md b/readme.md index fe393e3..efd7846 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,3 @@ - **SideNoder** - A **cross platform sideloader** for Quest(1&2&3) standalone vr headset. # Quest 3 compatibility fix added in v 0.8.0 @@ -24,32 +23,37 @@ What makes sidenoder better than other sideloaders ? ## Running the compiled version #### Run precompiled release on windows: + 1. Install latest [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) 2. Download and unpack(or run Setup.exe) the latest [windows release](https://github.com/VRPirates/sidenoder/releases/latest) 3. Run the `sidenoder.exe` application #### Run precompiled release on linux: + 1. Download and unpack the latest [AppImage/deb](https://github.com/VRPirates/sidenoder/releases/latest) 2. Make the AppImage executable. Or install deb package 3. Run the AppImage #### Run precompiled release on mac: + 1. Install latest [osxfuse](https://github.com/osxfuse/osxfuse/releases) (4.2.0+) 2. Download and unpack the latest [mac release](https://github.com/VRPirates/sidenoder/releases/latest) (.App) 3. Run the .App - --- ## MacOS compatibility To install on mac one has to manually install latest rclone in Terminal using following command: + ```bash sudo -v ; curl https://rclone.org/install.sh | sudo bash ``` + Other version of rclone will not work. Also android tools and scrcpy is required: + ```bash brew install scrcpy brew install android-platform-tools @@ -60,4 +64,3 @@ brew install android-platform-tools Please report any issues here : https://github.com/VRPirates/sidenoder/issues - diff --git a/removeLocales.js b/removeLocales.js index a5cffcb..14301b4 100644 --- a/removeLocales.js +++ b/removeLocales.js @@ -1,12 +1,12 @@ //https://www.electron.build/configuration/configuration#afterpack -const LOCALES = ['en-US.pak', 'ru.pak']; +const LOCALES = ["en-US.pak", "ru.pak"]; -exports.default = async function(context) { +exports.default = async function (context) { // console.log(context); - var fs = require('fs'); - var localeDir = context.appOutDir + '/locales/'; + var fs = require("fs"); + var localeDir = context.appOutDir + "/locales/"; - fs.readdir(localeDir, function(err, files) { + fs.readdir(localeDir, function (err, files) { //files is array of filenames (basename form) if (!(files && files.length)) return; for (const file of files) { @@ -14,4 +14,4 @@ exports.default = async function(context) { fs.unlinkSync(localeDir + file); } }); -} +}; diff --git a/tools.js b/tools.js index 4e511b7..dda4c95 100644 --- a/tools.js +++ b/tools.js @@ -1,2689 +1,2833 @@ -const exec = require('child_process').exec; -// const fs = require('fs'); -const fs = require('fs'); -const fsp = fs.promises; -const util = require('util'); -const path = require('path'); -const crypto = require('crypto'); -const commandExists = require('command-exists'); -const { dialog } = require('electron') -const ApkReader = require('adbkit-apkreader'); -const adbkit = require('@devicefarmer/adbkit').default; -const adb = adbkit.createClient(); -const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); -const WAE = require('web-auto-extractor').default -// const HttpProxyAgent = require('https-proxy-agent'); // TODO add https proxy support -const { SocksProxyAgent } = require('socks-proxy-agent'); -const url = require('url'); -// const ApkReader = require('node-apk-parser'); -const fixPath = (...args) => import('fix-path').then(({default: fixPath}) => fixPath(...args)); -// adb.kill(); - -const pkg = require('./package.json'); -const _sec = 1000; -const _min = 60 * _sec; - -let CHECK_META_PERIOD = 2 * _min; -const l = 32; -const configLocationOld = path.join(global.homedir, 'sidenoder-config.json'); -const configLocation = path.join(global.sidenoderHome, 'config.json'); - -let agentOculus, - agentSteam, - agentSQ; - -init(); - -const GAME_LIST_NAMES = global.currentConfiguration.gameListNames || [ - 'FFA.txt', - 'GameList.txt', - 'VRP-GameList.txt/VRP-GameList.txt', - 'VRP-GameList.txt', - 'Dynamic.txt', -]; -let META_VERSION = []; -let QUEST_ICONS = []; -let cacheOculusGames = false; -let KMETAS = {}; -let KMETAS2 = {}; - -let adbCmd = 'adb'; -let grep_cmd = '| grep '; -if (platform == 'win') { - grep_cmd = '| findstr '; -} - -let RCLONE_ID = 0; - - -module.exports = -{ -//properties - resetCache, -//methods - getDeviceSync, - trackDevices, - checkDeps, - checkMount, - mount, - killRClone, - getDir, - returnError, - sideloadFolder, - getInstalledApps, - getInstalledAppsWithUpdates, - getApkFromFolder, - uninstall, - getDirListing, - getPackageInfo, - wifiGetStat, - wifiEnable, - connectWireless, - disconnectWireless, - enableMTP, - startSCRCPY, - rebootDevice, - rebootRecovery, - rebootBootloader, - sideloadFile, - getLaunchActivity, - getActivities, - startActivity, - devOpenUrl, - checkAppTools, - changeAppConfig, - backupApp, - backupAppData, - restoreAppData, - getDeviceInfo, - getStorageInfo, - getUserInfo, - getFwInfo, - getBatteryInfo, - changeConfig, - reloadConfig, - execShellCommand, - updateRcloneProgress, - deviceTweaksGet, - deviceTweaksSet, - appInfo, - appInfoEvents, - isIdle, - wakeUp, - detectInstallTxt, - detectNoteTxt, - // ... -} - -async function getDeviceInfo() { - if (!global.adbDevice) { - return { - success: false, - }; - } - // console.log('getDeviceInfo()'); - - const storage = await getStorageInfo(); - const user = await getUserInfo(); - const fw = await getFwInfo(); - const battery = await getBatteryInfo(); - const ip = await getDeviceIp(); - const wifi = await wifiGetStat(); - - const res = { - success: !!storage, - storage, - user, - fw, - battery, - ip, - wifi, - }; - - // console.log('getDeviceInfo', res); - return res; -} - -async function getFwInfo() { - console.log('getFwInfo()'); - const res = await adbShell('getprop ro.build.branch'); - if (!res) return false; - - return { - version: res.replace('releases-oculus-', ''), - } -} - -async function getBatteryInfo() { - console.log('getBatteryInfo()'); - const res = await adbShell('dumpsys battery'); - if (!res) return false; - - return parceOutOptions(res); -} - -async function getUserInfo() { - if (global.currentConfiguration.userHide) return { - name: 'hidden' - }; - - console.log('getUserInfo()'); - const res = await adbShell('dumpsys user | grep UserInfo'); - if (!res) return false; - - return { - name: res.split(':')[1], - } -} - -async function deviceTweaksGet(arg) { - console.log('deviceTweaksGet()', arg); - let res = { - cmd: 'get', - // mp_name: '', - // guardian_pause: '0', - // frc: '0', - // gRR: '72', - // gCA: '-1', - // gFFR: '2', - // CPU: '2', - // GPU: '2', - // vres: '1024', - // cres: '640x480', - // gSSO: '1440x1584', - }; - - if (arg.key === 'mp_name') res.mp_name = (await adbShell('settings get global username')); - if (arg.key === 'guardian_pause') res.guardian_pause = (await adbShell('getprop debug.oculus.guardian_pause')); - if (arg.key === 'frc') res.frc = (await adbShell('getprop debug.oculus.fullRateCapture')); - if (arg.key === 'gRR') res.gRR = (await adbShell('getprop debug.oculus.refreshRate')); - if (arg.key === 'gCA') res.gCA = (await adbShell('getprop debug.oculus.forceChroma')); - if (arg.key === 'gFFR') res.gFFR = (await adbShell('getprop debug.oculus.foveation.level')); - if (arg.key === 'CPU') res.CPU = (await adbShell('getprop debug.oculus.cpuLevel')); - if (arg.key === 'GPU') res.GPU = (await adbShell('getprop debug.oculus.gpuLevel')); - if (arg.key === 'vres') res.vres = (await adbShell('getprop debug.oculus.videoResolution')); - if (arg.key === 'cres') { - let captureDims = - (await adbShell("getprop debug.oculus.capture.width")) + - "x" + - (await adbShell("getprop debug.oculus.capture.height")); - - // Default when not set - if (captureDims === "x") { - captureDims = "3840x1920"; - } - res.cres = captureDims; - } - if (arg.key === 'gSSO') res.gSSO = (await adbShell('getprop debug.oculus.textureWidth')) + 'x' + (await adbShell('getprop debug.oculus.textureHeight')); - //oculus.capture.bitrate - - return res; -} - -async function deviceTweaksSet(arg) { - console.log('deviceTweaksSet()', arg); - let res = {cmd: 'set'}; - if (typeof arg.mp_name != 'undefined') { - res.mp_name = await adbShell('settings put global username ' + arg.mp_name); - } - - if (typeof arg.guardian_pause != 'undefined') { - res.guardian_pause = await adbShell('setprop debug.oculus.guardian_pause ' + (arg.guardian_pause ? '1' : '0')); - } - if (typeof arg.frc != 'undefined') { - res.frc = await adbShell('setprop debug.oculus.fullRateCapture ' + (arg.frc ? '1' : '0')); - } - - if (typeof arg.gRR != 'undefined') { - res.gRR = await adbShell('setprop debug.oculus.refreshRate ' + arg.gRR); - } - - if (typeof arg.gCA != 'undefined') { - res.gCA = await adbShell('setprop debug.oculus.forceChroma ' + arg.gCA); - } - - if (typeof arg.gFFR != 'undefined') { - res.gFFR = await adbShell('setprop debug.oculus.foveation.level ' + arg.gFFR); - } - - if (typeof arg.CPU != 'undefined') { - res.CPU = await adbShell('setprop debug.oculus.cpuLevel ' + arg.CPU); - } - - if (typeof arg.GPU != 'undefined') { - res.GPU = await adbShell('setprop debug.oculus.gpuLevel ' + arg.GPU); - } - - if (typeof arg.vres != 'undefined') { - res.vres = await adbShell('setprop debug.oculus.videoResolution ' + arg.vres); - } - - if (typeof arg.cres != 'undefined') { - const [width, height] = arg.cres.split('x'); - await adbShell('setprop debug.oculus.capture.width ' + width); - res.cres = await adbShell('setprop debug.oculus.capture.height ' + height); - } - - if (typeof arg.gSSO != 'undefined') { - const [width, height] = arg.gSSO.split('x'); - await adbShell('setprop debug.oculus.textureWidth ' + width); - await adbShell('setprop debug.oculus.textureHeight ' + height); - res.gSSO = await adbShell('settings put system font_scale 0.85 && settings put system font_scale 1.0'); - } - - return res; -} - -async function getStorageInfo() { - console.log('getStorageInfo()'); - - const linematch = await adbShell('df -h | grep "/storage/emulated"'); - if (!linematch) return false; - - const refree = new RegExp('([0-9(.{1})]+[a-zA-Z%])', 'g'); - const storage = linematch.match(refree); - console.log(storage) - - if (storage.length == 3) { - return { - size: storage[0], - used: storage[1], - free: 0, - percent: storage[2], - }; - } - - return { - size: storage[0], - used: storage[1], - free: storage[2], - percent: storage[3], - }; -} - -async function getLaunchActivity(pkg) { - console.log('startApp()', pkg); - const activity = await adbShell(`dumpsys package ${pkg} | grep -A 1 'filter' | head -n 1 | cut -d ' ' -f 10`); - return startActivity(activity); -} - -async function getActivities(pkg, activity = false) { - console.log('getActivities()', pkg); - - let activities = await adbShell(`dumpsys package | grep -Eo '^[[:space:]]+[0-9a-f]+[[:space:]]+${pkg}/[^[:space:]]+' | grep -oE '[^[:space:]]+$'`); - if (!activities) return false; - - activities = activities.split('\n'); - // activities.pop(); - console.log({ pkg, activities }); - // TODO: check manifest.application.launcherActivities - - return activities; -} - -async function startActivity(activity) { - console.log('startActivity()', activity); - wakeUp(); - const result = await adbShell(`am start ${activity}`); // TODO activity selection - - console.log('startActivity', activity, result); - return result; -} - -async function devOpenUrl(url) { - console.log('devOpenUrl', url); - wakeUp(); - const result = await adbShell(`am start -a android.intent.action.VIEW -d "${url}"`); // TODO activity selection - - console.log('devOpenUrl', url, result); - return result; -} - -async function readAppCfg(pkg) { - let config = await adbShell(`cat /sdcard/Android/data/${pkg}/private/config.json 1>&1 2> /dev/null`); - try { - config = config && JSON.parse(config); - } - catch (e) { - console.error('readAppCfg', e); - config = false; - } - - return config; -} - -async function checkAppTools(pkg) { - const backupPath = path.join(global.sidenoderHome, 'backup_data', pkg); - const availableBackup = await adbFileExists(`/sdcard/Android/data/${pkg}`); - let availableRestore = false; - let availableConfig = false; - if (await fsp.exists(backupPath)) { - try { - availableRestore = await fsp.readFile(`${backupPath}/time.txt`, 'utf8'); - } - catch (err) { - availableRestore = 1; - } - } - - if (availableBackup) { - availableConfig = await readAppCfg(pkg); - } - - return { - success: true, - pkg, - availableRestore, - availableConfig, - }; -} - -async function changeAppConfig(pkg, key, val) { - console.log('changeAppConfig()', { pkg, key, val }); - const res = { - pkg, - key, - val, - success: false, - }; - - let config = await readAppCfg(pkg); - try { - config = Object.assign(config, { [key]: val }); - adbShell(`echo '${JSON.stringify(config)}' > "/sdcard/Android/data/${pkg}/private/config.json"`); - config = await readAppCfg(pkg); - res.val = config && config[key]; - res.success = !!config; - } - catch(e) { - console.error('changeAppConfig', res, e); - } - - return res; -} - -// Implementation ---------------------------------- - -async function getDeviceIp() { - // let ip = await adb.getDHCPIpAddress(global.adbDevice); - // if (ip) return ip; - if (!global.adbDevice && global.currentConfiguration.lastIp) { - return global.currentConfiguration.lastIp; - } - - let ip = await adbShell(`ip -o route get to 8.8.8.8 | sed -n 's/.*src \\([0-9.]\\+\\).*/\\1/p'`); - console.log({ip}); - if (ip) return ip; - - ip = await adbShell(`ip addr show wlan0 | grep 'inet ' | cut -d ' ' -f 6 | cut -d / -f 1`); - console.log({ip}); - if (ip) return ip; - return false; -} - -async function wifiGetStat() { - const on = await adbShell('settings get global wifi_on'); - return on && +on; -} - -async function wifiEnable(enable) { - return adbShell(`svc wifi ${enable ? 'enable' : 'disable'}`); -} - -async function connectWireless() { - const on = await adbShell('settings get global wifi_on'); - if (!(await wifiGetStat())) { - console.error('connectWireless', 'wifi disabled'); - await wifiEnable(true); - return false; - } - - // await adbShell(`setprop service.adb.tcp.port 5555`); - // TODO: save ip & try use it - const ip = await getDeviceIp(); - console.log({ ip }); - if (!ip) return false; - - try { - if (global.adbDevice) { - const device = adb.getDevice(global.adbDevice); - const port = await device.tcpip(); - await device.waitForDevice(); - console.log('set tcpip', port); - await changeConfig('lastIp', ip); - } - - const deviceTCP = await adb.connect(ip, 5555); - // await deviceTCP.waitForDevice(); - console.log('connectWireless', { ip, res: deviceTCP }); - - return ip; - } - catch (err) { - console.error('connectWireless', err); - await changeConfig('lastIp', ''); - return false; - } -} - -async function disconnectWireless() { - const ip = await getDeviceIp(); - if (!ip) return false; - - try { - const res = await adb.disconnect(ip, 5555); - // const res = await adb.usb(global.adbDevice); - console.log('disconnectWireless', { ip, res }); - // await changeConfig('lastIp', ''); - // await getDeviceSync(); - return res; - } - catch (err) { - console.error('disconnectWireless.error', err); - return !(await isWireless()); - } -} - -async function isWireless() { - try { - const devices = await adb.listDevices(); - for (const device of devices) { - if (!device.id.includes(':5555')) continue; - if (['offline', 'authorizing'].includes(device.type)) continue; - if (['unauthorized'].includes(device.type)) { - win.webContents.send('alert', 'Please authorize adb access on your device'); - continue; - } - - console.log('device.id', device.type); - return device.id; - } - - return false; - } - catch (err) { - console.error('Something went wrong:', err.stack); - return false; - } -} - -async function enableMTP() { - const res = await adbShell(`svc usb setFunctions mtp`); - console.log('enableMTP', { res }); - return res; -} - -async function isIdle() { - const res = await adbShell(`dumpsys deviceidle | grep mScreenOn`); - console.log(res, res.includes('true')); - return !res.includes('true'); -} - -async function wakeUp() { - if (!(await isIdle())) return; - return adbShell(`input keyevent KEYCODE_POWER`); -} - -async function startSCRCPY() { - console.log('startSCRCPY()'); - if (!global.currentConfiguration.scrcpyPath && !(await commandExists('scrcpy'))) { - returnError('Can`t find scrcpy binary'); - return; - } - - const scrcpyCmd = `"${global.currentConfiguration.scrcpyPath || 'scrcpy'}" ` + - (global.currentConfiguration.scrcpyCrop ? `--crop ${global.currentConfiguration.scrcpyCrop} `: '') + - `-b ${global.currentConfiguration.scrcpyBitrate || 1}M ` + - (global.currentConfiguration.scrcpyFps ? `--max-fps ${global.currentConfiguration.scrcpyFps} ` : '') + - (global.currentConfiguration.scrcpySize ? `--max-size ${global.currentConfiguration.scrcpySize} ` : '') + - (!global.currentConfiguration.scrcpyWindow ? '-f ' : '') + - (global.currentConfiguration.scrcpyOnTop ? '--always-on-top ' : '') + - (!global.currentConfiguration.scrcpyControl ? '-n ' : '') + - '--window-title "SideNoder Stream" ' + - `-s ${global.adbDevice} `; - console.log({ scrcpyCmd }); - wakeUp(); - exec(scrcpyCmd, (error, stdout, stderr) => { - if (error) { - console.error('scrcpy error:', error); - win.webContents.send('cmd_sended', { success: error }); - return; - } - - if (stderr) { - console.error('scrcpy stderr:', stderr); - // win.webContents.send('cmd_sended', { success: stderr }); - return; - } - - console.log('scrcpy stdout:', stdout); - }); - - return scrcpyCmd; -} - -async function rebootDevice() { - const res = await adbShell(`reboot`); - console.log('rebootDevice', { res }); - return res; -} -async function rebootRecovery() { - const res = await adbShell(`reboot recovery`); - console.log('rebootRecovery', { res }); - return res; -} -async function rebootBootloader() { - const res = await adbShell(`reboot bootloader`); - console.log('rebootBootloader', { res }); - return res; -} -async function sideloadFile(path) { - const res = await execShellCommand(`"${adbCmd}" sideload "${path}"`); - console.log('sideloadFile', { res }); - return res; -} - -async function getDeviceSync(attempt = 0) { - try { - // const lastDevice = global.adbDevice; - const devices = await adb.listDevices(); - console.log({ devices }); - global.adbDevice = false; - for (const device of devices) { - if (['offline', 'authorizing'].includes(device.type)) continue; - if (['unauthorized'].includes(device.type)) { - win.webContents.send('alert', 'Please authorize adb access on your device'); - continue; - } - - if ( - !global.currentConfiguration.allowOtherDevices - && await adbShell('getprop ro.product.brand', device.id) != 'oculus' - ) continue; - - global.adbDevice = device.id; - } - - - /*if (!global.adbDevice && devices.length > 0 && attempt < 1) { - return setTimeout(()=> getDeviceSync(attempt + 1), 1000); - }*/ - // if (lastDevice == global.adbDevice) return; - - win.webContents.send('check_device', { success: global.adbDevice }); - - return global.adbDevice; - } - catch (err) { - console.error('Something went wrong:', err.stack); - } -} - - -/** - * Executes a shell command and return it as a Promise. - * @param cmd {string} - * @return {Promise} - */ -async function adbShell(cmd, deviceId = global.adbDevice, skipRead = false) { - try { - if (!deviceId) { - throw 'device not defined'; - } - - global.adbError = null; - const r = await adb.getDevice(deviceId).shell(cmd); - // console.timeLog(cmd); - if (skipRead) { - console.log(`adbShell[${deviceId}]`, { cmd, skipRead }); - return true; - } - - let output = await adbkit.util.readAll(r); - output = await output.toString(); - // output = output.split('\n'); - // const end = output.pop(); - // if (end != '') output.push(); - console.log(`adbShell[${deviceId}]`, { cmd, output }); - if (output.substr(-1) == '\n') return output.slice(0, -1); - return output; - } - catch (err) { - console.error(`adbShell[${deviceId}]: err`, {cmd}, err); - global.adbError = err; - if (err.toString() == `FailError: Failure: 'device offline'`) { - getDeviceSync(); - } - - return false; - } -} - -function parceOutOptions(line) { - let opts = {}; - for (let l of line.split('\n')) { - l = l.split(' ').join(''); - let [k, v] = l.split(':'); - - if (v == 'true') v = true; - if (v == 'false') v = false; - if (!isNaN(+v)) v = +v; - - opts[k] = v; - } - - return opts; -} - -// on empty dirrectory return false -async function adbFileExists(path) { - const r = await adbShell(`ls "${path}" 1>&1 2> /dev/null`); - return r; -} - -async function adbPull(orig, dest, sync = false) { - console.log('adbPull', orig, dest); - const transfer = sync - ? await sync.pull(orig) - : await adb.getDevice(global.adbDevice).pull(orig); - return new Promise(function(resolve, reject) { - let c = 0; - transfer.on('progress', (stats) => { - c++; - if (c % 40 != 1) return; // skip 20 events - - // console.log(orig + ' pulled', stats); - const res = { - cmd: 'pull', - bytes: stats.bytesTransferred, - size: 0, - percentage: 0, - speedAvg: 0, - eta: 0, - name: orig, - } - win.webContents.send('process_data', res); - }); - transfer.on('end', () => { - console.log(orig, 'pull complete'); - win.webContents.send('process_data', false); - resolve(true); - }); - transfer.on('error', (err) => { - console.error('adb_pull_stderr', err); - win.webContents.send('process_data', false); - reject(err); - }); - transfer.pipe(fs.createWriteStream(dest)); - }); -} - -async function adbPullFolder(orig, dest, sync = false) { - console.log('pullFolder', orig, dest); - /*let need_close = false; - if (!sync) { - need_close = true; - sync = await adb.getDevice(global.adbDevice).syncService(); - }*/ - - let actions = []; - await fsp.mkdir(dest, { recursive: true }); - const files = sync - ? await sync.readdir(orig) - : await adb.getDevice(global.adbDevice).readdir(orig); - - for (const file of files) { - const new_orig = `${orig}/${file.name}`; - const new_dest = path.join(dest, file.name); - if (file.isFile()) { - actions.push(adbPull(new_orig, new_dest, sync)); // file.size - continue; - } - - actions.push(adbPullFolder(new_orig, new_dest, sync)); - } - - await Promise.all(actions); - - // if (need_close) sync.end(); - - return true; -} - -async function adbPush(orig, dest, sync = false) { - console.log('adbPush', orig, dest); - const transfer = sync - ? await sync.pushFile(orig, dest) - : await adb.getDevice(global.adbDevice).push(orig, dest); - const stats = await fsp.lstat(orig); - const size = stats.size; - - return new Promise(function(resolve, reject) { - let c = 0 - transfer.on('progress', (stats) => { - c++; - if (c % 40 != 1) return; // skip 20 events - - // console.log(orig + ' pushed', stats); - const res = { - cmd: 'push', - bytes: stats.bytesTransferred, - size, - percentage: (stats.bytesTransferred * 100 / size).toFixed(2), - speedAvg: 0, - eta: 0, - name: orig, - } - win.webContents.send('process_data', res); - }); - transfer.on('end', () => { - console.log(orig, 'push complete'); - win.webContents.send('process_data', false); - resolve(true); - }); - transfer.on('error', (err) => { - console.error('adb_push_stderr', err); - win.webContents.send('process_data', false); - reject(err); - }); - }); -} - -async function adbPushFolder(orig, dest, sync = false) { - console.log('pushFolder', orig, dest); - - const stat = await fsp.lstat(orig); - console.log({ orig, stat }, stat.isFile()); - if (stat.isFile()) return adbPush(orig, dest); - - /*let need_close = false; - if (!sync) { - need_close = true; - sync = await adb.getDevice(global.adbDevice).syncService(); - }*/ - - let actions = []; - await adbShell(`mkdir -p ${dest}`, global.adbDevice, true); - const files = await fsp.readdir(orig, { withFileTypes: true }); - for (const file of files) { - const new_orig = path.join(orig, file.name); - const new_dest = `${dest}/${file.name}`; - if (file.isFile()) { - actions.push(adbPush(new_orig, new_dest, sync)); - continue; - } - - actions.push(adbPushFolder(new_orig, new_dest, sync)); - } - - await Promise.all(actions); - - // if (need_close) sync.end(); - - return true; -} - -async function adbInstall(apk) { - console.log('adbInstall', apk); - const temp_path = '/data/local/tmp/install.apk'; - - await adbPush(apk, temp_path, false, false); - try { - await adb.getDevice(global.adbDevice).installRemote(temp_path); - } - catch (err) { - adbShell(`rm ${temp_path}`); - throw err; - } - - return true; -} - -function execShellCommand(cmd, ignoreError = false, buffer = 100) { - console.log({cmd}); - return new Promise((resolve, reject) => { - exec(cmd, {maxBuffer: 1024 * buffer}, (error, stdout, stderr) => { - if (error) { - if (ignoreError) return resolve(false); - console.error('exec_error', cmd, error); - return reject(error); - } - - if (stdout || !stderr) { - console.log('exec_stdout', cmd, stdout); - return resolve(stdout); - } - else { - if (ignoreError) return resolve(false); - console.error('exec_stderr', cmd, stderr); - return reject(stderr); - } - }); - }); -} - - -async function trackDevices() { - console.log('trackDevices()'); - await getDeviceSync(); - - try { - const tracker = await adb.trackDevices() - tracker.on('add', async (device) => { - console.log('Device was plugged in', device.id); - // await getDeviceSync(); - }); - - tracker.on('remove', async (device) => { - console.log('Device was unplugged', device.id); - // await getDeviceSync(); - }); - - tracker.on('change', async (device) => { // TODO: // need fix double run - console.log('Device was changed', device.id); - await getDeviceSync(); - }); - - tracker.on('end', () => { - console.error('Tracking stopped'); - trackDevices(); - }); - } - catch (err) { - console.error('Something went wrong:', err.stack); - returnError(err); - } -} - -async function appInfo(args) { - const { res, pkg } = args; - const app = KMETAS[pkg]; - - let data = { - res, - pkg, - id: 0, - name: app.simpleName, - short_description: '', - detailed_description: '', - about_the_game: '', - supported_languages: '', - genres: [], - header_image: '', - screenshots: [], - url: '', - }; - - try { - if (res == 'steam') { - const steam = app && app.steam; - if (!steam || !steam.id) throw 'incorrect args'; - - data.id = steam.id; - data.url = `https://store.steampowered.com/app/${data.id}/`; - - const resp = await fetchTimeout(`https://store.steampowered.com/api/appdetails?appids=${data.id}`, { - headers: { 'Accept-Language': global.locale + ',en-US;q=0.5,en;q=0.3' }, - agent: agentSteam, - }); - const json = await resp.json(); - // console.log({ json }); - - Object.assign(data, json[data.id].data); - } - - if (res == 'oculus') { - const oculus = app && app.oculus; - if (!oculus || !oculus.id) throw 'incorrect args'; - // console.log({ oculus }); - - data.id = oculus.id; - data.url = `https://www.oculus.com/experiences/quest/${data.id}`; - // data.genres = oculus.genres && oculus.genres.split(', '); - - //https://computerelite.github.io - let resp = await fetchTimeout(`https://graph.oculus.com/graphql?forced_locale=${global.locale}`, { - method: 'POST', - body: `access_token=OC|1317831034909742|&variables={"itemId":"${oculus.id}","first":1}&doc_id=5373392672732392`, - headers: { - 'Accept-Language': global.locale + ',en-US;q=0.5,en;q=0.3', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Origin': 'https://www.oculus.com', - }, - agent: agentOculus, - }); - try { - let json = await resp.json(); - // console.log('json', json); - if (json.error) throw json.error; - - const meta = json.data.node; - if (!meta) throw 'empty json.data.node'; - - data.name = meta.appName; - data.detailed_description = meta.display_long_description && meta.display_long_description.split('\n').join('
'); - data.genres = meta.genre_names; - - if (meta.supported_in_app_languages) { - data.supported_languages = meta.supported_in_app_languages.map(({name}) => name).join(', '); - } - //meta.internet_connection_name, - //meta.quality_rating_aggregate, - //meta.release_date, - - if (meta.website_page_meta) { - data.header_image = meta.website_page_meta.image_url; - data.short_description = meta.website_page_meta.description && meta.website_page_meta.description.split('\n').join('
'); - data.url = meta.website_page_meta.page_url; - } - - if (meta.screenshots) { - for (const { uri } of meta.screenshots) { - data.screenshots.push({ - // id, - path_thumbnail: uri, - }); - } - } - - if (meta.trailer && meta.trailer.uri) { - data.movies = [{ mp4: { '480': meta.trailer.uri } }]; - } - } - catch(err) { - console.error(res, 'fetch error', err); - - resp = await fetchTimeout(`${data.url}?locale=${global.locale}`, { - agent: agentOculus, - }); - const meta = await WAE().parse(await resp.text()); - const { metatags } = meta; - // console.log('meta', meta); - - data.name = metatags['og:title'][0].replace(' on Oculus Quest', ''); - data.header_image = metatags['og:image'][0]; - data.short_description = metatags['og:description'][0] && metatags['og:description'][0].split('\n').join('
'); - data.url = metatags['al:web:url'][0]; - - const jsonld = meta.jsonld.Product && meta.jsonld.Product[0] || JSON.parse(metatags['json-ld'][0]); - // console.log(jsonld); - - if (jsonld) { - if (jsonld.name) data.name = jsonld.name; - data.detailed_description = jsonld.description && jsonld.description.split('\n').join('
'); - - if (jsonld.image) { - for (const id in jsonld.image) { - if (['0', '1', '2'].includes(id)) continue; // skip resizes of header - - data.screenshots.push({ - id, - path_thumbnail: jsonld.image[id], - }); - } - } - } - } - } - - if (res == 'sq') { - const sq = app && app.sq; - if (!sq || !sq.id) throw 'incorrect args'; - // console.log({ sq }); - - data.id = sq.id; - data.url = `https://sidequestvr.com/app/${data.id}/`; - - const resp = await fetchTimeout(`https://api.sidequestvr.com/get-app`, { - method: 'POST', - body: JSON.stringify({ apps_id: data.id }), - headers: { - 'Accept-Language': global.locale + ',en-US;q=0.5,en;q=0.3', - 'Content-Type': 'application/json', - 'Origin': 'https://sidequestvr.com', - 'Cookie': ' __stripe_mid=829427af-c8dd-47d1-a857-1dc73c95b947201218; cf_clearance=LkOSetFAXEs255r2rAMVK_hm_I0lawkUfJAedj1nkD0-1633288577-0-250; __stripe_sid=6e94bd6b-19a4-4c34-98d5-1dc46423dd2e2f3688', - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0', - }, - agent: agentSQ, - }); - const json = await resp.json(); - const meta = json.data[0]; - data.name = meta.name; - data.header_image = meta.image_url; - data.short_description = meta.summary; - data.detailed_description = meta.description.split('\n').join('
'); - if (meta.video_url) - data.youtube = [meta.video_url - .replace('youtube.com', 'youtube.com/embed') - .replace('youtu.be', 'youtube.com/embed') - .replace('/embed/embed', '/embed') - .replace('/watch?v=', '/') - ]; - - const resp_img = await fetchTimeout(`https://api.sidequestvr.com/get-app-screenshots`, { - method: 'POST', - body: JSON.stringify({ apps_id: data.id }), - headers: { - 'Content-Type': 'application/json', - 'Origin': 'https://sidequestvr.com', - 'Cookie': ' __stripe_mid=829427af-c8dd-47d1-a857-1dc73c95b947201218; cf_clearance=LkOSetFAXEs255r2rAMVK_hm_I0lawkUfJAedj1nkD0-1633288577-0-250; __stripe_sid=6e94bd6b-19a4-4c34-98d5-1dc46423dd2e2f3688', - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0', - - }, - agent: agentSQ, - }); - const json_img = await resp_img.json(); - for (const id in json_img.data) { - data.screenshots.push({ - id, - path_thumbnail: json_img.data[id].image_url, - }); - } - } - } - catch (err) { - console.error('appInfo', { args, data }, err); - } - - return { success: true, data }; -} - -async function appInfoEvents(args) { - const { res, pkg } = args; - const app = KMETAS[pkg]; - let data = { - res, - pkg, - events: [], - }; - - try { - if (res == 'steam') { - const steam = app && app.steam; - if (!steam || !steam.id) throw 'incorrect args'; - - data.url = `https://store.steampowered.com/news/app/${steam.id}/`; - - const resp = await fetchTimeout(`http://api.steampowered.com/ISteamNews/GetNewsForApp/v0002?appid=${steam.id}`, { - headers: { 'Accept-Language': global.locale + ',en-US;q=0.5,en;q=0.3' }, - agent: agentSteam, - }); - const json = await resp.json(); - // console.log({ json }); - - const events = json.appnews.newsitems; - for (const e of events) { - const event = { - title: e.title, - url: e.url, - date: (new Date(e.date * _sec)).toLocaleString(), - // author: e.author, - } - - event.contents = e.contents - .split('\n').join('
') - .split('[img]').join('
') - .split('{STEAM_CLAN_IMAGE}').join('https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/clans') - .split('[list]').join('') - .split('[*]').join('
  • ') - // .split('[b]').join('') - // .split('[/b]').join('') - // .split('[i]').join('') - // .split('[/i]').join('') - .split('[').join('<') - .split(']').join('>') - data.events.push(event); - } - } - - if (res == 'oculus') { - const oculus = app && app.oculus; - if (!oculus || !oculus.id) throw 'incorrect args'; - - // data.url = `https://store.steampowered.com/news/app/${steam.id}/`; - - let resp = await fetchTimeout(`https://graph.oculus.com/graphql?forced_locale=${global.locale}`, { - method: 'POST', - body: `access_token=OC|1317831034909742|&variables={"id":"${oculus.id}"}&doc_id=1586217024733717`, - headers: { - 'Accept-Language': global.locale + ',en-US;q=0.5,en;q=0.3', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Origin': 'https://www.oculus.com', - }, - agent: agentOculus, - }); - try { - let json = await resp.json(); - if (json.error) throw json.error; - - // console.log({ json }); - const events = json.data.node.supportedBinaries.edges; - for (const { node } of events) { - const e = node; - const event = { - id: e.id, - title: `${e.version} (versionCode: ${e.versionCode})`, - contents: e.changeLog && e.changeLog.split('\n').join('
    '), - // richChangeLog: e.richChangeLog, - // url: '', - // date: '', - // author: '', - }; - - if (e.richChangeLog) console.log('RICHCHANGELOG', e.richChangeLog); - - data.events.push(event); - } - } - catch (err) { - console.error(res, 'fetch error', err); - } - - resp = await fetch(`https://computerelite.github.io/tools/Oculus/OlderAppVersions/${oculus.id}.json`); - json = await resp.json(); - // console.log({ json }); - const events = json.data.node.binaries.edges; - for (const { node } of events) { - const e = node; - let found = false; - for (const i in data.events) { - if (data.events[i].id != e.id) continue; - - data.events[i].date = (new Date(e.created_date * _sec)).toLocaleString(); - found = true; - break; - } - if (found) continue; - - const event = { - id: e.id, - title: `${e.version} (versionCode: ${e.version_code})`, - date: (new Date(e.created_date * _sec)).toLocaleString(), - contents: e.change_log.split('\n').join('
    '), - // url: '', - // author: '', - }; - - data.events.push(event); - } - } - - if (res == 'sq') { - const sq = app && app.sq; - if (!sq || !sq.id) throw 'incorrect args'; - // console.log({ sq }); - - for (const is_news of [true, false]) { - const resp = await fetchTimeout(`https://api.sidequestvr.com/events-list`, { - method: 'POST', - body: JSON.stringify({ apps_id: sq.id, is_news }), - headers: { - 'Accept-Language': global.locale + ',en-US;q=0.5,en;q=0.3', - 'Content-Type': 'application/json', - 'Origin': 'https://sidequestvr.com', - 'Cookie': ' __stripe_mid=829427af-c8dd-47d1-a857-1dc73c95b947201218; cf_clearance=LkOSetFAXEs255r2rAMVK_hm_I0lawkUfJAedj1nkD0-1633288577-0-250; __stripe_sid=6e94bd6b-19a4-4c34-98d5-1dc46423dd2e2f3688', - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0', - }, - agent: agentSQ, - }); - const json = await resp.json(); - // console.log({ json }); - for (const e of json.data) { - const event = { - id: e.events_id, - title: e.event_name, - url: e.event_url, - date: (new Date(e.start_time * _sec)).toLocaleString(), - contents: '', - // author: '', - }; - - if (e.event_image) { - event.contents += `
    `; - } - - if (e.event_description) { - event.contents += e.event_description.split('\n').join('
    '); - } - - data.events.push(event); - } - } - - } - } - catch (err) { - console.error('appInfoEvents', { args, data }, err); - } - - return { success: true, data }; -} - -async function checkMount(attempt = 0) { - console.log('checkMount()', attempt); - try { - attempt++; - - if (!(await fsp.readdir(global.mountFolder)).length && attempt < 15) { - return new Promise((res, rej) => { - setTimeout(() => { - checkMount(attempt).then(res).catch(rej); - }, _sec); - }); - } - - const resp = await fetch('http://127.0.0.1:5572/rc/noop', { - method: 'post', - }); - - global.mounted = resp.ok; - console.log('checkMount', global.mounted); - return global.mounted; - //setTimeout(updateRcloneProgress, 2000); - } - catch (e) { - console.warn('checkMount', e); - global.mounted = false; - return false; - } -} - -async function checkDeps(arg){ - console.log('checkDeps()', arg); - let res = { - [arg]: { - version: false, - cmd: false, - error: false, - } - }; - - try { - if (arg == 'adb') { - let globalAdb = false; - try { - globalAdb = await commandExists('adb'); - } catch (e) {} - - res[arg].cmd = adbCmd = globalAdb ? 'adb' : (await fetchBinary('adb')); - try { - await execShellCommand(`"${res[arg].cmd}" start-server`); - } - catch (err) { - if (!err.toString().includes('daemon started successfully')) - throw err; - } - - res[arg].version = 'adbkit v.' + (await adb.version()) + '\n' + await execShellCommand(`"${res[arg].cmd}" version`); - - await trackDevices(); - } - - if (arg == 'rclone') { - // module with autodownload https://github.com/sntran/rclone.js/blob/main/index.js - // res.rclone.cmd = global.currentConfiguration.rclonePath || await commandExists('rclone'); - res[arg].cmd = await fetchBinary('rclone'); - res[arg].version = await execShellCommand(`"${res[arg].cmd}" --version`); - } - - if (arg == 'zip') { - res[arg].cmd = await fetchBinary('7za'); - res[arg].version = await execShellCommand(`"${res[arg].cmd}" --help ${grep_cmd} "7-Zip"`); - console.log(res[arg].version); - } - - if (arg == 'scrcpy') { - res[arg].cmd = global.currentConfiguration.scrcpyPath || await commandExists('scrcpy'); - try { - res[arg].version = await execShellCommand(`"${res[arg].cmd}" --version`); - } - catch(err) { - res[arg].version = err; // don`t know why version at std_err(( - } - } - } - catch (e) { - console.error('checkDeps', arg, e); - res[arg].error = e && e.toString(); - } - - res.success = true; - return res; -} - -async function fetchBinary(bin) { - const cfgKey = `${bin}Path`; - const cmd = global.currentConfiguration[cfgKey]; - if (cmd) return cmd; - - const file = global.platform == 'win' ? `${bin}.exe` : bin; - - const binPath = path.join(sidenoderHome, file); - const branch = /*bin == 'rclone' ? 'new' :*/ 'master'; - const binUrl = `https://raw.githubusercontent.com/vKolerts/${bin}-bin/${branch}/${global.platform}/${global.arch}/${file}`; - await fetchFile(binUrl, binPath); - - if (bin == 'adb' && global.platform == 'win') { - const libFile = 'AdbWinApi.dll'; - const libUrl = `https://raw.githubusercontent.com/vKolerts/${bin}-bin/master/${global.platform}/${global.arch}/${libFile}`; - await fetchFile(libUrl, path.join(sidenoderHome, libFile)); - - const usbLibFile = 'AdbWinUsbApi.dll'; - const usbLibUrl = `https://raw.githubusercontent.com/vKolerts/${bin}-bin/master/${global.platform}/${global.arch}/${usbLibFile}`; - await fetchFile(usbLibUrl, path.join(sidenoderHome, usbLibFile)); - } - - return changeConfig(cfgKey, binPath); -} - -async function fetchFile(url, dest) { - console.log('fetchFile', { url, dest }); - const resp = await fetch(url); - if (!resp.ok) throw new Error(`Can't download '${url}': ${resp.statusText}`); - - if (await fsp.exists(dest)) await fsp.unlink(dest); - return fsp.writeFile(dest, await resp.buffer(), { mode: 0o755 }); -} - -function returnError(message) { - console.log('returnError()'); - global.win.loadURL(`file://${__dirname}/views/error.twig`) - global.twig.view = { - message: message, - } -} - - -async function killRClone() { - RCLONE_ID++; - const killCmd = platform == 'win' - ? `taskkill.exe /F /T /IM rclone.exe` - : `killall -9 rclone`; - console.log('try kill rclone'); - return new Promise((res, rej) => { - exec(killCmd, (error, stdout, stderr) => { - if (error) { - console.log(killCmd, 'error:', error); - return rej(error); - } - - if (stderr) { - console.log(killCmd, 'stderr:', stderr); - return rej(stderr); - } - - console.log(killCmd, 'stdout:', stdout); - return res(stdout); - }); - }) -} - -async function parseRcloneSections(newCfg = false) { - console.warn('parseRcloneSections', newCfg); - if (!global.currentConfiguration.rclonePath) { - return console.error('rclone binary not defined'); - } - - if (!global.currentConfiguration.rcloneConf) { - return console.error('rclone config not defined'); - } - - try { - const rcloneCmd = global.currentConfiguration.rclonePath; - const out = await execShellCommand(`"${rcloneCmd}" --config="${global.currentConfiguration.rcloneConf}" listremotes`); - if (!out) { - return console.error('rclone config is empty', global.currentConfiguration.rcloneConf, out); - } - - const sections = out.split('\n').map(section => section.replace(/:$/, '')); - if (sections.length) sections.pop(); - if (!sections.length) { - return console.error('rclone config sections not found', global.currentConfiguration.rcloneConf, { out, sections }); - } - - global.rcloneSections = sections; - } - catch (err) { - const cfg = await fsp.readFile(global.currentConfiguration.rcloneConf, 'utf8'); - - if (!cfg) return console.error('rclone config is empty', global.currentConfiguration.rcloneConf); - - const lines = cfg.split('\n'); - let sections = []; - for (const line of lines) { - if (line[0] != '[') continue; - const section = line.match(/\[(.*?)\]/)[1]; - sections.push(section); - } - - global.rcloneSections = sections; - } - - if (newCfg || !global.currentConfiguration.cfgSection) { - await changeConfig('cfgSection', global.rcloneSections[0]); - } - - // console.log({ sections: global.rcloneSections }); - return global.rcloneSections; -} - -async function umount() { - if (platform == 'win') { - if (!(await fsp.exists(global.mountFolder))) return; - - await fsp.rmdir(global.mountFolder, { recursive: true }); - return; - } - - await execShellCommand(`umount "${global.mountFolder}"`, true); - await execShellCommand(`fusermount -uz "${global.mountFolder}"`, true); - await fsp.mkdir(global.mountFolder, { recursive: true }); -} - -async function mount() { - if (!global.currentConfiguration.rclonePath || !global.currentConfiguration.rcloneConf) { - win.webContents.send('alert', 'Rclone not configured'); - } - - // if (await checkMount(13)) { - // return; - try { - await killRClone(); - } - catch (err) { - console.log('rclone not started'); - } - // } - - await umount(); - - if (global.mounted) { - return global.mounted = false; - } - - const myId = RCLONE_ID; - const mountCmd = global.currentConfiguration.mountCmd; - const rcloneCmd = global.currentConfiguration.rclonePath; - console.log('start rclone'); - exec(`"${rcloneCmd}" ${mountCmd} --read-only --rc --rc-no-auth --config="${global.currentConfiguration.rcloneConf}" ${global.currentConfiguration.cfgSection}: "${global.mountFolder}"`, (error, stdout, stderr) => { - if (error) { - console.error('rclone error:', error); - if (RCLONE_ID != myId) error = false; - console.log({ RCLONE_ID, myId }); - win.webContents.send('check_mount', { success: false, error }); - // checkMount(); - /*if (error.message.search('transport endpoint is not connected')) { - console.log('GEVONDE'); - }*/ - - return; - } - - if (stderr) { - console.log('rclone stderr:', stderr); - return; - } - - console.log('rclone stdout:', stdout); - }); -} - -function resetCache(folder) { - console.log('resetCache', folder); - const oculusGamesDir = path.join(global.mountFolder, global.currentConfiguration.mntGamePath).replace(/\\/g, '/'); - - if (folder == oculusGamesDir) { - cacheOculusGames = false; - return true; - } - - return false; -} - - -async function getDir(folder) { - const oculusGamesDir = path.join(global.mountFolder, global.currentConfiguration.mntGamePath).replace(/\\/g, '/'); - //console.log(folder, oculusGamesDir); - if ( - folder == oculusGamesDir - && global.currentConfiguration.cacheOculusGames - && cacheOculusGames - ) { - console.log('getDir return from cache', folder); - return cacheOculusGames; - } - - try { - const files = await fsp.readdir(folder/*, { withFileTypes: true }*/); - let gameList = {}; - let gameListName2Package = {}; - let installedApps = {} - let gameListName = false; - try { - // throw 'test'; - for (const name of GAME_LIST_NAMES) { - if (!fs.existsSync(path.join(folder, name))) continue; - // if (!files.includes(name)) continue; - gameListName = name; - break; - } - - if (gameListName) { - const list = (await fsp.readFile(path.join(folder, gameListName), 'utf8')).split('\n'); - let listVer; - if (!list.length) throw gameListName + ' is empty'; - - for (const line of list) { - const meta = line.split(';'); - if (!listVer) { - listVer = meta[2] == 'Release APK Path' ? 1 : 2; - console.log({ gameListName, listVer }); - continue; - } - - if (listVer == 1) { - gameList[meta[1]] = { - simpleName: meta[0], - releaseName: meta[1], - packageName: meta[3], - versionCode: meta[4], - versionName: meta[5], - imagePath: `file://${global.tmpdir}/mnt/${global.currentConfiguration.mntGamePath}/.meta/thumbnails/${meta[3]}.jpg`, - } - } - else if (listVer == 2) { - gameList[meta[1]] = { - simpleName: meta[0], - releaseName: meta[1], - packageName: meta[2], - versionCode: meta[3], - imagePath: `file://${global.tmpdir}/mnt/${global.currentConfiguration.mntGamePath}/.meta/thumbnails/${meta[2]}.jpg`, - size: meta[5], - } - } - - } - } - } - catch (err) { - console.error(`${gameListName} failed`, err); - gameListName = { - err: `Can't parse GameList.txt -
    Maybe issue of server - please attempt to switch mirror at settings. -
    Actual mirrors posted there http://t.me/sidenoder`, - }; - } - - // console.log(gameList); - - try { - if (global.adbDevice) { - installedApps = await getInstalledApps(true); - } - } - catch (err) { - console.error('Can`t get installed apps', err); - } - - let fileNames = await Promise.all(files.map(async (fileName) => { - // console.log(fileName); - - const info = await fsp.lstat(path.join(folder, fileName)); - let steamId = false, - sqId = false, - oculusId = false, - imagePath = false, - versionCode = '', - versionName = '', - simpleName = fileName, - packageName = false, - note = '', - kmeta = false, - mp = false, - installed = 0, - size = false, - newItem = false; - - let isGameFolder = false; - - if (info.isDirectory()) { - const dirCont = await fsp.readdir(path.join(folder, fileName)); - - isGameFolder = - dirCont.filter((file) => { - return /.*\.apk/.test(file); - }).length > 0; - } - - let gameMeta = false; - - if (isGameFolder) { - gameMeta = gameList[fileName]; - - if (!gameMeta) { - // If gameMeta is still not defined then there is no game with a - // matching version number. We now query gameList using the game name - // without the version number. - let regex = /^([\w -.,!?&+™®'"]+) v\d+\+/; - if (regex.test(fileName)) { - // Only do this if this is a folder containing an apk file. - if (info.isDirectory()) { - const dirCont = await fsp.readdir(path.join(folder, fileName)); - const isGameFolder = - dirCont.filter((file) => { - return /.*\.apk/.test(file); - }).length > 0; - - if (isGameFolder) { - const match = fileName.match(regex)[1]; - gameMeta = gameList[match]; - } - } - } - } - } - - if (gameMeta) { - simpleName = gameMeta.simpleName; - packageName = gameMeta.packageName; - versionCode = gameMeta.versionCode; - versionName = gameMeta.versionName; - simpleName = gameMeta.simpleName; - size = gameMeta.size; - imagePath = gameMeta.imagePath; - - let regex = /\((.*?)\)/; - if (regex.test(gameMeta.releaseName)) { - const match = gameMeta.releaseName.match(regex)[0]; - note += match.replace(", only autoinstalls with Rookie", ""); - } - } - - // Include local notes. This allows users to add notes in brackets to - // their filenames to override the original note. These notes are rendered - // in the game's browse card. - let regex = /\((.*?)\)/; - if (regex.test(fileName)) { - const match = fileName.match(/\((.*?)\)/)[0]; - if (match !== note) { - // This note differs from the original so it has been overriden in the - // filename, so we replace it. - note = match.replace(", only autoinstalls with Rookie", ""); - } - } - - regex = /^([\w -.,!?&+™®'"]+) v\d+\+/; - if (gameListName && !packageName && regex.test(fileName)) { - simpleName = fileName.match(regex)[1]; - packageName = KMETAS2[escString(simpleName)]; - } - - regex = /v(\d+)\+/; - if (!versionCode && regex.test(fileName)) { - versionCode = fileName.match(regex)[1]; - } - - regex = /v\d+\+([\w.]*) /; - if (!versionName && regex.test(fileName)) { - versionName = fileName.match(regex)[1]; - } - - if (!versionCode && (new RegExp('.*\ -versionCode-')).test(fileName)) { - versionCode = fileName.match(/-versionCode-([0-9]*)/)[1]; - } - - if (!packageName && (new RegExp('.*\ -packageName-')).test(fileName)) { - packageName = fileName.match(/-packageName-([a-zA-Z0-9.]*)/)[1]; - } - - // obbs path the same =( - if (gameListName && !packageName && KMETAS[fileName]) { - packageName = fileName; - } - - - if (packageName) { - if (!imagePath) { - if (QUEST_ICONS.includes(packageName + '.jpg')) { - imagePath = `https://raw.githubusercontent.com/vKolerts/quest_icons/master/250/${packageName}.jpg`; - } else if (!imagePath) { - imagePath = 'unknown.png'; - } - } - - kmeta = KMETAS[packageName]; - installedApp = installedApps[packageName]; - if (installedApp) { - installed = 1; - if (versionCode && versionCode > installedApp.versionCode) { - installed++; - } - } - } - - if (gameListName && !packageName && versionCode) { - packageName = 'can`t parse package name'; - imagePath = 'unknown.png'; - } - - if (kmeta) { - steamId = !!(kmeta.steam && kmeta.steam.id); - oculusId = !!(kmeta.oculus && kmeta.oculus.id); - sqId = !!(kmeta.sq && kmeta.sq.id); - simpleName = simpleName || kmeta.simpleName; - mp = kmeta.mp || !!kmeta.mp; - } - else { - newItem = true; - } - - simpleName = await cleanUpFoldername(simpleName); - const isFile = info.isFile() - || (info.isSymbolicLink() && fileName.includes('.')); // not well - - return { - name: fileName, - simpleName, - isFile, - isLink: info.isSymbolicLink(), - steamId, - sqId, - oculusId, - imagePath, - versionCode, - versionName, - packageName, - size, - note, - newItem, - info, - mp, - installed, - createdAt: new Date(info.mtimeMs), - filePath: path.join(folder, fileName).replace(/\\/g, '/'), - }; - })); - // console.log({ fileNames }); - - const sortFileMode = global.currentConfiguration.sortFiles || "name"; - const sortByName = sortFileMode.startsWith("name"); - const asc = !sortFileMode.endsWith("-desc"); - fileNames - .sort((a, b) => { - const valA = sortByName ? a.simpleName.toLowerCase() : a.info.mtimeMs; - const valB = sortByName ? b.simpleName.toLowerCase() : b.info.mtimeMs; - - if (valA < valB) { - return asc ? -1 : 1; - } - if (valA > valB) { - return asc ? 1 : -1; - } - return 0; - }) - .sort((a, b) => { - if (a.isFile && !b.isFile) { - return 1; - } - if (!a.isFile && b.isFile) { - return -1; - } - return 0; - }); - // console.log(fileNames) - - if ( - folder == oculusGamesDir - && global.currentConfiguration.cacheOculusGames - ) { - console.log('getDir cached', folder); - cacheOculusGames = fileNames; - } - - if (gameListName && gameListName.err) { - fileNames.unshift({name: gameListName.err}); - } - - return fileNames; - } - catch (error) { - console.error('Can`t open folder ' + folder, error); - //returnError(e.message) - return false; - } -} - -async function cleanUpFoldername(simpleName) { - // simpleName = simpleName.split('-packageName-')[0]; - simpleName = simpleName.split('-versionCode-')[0]; - simpleName = simpleName.split(/ v[0-9]/)[0]; - return simpleName; -} - -async function getDirListing(folder){ - const files = await fsp.readdir(folder); - let fileNames = await Promise.all(files.map(async (file) => { - return path.join(folder, file).replace(/\\/g, '/') - })); - - return fileNames; -} - - -async function backupApp({ location, pkg }) { - console.log('backupApp()', pkg, location); - let apk = await adbShell(`pm path ${pkg}`); - apk = apk.replace("package:", ""); - - let folderName = pkg; - - for (const app of global.installedApps) { - if (app['packageName'] != pkg) continue; - folderName = `${app['simpleName']} -versionCode-${app['versionCode']} -packageName-${pkg}`; - break; - } - - location = path.join(location, folderName); - console.log({ location, apk }); - - await fsp.mkdir(location, { recursive: true }); - await adbPull(apk, path.join(location, 'base.apk')); - const obbsPath = `/sdcard/Android/obb/${pkg}`; - if (!(await adbFileExists(obbsPath))) return true; - - await adbPullFolder(obbsPath, path.join(location, pkg)); - - return true; -} - -const backupPrefsPath = '/sdcard/Download/backup/data/data'; -async function backupAppData(packageName, backupPath = path.join(global.sidenoderHome, 'backup_data')) { - console.log('backupAppData()', packageName); - backupPath = path.join(backupPath, packageName); - if (await adbFileExists(`/sdcard/Android/data/${packageName}`)) { - await adbPullFolder(`/sdcard/Android/data/${packageName}`, path.join(backupPath, 'Android', packageName)); - } - else { - console.log(`skip backup Android/data/${packageName}`); - } - - await copyAppPrefs(packageName); - await adbPullFolder(`${backupPrefsPath}/${packageName}`, path.join(backupPath, 'data', packageName)); - await adbShell(`rm -r "${backupPrefsPath}/${packageName}"`); - - fsp.writeFile(`${backupPath}/time.txt`, Date.now()); - return true; -} - -async function restoreAppData(packageName, backupPath = path.join(global.sidenoderHome, 'backup_data')) { - console.log('restoreAppData()', packageName); - backupPath = path.join(backupPath, packageName); - if (!(await fsp.exists(backupPath))) throw `Backup not found ${backupPath}`; - - await adbPushFolder(path.join(backupPath, 'Android', packageName), `/sdcard/Android/data/${packageName}`); - await adbPushFolder(path.join(backupPath, 'data', packageName), `${backupPrefsPath}/${packageName}`); - await restoreAppPrefs(packageName); - return true; -} - -async function copyAppPrefs(packageName, removeAfter = false) { - const cmd = removeAfter ? 'mv -f' : 'cp -rf'; - await adbShell(`mkdir -p "${backupPrefsPath}"`); - return adbShell(`run-as ${packageName} ${cmd} "/data/data/${packageName}" "${backupPrefsPath}/"`); -} - -async function restoreAppPrefs(packageName, removeAfter = true) { - const cmd = removeAfter ? 'mv -f' : 'cp -rf'; - const backup_path = `${backupPrefsPath}/${packageName}`; - if (!(await adbFileExists(backup_path))) return; - - return adbShell(`run-as ${packageName} ${cmd} "${backup_path}" "/data/data/"`); -} - -async function sideloadFolder(arg) { - location = arg.path; - console.log('sideloadFolder()', arg); - let res = { - device: 'done', - aapt: false, - check: false, - backup: false, - uninstall: false, - restore: false, - download: false, - apk: false, - download_obb: false, - push_obb: false, - done: false, - update: false, - error: '', - location, - } - - win.webContents.send('sideload_process', res); - - if (location.endsWith('.apk')) { - apkfile = location; - location = path.dirname(location); - } - else { - returnError('not an apk file'); - return; - } - - console.log('start sideload: ' + apkfile); - - fromremote = false; - if (location.includes(global.mountFolder)) { - fromremote = true; - } - - console.log('fromremote:', fromremote); - - packageName = ''; - let apktmp = ''; - try { - if (!fromremote) { - res.download = 'skip'; - } - else { - res.download = 'processing'; - win.webContents.send('sideload_process', res); - - apktmp = path.join(global.tmpdir, path.basename(apkfile)); - console.log('is remote, copying to '+ apktmp) - - if (await fsp.exists(apktmp)) { - console.log('is remote, ' + apktmp + 'already exists, using'); - res.download = 'skip'; - } - else { - const tmpname = `${apktmp}.part`; - if (await fsp.exists(tmpname)) await fsp.unlink(tmpname); - await fsp.copyFile(apkfile, tmpname); - await fsp.rename(tmpname, apktmp); - res.download = 'done'; - } - - apkfile = apktmp; - } - } - catch (e) { - // returnError(e); - console.error(e); - res.download = 'fail'; - res.done = 'fail'; - res.error = e; - return win.webContents.send('sideload_process', res); - } - - res.aapt = 'processing'; - win.webContents.send('sideload_process', res); - - try { - packageinfo = await getPackageInfo(apkfile); - - packageName = packageinfo.packageName; - console.log({ apkfile, packageinfo, packageName }); - } - catch (e) { - // returnError(e); - console.error(e); - res.aapt = 'fail'; - res.done = 'fail'; - res.error = e; - return win.webContents.send('sideload_process', res); - } - - if (!packageName) { - const e = `Can't parse packageName of ${apkfile}`; - // returnError(new Error(e)); - console.error(e); - res.aapt = 'fail'; - res.done = 'fail'; - res.error = e; - return win.webContents.send('sideload_process', res); - } - - - res.aapt = 'done'; - res.check = 'processing'; - win.webContents.send('sideload_process', res); - - console.log('checking if installed'); - let installed = false; - try { - installed = await adb.getDevice(global.adbDevice).isInstalled(packageName); - res.check = 'done'; - } - catch (err) { - console.error('check', e); - res.check = 'fail'; - res.error = e; - // TODO: maybe return; - } - - res.backup = 'processing'; - win.webContents.send('sideload_process', res); - // const backup_path = `${global.tmpdir}/sidenoder_restore_backup/`; - const backup_path = '/sdcard/Download/backup/Android/data/'; - - // TODO: if adbExist - if (installed) { - console.log('doing adb pull appdata (ignore error)'); - try { - await adbShell(`mkdir -p "${backup_path}"`); - await adbShell(`mv "/sdcard/Android/data/${packageName}" "${backup_path}"`); - await copyAppPrefs(packageName); - // await backupAppData(packageName, backup_path); - res.backup = 'done'; - } - catch (e) { - console.error('backup', e); - res.backup = 'fail'; - res.error = e; - // TODO: maybe return; - } - } - else { - res.backup = 'skip'; - } - - res.uninstall = 'processing'; - win.webContents.send('sideload_process', res); - - if (installed) { - console.log('doing adb uninstall (ignore error)'); - try { - await adb.getDevice(global.adbDevice).uninstall(packageName); - res.uninstall = 'done'; - console.log('uninstall done', packageName); - } - catch (e) { - console.error('uninstall', e); - res.uninstall = 'fail'; - res.error = e; - } - } - else { - res.uninstall = 'skip'; - } - - console.log('doing adb install'); - res.apk = 'processing'; - win.webContents.send('sideload_process', res); - - try { - await adbInstall(apkfile); - console.log('apk done', packageName); - res.apk = 'done'; - } - catch (e) { - // returnError(e); - console.error(e); - res.apk = 'fail'; - res.done = 'fail'; - res.error = e; - return win.webContents.send('sideload_process', res); - } - - res.restore = 'processing'; - win.webContents.send('sideload_process', res); - - if (/*installed || */await adbFileExists(`${backup_path}${packageName}`)) { - console.log('doing adb push appdata (ignore error)'); - try { - // await restoreAppData(packageName, backup_path); - await adbShell(`mv "${backup_path}${packageName}" "/sdcard/Android/data/"`); - await restoreAppPrefs(packageName); - res.restore = 'done'; - console.log('restore done', packageName); - } - catch (e) { - console.error('restore', e); - res.restore = 'fail'; - res.error = e; - // TODO: maybe return; - } - } - else { - res.restore = 'skip'; - } - - res.remove_obb = 'processing'; - win.webContents.send('sideload_process', res); - - const obbFolderOrig = path.join(location, packageName); - console.log({ obbFolderOrig }); - try { - if (!(await fsp.exists(obbFolderOrig))) throw 'Can`t find obbs folder'; - obbFolderDest = `/sdcard/Android/obb/${packageName}`; - console.log('DATAFOLDER to copy:' + obbFolderDest); - } - catch (error) { - console.log(error); - obbFolderDest = false; - res.remove_obb = 'skip'; - res.download_obb = 'skip'; - res.push_obb = 'skip'; - win.webContents.send('sideload_process', res); - } - - let obbFiles = []; - if (!obbFolderDest) { - res.download_obb = 'skip'; - res.push_obb = 'skip'; - } - else { - console.log('doing obb rm'); - try { - await adbShell(`rm -r "${obbFolderDest}"`); - await adbShell(`mkdir -p ${obbFolderDest}`, global.adbDevice, true) - res.remove_obb = 'done'; - console.log('remove_obb done', packageName); - } - catch (e) { - res.remove_obb = 'skip'; - console.log(e); - } - - res.download_obb = 'processing'; - win.webContents.send('sideload_process', res); - - try { - obbFiles = await fsp.readdir(obbFolderOrig); - console.log('obbFiles: ', obbFiles.length); - - res.download_obb = (fromremote ? '0' : obbFiles.length) + '/' + obbFiles.length; - res.push_obb = '0/' + obbFiles.length; - win.webContents.send('sideload_process', res); - - const tmpFolder = path.join(global.tmpdir, packageName); - if (fromremote) { - await fsp.mkdir(tmpFolder, { recursive: true }); - } - - for (const obbName of obbFiles) { - const obb = path.join(obbFolderOrig, obbName); - console.log('obb File: ' + obbName); - console.log('doing obb push'); - const destFile = `${obbFolderDest}/${obbName}`; - - if (fromremote) { - const obbtmp = path.join(tmpFolder, obbName); - console.log('obb is remote, copying to ' + obbtmp); - - if (await fsp.exists(obbtmp)) { - console.log(`obb is remote, ${obbtmp} already exists, using`); - } - else { - const tmpname = `${obbtmp}.part`; - if (await fsp.exists(tmpname)) await fsp.unlink(tmpname); - await fsp.copyFile(obb, tmpname); - await fsp.rename(tmpname, obbtmp); - } - - res.download_obb = (+res.download_obb.split('/')[0] + 1) + '/' + obbFiles.length; - win.webContents.send('sideload_process', res); - await adbPush(obbtmp, `${destFile}`, false); - } - else { - await adbShell(`mkdir -p ${obbFolderDest}`, global.adbDevice, true) - await adbPush(obb, `${destFile}`, false); - } - - res.push_obb = (+res.push_obb.split('/')[0] + 1) + '/' + obbFiles.length; - win.webContents.send('sideload_process', res); - } - - if (fromremote) { - //TODO: check settings - await fsp.rm(tmpFolder, { recursive: true }); - } - } - catch (e) { - console.error('obbs processing', e); - if (fromremote) { - res.download_obb = 'fail'; - } - - res.push_obb = 'fail'; - res.done = 'fail'; - res.error = e; - return win.webContents.send('sideload_process', res); - } - } - - if (fromremote) { - //TODO: check settings - await fsp.unlink(apktmp); - } - - res.done = 'done'; - res.update = arg.update; - win.webContents.send('sideload_process', res); - console.log('DONE'); - return; -} - - - -async function getPackageInfo(apkPath) { - const reader = await ApkReader.open(apkPath); - const manifest = await reader.readManifest(); - - info = { - packageName: manifest.package, - versionCode: manifest.versionCode, - versionName: manifest.versionName, - }; - - return info; -} - -async function getInstalledApps(obj = false) { - let apps = await adbShell(`pm list packages -3 --show-versioncode`); - apps = apps.split('\n'); - // apps.pop(); - appinfo = {}; - - for (const appLine of apps) { - const [packageName, versionCode] = appLine.slice(8).split(' versionCode:'); - - const info = []; - info['simpleName'] = KMETAS[packageName] && KMETAS[packageName].simpleName || packageName; - info['packageName'] = packageName; - info['versionCode'] = versionCode; - info['imagePath'] = QUEST_ICONS.includes(packageName + '.jpg') - ? `https://raw.githubusercontent.com/vKolerts/quest_icons/master/250/${packageName}.jpg` - : `http://cdn.apk-cloud.com/detail/image/${packageName}-w130.png`//'unknown.png'; - - appinfo[packageName] = info; - } - - - const sortAppMode = global.currentConfiguration.sortApps || "simplename"; - const sortByName = sortAppMode.startsWith("simplename"); - const asc = !sortAppMode.endsWith("-desc"); - global.installedApps = Object.values(appinfo).sort((a, b) => { - const valA = (sortByName ? a.simpleName : a.packageName).toLowerCase(); - const valB = (sortByName ? b.simpleName : b.packageName).toLowerCase(); - if (valA < valB) { - return asc ? -1 : 1; - } - if (valA > valB) { - return asc ? 1 : -1; - } - return 0; - }); - - return obj ? appinfo : global.installedApps; -} - -async function getInstalledAppsWithUpdates() { - const remotePath = path.join(global.mountFolder, global.currentConfiguration.mntGamePath); // TODO: folder path to config - const list = await getDir(remotePath); - let remotePackages = {}; - let remoteList = {}; - - if (list) for (const app of list) { - const { name, packageName, versionCode, simpleName, filePath, size } = app; - if (!packageName) continue; - - if (!remotePackages[packageName]) remotePackages[packageName] = []; - remotePackages[packageName].push(name); - - remoteList[name] = { - versionCode, - simpleName, - filePath, - size, - }; - }; - - const remoteKeys = Object.keys(remotePackages); - - const apps = global.installedApps || await getInstalledApps(); - let updates = []; - for (const app of apps) { - const packageName = app['packageName']; - // console.log(packageName, 'checking'); - - if (!remoteKeys.includes(packageName)) continue; - - for (name of remotePackages[packageName]) { - const pkg = remoteList[name]; - const installedVersion = app['versionCode']; - const remoteversion = pkg.versionCode; - - // console.log({ packageName, installedVersion, remoteversion }); - // console.log({ pkg }); - - if (remoteversion <= installedVersion) continue; - - app['simpleName'] = pkg.simpleName; - app['update'] = []; - app['update']['path'] = pkg.filePath; - app['update']['size'] = pkg.size; - app['update']['versionCode'] = remoteversion; - updates.push(app); - - console.log(packageName, 'UPDATE AVAILABLE'); - } - } - - global.installedApps = false; - return updates; -} - - -async function detectNoteTxt(files, folder) { - // TODO: check .meta/notes - - if (typeof files == 'string') { - folder = files; - files = false; - } - - if (!files) { - files = await fsp.readdir(folder); - } - - if (files.includes('notes.txt')) { - return fsp.readFile(path.join(folder, 'notes.txt'), 'utf8'); - } - - return false; -} - -async function detectInstallTxt(files, folder) { - if (typeof files == 'string') { - folder = files; - files = false; - } - - if (!files) { - files = await fsp.readdir(folder); - } - - const installTxNames = [ - 'install.txt', - 'Install.txt', - ]; - - for (const name of installTxNames) { - if (files.includes(name)) { - return fsp.readFile(path.join(folder, name), 'utf8'); - } - } - - return false; -} - - -async function getApkFromFolder(folder){ - let res = { - path: false, - install_desc: false, - } - - const files = await fsp.readdir(folder); - res.install_desc = await detectInstallTxt(files, folder); - res.notes = await detectNoteTxt(files, folder); - console.log({ files }); - - for (file of files) { - if (file.endsWith('.apk')) { - res.path = path.join(folder, file).replace(/\\/g, "/"); - return res; - } - } - - returnError('No apk found in ' + folder); - return res; -} - -async function uninstall(packageName){ - resp = await adb.getDevice(global.adbDevice).uninstall(packageName); -} - - -let rcloneProgress = false; -async function updateRcloneProgress() { - try { - const response = await fetch('http://127.0.0.1:5572/core/stats', { method: 'POST' }) - const data = await response.json(); - if (!data.transferring || !data.transferring[0]) throw 'no data'; - const transferring = data.transferring[0]; - rcloneProgress = { - cmd: 'download', - bytes: transferring.bytes, - size: transferring.size, - percentage: transferring.percentage, - speedAvg: transferring.speedAvg, - eta: transferring.eta, - name: transferring.name, - }; - //console.log('sending rclone data'); - win.webContents.send('process_data', rcloneProgress); - } - catch (error) { - //console.error('Fetch-Error:', error); - if (rcloneProgress) { - rcloneProgress = false; - win.webContents.send('process_data', rcloneProgress); - } - } - - setTimeout(updateRcloneProgress, 2000); -} - -async function init() { - fsp.exists = (p) => fsp.access(p).then(() => true).catch(e => false); - - await initLogs(); - - console.log({ platform, arch, version, sidenoderHome }, process.platform, process.arch, process.argv); - - await loadMeta(); -} - -async function loadMeta() { - try { - const res = await fetch('https://raw.githubusercontent.com/vKolerts/quest_icons/master/version?' + Date.now()); - version = await res.text(); - if (version == META_VERSION) return setTimeout(loadMeta, CHECK_META_PERIOD); - - META_VERSION = version; - console.log('Meta version', META_VERSION); - } - catch (err) { - console.error('can`t get meta version', err); - } - - try { - const res = await fetch('https://raw.githubusercontent.com/vKolerts/quest_icons/master/list.json?' + Date.now()); - QUEST_ICONS = await res.json(); - console.log('icons list loaded'); - } - catch (err) { - console.error('can`t get quest_icons', err); - } - - try { - const res = await fetch('https://raw.githubusercontent.com/vKolerts/quest_icons/master/.e?' + Date.now()); - const text = await res.text(); - const iv = Buffer.from(text.substring(0, l), 'hex'); - const secret = crypto.createHash(hash_alg).update(pkg.author.split(' ')[0].repeat(2)).digest('base64').substr(0, l); - const decipher = crypto.createDecipheriv('aes-256-cbc', secret, iv); - const encrypted = text.substring(l); - KMETAS = JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8')); - for (const pkg of Object.keys(KMETAS)) { - KMETAS2[escString(KMETAS[pkg].simpleName)] = pkg; - } - - console.log('kmetas loaded'); - } - catch (err) { - console.error('can`t get kmetas', err); - } - - setTimeout(loadMeta, CHECK_META_PERIOD); -} - -function escString(val) { - let res = val.toLowerCase(); - res = res.replace(/[-\_:.,!?\"'&™®| ]/g, ''); - return res; -} - -async function initLogs() { - const log_path = path.join(sidenoderHome, 'debug_last.log'); - if (await fsp.exists(log_path)) { - await fsp.unlink(log_path); - } - else { - await fsp.mkdir(sidenoderHome, { recursive: true }); - } - - const log_file = fs.createWriteStream(log_path, { flags: 'w' }); - const log_stdout = process.stdout; - - function dateF() { - const d = new Date(); - return `[${d.toLocaleString()}.${d.getMilliseconds()}] `; - } - - console.log = function(...d) { - let line = ''; - let line_color = ''; - for (const l of d) { - if (typeof l == 'string') { - line += l + ' '; - line_color += l + ' '; - continue; - } - - const formated = util.format(l); - line += formated + ' '; - line_color += '\x1b[32m' + formated + '\x1b[0m '; - } - - log_stdout.write(dateF() + line_color + '\n'); - log_file.write(dateF() + line + '\n'); - }; - - console.error = function(...d) { - let line = ''; - for (const l of d) { - line += util.format(l) + ' '; - } - - log_stdout.write(`\x1b[31m${dateF()}ERROR: ` + line + '\x1b[0m\n'); - log_file.write(dateF()+ 'ERROR: ' + line + '\n'); - }; - - console.warning = function(...d) { - let line = ''; - for (const l of d) { - line += util.format(l) + ' '; - } - - log_stdout.write(`\x1b[33m${dateF()}WARN: ` + line + '\x1b[0m\n'); - log_file.write(dateF() + 'WARN: ' + line + '\n'); - }; -} - -async function fetchTimeout(url = '', options = {}, timeout = 20 * 1000) { - const controller = new AbortController(); - options.signal = controller.signal; - setTimeout(() => { - controller.abort() - }, timeout); - - return fetch(url, options); -} - - - -async function saveConfig(config = global.currentConfiguration) { - await fsp.writeFile(configLocation, JSON.stringify(config, null, ' ')); -} - -async function reloadConfig() { - const defaultConfig = { - allowOtherDevices: false, - cacheOculusGames: true, - autoMount: false, - adbPath: '', - rclonePath: '', - rcloneConf: '', - mountCmd: 'mount', - cfgSection: '', - snapshotsDelete: true, - mntGamePath: 'Quest Games', - scrcpyBitrate: '5', - scrcpyCrop: '1600:900:2017:510', - lastIp: '', - userHide: false, - dirBookmarks: [ - { name: 'Sidenoder folder', path: global.sidenoderHome }, - ], - - proxyUrl: '', - proxyOculus: false, - proxySteam: false, - proxySQ: false, - }; - - if (await fsp.exists(configLocationOld)) { - await fsp.rename(configLocationOld, configLocation); - } - - - if (await fsp.exists(configLocation)) { - console.log('Config exist, using ' + configLocation); - global.currentConfiguration = Object.assign(defaultConfig, require(configLocation)); - } - else { - console.log('Config doesnt exist, creating ') + configLocation; - await saveConfig(defaultConfig); - global.currentConfiguration = defaultConfig; - } - - if (global.currentConfiguration.tmpdir) { - global.tmpdir = global.currentConfiguration.tmpdir; - } - - if (!global.currentConfiguration.dirBookmarks) { - global.currentConfiguration.dirBookmarks = defaultConfig.dirBookmarks; - } - - proxySettings() - - await parseRcloneSections(); -} - -function proxySettings(proxyUrl = global.currentConfiguration.proxyUrl) { - const { proxyOculus, proxySteam, proxySQ } = global.currentConfiguration; - - agentOculus = proxyUrl && proxyOculus ? new SocksProxyAgent(proxyUrl) : undefined; - agentSteam = proxyUrl && proxySteam ? new SocksProxyAgent(proxyUrl) : undefined; - agentSQ = proxyUrl && proxySQ ? new SocksProxyAgent(proxyUrl) : undefined; -} - -async function changeConfig(key, value) { - console.log('cfg.update', key, value); - if (key == 'proxyUrl') proxySettings(value); - if (['proxyOculus', 'proxySteam', 'proxySQ'].includes(key)) proxySettings(); - - global.currentConfiguration[key] = value; - await saveConfig(); - - if (key == 'rcloneConf') await parseRcloneSections(true); - if (key == 'tmpdir') global.tmpdir = value || require('os').tmpdir().replace(/\\/g, '/'); - - return value; -} +const exec = require("child_process").exec; +// const fs = require('fs'); +const fs = require("fs"); +const fsp = fs.promises; +const util = require("util"); +const path = require("path"); +const crypto = require("crypto"); +const commandExists = require("command-exists"); +const { dialog } = require("electron"); +const ApkReader = require("adbkit-apkreader"); +const adbkit = require("@devicefarmer/adbkit").default; +const adb = adbkit.createClient(); +const fetch = (...args) => + import("node-fetch").then(({ default: fetch }) => fetch(...args)); +const WAE = require("web-auto-extractor").default; +// const HttpProxyAgent = require('https-proxy-agent'); // TODO add https proxy support +const { SocksProxyAgent } = require("socks-proxy-agent"); +const url = require("url"); +// const ApkReader = require('node-apk-parser'); +const fixPath = (...args) => + import("fix-path").then(({ default: fixPath }) => fixPath(...args)); +// adb.kill(); + +const pkg = require("./package.json"); +const _sec = 1000; +const _min = 60 * _sec; + +let CHECK_META_PERIOD = 2 * _min; +const l = 32; +const configLocationOld = path.join(global.homedir, "sidenoder-config.json"); +const configLocation = path.join(global.sidenoderHome, "config.json"); + +let agentOculus, agentSteam, agentSQ; + +init(); + +const GAME_LIST_NAMES = global.currentConfiguration.gameListNames || [ + "FFA.txt", + "GameList.txt", + "VRP-GameList.txt/VRP-GameList.txt", + "VRP-GameList.txt", + "Dynamic.txt", +]; +let META_VERSION = []; +let QUEST_ICONS = []; +let cacheOculusGames = false; +let KMETAS = {}; +let KMETAS2 = {}; + +let adbCmd = "adb"; +let grep_cmd = "| grep "; +if (platform == "win") { + grep_cmd = "| findstr "; +} + +let RCLONE_ID = 0; + +module.exports = { + //properties + resetCache, + //methods + getDeviceSync, + trackDevices, + checkDeps, + checkMount, + mount, + killRClone, + getDir, + returnError, + sideloadFolder, + getInstalledApps, + getInstalledAppsWithUpdates, + getApkFromFolder, + uninstall, + getDirListing, + getPackageInfo, + wifiGetStat, + wifiEnable, + connectWireless, + disconnectWireless, + enableMTP, + startSCRCPY, + rebootDevice, + rebootRecovery, + rebootBootloader, + sideloadFile, + getLaunchActivity, + getActivities, + startActivity, + devOpenUrl, + checkAppTools, + changeAppConfig, + backupApp, + backupAppData, + restoreAppData, + getDeviceInfo, + getStorageInfo, + getUserInfo, + getFwInfo, + getBatteryInfo, + changeConfig, + reloadConfig, + execShellCommand, + updateRcloneProgress, + deviceTweaksGet, + deviceTweaksSet, + appInfo, + appInfoEvents, + isIdle, + wakeUp, + detectInstallTxt, + detectNoteTxt, + // ... +}; + +async function getDeviceInfo() { + if (!global.adbDevice) { + return { + success: false, + }; + } + // console.log('getDeviceInfo()'); + + const storage = await getStorageInfo(); + const user = await getUserInfo(); + const fw = await getFwInfo(); + const battery = await getBatteryInfo(); + const ip = await getDeviceIp(); + const wifi = await wifiGetStat(); + + const res = { + success: !!storage, + storage, + user, + fw, + battery, + ip, + wifi, + }; + + // console.log('getDeviceInfo', res); + return res; +} + +async function getFwInfo() { + console.log("getFwInfo()"); + const res = await adbShell("getprop ro.build.branch"); + if (!res) return false; + + return { + version: res.replace("releases-oculus-", ""), + }; +} + +async function getBatteryInfo() { + console.log("getBatteryInfo()"); + const res = await adbShell("dumpsys battery"); + if (!res) return false; + + return parceOutOptions(res); +} + +async function getUserInfo() { + if (global.currentConfiguration.userHide) + return { + name: "hidden", + }; + + console.log("getUserInfo()"); + const res = await adbShell("dumpsys user | grep UserInfo"); + if (!res) return false; + + return { + name: res.split(":")[1], + }; +} + +async function deviceTweaksGet(arg) { + console.log("deviceTweaksGet()", arg); + let res = { + cmd: "get", + // mp_name: '', + // guardian_pause: '0', + // frc: '0', + // gRR: '72', + // gCA: '-1', + // gFFR: '2', + // CPU: '2', + // GPU: '2', + // vres: '1024', + // cres: '640x480', + // gSSO: '1440x1584', + }; + + if (arg.key === "mp_name") + res.mp_name = await adbShell("settings get global username"); + if (arg.key === "guardian_pause") + res.guardian_pause = await adbShell("getprop debug.oculus.guardian_pause"); + if (arg.key === "frc") + res.frc = await adbShell("getprop debug.oculus.fullRateCapture"); + if (arg.key === "gRR") + res.gRR = await adbShell("getprop debug.oculus.refreshRate"); + if (arg.key === "gCA") + res.gCA = await adbShell("getprop debug.oculus.forceChroma"); + if (arg.key === "gFFR") + res.gFFR = await adbShell("getprop debug.oculus.foveation.level"); + if (arg.key === "CPU") + res.CPU = await adbShell("getprop debug.oculus.cpuLevel"); + if (arg.key === "GPU") + res.GPU = await adbShell("getprop debug.oculus.gpuLevel"); + if (arg.key === "vres") + res.vres = await adbShell("getprop debug.oculus.videoResolution"); + if (arg.key === "cres") { + let captureDims = + (await adbShell("getprop debug.oculus.capture.width")) + + "x" + + (await adbShell("getprop debug.oculus.capture.height")); + + // Default when not set + if (captureDims === "x") { + captureDims = "3840x1920"; + } + res.cres = captureDims; + } + if (arg.key === "gSSO") + res.gSSO = + (await adbShell("getprop debug.oculus.textureWidth")) + + "x" + + (await adbShell("getprop debug.oculus.textureHeight")); + //oculus.capture.bitrate + + return res; +} + +async function deviceTweaksSet(arg) { + console.log("deviceTweaksSet()", arg); + let res = { cmd: "set" }; + if (typeof arg.mp_name != "undefined") { + res.mp_name = await adbShell("settings put global username " + arg.mp_name); + } + + if (typeof arg.guardian_pause != "undefined") { + res.guardian_pause = await adbShell( + "setprop debug.oculus.guardian_pause " + (arg.guardian_pause ? "1" : "0"), + ); + } + if (typeof arg.frc != "undefined") { + res.frc = await adbShell( + "setprop debug.oculus.fullRateCapture " + (arg.frc ? "1" : "0"), + ); + } + + if (typeof arg.gRR != "undefined") { + res.gRR = await adbShell("setprop debug.oculus.refreshRate " + arg.gRR); + } + + if (typeof arg.gCA != "undefined") { + res.gCA = await adbShell("setprop debug.oculus.forceChroma " + arg.gCA); + } + + if (typeof arg.gFFR != "undefined") { + res.gFFR = await adbShell( + "setprop debug.oculus.foveation.level " + arg.gFFR, + ); + } + + if (typeof arg.CPU != "undefined") { + res.CPU = await adbShell("setprop debug.oculus.cpuLevel " + arg.CPU); + } + + if (typeof arg.GPU != "undefined") { + res.GPU = await adbShell("setprop debug.oculus.gpuLevel " + arg.GPU); + } + + if (typeof arg.vres != "undefined") { + res.vres = await adbShell( + "setprop debug.oculus.videoResolution " + arg.vres, + ); + } + + if (typeof arg.cres != "undefined") { + const [width, height] = arg.cres.split("x"); + await adbShell("setprop debug.oculus.capture.width " + width); + res.cres = await adbShell("setprop debug.oculus.capture.height " + height); + } + + if (typeof arg.gSSO != "undefined") { + const [width, height] = arg.gSSO.split("x"); + await adbShell("setprop debug.oculus.textureWidth " + width); + await adbShell("setprop debug.oculus.textureHeight " + height); + res.gSSO = await adbShell( + "settings put system font_scale 0.85 && settings put system font_scale 1.0", + ); + } + + return res; +} + +async function getStorageInfo() { + console.log("getStorageInfo()"); + + const linematch = await adbShell('df -h | grep "/storage/emulated"'); + if (!linematch) return false; + + const refree = new RegExp("([0-9(.{1})]+[a-zA-Z%])", "g"); + const storage = linematch.match(refree); + console.log(storage); + + if (storage.length == 3) { + return { + size: storage[0], + used: storage[1], + free: 0, + percent: storage[2], + }; + } + + return { + size: storage[0], + used: storage[1], + free: storage[2], + percent: storage[3], + }; +} + +async function getLaunchActivity(pkg) { + console.log("startApp()", pkg); + const activity = await adbShell( + `dumpsys package ${pkg} | grep -A 1 'filter' | head -n 1 | cut -d ' ' -f 10`, + ); + return startActivity(activity); +} + +async function getActivities(pkg, activity = false) { + console.log("getActivities()", pkg); + + let activities = await adbShell( + `dumpsys package | grep -Eo '^[[:space:]]+[0-9a-f]+[[:space:]]+${pkg}/[^[:space:]]+' | grep -oE '[^[:space:]]+$'`, + ); + if (!activities) return false; + + activities = activities.split("\n"); + // activities.pop(); + console.log({ pkg, activities }); + // TODO: check manifest.application.launcherActivities + + return activities; +} + +async function startActivity(activity) { + console.log("startActivity()", activity); + wakeUp(); + const result = await adbShell(`am start ${activity}`); // TODO activity selection + + console.log("startActivity", activity, result); + return result; +} + +async function devOpenUrl(url) { + console.log("devOpenUrl", url); + wakeUp(); + const result = await adbShell( + `am start -a android.intent.action.VIEW -d "${url}"`, + ); // TODO activity selection + + console.log("devOpenUrl", url, result); + return result; +} + +async function readAppCfg(pkg) { + let config = await adbShell( + `cat /sdcard/Android/data/${pkg}/private/config.json 1>&1 2> /dev/null`, + ); + try { + config = config && JSON.parse(config); + } catch (e) { + console.error("readAppCfg", e); + config = false; + } + + return config; +} + +async function checkAppTools(pkg) { + const backupPath = path.join(global.sidenoderHome, "backup_data", pkg); + const availableBackup = await adbFileExists(`/sdcard/Android/data/${pkg}`); + let availableRestore = false; + let availableConfig = false; + if (await fsp.exists(backupPath)) { + try { + availableRestore = await fsp.readFile(`${backupPath}/time.txt`, "utf8"); + } catch (err) { + availableRestore = 1; + } + } + + if (availableBackup) { + availableConfig = await readAppCfg(pkg); + } + + return { + success: true, + pkg, + availableRestore, + availableConfig, + }; +} + +async function changeAppConfig(pkg, key, val) { + console.log("changeAppConfig()", { pkg, key, val }); + const res = { + pkg, + key, + val, + success: false, + }; + + let config = await readAppCfg(pkg); + try { + config = Object.assign(config, { [key]: val }); + adbShell( + `echo '${JSON.stringify(config)}' > "/sdcard/Android/data/${pkg}/private/config.json"`, + ); + config = await readAppCfg(pkg); + res.val = config && config[key]; + res.success = !!config; + } catch (e) { + console.error("changeAppConfig", res, e); + } + + return res; +} + +// Implementation ---------------------------------- + +async function getDeviceIp() { + // let ip = await adb.getDHCPIpAddress(global.adbDevice); + // if (ip) return ip; + if (!global.adbDevice && global.currentConfiguration.lastIp) { + return global.currentConfiguration.lastIp; + } + + let ip = await adbShell( + `ip -o route get to 8.8.8.8 | sed -n 's/.*src \\([0-9.]\\+\\).*/\\1/p'`, + ); + console.log({ ip }); + if (ip) return ip; + + ip = await adbShell( + `ip addr show wlan0 | grep 'inet ' | cut -d ' ' -f 6 | cut -d / -f 1`, + ); + console.log({ ip }); + if (ip) return ip; + return false; +} + +async function wifiGetStat() { + const on = await adbShell("settings get global wifi_on"); + return on && +on; +} + +async function wifiEnable(enable) { + return adbShell(`svc wifi ${enable ? "enable" : "disable"}`); +} + +async function connectWireless() { + const on = await adbShell("settings get global wifi_on"); + if (!(await wifiGetStat())) { + console.error("connectWireless", "wifi disabled"); + await wifiEnable(true); + return false; + } + + // await adbShell(`setprop service.adb.tcp.port 5555`); + // TODO: save ip & try use it + const ip = await getDeviceIp(); + console.log({ ip }); + if (!ip) return false; + + try { + if (global.adbDevice) { + const device = adb.getDevice(global.adbDevice); + const port = await device.tcpip(); + await device.waitForDevice(); + console.log("set tcpip", port); + await changeConfig("lastIp", ip); + } + + const deviceTCP = await adb.connect(ip, 5555); + // await deviceTCP.waitForDevice(); + console.log("connectWireless", { ip, res: deviceTCP }); + + return ip; + } catch (err) { + console.error("connectWireless", err); + await changeConfig("lastIp", ""); + return false; + } +} + +async function disconnectWireless() { + const ip = await getDeviceIp(); + if (!ip) return false; + + try { + const res = await adb.disconnect(ip, 5555); + // const res = await adb.usb(global.adbDevice); + console.log("disconnectWireless", { ip, res }); + // await changeConfig('lastIp', ''); + // await getDeviceSync(); + return res; + } catch (err) { + console.error("disconnectWireless.error", err); + return !(await isWireless()); + } +} + +async function isWireless() { + try { + const devices = await adb.listDevices(); + for (const device of devices) { + if (!device.id.includes(":5555")) continue; + if (["offline", "authorizing"].includes(device.type)) continue; + if (["unauthorized"].includes(device.type)) { + win.webContents.send( + "alert", + "Please authorize adb access on your device", + ); + continue; + } + + console.log("device.id", device.type); + return device.id; + } + + return false; + } catch (err) { + console.error("Something went wrong:", err.stack); + return false; + } +} + +async function enableMTP() { + const res = await adbShell(`svc usb setFunctions mtp`); + console.log("enableMTP", { res }); + return res; +} + +async function isIdle() { + const res = await adbShell(`dumpsys deviceidle | grep mScreenOn`); + console.log(res, res.includes("true")); + return !res.includes("true"); +} + +async function wakeUp() { + if (!(await isIdle())) return; + return adbShell(`input keyevent KEYCODE_POWER`); +} + +async function startSCRCPY() { + console.log("startSCRCPY()"); + if ( + !global.currentConfiguration.scrcpyPath && + !(await commandExists("scrcpy")) + ) { + returnError("Can`t find scrcpy binary"); + return; + } + + const scrcpyCmd = + `"${global.currentConfiguration.scrcpyPath || "scrcpy"}" ` + + (global.currentConfiguration.scrcpyCrop + ? `--crop ${global.currentConfiguration.scrcpyCrop} ` + : "") + + `-b ${global.currentConfiguration.scrcpyBitrate || 1}M ` + + (global.currentConfiguration.scrcpyFps + ? `--max-fps ${global.currentConfiguration.scrcpyFps} ` + : "") + + (global.currentConfiguration.scrcpySize + ? `--max-size ${global.currentConfiguration.scrcpySize} ` + : "") + + (!global.currentConfiguration.scrcpyWindow ? "-f " : "") + + (global.currentConfiguration.scrcpyOnTop ? "--always-on-top " : "") + + (!global.currentConfiguration.scrcpyControl ? "-n " : "") + + '--window-title "SideNoder Stream" ' + + `-s ${global.adbDevice} `; + console.log({ scrcpyCmd }); + wakeUp(); + exec(scrcpyCmd, (error, stdout, stderr) => { + if (error) { + console.error("scrcpy error:", error); + win.webContents.send("cmd_sended", { success: error }); + return; + } + + if (stderr) { + console.error("scrcpy stderr:", stderr); + // win.webContents.send('cmd_sended', { success: stderr }); + return; + } + + console.log("scrcpy stdout:", stdout); + }); + + return scrcpyCmd; +} + +async function rebootDevice() { + const res = await adbShell(`reboot`); + console.log("rebootDevice", { res }); + return res; +} +async function rebootRecovery() { + const res = await adbShell(`reboot recovery`); + console.log("rebootRecovery", { res }); + return res; +} +async function rebootBootloader() { + const res = await adbShell(`reboot bootloader`); + console.log("rebootBootloader", { res }); + return res; +} +async function sideloadFile(path) { + const res = await execShellCommand(`"${adbCmd}" sideload "${path}"`); + console.log("sideloadFile", { res }); + return res; +} + +async function getDeviceSync(attempt = 0) { + try { + // const lastDevice = global.adbDevice; + const devices = await adb.listDevices(); + console.log({ devices }); + global.adbDevice = false; + for (const device of devices) { + if (["offline", "authorizing"].includes(device.type)) continue; + if (["unauthorized"].includes(device.type)) { + win.webContents.send( + "alert", + "Please authorize adb access on your device", + ); + continue; + } + + if ( + !global.currentConfiguration.allowOtherDevices && + (await adbShell("getprop ro.product.brand", device.id)) != "oculus" + ) + continue; + + global.adbDevice = device.id; + } + + /*if (!global.adbDevice && devices.length > 0 && attempt < 1) { + return setTimeout(()=> getDeviceSync(attempt + 1), 1000); + }*/ + // if (lastDevice == global.adbDevice) return; + + win.webContents.send("check_device", { success: global.adbDevice }); + + return global.adbDevice; + } catch (err) { + console.error("Something went wrong:", err.stack); + } +} + +/** + * Executes a shell command and return it as a Promise. + * @param cmd {string} + * @return {Promise} + */ +async function adbShell(cmd, deviceId = global.adbDevice, skipRead = false) { + try { + if (!deviceId) { + throw "device not defined"; + } + + global.adbError = null; + const r = await adb.getDevice(deviceId).shell(cmd); + // console.timeLog(cmd); + if (skipRead) { + console.log(`adbShell[${deviceId}]`, { cmd, skipRead }); + return true; + } + + let output = await adbkit.util.readAll(r); + output = await output.toString(); + // output = output.split('\n'); + // const end = output.pop(); + // if (end != '') output.push(); + console.log(`adbShell[${deviceId}]`, { cmd, output }); + if (output.substr(-1) == "\n") return output.slice(0, -1); + return output; + } catch (err) { + console.error(`adbShell[${deviceId}]: err`, { cmd }, err); + global.adbError = err; + if (err.toString() == `FailError: Failure: 'device offline'`) { + getDeviceSync(); + } + + return false; + } +} + +function parceOutOptions(line) { + let opts = {}; + for (let l of line.split("\n")) { + l = l.split(" ").join(""); + let [k, v] = l.split(":"); + + if (v == "true") v = true; + if (v == "false") v = false; + if (!isNaN(+v)) v = +v; + + opts[k] = v; + } + + return opts; +} + +// on empty dirrectory return false +async function adbFileExists(path) { + const r = await adbShell(`ls "${path}" 1>&1 2> /dev/null`); + return r; +} + +async function adbPull(orig, dest, sync = false) { + console.log("adbPull", orig, dest); + const transfer = sync + ? await sync.pull(orig) + : await adb.getDevice(global.adbDevice).pull(orig); + return new Promise(function (resolve, reject) { + let c = 0; + transfer.on("progress", (stats) => { + c++; + if (c % 40 != 1) return; // skip 20 events + + // console.log(orig + ' pulled', stats); + const res = { + cmd: "pull", + bytes: stats.bytesTransferred, + size: 0, + percentage: 0, + speedAvg: 0, + eta: 0, + name: orig, + }; + win.webContents.send("process_data", res); + }); + transfer.on("end", () => { + console.log(orig, "pull complete"); + win.webContents.send("process_data", false); + resolve(true); + }); + transfer.on("error", (err) => { + console.error("adb_pull_stderr", err); + win.webContents.send("process_data", false); + reject(err); + }); + transfer.pipe(fs.createWriteStream(dest)); + }); +} + +async function adbPullFolder(orig, dest, sync = false) { + console.log("pullFolder", orig, dest); + /*let need_close = false; + if (!sync) { + need_close = true; + sync = await adb.getDevice(global.adbDevice).syncService(); + }*/ + + let actions = []; + await fsp.mkdir(dest, { recursive: true }); + const files = sync + ? await sync.readdir(orig) + : await adb.getDevice(global.adbDevice).readdir(orig); + + for (const file of files) { + const new_orig = `${orig}/${file.name}`; + const new_dest = path.join(dest, file.name); + if (file.isFile()) { + actions.push(adbPull(new_orig, new_dest, sync)); // file.size + continue; + } + + actions.push(adbPullFolder(new_orig, new_dest, sync)); + } + + await Promise.all(actions); + + // if (need_close) sync.end(); + + return true; +} + +async function adbPush(orig, dest, sync = false) { + console.log("adbPush", orig, dest); + const transfer = sync + ? await sync.pushFile(orig, dest) + : await adb.getDevice(global.adbDevice).push(orig, dest); + const stats = await fsp.lstat(orig); + const size = stats.size; + + return new Promise(function (resolve, reject) { + let c = 0; + transfer.on("progress", (stats) => { + c++; + if (c % 40 != 1) return; // skip 20 events + + // console.log(orig + ' pushed', stats); + const res = { + cmd: "push", + bytes: stats.bytesTransferred, + size, + percentage: ((stats.bytesTransferred * 100) / size).toFixed(2), + speedAvg: 0, + eta: 0, + name: orig, + }; + win.webContents.send("process_data", res); + }); + transfer.on("end", () => { + console.log(orig, "push complete"); + win.webContents.send("process_data", false); + resolve(true); + }); + transfer.on("error", (err) => { + console.error("adb_push_stderr", err); + win.webContents.send("process_data", false); + reject(err); + }); + }); +} + +async function adbPushFolder(orig, dest, sync = false) { + console.log("pushFolder", orig, dest); + + const stat = await fsp.lstat(orig); + console.log({ orig, stat }, stat.isFile()); + if (stat.isFile()) return adbPush(orig, dest); + + /*let need_close = false; + if (!sync) { + need_close = true; + sync = await adb.getDevice(global.adbDevice).syncService(); + }*/ + + let actions = []; + await adbShell(`mkdir -p ${dest}`, global.adbDevice, true); + const files = await fsp.readdir(orig, { withFileTypes: true }); + for (const file of files) { + const new_orig = path.join(orig, file.name); + const new_dest = `${dest}/${file.name}`; + if (file.isFile()) { + actions.push(adbPush(new_orig, new_dest, sync)); + continue; + } + + actions.push(adbPushFolder(new_orig, new_dest, sync)); + } + + await Promise.all(actions); + + // if (need_close) sync.end(); + + return true; +} + +async function adbInstall(apk) { + console.log("adbInstall", apk); + const temp_path = "/data/local/tmp/install.apk"; + + await adbPush(apk, temp_path, false, false); + try { + await adb.getDevice(global.adbDevice).installRemote(temp_path); + } catch (err) { + adbShell(`rm ${temp_path}`); + throw err; + } + + return true; +} + +function execShellCommand(cmd, ignoreError = false, buffer = 100) { + console.log({ cmd }); + return new Promise((resolve, reject) => { + exec(cmd, { maxBuffer: 1024 * buffer }, (error, stdout, stderr) => { + if (error) { + if (ignoreError) return resolve(false); + console.error("exec_error", cmd, error); + return reject(error); + } + + if (stdout || !stderr) { + console.log("exec_stdout", cmd, stdout); + return resolve(stdout); + } else { + if (ignoreError) return resolve(false); + console.error("exec_stderr", cmd, stderr); + return reject(stderr); + } + }); + }); +} + +async function trackDevices() { + console.log("trackDevices()"); + await getDeviceSync(); + + try { + const tracker = await adb.trackDevices(); + tracker.on("add", async (device) => { + console.log("Device was plugged in", device.id); + // await getDeviceSync(); + }); + + tracker.on("remove", async (device) => { + console.log("Device was unplugged", device.id); + // await getDeviceSync(); + }); + + tracker.on("change", async (device) => { + // TODO: // need fix double run + console.log("Device was changed", device.id); + await getDeviceSync(); + }); + + tracker.on("end", () => { + console.error("Tracking stopped"); + trackDevices(); + }); + } catch (err) { + console.error("Something went wrong:", err.stack); + returnError(err); + } +} + +async function appInfo(args) { + const { res, pkg } = args; + const app = KMETAS[pkg]; + + let data = { + res, + pkg, + id: 0, + name: app.simpleName, + short_description: "", + detailed_description: "", + about_the_game: "", + supported_languages: "", + genres: [], + header_image: "", + screenshots: [], + url: "", + }; + + try { + if (res == "steam") { + const steam = app && app.steam; + if (!steam || !steam.id) throw "incorrect args"; + + data.id = steam.id; + data.url = `https://store.steampowered.com/app/${data.id}/`; + + const resp = await fetchTimeout( + `https://store.steampowered.com/api/appdetails?appids=${data.id}`, + { + headers: { + "Accept-Language": global.locale + ",en-US;q=0.5,en;q=0.3", + }, + agent: agentSteam, + }, + ); + const json = await resp.json(); + // console.log({ json }); + + Object.assign(data, json[data.id].data); + } + + if (res == "oculus") { + const oculus = app && app.oculus; + if (!oculus || !oculus.id) throw "incorrect args"; + // console.log({ oculus }); + + data.id = oculus.id; + data.url = `https://www.oculus.com/experiences/quest/${data.id}`; + // data.genres = oculus.genres && oculus.genres.split(', '); + + //https://computerelite.github.io + let resp = await fetchTimeout( + `https://graph.oculus.com/graphql?forced_locale=${global.locale}`, + { + method: "POST", + body: `access_token=OC|1317831034909742|&variables={"itemId":"${oculus.id}","first":1}&doc_id=5373392672732392`, + headers: { + "Accept-Language": global.locale + ",en-US;q=0.5,en;q=0.3", + "Content-Type": "application/x-www-form-urlencoded", + Origin: "https://www.oculus.com", + }, + agent: agentOculus, + }, + ); + try { + let json = await resp.json(); + // console.log('json', json); + if (json.error) throw json.error; + + const meta = json.data.node; + if (!meta) throw "empty json.data.node"; + + data.name = meta.appName; + data.detailed_description = + meta.display_long_description && + meta.display_long_description.split("\n").join("
    "); + data.genres = meta.genre_names; + + if (meta.supported_in_app_languages) { + data.supported_languages = meta.supported_in_app_languages + .map(({ name }) => name) + .join(", "); + } + //meta.internet_connection_name, + //meta.quality_rating_aggregate, + //meta.release_date, + + if (meta.website_page_meta) { + data.header_image = meta.website_page_meta.image_url; + data.short_description = + meta.website_page_meta.description && + meta.website_page_meta.description.split("\n").join("
    "); + data.url = meta.website_page_meta.page_url; + } + + if (meta.screenshots) { + for (const { uri } of meta.screenshots) { + data.screenshots.push({ + // id, + path_thumbnail: uri, + }); + } + } + + if (meta.trailer && meta.trailer.uri) { + data.movies = [{ mp4: { 480: meta.trailer.uri } }]; + } + } catch (err) { + console.error(res, "fetch error", err); + + resp = await fetchTimeout(`${data.url}?locale=${global.locale}`, { + agent: agentOculus, + }); + const meta = await WAE().parse(await resp.text()); + const { metatags } = meta; + // console.log('meta', meta); + + data.name = metatags["og:title"][0].replace(" on Oculus Quest", ""); + data.header_image = metatags["og:image"][0]; + data.short_description = + metatags["og:description"][0] && + metatags["og:description"][0].split("\n").join("
    "); + data.url = metatags["al:web:url"][0]; + + const jsonld = + (meta.jsonld.Product && meta.jsonld.Product[0]) || + JSON.parse(metatags["json-ld"][0]); + // console.log(jsonld); + + if (jsonld) { + if (jsonld.name) data.name = jsonld.name; + data.detailed_description = + jsonld.description && jsonld.description.split("\n").join("
    "); + + if (jsonld.image) { + for (const id in jsonld.image) { + if (["0", "1", "2"].includes(id)) continue; // skip resizes of header + + data.screenshots.push({ + id, + path_thumbnail: jsonld.image[id], + }); + } + } + } + } + } + + if (res == "sq") { + const sq = app && app.sq; + if (!sq || !sq.id) throw "incorrect args"; + // console.log({ sq }); + + data.id = sq.id; + data.url = `https://sidequestvr.com/app/${data.id}/`; + + const resp = await fetchTimeout(`https://api.sidequestvr.com/get-app`, { + method: "POST", + body: JSON.stringify({ apps_id: data.id }), + headers: { + "Accept-Language": global.locale + ",en-US;q=0.5,en;q=0.3", + "Content-Type": "application/json", + Origin: "https://sidequestvr.com", + Cookie: + " __stripe_mid=829427af-c8dd-47d1-a857-1dc73c95b947201218; cf_clearance=LkOSetFAXEs255r2rAMVK_hm_I0lawkUfJAedj1nkD0-1633288577-0-250; __stripe_sid=6e94bd6b-19a4-4c34-98d5-1dc46423dd2e2f3688", + "User-Agent": + "Mozilla/5.0 (X11; Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0", + }, + agent: agentSQ, + }); + const json = await resp.json(); + const meta = json.data[0]; + data.name = meta.name; + data.header_image = meta.image_url; + data.short_description = meta.summary; + data.detailed_description = meta.description.split("\n").join("
    "); + if (meta.video_url) + data.youtube = [ + meta.video_url + .replace("youtube.com", "youtube.com/embed") + .replace("youtu.be", "youtube.com/embed") + .replace("/embed/embed", "/embed") + .replace("/watch?v=", "/"), + ]; + + const resp_img = await fetchTimeout( + `https://api.sidequestvr.com/get-app-screenshots`, + { + method: "POST", + body: JSON.stringify({ apps_id: data.id }), + headers: { + "Content-Type": "application/json", + Origin: "https://sidequestvr.com", + Cookie: + " __stripe_mid=829427af-c8dd-47d1-a857-1dc73c95b947201218; cf_clearance=LkOSetFAXEs255r2rAMVK_hm_I0lawkUfJAedj1nkD0-1633288577-0-250; __stripe_sid=6e94bd6b-19a4-4c34-98d5-1dc46423dd2e2f3688", + "User-Agent": + "Mozilla/5.0 (X11; Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0", + }, + agent: agentSQ, + }, + ); + const json_img = await resp_img.json(); + for (const id in json_img.data) { + data.screenshots.push({ + id, + path_thumbnail: json_img.data[id].image_url, + }); + } + } + } catch (err) { + console.error("appInfo", { args, data }, err); + } + + return { success: true, data }; +} + +async function appInfoEvents(args) { + const { res, pkg } = args; + const app = KMETAS[pkg]; + let data = { + res, + pkg, + events: [], + }; + + try { + if (res == "steam") { + const steam = app && app.steam; + if (!steam || !steam.id) throw "incorrect args"; + + data.url = `https://store.steampowered.com/news/app/${steam.id}/`; + + const resp = await fetchTimeout( + `http://api.steampowered.com/ISteamNews/GetNewsForApp/v0002?appid=${steam.id}`, + { + headers: { + "Accept-Language": global.locale + ",en-US;q=0.5,en;q=0.3", + }, + agent: agentSteam, + }, + ); + const json = await resp.json(); + // console.log({ json }); + + const events = json.appnews.newsitems; + for (const e of events) { + const event = { + title: e.title, + url: e.url, + date: new Date(e.date * _sec).toLocaleString(), + // author: e.author, + }; + + event.contents = e.contents + .split("\n") + .join("
    ") + .split("[img]") + .join('
    ') + .split("{STEAM_CLAN_IMAGE}") + .join( + "https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/clans", + ) + .split("[list]") + .join("
      ") + .split("[/list]") + .join("
    ") + .split("[*]") + .join("
  • ") + // .split('[b]').join('') + // .split('[/b]').join('') + // .split('[i]').join('') + // .split('[/i]').join('') + .split("[") + .join("<") + .split("]") + .join(">"); + data.events.push(event); + } + } + + if (res == "oculus") { + const oculus = app && app.oculus; + if (!oculus || !oculus.id) throw "incorrect args"; + + // data.url = `https://store.steampowered.com/news/app/${steam.id}/`; + + let resp = await fetchTimeout( + `https://graph.oculus.com/graphql?forced_locale=${global.locale}`, + { + method: "POST", + body: `access_token=OC|1317831034909742|&variables={"id":"${oculus.id}"}&doc_id=1586217024733717`, + headers: { + "Accept-Language": global.locale + ",en-US;q=0.5,en;q=0.3", + "Content-Type": "application/x-www-form-urlencoded", + Origin: "https://www.oculus.com", + }, + agent: agentOculus, + }, + ); + try { + let json = await resp.json(); + if (json.error) throw json.error; + + // console.log({ json }); + const events = json.data.node.supportedBinaries.edges; + for (const { node } of events) { + const e = node; + const event = { + id: e.id, + title: `${e.version} (versionCode: ${e.versionCode})`, + contents: e.changeLog && e.changeLog.split("\n").join("
    "), + // richChangeLog: e.richChangeLog, + // url: '', + // date: '', + // author: '', + }; + + if (e.richChangeLog) console.log("RICHCHANGELOG", e.richChangeLog); + + data.events.push(event); + } + } catch (err) { + console.error(res, "fetch error", err); + } + + resp = await fetch( + `https://computerelite.github.io/tools/Oculus/OlderAppVersions/${oculus.id}.json`, + ); + json = await resp.json(); + // console.log({ json }); + const events = json.data.node.binaries.edges; + for (const { node } of events) { + const e = node; + let found = false; + for (const i in data.events) { + if (data.events[i].id != e.id) continue; + + data.events[i].date = new Date( + e.created_date * _sec, + ).toLocaleString(); + found = true; + break; + } + if (found) continue; + + const event = { + id: e.id, + title: `${e.version} (versionCode: ${e.version_code})`, + date: new Date(e.created_date * _sec).toLocaleString(), + contents: e.change_log.split("\n").join("
    "), + // url: '', + // author: '', + }; + + data.events.push(event); + } + } + + if (res == "sq") { + const sq = app && app.sq; + if (!sq || !sq.id) throw "incorrect args"; + // console.log({ sq }); + + for (const is_news of [true, false]) { + const resp = await fetchTimeout( + `https://api.sidequestvr.com/events-list`, + { + method: "POST", + body: JSON.stringify({ apps_id: sq.id, is_news }), + headers: { + "Accept-Language": global.locale + ",en-US;q=0.5,en;q=0.3", + "Content-Type": "application/json", + Origin: "https://sidequestvr.com", + Cookie: + " __stripe_mid=829427af-c8dd-47d1-a857-1dc73c95b947201218; cf_clearance=LkOSetFAXEs255r2rAMVK_hm_I0lawkUfJAedj1nkD0-1633288577-0-250; __stripe_sid=6e94bd6b-19a4-4c34-98d5-1dc46423dd2e2f3688", + "User-Agent": + "Mozilla/5.0 (X11; Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0", + }, + agent: agentSQ, + }, + ); + const json = await resp.json(); + // console.log({ json }); + for (const e of json.data) { + const event = { + id: e.events_id, + title: e.event_name, + url: e.event_url, + date: new Date(e.start_time * _sec).toLocaleString(), + contents: "", + // author: '', + }; + + if (e.event_image) { + event.contents += `
    `; + } + + if (e.event_description) { + event.contents += e.event_description.split("\n").join("
    "); + } + + data.events.push(event); + } + } + } + } catch (err) { + console.error("appInfoEvents", { args, data }, err); + } + + return { success: true, data }; +} + +async function checkMount(attempt = 0) { + console.log("checkMount()", attempt); + try { + attempt++; + + if (!(await fsp.readdir(global.mountFolder)).length && attempt < 15) { + return new Promise((res, rej) => { + setTimeout(() => { + checkMount(attempt).then(res).catch(rej); + }, _sec); + }); + } + + const resp = await fetch("http://127.0.0.1:5572/rc/noop", { + method: "post", + }); + + global.mounted = resp.ok; + console.log("checkMount", global.mounted); + return global.mounted; + //setTimeout(updateRcloneProgress, 2000); + } catch (e) { + console.warn("checkMount", e); + global.mounted = false; + return false; + } +} + +async function checkDeps(arg) { + console.log("checkDeps()", arg); + let res = { + [arg]: { + version: false, + cmd: false, + error: false, + }, + }; + + try { + if (arg == "adb") { + let globalAdb = false; + try { + globalAdb = await commandExists("adb"); + } catch (e) {} + + res[arg].cmd = adbCmd = globalAdb ? "adb" : await fetchBinary("adb"); + try { + await execShellCommand(`"${res[arg].cmd}" start-server`); + } catch (err) { + if (!err.toString().includes("daemon started successfully")) throw err; + } + + res[arg].version = + "adbkit v." + + (await adb.version()) + + "\n" + + (await execShellCommand(`"${res[arg].cmd}" version`)); + + await trackDevices(); + } + + if (arg == "rclone") { + // module with autodownload https://github.com/sntran/rclone.js/blob/main/index.js + // res.rclone.cmd = global.currentConfiguration.rclonePath || await commandExists('rclone'); + res[arg].cmd = await fetchBinary("rclone"); + res[arg].version = await execShellCommand(`"${res[arg].cmd}" --version`); + } + + if (arg == "zip") { + res[arg].cmd = await fetchBinary("7za"); + res[arg].version = await execShellCommand( + `"${res[arg].cmd}" --help ${grep_cmd} "7-Zip"`, + ); + console.log(res[arg].version); + } + + if (arg == "scrcpy") { + res[arg].cmd = + global.currentConfiguration.scrcpyPath || + (await commandExists("scrcpy")); + try { + res[arg].version = await execShellCommand( + `"${res[arg].cmd}" --version`, + ); + } catch (err) { + res[arg].version = err; // don`t know why version at std_err(( + } + } + } catch (e) { + console.error("checkDeps", arg, e); + res[arg].error = e && e.toString(); + } + + res.success = true; + return res; +} + +async function fetchBinary(bin) { + const cfgKey = `${bin}Path`; + const cmd = global.currentConfiguration[cfgKey]; + if (cmd) return cmd; + + const file = global.platform == "win" ? `${bin}.exe` : bin; + + const binPath = path.join(sidenoderHome, file); + const branch = /*bin == 'rclone' ? 'new' :*/ "master"; + const binUrl = `https://raw.githubusercontent.com/vKolerts/${bin}-bin/${branch}/${global.platform}/${global.arch}/${file}`; + await fetchFile(binUrl, binPath); + + if (bin == "adb" && global.platform == "win") { + const libFile = "AdbWinApi.dll"; + const libUrl = `https://raw.githubusercontent.com/vKolerts/${bin}-bin/master/${global.platform}/${global.arch}/${libFile}`; + await fetchFile(libUrl, path.join(sidenoderHome, libFile)); + + const usbLibFile = "AdbWinUsbApi.dll"; + const usbLibUrl = `https://raw.githubusercontent.com/vKolerts/${bin}-bin/master/${global.platform}/${global.arch}/${usbLibFile}`; + await fetchFile(usbLibUrl, path.join(sidenoderHome, usbLibFile)); + } + + return changeConfig(cfgKey, binPath); +} + +async function fetchFile(url, dest) { + console.log("fetchFile", { url, dest }); + const resp = await fetch(url); + if (!resp.ok) throw new Error(`Can't download '${url}': ${resp.statusText}`); + + if (await fsp.exists(dest)) await fsp.unlink(dest); + return fsp.writeFile(dest, await resp.buffer(), { mode: 0o755 }); +} + +function returnError(message) { + console.log("returnError()"); + global.win.loadURL(`file://${__dirname}/views/error.twig`); + global.twig.view = { + message: message, + }; +} + +async function killRClone() { + RCLONE_ID++; + const killCmd = + platform == "win" + ? `taskkill.exe /F /T /IM rclone.exe` + : `killall -9 rclone`; + console.log("try kill rclone"); + return new Promise((res, rej) => { + exec(killCmd, (error, stdout, stderr) => { + if (error) { + console.log(killCmd, "error:", error); + return rej(error); + } + + if (stderr) { + console.log(killCmd, "stderr:", stderr); + return rej(stderr); + } + + console.log(killCmd, "stdout:", stdout); + return res(stdout); + }); + }); +} + +async function parseRcloneSections(newCfg = false) { + console.warn("parseRcloneSections", newCfg); + if (!global.currentConfiguration.rclonePath) { + return console.error("rclone binary not defined"); + } + + if (!global.currentConfiguration.rcloneConf) { + return console.error("rclone config not defined"); + } + + try { + const rcloneCmd = global.currentConfiguration.rclonePath; + const out = await execShellCommand( + `"${rcloneCmd}" --config="${global.currentConfiguration.rcloneConf}" listremotes`, + ); + if (!out) { + return console.error( + "rclone config is empty", + global.currentConfiguration.rcloneConf, + out, + ); + } + + const sections = out + .split("\n") + .map((section) => section.replace(/:$/, "")); + if (sections.length) sections.pop(); + if (!sections.length) { + return console.error( + "rclone config sections not found", + global.currentConfiguration.rcloneConf, + { out, sections }, + ); + } + + global.rcloneSections = sections; + } catch (err) { + const cfg = await fsp.readFile( + global.currentConfiguration.rcloneConf, + "utf8", + ); + + if (!cfg) + return console.error( + "rclone config is empty", + global.currentConfiguration.rcloneConf, + ); + + const lines = cfg.split("\n"); + let sections = []; + for (const line of lines) { + if (line[0] != "[") continue; + const section = line.match(/\[(.*?)\]/)[1]; + sections.push(section); + } + + global.rcloneSections = sections; + } + + if (newCfg || !global.currentConfiguration.cfgSection) { + await changeConfig("cfgSection", global.rcloneSections[0]); + } + + // console.log({ sections: global.rcloneSections }); + return global.rcloneSections; +} + +async function umount() { + if (platform == "win") { + if (!(await fsp.exists(global.mountFolder))) return; + + await fsp.rmdir(global.mountFolder, { recursive: true }); + return; + } + + await execShellCommand(`umount "${global.mountFolder}"`, true); + await execShellCommand(`fusermount -uz "${global.mountFolder}"`, true); + await fsp.mkdir(global.mountFolder, { recursive: true }); +} + +async function mount() { + if ( + !global.currentConfiguration.rclonePath || + !global.currentConfiguration.rcloneConf + ) { + win.webContents.send("alert", "Rclone not configured"); + } + + // if (await checkMount(13)) { + // return; + try { + await killRClone(); + } catch (err) { + console.log("rclone not started"); + } + // } + + await umount(); + + if (global.mounted) { + return (global.mounted = false); + } + + const myId = RCLONE_ID; + const mountCmd = global.currentConfiguration.mountCmd; + const rcloneCmd = global.currentConfiguration.rclonePath; + console.log("start rclone"); + exec( + `"${rcloneCmd}" ${mountCmd} --read-only --rc --rc-no-auth --config="${global.currentConfiguration.rcloneConf}" ${global.currentConfiguration.cfgSection}: "${global.mountFolder}"`, + (error, stdout, stderr) => { + if (error) { + console.error("rclone error:", error); + if (RCLONE_ID != myId) error = false; + console.log({ RCLONE_ID, myId }); + win.webContents.send("check_mount", { success: false, error }); + // checkMount(); + /*if (error.message.search('transport endpoint is not connected')) { + console.log('GEVONDE'); + }*/ + + return; + } + + if (stderr) { + console.log("rclone stderr:", stderr); + return; + } + + console.log("rclone stdout:", stdout); + }, + ); +} + +function resetCache(folder) { + console.log("resetCache", folder); + const oculusGamesDir = path + .join(global.mountFolder, global.currentConfiguration.mntGamePath) + .replace(/\\/g, "/"); + + if (folder == oculusGamesDir) { + cacheOculusGames = false; + return true; + } + + return false; +} + +async function getDir(folder) { + const oculusGamesDir = path + .join(global.mountFolder, global.currentConfiguration.mntGamePath) + .replace(/\\/g, "/"); + //console.log(folder, oculusGamesDir); + if ( + folder == oculusGamesDir && + global.currentConfiguration.cacheOculusGames && + cacheOculusGames + ) { + console.log("getDir return from cache", folder); + return cacheOculusGames; + } + + try { + const files = await fsp.readdir(folder /*, { withFileTypes: true }*/); + let gameList = {}; + let gameListName2Package = {}; + let installedApps = {}; + let gameListName = false; + try { + // throw 'test'; + for (const name of GAME_LIST_NAMES) { + if (!fs.existsSync(path.join(folder, name))) continue; + // if (!files.includes(name)) continue; + gameListName = name; + break; + } + + if (gameListName) { + const list = ( + await fsp.readFile(path.join(folder, gameListName), "utf8") + ).split("\n"); + let listVer; + if (!list.length) throw gameListName + " is empty"; + + for (const line of list) { + const meta = line.split(";"); + if (!listVer) { + listVer = meta[2] == "Release APK Path" ? 1 : 2; + console.log({ gameListName, listVer }); + continue; + } + + if (listVer == 1) { + gameList[meta[1]] = { + simpleName: meta[0], + releaseName: meta[1], + packageName: meta[3], + versionCode: meta[4], + versionName: meta[5], + imagePath: `file://${global.tmpdir}/mnt/${global.currentConfiguration.mntGamePath}/.meta/thumbnails/${meta[3]}.jpg`, + }; + } else if (listVer == 2) { + gameList[meta[1]] = { + simpleName: meta[0], + releaseName: meta[1], + packageName: meta[2], + versionCode: meta[3], + imagePath: `file://${global.tmpdir}/mnt/${global.currentConfiguration.mntGamePath}/.meta/thumbnails/${meta[2]}.jpg`, + size: meta[5], + }; + } + } + } + } catch (err) { + console.error(`${gameListName} failed`, err); + gameListName = { + err: `Can't parse GameList.txt +
    Maybe issue of server - please attempt to switch mirror at settings. +
    Actual mirrors posted there http://t.me/sidenoder`, + }; + } + + // console.log(gameList); + + try { + if (global.adbDevice) { + installedApps = await getInstalledApps(true); + } + } catch (err) { + console.error("Can`t get installed apps", err); + } + + let fileNames = await Promise.all( + files.map(async (fileName) => { + // console.log(fileName); + + const info = await fsp.lstat(path.join(folder, fileName)); + let steamId = false, + sqId = false, + oculusId = false, + imagePath = false, + versionCode = "", + versionName = "", + simpleName = fileName, + packageName = false, + note = "", + kmeta = false, + mp = false, + installed = 0, + size = false, + newItem = false; + + let isGameFolder = false; + + if (info.isDirectory()) { + const dirCont = await fsp.readdir(path.join(folder, fileName)); + + isGameFolder = + dirCont.filter((file) => { + return /.*\.apk/.test(file); + }).length > 0; + } + + let gameMeta = false; + + if (isGameFolder) { + gameMeta = gameList[fileName]; + + if (!gameMeta) { + // If gameMeta is still not defined then there is no game with a + // matching version number. We now query gameList using the game name + // without the version number. + let regex = /^([\w -.,!?&+™®'"]+) v\d+\+/; + if (regex.test(fileName)) { + // Only do this if this is a folder containing an apk file. + if (info.isDirectory()) { + const dirCont = await fsp.readdir(path.join(folder, fileName)); + const isGameFolder = + dirCont.filter((file) => { + return /.*\.apk/.test(file); + }).length > 0; + + if (isGameFolder) { + const match = fileName.match(regex)[1]; + gameMeta = gameList[match]; + } + } + } + } + } + + if (gameMeta) { + simpleName = gameMeta.simpleName; + packageName = gameMeta.packageName; + versionCode = gameMeta.versionCode; + versionName = gameMeta.versionName; + simpleName = gameMeta.simpleName; + size = gameMeta.size; + imagePath = gameMeta.imagePath; + + let regex = /\((.*?)\)/; + if (regex.test(gameMeta.releaseName)) { + const match = gameMeta.releaseName.match(regex)[0]; + note += match.replace(", only autoinstalls with Rookie", ""); + } + } + + // Include local notes. This allows users to add notes in brackets to + // their filenames to override the original note. These notes are rendered + // in the game's browse card. + let regex = /\((.*?)\)/; + if (regex.test(fileName)) { + const match = fileName.match(/\((.*?)\)/)[0]; + if (match !== note) { + // This note differs from the original so it has been overriden in the + // filename, so we replace it. + note = match.replace(", only autoinstalls with Rookie", ""); + } + } + + regex = /^([\w -.,!?&+™®'"]+) v\d+\+/; + if (gameListName && !packageName && regex.test(fileName)) { + simpleName = fileName.match(regex)[1]; + packageName = KMETAS2[escString(simpleName)]; + } + + regex = /v(\d+)\+/; + if (!versionCode && regex.test(fileName)) { + versionCode = fileName.match(regex)[1]; + } + + regex = /v\d+\+([\w.]*) /; + if (!versionName && regex.test(fileName)) { + versionName = fileName.match(regex)[1]; + } + + if (!versionCode && new RegExp(".* -versionCode-").test(fileName)) { + versionCode = fileName.match(/-versionCode-([0-9]*)/)[1]; + } + + if (!packageName && new RegExp(".* -packageName-").test(fileName)) { + packageName = fileName.match(/-packageName-([a-zA-Z0-9.]*)/)[1]; + } + + // obbs path the same =( + if (gameListName && !packageName && KMETAS[fileName]) { + packageName = fileName; + } + + if (packageName) { + if (!imagePath) { + if (QUEST_ICONS.includes(packageName + ".jpg")) { + imagePath = `https://raw.githubusercontent.com/vKolerts/quest_icons/master/250/${packageName}.jpg`; + } else if (!imagePath) { + imagePath = "unknown.png"; + } + } + + kmeta = KMETAS[packageName]; + installedApp = installedApps[packageName]; + if (installedApp) { + installed = 1; + if (versionCode && versionCode > installedApp.versionCode) { + installed++; + } + } + } + + if (gameListName && !packageName && versionCode) { + packageName = "can`t parse package name"; + imagePath = "unknown.png"; + } + + if (kmeta) { + steamId = !!(kmeta.steam && kmeta.steam.id); + oculusId = !!(kmeta.oculus && kmeta.oculus.id); + sqId = !!(kmeta.sq && kmeta.sq.id); + simpleName = simpleName || kmeta.simpleName; + mp = kmeta.mp || !!kmeta.mp; + } else { + newItem = true; + } + + simpleName = await cleanUpFoldername(simpleName); + const isFile = + info.isFile() || (info.isSymbolicLink() && fileName.includes(".")); // not well + + return { + name: fileName, + simpleName, + isFile, + isLink: info.isSymbolicLink(), + steamId, + sqId, + oculusId, + imagePath, + versionCode, + versionName, + packageName, + size, + note, + newItem, + info, + mp, + installed, + createdAt: new Date(info.mtimeMs), + filePath: path.join(folder, fileName).replace(/\\/g, "/"), + }; + }), + ); + // console.log({ fileNames }); + + const sortFileMode = global.currentConfiguration.sortFiles || "name"; + const sortByName = sortFileMode.startsWith("name"); + const asc = !sortFileMode.endsWith("-desc"); + fileNames + .sort((a, b) => { + const valA = sortByName ? a.simpleName.toLowerCase() : a.info.mtimeMs; + const valB = sortByName ? b.simpleName.toLowerCase() : b.info.mtimeMs; + + if (valA < valB) { + return asc ? -1 : 1; + } + if (valA > valB) { + return asc ? 1 : -1; + } + return 0; + }) + .sort((a, b) => { + if (a.isFile && !b.isFile) { + return 1; + } + if (!a.isFile && b.isFile) { + return -1; + } + return 0; + }); + // console.log(fileNames) + + if ( + folder == oculusGamesDir && + global.currentConfiguration.cacheOculusGames + ) { + console.log("getDir cached", folder); + cacheOculusGames = fileNames; + } + + if (gameListName && gameListName.err) { + fileNames.unshift({ name: gameListName.err }); + } + + return fileNames; + } catch (error) { + console.error("Can`t open folder " + folder, error); + //returnError(e.message) + return false; + } +} + +async function cleanUpFoldername(simpleName) { + // simpleName = simpleName.split('-packageName-')[0]; + simpleName = simpleName.split("-versionCode-")[0]; + simpleName = simpleName.split(/ v[0-9]/)[0]; + return simpleName; +} + +async function getDirListing(folder) { + const files = await fsp.readdir(folder); + let fileNames = await Promise.all( + files.map(async (file) => { + return path.join(folder, file).replace(/\\/g, "/"); + }), + ); + + return fileNames; +} + +async function backupApp({ location, pkg }) { + console.log("backupApp()", pkg, location); + let apk = await adbShell(`pm path ${pkg}`); + apk = apk.replace("package:", ""); + + let folderName = pkg; + + for (const app of global.installedApps) { + if (app["packageName"] != pkg) continue; + folderName = `${app["simpleName"]} -versionCode-${app["versionCode"]} -packageName-${pkg}`; + break; + } + + location = path.join(location, folderName); + console.log({ location, apk }); + + await fsp.mkdir(location, { recursive: true }); + await adbPull(apk, path.join(location, "base.apk")); + const obbsPath = `/sdcard/Android/obb/${pkg}`; + if (!(await adbFileExists(obbsPath))) return true; + + await adbPullFolder(obbsPath, path.join(location, pkg)); + + return true; +} + +const backupPrefsPath = "/sdcard/Download/backup/data/data"; +async function backupAppData( + packageName, + backupPath = path.join(global.sidenoderHome, "backup_data"), +) { + console.log("backupAppData()", packageName); + backupPath = path.join(backupPath, packageName); + if (await adbFileExists(`/sdcard/Android/data/${packageName}`)) { + await adbPullFolder( + `/sdcard/Android/data/${packageName}`, + path.join(backupPath, "Android", packageName), + ); + } else { + console.log(`skip backup Android/data/${packageName}`); + } + + await copyAppPrefs(packageName); + await adbPullFolder( + `${backupPrefsPath}/${packageName}`, + path.join(backupPath, "data", packageName), + ); + await adbShell(`rm -r "${backupPrefsPath}/${packageName}"`); + + fsp.writeFile(`${backupPath}/time.txt`, Date.now()); + return true; +} + +async function restoreAppData( + packageName, + backupPath = path.join(global.sidenoderHome, "backup_data"), +) { + console.log("restoreAppData()", packageName); + backupPath = path.join(backupPath, packageName); + if (!(await fsp.exists(backupPath))) throw `Backup not found ${backupPath}`; + + await adbPushFolder( + path.join(backupPath, "Android", packageName), + `/sdcard/Android/data/${packageName}`, + ); + await adbPushFolder( + path.join(backupPath, "data", packageName), + `${backupPrefsPath}/${packageName}`, + ); + await restoreAppPrefs(packageName); + return true; +} + +async function copyAppPrefs(packageName, removeAfter = false) { + const cmd = removeAfter ? "mv -f" : "cp -rf"; + await adbShell(`mkdir -p "${backupPrefsPath}"`); + return adbShell( + `run-as ${packageName} ${cmd} "/data/data/${packageName}" "${backupPrefsPath}/"`, + ); +} + +async function restoreAppPrefs(packageName, removeAfter = true) { + const cmd = removeAfter ? "mv -f" : "cp -rf"; + const backup_path = `${backupPrefsPath}/${packageName}`; + if (!(await adbFileExists(backup_path))) return; + + return adbShell( + `run-as ${packageName} ${cmd} "${backup_path}" "/data/data/"`, + ); +} + +async function sideloadFolder(arg) { + location = arg.path; + console.log("sideloadFolder()", arg); + let res = { + device: "done", + aapt: false, + check: false, + backup: false, + uninstall: false, + restore: false, + download: false, + apk: false, + download_obb: false, + push_obb: false, + done: false, + update: false, + error: "", + location, + }; + + win.webContents.send("sideload_process", res); + + if (location.endsWith(".apk")) { + apkfile = location; + location = path.dirname(location); + } else { + returnError("not an apk file"); + return; + } + + console.log("start sideload: " + apkfile); + + fromremote = false; + if (location.includes(global.mountFolder)) { + fromremote = true; + } + + console.log("fromremote:", fromremote); + + packageName = ""; + let apktmp = ""; + try { + if (!fromremote) { + res.download = "skip"; + } else { + res.download = "processing"; + win.webContents.send("sideload_process", res); + + apktmp = path.join(global.tmpdir, path.basename(apkfile)); + console.log("is remote, copying to " + apktmp); + + if (await fsp.exists(apktmp)) { + console.log("is remote, " + apktmp + "already exists, using"); + res.download = "skip"; + } else { + const tmpname = `${apktmp}.part`; + if (await fsp.exists(tmpname)) await fsp.unlink(tmpname); + await fsp.copyFile(apkfile, tmpname); + await fsp.rename(tmpname, apktmp); + res.download = "done"; + } + + apkfile = apktmp; + } + } catch (e) { + // returnError(e); + console.error(e); + res.download = "fail"; + res.done = "fail"; + res.error = e; + return win.webContents.send("sideload_process", res); + } + + res.aapt = "processing"; + win.webContents.send("sideload_process", res); + + try { + packageinfo = await getPackageInfo(apkfile); + + packageName = packageinfo.packageName; + console.log({ apkfile, packageinfo, packageName }); + } catch (e) { + // returnError(e); + console.error(e); + res.aapt = "fail"; + res.done = "fail"; + res.error = e; + return win.webContents.send("sideload_process", res); + } + + if (!packageName) { + const e = `Can't parse packageName of ${apkfile}`; + // returnError(new Error(e)); + console.error(e); + res.aapt = "fail"; + res.done = "fail"; + res.error = e; + return win.webContents.send("sideload_process", res); + } + + res.aapt = "done"; + res.check = "processing"; + win.webContents.send("sideload_process", res); + + console.log("checking if installed"); + let installed = false; + try { + installed = await adb.getDevice(global.adbDevice).isInstalled(packageName); + res.check = "done"; + } catch (err) { + console.error("check", e); + res.check = "fail"; + res.error = e; + // TODO: maybe return; + } + + res.backup = "processing"; + win.webContents.send("sideload_process", res); + // const backup_path = `${global.tmpdir}/sidenoder_restore_backup/`; + const backup_path = "/sdcard/Download/backup/Android/data/"; + + // TODO: if adbExist + if (installed) { + console.log("doing adb pull appdata (ignore error)"); + try { + await adbShell(`mkdir -p "${backup_path}"`); + await adbShell( + `mv "/sdcard/Android/data/${packageName}" "${backup_path}"`, + ); + await copyAppPrefs(packageName); + // await backupAppData(packageName, backup_path); + res.backup = "done"; + } catch (e) { + console.error("backup", e); + res.backup = "fail"; + res.error = e; + // TODO: maybe return; + } + } else { + res.backup = "skip"; + } + + res.uninstall = "processing"; + win.webContents.send("sideload_process", res); + + if (installed) { + console.log("doing adb uninstall (ignore error)"); + try { + await adb.getDevice(global.adbDevice).uninstall(packageName); + res.uninstall = "done"; + console.log("uninstall done", packageName); + } catch (e) { + console.error("uninstall", e); + res.uninstall = "fail"; + res.error = e; + } + } else { + res.uninstall = "skip"; + } + + console.log("doing adb install"); + res.apk = "processing"; + win.webContents.send("sideload_process", res); + + try { + await adbInstall(apkfile); + console.log("apk done", packageName); + res.apk = "done"; + } catch (e) { + // returnError(e); + console.error(e); + res.apk = "fail"; + res.done = "fail"; + res.error = e; + return win.webContents.send("sideload_process", res); + } + + res.restore = "processing"; + win.webContents.send("sideload_process", res); + + if (/*installed || */ await adbFileExists(`${backup_path}${packageName}`)) { + console.log("doing adb push appdata (ignore error)"); + try { + // await restoreAppData(packageName, backup_path); + await adbShell( + `mv "${backup_path}${packageName}" "/sdcard/Android/data/"`, + ); + await restoreAppPrefs(packageName); + res.restore = "done"; + console.log("restore done", packageName); + } catch (e) { + console.error("restore", e); + res.restore = "fail"; + res.error = e; + // TODO: maybe return; + } + } else { + res.restore = "skip"; + } + + res.remove_obb = "processing"; + win.webContents.send("sideload_process", res); + + const obbFolderOrig = path.join(location, packageName); + console.log({ obbFolderOrig }); + try { + if (!(await fsp.exists(obbFolderOrig))) throw "Can`t find obbs folder"; + obbFolderDest = `/sdcard/Android/obb/${packageName}`; + console.log("DATAFOLDER to copy:" + obbFolderDest); + } catch (error) { + console.log(error); + obbFolderDest = false; + res.remove_obb = "skip"; + res.download_obb = "skip"; + res.push_obb = "skip"; + win.webContents.send("sideload_process", res); + } + + let obbFiles = []; + if (!obbFolderDest) { + res.download_obb = "skip"; + res.push_obb = "skip"; + } else { + console.log("doing obb rm"); + try { + await adbShell(`rm -r "${obbFolderDest}"`); + await adbShell(`mkdir -p ${obbFolderDest}`, global.adbDevice, true); + res.remove_obb = "done"; + console.log("remove_obb done", packageName); + } catch (e) { + res.remove_obb = "skip"; + console.log(e); + } + + res.download_obb = "processing"; + win.webContents.send("sideload_process", res); + + try { + obbFiles = await fsp.readdir(obbFolderOrig); + console.log("obbFiles: ", obbFiles.length); + + res.download_obb = + (fromremote ? "0" : obbFiles.length) + "/" + obbFiles.length; + res.push_obb = "0/" + obbFiles.length; + win.webContents.send("sideload_process", res); + + const tmpFolder = path.join(global.tmpdir, packageName); + if (fromremote) { + await fsp.mkdir(tmpFolder, { recursive: true }); + } + + for (const obbName of obbFiles) { + const obb = path.join(obbFolderOrig, obbName); + console.log("obb File: " + obbName); + console.log("doing obb push"); + const destFile = `${obbFolderDest}/${obbName}`; + + if (fromremote) { + const obbtmp = path.join(tmpFolder, obbName); + console.log("obb is remote, copying to " + obbtmp); + + if (await fsp.exists(obbtmp)) { + console.log(`obb is remote, ${obbtmp} already exists, using`); + } else { + const tmpname = `${obbtmp}.part`; + if (await fsp.exists(tmpname)) await fsp.unlink(tmpname); + await fsp.copyFile(obb, tmpname); + await fsp.rename(tmpname, obbtmp); + } + + res.download_obb = + +res.download_obb.split("/")[0] + 1 + "/" + obbFiles.length; + win.webContents.send("sideload_process", res); + await adbPush(obbtmp, `${destFile}`, false); + } else { + await adbShell(`mkdir -p ${obbFolderDest}`, global.adbDevice, true); + await adbPush(obb, `${destFile}`, false); + } + + res.push_obb = +res.push_obb.split("/")[0] + 1 + "/" + obbFiles.length; + win.webContents.send("sideload_process", res); + } + + if (fromremote) { + //TODO: check settings + await fsp.rm(tmpFolder, { recursive: true }); + } + } catch (e) { + console.error("obbs processing", e); + if (fromremote) { + res.download_obb = "fail"; + } + + res.push_obb = "fail"; + res.done = "fail"; + res.error = e; + return win.webContents.send("sideload_process", res); + } + } + + if (fromremote) { + //TODO: check settings + await fsp.unlink(apktmp); + } + + res.done = "done"; + res.update = arg.update; + win.webContents.send("sideload_process", res); + console.log("DONE"); + return; +} + +async function getPackageInfo(apkPath) { + const reader = await ApkReader.open(apkPath); + const manifest = await reader.readManifest(); + + info = { + packageName: manifest.package, + versionCode: manifest.versionCode, + versionName: manifest.versionName, + }; + + return info; +} + +async function getInstalledApps(obj = false) { + let apps = await adbShell(`pm list packages -3 --show-versioncode`); + apps = apps.split("\n"); + // apps.pop(); + appinfo = {}; + + for (const appLine of apps) { + const [packageName, versionCode] = appLine.slice(8).split(" versionCode:"); + + const info = []; + info["simpleName"] = + (KMETAS[packageName] && KMETAS[packageName].simpleName) || packageName; + info["packageName"] = packageName; + info["versionCode"] = versionCode; + info["imagePath"] = QUEST_ICONS.includes(packageName + ".jpg") + ? `https://raw.githubusercontent.com/vKolerts/quest_icons/master/250/${packageName}.jpg` + : `http://cdn.apk-cloud.com/detail/image/${packageName}-w130.png`; //'unknown.png'; + + appinfo[packageName] = info; + } + + const sortAppMode = global.currentConfiguration.sortApps || "simplename"; + const sortByName = sortAppMode.startsWith("simplename"); + const asc = !sortAppMode.endsWith("-desc"); + global.installedApps = Object.values(appinfo).sort((a, b) => { + const valA = (sortByName ? a.simpleName : a.packageName).toLowerCase(); + const valB = (sortByName ? b.simpleName : b.packageName).toLowerCase(); + if (valA < valB) { + return asc ? -1 : 1; + } + if (valA > valB) { + return asc ? 1 : -1; + } + return 0; + }); + + return obj ? appinfo : global.installedApps; +} + +async function getInstalledAppsWithUpdates() { + const remotePath = path.join( + global.mountFolder, + global.currentConfiguration.mntGamePath, + ); // TODO: folder path to config + const list = await getDir(remotePath); + let remotePackages = {}; + let remoteList = {}; + + if (list) + for (const app of list) { + const { name, packageName, versionCode, simpleName, filePath, size } = + app; + if (!packageName) continue; + + if (!remotePackages[packageName]) remotePackages[packageName] = []; + remotePackages[packageName].push(name); + + remoteList[name] = { + versionCode, + simpleName, + filePath, + size, + }; + } + + const remoteKeys = Object.keys(remotePackages); + + const apps = global.installedApps || (await getInstalledApps()); + let updates = []; + for (const app of apps) { + const packageName = app["packageName"]; + // console.log(packageName, 'checking'); + + if (!remoteKeys.includes(packageName)) continue; + + for (name of remotePackages[packageName]) { + const pkg = remoteList[name]; + const installedVersion = app["versionCode"]; + const remoteversion = pkg.versionCode; + + // console.log({ packageName, installedVersion, remoteversion }); + // console.log({ pkg }); + + if (remoteversion <= installedVersion) continue; + + app["simpleName"] = pkg.simpleName; + app["update"] = []; + app["update"]["path"] = pkg.filePath; + app["update"]["size"] = pkg.size; + app["update"]["versionCode"] = remoteversion; + updates.push(app); + + console.log(packageName, "UPDATE AVAILABLE"); + } + } + + global.installedApps = false; + return updates; +} + +async function detectNoteTxt(files, folder) { + // TODO: check .meta/notes + + if (typeof files == "string") { + folder = files; + files = false; + } + + if (!files) { + files = await fsp.readdir(folder); + } + + if (files.includes("notes.txt")) { + return fsp.readFile(path.join(folder, "notes.txt"), "utf8"); + } + + return false; +} + +async function detectInstallTxt(files, folder) { + if (typeof files == "string") { + folder = files; + files = false; + } + + if (!files) { + files = await fsp.readdir(folder); + } + + const installTxNames = ["install.txt", "Install.txt"]; + + for (const name of installTxNames) { + if (files.includes(name)) { + return fsp.readFile(path.join(folder, name), "utf8"); + } + } + + return false; +} + +async function getApkFromFolder(folder) { + let res = { + path: false, + install_desc: false, + }; + + const files = await fsp.readdir(folder); + res.install_desc = await detectInstallTxt(files, folder); + res.notes = await detectNoteTxt(files, folder); + console.log({ files }); + + for (file of files) { + if (file.endsWith(".apk")) { + res.path = path.join(folder, file).replace(/\\/g, "/"); + return res; + } + } + + returnError("No apk found in " + folder); + return res; +} + +async function uninstall(packageName) { + resp = await adb.getDevice(global.adbDevice).uninstall(packageName); +} + +let rcloneProgress = false; +async function updateRcloneProgress() { + try { + const response = await fetch("http://127.0.0.1:5572/core/stats", { + method: "POST", + }); + const data = await response.json(); + if (!data.transferring || !data.transferring[0]) throw "no data"; + const transferring = data.transferring[0]; + rcloneProgress = { + cmd: "download", + bytes: transferring.bytes, + size: transferring.size, + percentage: transferring.percentage, + speedAvg: transferring.speedAvg, + eta: transferring.eta, + name: transferring.name, + }; + //console.log('sending rclone data'); + win.webContents.send("process_data", rcloneProgress); + } catch (error) { + //console.error('Fetch-Error:', error); + if (rcloneProgress) { + rcloneProgress = false; + win.webContents.send("process_data", rcloneProgress); + } + } + + setTimeout(updateRcloneProgress, 2000); +} + +async function init() { + fsp.exists = (p) => + fsp + .access(p) + .then(() => true) + .catch((e) => false); + + await initLogs(); + + console.log( + { platform, arch, version, sidenoderHome }, + process.platform, + process.arch, + process.argv, + ); + + await loadMeta(); +} + +async function loadMeta() { + try { + const res = await fetch( + "https://raw.githubusercontent.com/vKolerts/quest_icons/master/version?" + + Date.now(), + ); + version = await res.text(); + if (version == META_VERSION) return setTimeout(loadMeta, CHECK_META_PERIOD); + + META_VERSION = version; + console.log("Meta version", META_VERSION); + } catch (err) { + console.error("can`t get meta version", err); + } + + try { + const res = await fetch( + "https://raw.githubusercontent.com/vKolerts/quest_icons/master/list.json?" + + Date.now(), + ); + QUEST_ICONS = await res.json(); + console.log("icons list loaded"); + } catch (err) { + console.error("can`t get quest_icons", err); + } + + try { + const res = await fetch( + "https://raw.githubusercontent.com/vKolerts/quest_icons/master/.e?" + + Date.now(), + ); + const text = await res.text(); + const iv = Buffer.from(text.substring(0, l), "hex"); + const secret = crypto + .createHash(hash_alg) + .update(pkg.author.split(" ")[0].repeat(2)) + .digest("base64") + .substr(0, l); + const decipher = crypto.createDecipheriv("aes-256-cbc", secret, iv); + const encrypted = text.substring(l); + KMETAS = JSON.parse( + decipher.update(encrypted, "base64", "utf8") + decipher.final("utf8"), + ); + for (const pkg of Object.keys(KMETAS)) { + KMETAS2[escString(KMETAS[pkg].simpleName)] = pkg; + } + + console.log("kmetas loaded"); + } catch (err) { + console.error("can`t get kmetas", err); + } + + setTimeout(loadMeta, CHECK_META_PERIOD); +} + +function escString(val) { + let res = val.toLowerCase(); + res = res.replace(/[-\_:.,!?\"'&™®| ]/g, ""); + return res; +} + +async function initLogs() { + const log_path = path.join(sidenoderHome, "debug_last.log"); + if (await fsp.exists(log_path)) { + await fsp.unlink(log_path); + } else { + await fsp.mkdir(sidenoderHome, { recursive: true }); + } + + const log_file = fs.createWriteStream(log_path, { flags: "w" }); + const log_stdout = process.stdout; + + function dateF() { + const d = new Date(); + return `[${d.toLocaleString()}.${d.getMilliseconds()}] `; + } + + console.log = function (...d) { + let line = ""; + let line_color = ""; + for (const l of d) { + if (typeof l == "string") { + line += l + " "; + line_color += l + " "; + continue; + } + + const formated = util.format(l); + line += formated + " "; + line_color += "\x1b[32m" + formated + "\x1b[0m "; + } + + log_stdout.write(dateF() + line_color + "\n"); + log_file.write(dateF() + line + "\n"); + }; + + console.error = function (...d) { + let line = ""; + for (const l of d) { + line += util.format(l) + " "; + } + + log_stdout.write(`\x1b[31m${dateF()}ERROR: ` + line + "\x1b[0m\n"); + log_file.write(dateF() + "ERROR: " + line + "\n"); + }; + + console.warning = function (...d) { + let line = ""; + for (const l of d) { + line += util.format(l) + " "; + } + + log_stdout.write(`\x1b[33m${dateF()}WARN: ` + line + "\x1b[0m\n"); + log_file.write(dateF() + "WARN: " + line + "\n"); + }; +} + +async function fetchTimeout(url = "", options = {}, timeout = 20 * 1000) { + const controller = new AbortController(); + options.signal = controller.signal; + setTimeout(() => { + controller.abort(); + }, timeout); + + return fetch(url, options); +} + +async function saveConfig(config = global.currentConfiguration) { + await fsp.writeFile(configLocation, JSON.stringify(config, null, " ")); +} + +async function reloadConfig() { + const defaultConfig = { + allowOtherDevices: false, + cacheOculusGames: true, + autoMount: false, + adbPath: "", + rclonePath: "", + rcloneConf: "", + mountCmd: "mount", + cfgSection: "", + snapshotsDelete: true, + mntGamePath: "Quest Games", + scrcpyBitrate: "5", + scrcpyCrop: "1600:900:2017:510", + lastIp: "", + userHide: false, + dirBookmarks: [{ name: "Sidenoder folder", path: global.sidenoderHome }], + + proxyUrl: "", + proxyOculus: false, + proxySteam: false, + proxySQ: false, + }; + + if (await fsp.exists(configLocationOld)) { + await fsp.rename(configLocationOld, configLocation); + } + + if (await fsp.exists(configLocation)) { + console.log("Config exist, using " + configLocation); + global.currentConfiguration = Object.assign( + defaultConfig, + require(configLocation), + ); + } else { + console.log("Config doesnt exist, creating ") + configLocation; + await saveConfig(defaultConfig); + global.currentConfiguration = defaultConfig; + } + + if (global.currentConfiguration.tmpdir) { + global.tmpdir = global.currentConfiguration.tmpdir; + } + + if (!global.currentConfiguration.dirBookmarks) { + global.currentConfiguration.dirBookmarks = defaultConfig.dirBookmarks; + } + + proxySettings(); + + await parseRcloneSections(); +} + +function proxySettings(proxyUrl = global.currentConfiguration.proxyUrl) { + const { proxyOculus, proxySteam, proxySQ } = global.currentConfiguration; + + agentOculus = + proxyUrl && proxyOculus ? new SocksProxyAgent(proxyUrl) : undefined; + agentSteam = + proxyUrl && proxySteam ? new SocksProxyAgent(proxyUrl) : undefined; + agentSQ = proxyUrl && proxySQ ? new SocksProxyAgent(proxyUrl) : undefined; +} + +async function changeConfig(key, value) { + console.log("cfg.update", key, value); + if (key == "proxyUrl") proxySettings(value); + if (["proxyOculus", "proxySteam", "proxySQ"].includes(key)) proxySettings(); + + global.currentConfiguration[key] = value; + await saveConfig(); + + if (key == "rcloneConf") await parseRcloneSections(true); + if (key == "tmpdir") + global.tmpdir = value || require("os").tmpdir().replace(/\\/g, "/"); + + return value; +} + diff --git a/versioncheck.js b/versioncheck.js index 1d625e7..147faab 100644 --- a/versioncheck.js +++ b/versioncheck.js @@ -1,24 +1,26 @@ -const pkg = require('./package.json'); -const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); -const compareVersions = require('compare-versions'); +const pkg = require("./package.json"); +const fetch = (...args) => + import("node-fetch").then(({ default: fetch }) => fetch(...args)); +const compareVersions = require("compare-versions"); global.version = pkg.version; async function checkVersion() { try { - const res = await fetch('https://api.github.com/repos/VRPirates/sidenoder/releases/latest'); - const content = JSON.parse(await res.text()) + const res = await fetch( + "https://api.github.com/repos/VRPirates/sidenoder/releases/latest", + ); + const content = JSON.parse(await res.text()); const remoteversion = content.name; - console.log('Current version: ' + pkg.version); - console.log('Github version: ' + remoteversion); + console.log("Current version: " + pkg.version); + console.log("Github version: " + remoteversion); if (!remoteversion) return; - if (compareVersions.compare(remoteversion, pkg.version, '<=')) { - console.log('Using latest version'); - } - else { - console.log('requires update'); - win.webContents.send('notify_update', { + if (compareVersions.compare(remoteversion, pkg.version, "<=")) { + console.log("Using latest version"); + } else { + console.log("requires update"); + win.webContents.send("notify_update", { success: true, current: pkg.version, remote: remoteversion, @@ -26,8 +28,7 @@ async function checkVersion() { description: content.body, }); } + } catch (err) { + console.error("checkVersion.Fail", err); } - catch (err) { - console.error('checkVersion.Fail', err); - } -} \ No newline at end of file +} diff --git a/views/browse_include.twig b/views/browse_include.twig index 1450056..64ded21 100644 --- a/views/browse_include.twig +++ b/views/browse_include.twig @@ -4,13 +4,15 @@ + title="Up dir ../ (hotkey:backspace)" + > + title="Open by filebrowser" + > @@ -22,47 +24,54 @@ + title="(hotkey: ctrl+r)" + > Refresh + title="(hotkey: ctrl+f)" + > Search `; - } - else { + } else { id('storageDiv').innerHTML = 'Can`t get storage status'; } - let bat_charge = 'none'; let bat_note = ''; let bat_level = 'XX'; if (battery) { bat_level = battery.level; - if (battery.temperature) bat_note = `Temperature: ${battery.temperature / 10}°C`; + if (battery.temperature) + bat_note = `Temperature: ${battery.temperature / 10}°C`; if (battery.ACpowered) bat_charge = 'AC'; if (battery.USBpowered) bat_charge = 'USB'; if (battery.Wirelesspowered) bat_charge = 'AIR'; if (battery.Maxchargingcurrent && battery.Maxchargingvoltage) { const current = battery.Maxchargingcurrent / 1000000; const voltage = battery.Maxchargingvoltage / 1000000; - bat_note+= `\nMax Current: ${current}A\nMax Voltage: ${voltage}V\nMax Power: ${current * voltage}W`; + bat_note += `\nMax Current: ${current}A\nMax Voltage: ${voltage}V\nMax Power: ${current * voltage}W`; } } - id('deviceUserName').innerHTML = user && user.name || 'Unknown'; - id('deviceFwVersion').innerText = fw && fw.version || 'v.XX'; + id('deviceUserName').innerHTML = (user && user.name) || 'Unknown'; + id('deviceFwVersion').innerText = (fw && fw.version) || 'v.XX'; id('deviceBatteryLevel').innerText = bat_level; id('deviceBatteryLevel').parentNode.title = bat_note; id('deviceBatteryCharge').innerText = bat_charge; id('deviceWifi').innerText = wifi ? 'On' : 'Off'; - id('deviceIp').innerText = (wifi && ip) || remote.getGlobal('currentConfiguration').lastIp || 'X.X.X.X'; + id('deviceIp').innerText = + (wifi && ip) || + remote.getGlobal('currentConfiguration').lastIp || + 'X.X.X.X'; }); @@ -70,32 +65,32 @@
    - User: -
    Unknown + User:
    + Unknown
    - FW: -
    v.XX + FW:
    + v.XX
    - XX% -
    - Charge: - - + XX%
    + Charge:
    Off -
    IP: X.X.X.X - +
    + IP: X.X.X.X
    diff --git a/views/error.twig b/views/error.twig index 18f1ba0..e6ea603 100644 --- a/views/error.twig +++ b/views/error.twig @@ -1,32 +1,22 @@ {% extends 'layout.twig' %} -{% block onload %} -{% endblock %} +{% block onload %}{% endblock %} -{% block navbar %} -{% endblock %} +{% block navbar %}{% endblock %} {% block body %} - - -
    -
    Error
    -
    -
    - - {#

    message

    #} -

    {{error.status}}

    -
    {{error.stack}}
    - -
    {{message}}
    - - Try again - -
    -
    +
    +
    Error
    +
    +
    + {#

    message

    #} +

    {{ error.status }}

    +
    {{error.stack}}
    + +
    {{message}}
    + + Try again +
    - - - - +
    {% endblock %} diff --git a/views/index.twig b/views/index.twig index d7b260b..59255ae 100644 --- a/views/index.twig +++ b/views/index.twig @@ -1,136 +1,141 @@ {% extends 'layout.twig' %} {% block body %} - -
    - -
    -
    -

    - System check -

    -
    -
    -

    - Platform: {{ platform }} {{ arch }} -

    -

    - Temp Dir: {{ tmpdir }} -

    -

    - Mount Dir: {{ mountFolder }} -

    -

    - Sidenoder Dir: {{ sidenoderHome }} -

    -

    -
    Android Debug Bridge - checking..
    -

    -

    -
    RCLONE - checking..
    -

    -

    -
    7zip Archiver - checking..
    -

    -

    -
    SCRCPY - checking..
    -

    +
    +
    +
    +

    System check

    +
    +
    +

    + Platform: {{ platform }} + {{ arch }} +

    +

    Temp Dir: {{ tmpdir }}

    +

    Mount Dir: {{ mountFolder }}

    +

    + + Sidenoder Dir: {{ sidenoderHome }} +

    +

    +
    + Android Debug Bridge - checking.. +
    +

    +

    +
    + RCLONE - checking.. +
    +

    +

    +
    + 7zip Archiver - checking.. +
    +

    +

    +
    + SCRCPY - checking.. +
    +

    +
    -
    - + console.log('ONLOAD'); + {% endblock %} diff --git a/views/js/browse.js b/views/js/browse.js index 1a32574..50017e1 100644 --- a/views/js/browse.js +++ b/views/js/browse.js @@ -62,10 +62,10 @@ document.addEventListener("keydown", (e) => { } if ( - e.code == "Backspace" && - !$(".form-control").is(":focus") && - !$(".find-input").is(":focus") && - !$("#bookmarkName").is(":focus") + e.code == "Backspace" && + !$(".form-control").is(":focus") && + !$(".find-input").is(":focus") && + !$("#bookmarkName").is(":focus") ) { return upDir(); } @@ -91,7 +91,7 @@ function resizeLoc() { const width = window.innerWidth / 10 - 60; if (dir_path.title.length > width) { dir_path.innerText = - dir_path.title.substr(0, 8) + "..." + dir_path.title.slice(-(width - 10)); + dir_path.title.substr(0, 8) + "..." + dir_path.title.slice(-(width - 10)); } else { dir_path.innerText = dir_path.title; } @@ -121,14 +121,14 @@ function scrollByHistory() { function fixIcons() { $(".browse-folder").hover( - (e) => { - $(e.target).find("i").removeClass("fa-folder-o"); - $(e.target).find("i").addClass("fa-folder-open-o"); - }, - (e) => { - $(e.target).find("i").addClass("fa-folder-o"); - $(e.target).find("i").removeClass("fa-folder-open-o"); - }, + (e) => { + $(e.target).find("i").removeClass("fa-folder-o"); + $(e.target).find("i").addClass("fa-folder-open-o"); + }, + (e) => { + $(e.target).find("i").addClass("fa-folder-o"); + $(e.target).find("i").removeClass("fa-folder-open-o"); + }, ); } @@ -198,20 +198,20 @@ function loadDir(list) { // console.log(item); if (!item.createdAt) { cards_first.unshift( - `
    ${item.name}
    `, + `
    ${item.name}
    `, ); continue; } const modified = item.info.mtime.getTime(); const fullPath = item.filePath - .replace("\\", "/") - .replace("", ":") - .split("'") - .join("\\'"); + .replace("\\", "/") + .replace("", ":") + .split("'") + .join("\\'"); const symblink = item.isLink - ? ` ` - : ""; + ? ` ` + : ""; const name = symblink + item.name; if (item.isFile) { @@ -241,11 +241,11 @@ function loadDir(list) { } let newribbon = item.newItem - ? `
    NEW!
    ` - : ""; + ? `
    NEW!
    ` + : ""; if (item.mp) { let color = - item.mp.mp == "yes" ? "green" : item.mp.mp == "no" ? "red" : "yellow"; + item.mp.mp == "yes" ? "green" : item.mp.mp == "no" ? "red" : "yellow"; newribbon = `
    MP: ${item.mp.mp}
    `; } @@ -275,14 +275,14 @@ function loadDir(list) { } const youtubeUrl = - "https://www.youtube.com/results?search_query=oculus+quest+" + - escape(item.simpleName); + "https://www.youtube.com/results?search_query=oculus+quest+" + + escape(item.simpleName); selectBtn += ` `; const size = item.size - ? `${item.size} Mb` - : ` + ? `${item.size} Mb` + : ` get size `; @@ -301,8 +301,8 @@ function loadDir(list) {