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*
.env
.vite
ludusavi/
ludusavi/**
!ludusavi/config.yaml
hydra-python-rpc/
.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",
"lodash-es": "^4.17.21",
"parse-torrent": "^11.0.17",
"piscina": "^4.7.0",
"rc-virtual-list": "^3.16.1",
"react-hook-form": "^7.53.0",
"react-i18next": "^14.1.0",

View File

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

View File

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

View File

@@ -32,3 +32,5 @@ export const isPortableVersion = () => {
export const normalizePath = (str: string) =>
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();
if (!gotTheLock) app.quit();
app.commandLine.appendSwitch("--no-sandbox");
if (process.platform !== "linux") {
app.commandLine.appendSwitch("--no-sandbox");
}
i18n.init({
resources,

View File

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

View File

@@ -7,7 +7,7 @@ import os from "node:os";
import type { GameShop, User } from "@types";
import { backupsPath } from "@main/constants";
import { HydraApi } from "./hydra-api";
import { normalizePath } from "@main/helpers";
import { normalizePath, parseRegFile } from "@main/helpers";
import { logger } from "./logger";
import { WindowManager } from "./window-manager";
import axios from "axios";
@@ -17,6 +17,53 @@ import i18next, { t } from "i18next";
import { SystemPath } from "./system-path";
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) {
const language = i18next.language;
@@ -102,7 +149,9 @@ export class CloudSync {
shop,
objectId,
hostname: os.hostname(),
homeDir: normalizePath(SystemPath.getPath("home")),
winePrefixPath: game?.winePrefixPath ?? null,
homeDir: this.getProfilePaths(game?.winePrefixPath ?? null)
.userProfilePath,
downloadOptionTitle,
platform: os.platform(),
label,

View File

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

View File

@@ -1,70 +1,83 @@
import type { GameShop, LudusaviBackup, LudusaviConfig } from "@types";
import Piscina from "piscina";
import { app } from "electron";
import fs from "node:fs";
import path from "node:path";
import YAML from "yaml";
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
import { LUDUSAVI_MANIFEST_URL } from "@main/constants";
import cp from "node:child_process";
import { SystemPath } from "./system-path";
export class Ludusavi {
private static ludusaviPath = path.join(
SystemPath.getPath("appData"),
"ludusavi"
);
private static ludusaviConfigPath = path.join(
this.ludusaviPath,
private static ludusaviPath = app.isPackaged
? path.join(process.resourcesPath, "ludusavi")
: path.join(__dirname, "..", "..", "ludusavi");
private static binaryPath = path.join(this.ludusaviPath, "ludusavi");
private static configPath = path.join(
SystemPath.getPath("userData"),
"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(
fs.readFileSync(this.ludusaviConfigPath, "utf-8")
fs.readFileSync(this.configPath, "utf-8")
) as LudusaviConfig;
return config;
}
static async backupGame(
_shop: GameShop,
objectId: string,
backupPath: string,
winePrefix?: string | null
): Promise<LudusaviBackup> {
return this.worker.run(
{ title: objectId, backupPath, winePrefix },
{ name: "backupGame" }
);
public static async copyConfigFileToUserData() {
fs.cpSync(path.join(this.ludusaviPath, "config.yaml"), this.configPath);
}
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,
objectId: string,
winePrefix?: string | null
): Promise<LudusaviBackup | null> {
const config = await this.getConfig();
const backupData = await this.worker.run(
{ title: objectId, winePrefix, preview: true },
{ name: "backupGame" }
const backupData = await this.backupGame(
_shop,
objectId,
null,
winePrefix,
true
);
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) {
const config = await this.getConfig();
const filteredGames = config.customGames.filter(
@@ -105,6 +105,10 @@ export class Ludusavi {
}
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) => {
if (!prev) return null;
console.log("assets", assets);
return {
...prev,
assets,

View File

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

View File

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

109
yarn.lock
View File

@@ -1850,108 +1850,6 @@
dependencies:
"@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":
version "2.1.5"
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"
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:
version "3.1.0"
resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9"