feat: adding cross cloud save

This commit is contained in:
Chubby Granny Chaser
2025-05-11 19:07:30 +01:00
parent 6c55d667bd
commit 592ac45740
20 changed files with 303 additions and 298 deletions

3
.gitignore vendored
View File

@@ -7,7 +7,8 @@ out
*.log* *.log*
.env .env
.vite .vite
ludusavi/ ludusavi/**
!ludusavi/config.yaml
hydra-python-rpc/ hydra-python-rpc/
.python-version .python-version

10
index.ts Normal file
View File

@@ -0,0 +1,10 @@
import vdf from "vdf-parser";
import fs from "node:fs";
const vdfData = fs.readFileSync(
"/home/chubby/.local/share/Steam/userdata/1126196664/config/localconfig.vdf",
"utf-8"
);
const data = vdf.parse(vdfData);
console.log(data);

6
ludusavi/config.yaml Normal file
View File

@@ -0,0 +1,6 @@
manifest:
enable: false
secondary:
- url: https://cdn.losbroxas.org/manifest.yaml
enable: true
customGames: []

View File

@@ -62,7 +62,6 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"parse-torrent": "^11.0.17", "parse-torrent": "^11.0.17",
"piscina": "^4.7.0",
"rc-virtual-list": "^3.16.1", "rc-virtual-list": "^3.16.1",
"react-hook-form": "^7.53.0", "react-hook-form": "^7.53.0",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.0",

View File

@@ -2,8 +2,6 @@ import { app } from "electron";
import path from "node:path"; import path from "node:path";
import { SystemPath } from "./services/system-path"; import { SystemPath } from "./services/system-path";
export const LUDUSAVI_MANIFEST_URL = "https://cdn.losbroxas.org/manifest.yaml";
export const defaultDownloadsPath = SystemPath.getPath("downloads"); export const defaultDownloadsPath = SystemPath.getPath("downloads");
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging"); export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");

View File

@@ -1,74 +1,73 @@
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services"; import { CloudSync, HydraApi, logger, WindowManager } from "@main/services";
import fs from "node:fs"; import fs from "node:fs";
import * as tar from "tar"; import * as tar from "tar";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import axios from "axios"; import axios from "axios";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { backupsPath } from "@main/constants"; import { backupsPath } from "@main/constants";
import type { GameShop } from "@types"; import type { GameShop, LudusaviBackupMapping } from "@types";
import YAML from "yaml"; import YAML from "yaml";
import { normalizePath } from "@main/helpers"; import { normalizePath } from "@main/helpers";
import { SystemPath } from "@main/services/system-path"; import { SystemPath } from "@main/services/system-path";
import { gamesSublevel, levelKeys } from "@main/level";
export interface LudusaviBackup { export const transformLudusaviBackupPathIntoWindowsPath = (
files: { backupPath: string,
[key: string]: { winePrefixPath?: string | null
hash: string; ) => {
size: number; return backupPath.replace(winePrefixPath ?? "", "").replace("drive_c", "C:");
}; };
};
}
const replaceLudusaviBackupWithCurrentUser = ( const restoreLudusaviBackup = (
backupPath: string, backupPath: string,
title: string, title: string,
homeDir: string homeDir: string,
winePrefixPath?: string | null
) => { ) => {
const gameBackupPath = path.join(backupPath, title); const gameBackupPath = path.join(backupPath, title);
const mappingYamlPath = path.join(gameBackupPath, "mapping.yaml"); const mappingYamlPath = path.join(gameBackupPath, "mapping.yaml");
const data = fs.readFileSync(mappingYamlPath, "utf8"); const data = fs.readFileSync(mappingYamlPath, "utf8");
const manifest = YAML.parse(data) as { const manifest = YAML.parse(data) as {
backups: LudusaviBackup[]; backups: LudusaviBackupMapping[];
drives: Record<string, string>; drives: Record<string, string>;
}; };
const currentHomeDir = normalizePath(SystemPath.getPath("home")); const { userProfilePath, publicProfilePath } =
CloudSync.getProfilePaths(winePrefixPath);
/* Renaming logic */ manifest.backups.forEach((backup) => {
if (os.platform() === "win32") { Object.keys(backup.files).forEach((key) => {
const mappedHomeDir = path.join( const sourcePathWithDrives = Object.entries(manifest.drives).reduce(
gameBackupPath, (prev, [driveKey, driveValue]) => {
path.join("drive-C", homeDir.replace("C:", "")) return prev.replace(driveValue, driveKey);
},
key
); );
if (fs.existsSync(mappedHomeDir)) { const sourcePath = path.join(gameBackupPath, sourcePathWithDrives);
fs.renameSync(
mappedHomeDir, logger.info(`Source path: ${sourcePath}`);
path.join(gameBackupPath, "drive-C", currentHomeDir.replace("C:", ""))
); const destinationPath = transformLudusaviBackupPathIntoWindowsPath(
} key,
null
)
.replace(homeDir, userProfilePath)
.replace("C:/Users/Public", publicProfilePath);
logger.info(`Moving ${sourcePath} to ${destinationPath}`);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
if (fs.existsSync(destinationPath)) {
fs.unlinkSync(destinationPath);
} }
const backups = manifest.backups.map((backup: LudusaviBackup) => { fs.renameSync(sourcePath, destinationPath);
const files = Object.entries(backup.files).reduce((prev, [key, value]) => { });
const updatedKey = key.replace(homeDir, currentHomeDir);
return {
...prev,
[updatedKey]: value,
};
}, {});
return {
...backup,
files,
};
}); });
fs.writeFileSync(mappingYamlPath, YAML.stringify({ ...manifest, backups }));
}; };
const downloadGameArtifact = async ( const downloadGameArtifact = async (
@@ -78,6 +77,8 @@ const downloadGameArtifact = async (
gameArtifactId: string gameArtifactId: string
) => { ) => {
try { try {
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
const { downloadUrl, objectKey, homeDir } = await HydraApi.post<{ const { downloadUrl, objectKey, homeDir } = await HydraApi.post<{
downloadUrl: string; downloadUrl: string;
objectKey: string; objectKey: string;
@@ -109,34 +110,33 @@ const downloadGameArtifact = async (
response.data.pipe(writer); response.data.pipe(writer);
writer.on("error", (err) => { writer.on("error", (err) => {
logger.error("Failed to write zip", err); logger.error("Failed to write tar file", err);
throw err; throw err;
}); });
fs.mkdirSync(backupPath, { recursive: true }); fs.mkdirSync(backupPath, { recursive: true });
writer.on("close", () => { writer.on("close", async () => {
tar await tar.x({
.x({
file: zipLocation, file: zipLocation,
cwd: backupPath, cwd: backupPath,
}) });
.then(async () => {
replaceLudusaviBackupWithCurrentUser( restoreLudusaviBackup(
backupPath, backupPath,
objectId, objectId,
normalizePath(homeDir) normalizePath(homeDir),
game?.winePrefixPath
); );
Ludusavi.restoreBackup(backupPath).then(() => {
WindowManager.mainWindow?.webContents.send( WindowManager.mainWindow?.webContents.send(
`on-backup-download-complete-${objectId}-${shop}`, `on-backup-download-complete-${objectId}-${shop}`,
true true
); );
}); });
});
});
} catch (err) { } catch (err) {
logger.error("Failed to download game artifact", err);
WindowManager.mainWindow?.webContents.send( WindowManager.mainWindow?.webContents.send(
`on-backup-download-complete-${objectId}-${shop}`, `on-backup-download-complete-${objectId}-${shop}`,
false false

View File

@@ -1,5 +1,6 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { levelKeys, gamesSublevel } from "@main/level"; import { levelKeys, gamesSublevel } from "@main/level";
import { Wine } from "@main/services";
import type { GameShop } from "@types"; import type { GameShop } from "@types";
const selectGameWinePrefix = async ( const selectGameWinePrefix = async (
@@ -8,6 +9,10 @@ const selectGameWinePrefix = async (
objectId: string, objectId: string,
winePrefixPath: string | null winePrefixPath: string | null
) => { ) => {
if (winePrefixPath && !Wine.validatePrefix(winePrefixPath)) {
throw new Error("Invalid wine prefix path");
}
const gameKey = levelKeys.game(shop, objectId); const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey); const game = await gamesSublevel.get(gameKey);

View File

@@ -32,3 +32,5 @@ export const isPortableVersion = () => {
export const normalizePath = (str: string) => export const normalizePath = (str: string) =>
path.posix.normalize(str).replace(/\\/g, "/"); path.posix.normalize(str).replace(/\\/g, "/");
export * from "./reg-parser";

View File

@@ -0,0 +1,58 @@
type RegValue = string | number | null;
interface RegEntry {
path: string;
timestamp?: string;
values: Record<string, RegValue>;
}
export function parseRegFile(content: string): RegEntry[] {
const lines = content.split(/\r?\n/);
const entries: RegEntry[] = [];
let currentPath: string | null = null;
let currentEntry: RegEntry | null = null;
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line || line.startsWith(";") || line.startsWith(";;")) continue;
if (line.startsWith("#")) {
const match = line.match(/^#time=(\w+)/);
if (match && currentEntry) {
currentEntry.timestamp = match[1];
}
continue;
}
if (line.startsWith("[")) {
const match = line.match(/^\[(.+?)\](?:\s+\d+)?/);
if (match) {
if (currentEntry) entries.push(currentEntry);
currentPath = match[1];
currentEntry = { path: currentPath, values: {} };
}
} else if (currentEntry) {
const kvMatch = line.match(/^"?(.*?)"?=(.*)$/);
if (kvMatch) {
const [, key, rawValue] = kvMatch;
let value: RegValue;
if (rawValue === '""') {
value = "";
} else if (rawValue.startsWith("dword:")) {
value = parseInt(rawValue.slice(6), 16);
} else if (rawValue.startsWith('"') && rawValue.endsWith('"')) {
value = rawValue.slice(1, -1);
} else {
value = rawValue;
}
currentEntry.values[key || "@"] = value;
}
}
}
if (currentEntry) entries.push(currentEntry);
return entries;
}

View File

@@ -23,7 +23,9 @@ autoUpdater.logger = logger;
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit(); if (!gotTheLock) app.quit();
app.commandLine.appendSwitch("--no-sandbox"); if (process.platform !== "linux") {
app.commandLine.appendSwitch("--no-sandbox");
}
i18n.init({ i18n.init({
resources, resources,

View File

@@ -11,10 +11,10 @@ import {
RealDebridClient, RealDebridClient,
Aria2, Aria2,
DownloadManager, DownloadManager,
Ludusavi,
HydraApi, HydraApi,
uploadGamesBatch, uploadGamesBatch,
startMainLoop, startMainLoop,
Ludusavi,
} from "@main/services"; } from "@main/services";
export const loadState = async () => { export const loadState = async () => {
@@ -39,7 +39,7 @@ export const loadState = async () => {
TorBoxClient.authorize(userPreferences.torBoxApiToken); TorBoxClient.authorize(userPreferences.torBoxApiToken);
} }
Ludusavi.addManifestToLudusaviConfig(); Ludusavi.copyConfigFileToUserData();
await HydraApi.setupApi().then(() => { await HydraApi.setupApi().then(() => {
uploadGamesBatch(); uploadGamesBatch();

View File

@@ -7,7 +7,7 @@ import os from "node:os";
import type { GameShop, User } from "@types"; import type { GameShop, User } from "@types";
import { backupsPath } from "@main/constants"; import { backupsPath } from "@main/constants";
import { HydraApi } from "./hydra-api"; import { HydraApi } from "./hydra-api";
import { normalizePath } from "@main/helpers"; import { normalizePath, parseRegFile } from "@main/helpers";
import { logger } from "./logger"; import { logger } from "./logger";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import axios from "axios"; import axios from "axios";
@@ -17,6 +17,53 @@ import i18next, { t } from "i18next";
import { SystemPath } from "./system-path"; import { SystemPath } from "./system-path";
export class CloudSync { export class CloudSync {
public static getProfilePaths(winePrefixPath?: string | null) {
const currentHomeDir = normalizePath(SystemPath.getPath("home"));
if (process.platform === "linux") {
if (!winePrefixPath) {
throw new Error("Wine prefix path is required");
}
const userReg = fs.readFileSync(
path.join(winePrefixPath, "user.reg"),
"utf8"
);
const entries = parseRegFile(userReg);
const volatileEnvironment = entries.find(
(entry) => entry.path === "Volatile Environment"
);
if (!volatileEnvironment) {
throw new Error("Volatile environment not found in user.reg");
}
const { values } = volatileEnvironment;
const userProfile = String(values["USERPROFILE"]);
if (userProfile) {
return {
userProfilePath: path.join(
winePrefixPath,
normalizePath(userProfile.replace("C:", "drive_c"))
),
publicProfilePath: path.join(
winePrefixPath,
"drive_c",
"users",
"Public"
),
};
}
}
return {
userProfilePath: currentHomeDir,
publicProfilePath: path.join("C:", "Users", "Public"),
};
}
public static getBackupLabel(automatic: boolean) { public static getBackupLabel(automatic: boolean) {
const language = i18next.language; const language = i18next.language;
@@ -102,7 +149,9 @@ export class CloudSync {
shop, shop,
objectId, objectId,
hostname: os.hostname(), hostname: os.hostname(),
homeDir: normalizePath(SystemPath.getPath("home")), winePrefixPath: game?.winePrefixPath ?? null,
homeDir: this.getProfilePaths(game?.winePrefixPath ?? null)
.userProfilePath,
downloadOptionTitle, downloadOptionTitle,
platform: os.platform(), platform: os.platform(),
label, label,

View File

@@ -15,3 +15,4 @@ export * from "./aria2";
export * from "./ws"; export * from "./ws";
export * from "./system-path"; export * from "./system-path";
export * from "./library-sync"; export * from "./library-sync";
export * from "./wine";

View File

@@ -1,70 +1,83 @@
import type { GameShop, LudusaviBackup, LudusaviConfig } from "@types"; import type { GameShop, LudusaviBackup, LudusaviConfig } from "@types";
import Piscina from "piscina";
import { app } from "electron"; import { app } from "electron";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import YAML from "yaml"; import YAML from "yaml";
import cp from "node:child_process";
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
import { LUDUSAVI_MANIFEST_URL } from "@main/constants";
import { SystemPath } from "./system-path"; import { SystemPath } from "./system-path";
export class Ludusavi { export class Ludusavi {
private static ludusaviPath = path.join( private static ludusaviPath = app.isPackaged
SystemPath.getPath("appData"), ? path.join(process.resourcesPath, "ludusavi")
"ludusavi" : path.join(__dirname, "..", "..", "ludusavi");
);
private static ludusaviConfigPath = path.join( private static binaryPath = path.join(this.ludusaviPath, "ludusavi");
this.ludusaviPath, private static configPath = path.join(
SystemPath.getPath("userData"),
"config.yaml" "config.yaml"
); );
private static binaryPath = app.isPackaged
? path.join(process.resourcesPath, "ludusavi", "ludusavi")
: path.join(__dirname, "..", "..", "ludusavi", "ludusavi");
private static worker = new Piscina({
filename: ludusaviWorkerPath,
workerData: {
binaryPath: this.binaryPath,
},
maxThreads: 1,
});
static async getConfig() {
if (!fs.existsSync(this.ludusaviConfigPath)) {
await this.worker.run(undefined, { name: "generateConfig" });
}
public static async getConfig() {
const config = YAML.parse( const config = YAML.parse(
fs.readFileSync(this.ludusaviConfigPath, "utf-8") fs.readFileSync(this.configPath, "utf-8")
) as LudusaviConfig; ) as LudusaviConfig;
return config; return config;
} }
static async backupGame( public static async copyConfigFileToUserData() {
_shop: GameShop, fs.cpSync(path.join(this.ludusaviPath, "config.yaml"), this.configPath);
objectId: string,
backupPath: string,
winePrefix?: string | null
): Promise<LudusaviBackup> {
return this.worker.run(
{ title: objectId, backupPath, winePrefix },
{ name: "backupGame" }
);
} }
static async getBackupPreview( public static async backupGame(
_shop: GameShop,
objectId: string,
backupPath?: string | null,
winePrefix?: string | null,
preview?: boolean
): Promise<LudusaviBackup> {
return new Promise((resolve, reject) => {
const args = [
"--config",
this.ludusaviPath,
"backup",
objectId,
"--api",
"--force",
];
if (preview) args.push("--preview");
if (backupPath) args.push("--path", backupPath);
if (winePrefix) args.push("--wine-prefix", winePrefix);
cp.execFile(
this.binaryPath,
args,
(err: cp.ExecFileException | null, stdout: string) => {
if (err) {
return reject(err);
}
return resolve(JSON.parse(stdout) as LudusaviBackup);
}
);
});
}
public static async getBackupPreview(
_shop: GameShop, _shop: GameShop,
objectId: string, objectId: string,
winePrefix?: string | null winePrefix?: string | null
): Promise<LudusaviBackup | null> { ): Promise<LudusaviBackup | null> {
const config = await this.getConfig(); const config = await this.getConfig();
const backupData = await this.worker.run( const backupData = await this.backupGame(
{ title: objectId, winePrefix, preview: true }, _shop,
{ name: "backupGame" } objectId,
null,
winePrefix,
true
); );
const customGame = config.customGames.find( const customGame = config.customGames.find(
@@ -77,19 +90,6 @@ export class Ludusavi {
}; };
} }
static async restoreBackup(backupPath: string) {
return this.worker.run(backupPath, { name: "restoreBackup" });
}
static async addManifestToLudusaviConfig() {
const config = await this.getConfig();
config.manifest.enable = false;
config.manifest.secondary = [{ url: LUDUSAVI_MANIFEST_URL, enable: true }];
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
}
static async addCustomGame(title: string, savePath: string | null) { static async addCustomGame(title: string, savePath: string | null) {
const config = await this.getConfig(); const config = await this.getConfig();
const filteredGames = config.customGames.filter( const filteredGames = config.customGames.filter(
@@ -105,6 +105,10 @@ export class Ludusavi {
} }
config.customGames = filteredGames; config.customGames = filteredGames;
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
fs.writeFileSync(
path.join(this.ludusaviPath, "config.yaml"),
YAML.stringify(config)
);
} }
} }

23
src/main/services/wine.ts Normal file
View File

@@ -0,0 +1,23 @@
import fs from "node:fs";
import path from "node:path";
export class Wine {
public static validatePrefix(winePrefixPath: string) {
const requiredFiles = [
"system.reg",
"user.reg",
"userdef.reg",
"dosdevices",
"drive_c",
];
for (const file of requiredFiles) {
const filePath = path.join(winePrefixPath, file);
if (!fs.existsSync(filePath)) {
return false;
}
}
return true;
}
}

View File

@@ -1,56 +0,0 @@
import type { LudusaviBackup } from "@types";
import cp from "node:child_process";
import { workerData } from "node:worker_threads";
const { binaryPath } = workerData;
export const backupGame = ({
title,
backupPath,
preview = false,
winePrefix,
}: {
title: string;
backupPath: string;
preview?: boolean;
winePrefix?: string;
}) => {
return new Promise((resolve, reject) => {
const args = ["backup", title, "--api", "--force"];
if (preview) args.push("--preview");
if (backupPath) args.push("--path", backupPath);
if (winePrefix) args.push("--wine-prefix", winePrefix);
cp.execFile(
binaryPath,
args,
(err: cp.ExecFileException | null, stdout: string) => {
if (err) {
return reject(err);
}
return resolve(JSON.parse(stdout) as LudusaviBackup);
}
);
});
};
export const restoreBackup = (backupPath: string) => {
const result = cp.execFileSync(binaryPath, [
"restore",
"--path",
backupPath,
"--api",
"--force",
]);
return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
};
export const generateConfig = () => {
const result = cp.execFileSync(binaryPath, ["schema", "config"]);
return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
};

View File

@@ -160,7 +160,6 @@ export function GameDetailsContextProvider({
setShopDetails((prev) => { setShopDetails((prev) => {
if (!prev) return null; if (!prev) return null;
console.log("assets", assets);
return { return {
...prev, ...prev,
assets, assets,

View File

@@ -147,12 +147,16 @@ export function GameOptionsModal({
}); });
if (filePaths && filePaths.length > 0) { if (filePaths && filePaths.length > 0) {
try {
await window.electron.selectGameWinePrefix( await window.electron.selectGameWinePrefix(
game.shop, game.shop,
game.objectId, game.objectId,
filePaths[0] filePaths[0]
); );
await updateGame(); await updateGame();
} catch (error) {
showErrorToast(t("invalid_wine_prefix_path"));
}
} }
}; };

View File

@@ -40,3 +40,12 @@ export interface LudusaviConfig {
registry: []; registry: [];
}[]; }[];
} }
export interface LudusaviBackupMapping {
files: {
[key: string]: {
hash: string;
size: number;
};
};
}

109
yarn.lock
View File

@@ -1850,108 +1850,6 @@
dependencies: dependencies:
"@monaco-editor/loader" "^1.4.0" "@monaco-editor/loader" "^1.4.0"
"@napi-rs/nice-android-arm-eabi@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz#9a0cba12706ff56500df127d6f4caf28ddb94936"
integrity sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==
"@napi-rs/nice-android-arm64@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz#32fc32e9649bd759d2a39ad745e95766f6759d2f"
integrity sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==
"@napi-rs/nice-darwin-arm64@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz#d3c44c51b94b25a82d45803e2255891e833e787b"
integrity sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==
"@napi-rs/nice-darwin-x64@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz#f1b1365a8370c6a6957e90085a9b4873d0e6a957"
integrity sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==
"@napi-rs/nice-freebsd-x64@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz#4280f081efbe0b46c5165fdaea8b286e55a8f89e"
integrity sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==
"@napi-rs/nice-linux-arm-gnueabihf@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz#07aec23a9467ed35eb7602af5e63d42c5d7bd473"
integrity sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==
"@napi-rs/nice-linux-arm64-gnu@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz#038a77134cc6df3c48059d5a5e199d6f50fb9a90"
integrity sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==
"@napi-rs/nice-linux-arm64-musl@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz#715d0906582ba0cff025109f42e5b84ea68c2bcc"
integrity sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==
"@napi-rs/nice-linux-ppc64-gnu@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz#ac1c8f781c67b0559fa7a1cd4ae3ca2299dc3d06"
integrity sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==
"@napi-rs/nice-linux-riscv64-gnu@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz#b0a430549acfd3920ffd28ce544e2fe17833d263"
integrity sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==
"@napi-rs/nice-linux-s390x-gnu@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz#5b95caf411ad72a965885217db378c4d09733e97"
integrity sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==
"@napi-rs/nice-linux-x64-gnu@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz#a98cdef517549f8c17a83f0236a69418a90e77b7"
integrity sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==
"@napi-rs/nice-linux-x64-musl@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz#5e26843eafa940138aed437c870cca751c8a8957"
integrity sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==
"@napi-rs/nice-win32-arm64-msvc@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz#bd62617d02f04aa30ab1e9081363856715f84cd8"
integrity sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==
"@napi-rs/nice-win32-ia32-msvc@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz#b8b7aad552a24836027473d9b9f16edaeabecf18"
integrity sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==
"@napi-rs/nice-win32-x64-msvc@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz#37d8718b8f722f49067713e9f1e85540c9a3dd09"
integrity sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==
"@napi-rs/nice@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@napi-rs/nice/-/nice-1.0.1.tgz#483d3ff31e5661829a1efb4825591a135c3bfa7d"
integrity sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==
optionalDependencies:
"@napi-rs/nice-android-arm-eabi" "1.0.1"
"@napi-rs/nice-android-arm64" "1.0.1"
"@napi-rs/nice-darwin-arm64" "1.0.1"
"@napi-rs/nice-darwin-x64" "1.0.1"
"@napi-rs/nice-freebsd-x64" "1.0.1"
"@napi-rs/nice-linux-arm-gnueabihf" "1.0.1"
"@napi-rs/nice-linux-arm64-gnu" "1.0.1"
"@napi-rs/nice-linux-arm64-musl" "1.0.1"
"@napi-rs/nice-linux-ppc64-gnu" "1.0.1"
"@napi-rs/nice-linux-riscv64-gnu" "1.0.1"
"@napi-rs/nice-linux-s390x-gnu" "1.0.1"
"@napi-rs/nice-linux-x64-gnu" "1.0.1"
"@napi-rs/nice-linux-x64-musl" "1.0.1"
"@napi-rs/nice-win32-arm64-msvc" "1.0.1"
"@napi-rs/nice-win32-ia32-msvc" "1.0.1"
"@napi-rs/nice-win32-x64-msvc" "1.0.1"
"@nodelib/fs.scandir@2.1.5": "@nodelib/fs.scandir@2.1.5":
version "2.1.5" version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@@ -7722,13 +7620,6 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
piscina@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/piscina/-/piscina-4.7.0.tgz#68936fc77128db00541366531330138e366dc851"
integrity sha512-b8hvkpp9zS0zsfa939b/jXbe64Z2gZv0Ha7FYPNUiDIB1y2AtxcOZdfP8xN8HFjUaqQiT9gRlfjAsoL8vdJ1Iw==
optionalDependencies:
"@napi-rs/nice" "^1.0.1"
plist@3.1.0, plist@^3.0.4, plist@^3.0.5, plist@^3.1.0: plist@3.1.0, plist@^3.0.4, plist@^3.0.5, plist@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9"