feat: initial commit for BetterCurseForge client modification suite
This commit is contained in:
commit
c20abd8790
79 changed files with 13985 additions and 0 deletions
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# PyInstaller build outputs
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.manifest
|
||||||
|
|
||||||
|
# Node / npm
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
!patcher/vendor/asar/node_modules/
|
||||||
|
|
||||||
|
# VitePress build outputs & cache
|
||||||
|
docs/.vitepress/dist/
|
||||||
|
docs/.vitepress/cache/
|
||||||
|
|
||||||
|
# User data, backups, logs & temp files
|
||||||
|
*.vanilla
|
||||||
|
*.bak
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
repatch.log
|
||||||
|
watcher.log
|
||||||
|
temp*
|
||||||
|
|
||||||
|
# OS and Editor files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
38
BetterCurseForge.spec
Normal file
38
BetterCurseForge.spec
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['patcher\\patcher_gui.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[('bettercf', 'bettercf'), ('patcher\\vendor', 'vendor'), ('patcher\\patch_core.py', '.')],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='BetterCurseForge',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 BetterCurseForge Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
165
README.md
Normal file
165
README.md
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
# BetterCurseForge
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="docs/public/logo.svg" alt="BetterCurseForge Logo" width="128" height="128" />
|
||||||
|
|
||||||
|
### The modern, lightweight client modification for CurseForge Launcher.
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://github.com)
|
||||||
|
[](#features)
|
||||||
|
[](#-how-auto-repatch-works)
|
||||||
|
|
||||||
|
**Zero Ads • Custom Themes • Unlocked Skins • Extensible Plugins • Autonomous Auto-Repatch**
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🚫 **Complete Zero-Ad Experience**: Multi-layer ad elimination:
|
||||||
|
- Electron `session.webRequest` network request blocker stops Overwolf/Adrta/DoubleClick ad calls before they start.
|
||||||
|
- React component interception replaces ad renderers with clean empty nodes.
|
||||||
|
- Fail-safe CSS rules neutralize ad frames, banners, and swap buttons.
|
||||||
|
- 🎨 **First-Class Theme Engine**:
|
||||||
|
- Seamlessly embedded into CurseForge's native **Settings > Themes** screen.
|
||||||
|
- Live CSS injection with instant hot-reloading (no launcher restarts needed).
|
||||||
|
- Bundled with **Purple Night**—a custom dark theme featuring violet accents, sleek card elevations, and custom badge styling.
|
||||||
|
- 🔓 **Unlocked Native Premium Themes & Skins**:
|
||||||
|
- Unlocks all official CurseForge subscriber skins (PixelForge, LifeForge, FantasyForge, Halloween, etc.) without requiring a CurseForge subscription.
|
||||||
|
- 🧩 **Extensible Plugin System**:
|
||||||
|
- Modular plugin architecture with lifecycle hooks (`onEnable`, `onDisable`), safe IPC communication, and scoped styling.
|
||||||
|
- Includes sample plugins like **Compact Mode**.
|
||||||
|
- 🛡️ **Autonomous Auto-Repatch Engine**:
|
||||||
|
- **Survives in-app CurseForge updates automatically!**
|
||||||
|
- Offline, zero-delay ASAR extraction and repacking (< 3 seconds) using CurseForge's internal Node v24 runtime—no internet connection or external npm/npx required.
|
||||||
|
- Synchronizes Overwolf's binary `OWEASARSIG` integrity hashes directly inside `CurseForge.exe`.
|
||||||
|
- Background watcher daemon ensures your modifications are immediately re-applied when CurseForge updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Easy Installation (GUI)
|
||||||
|
|
||||||
|
1. Download the latest `BetterCurseForge.exe` from [Releases](https://github.com).
|
||||||
|
2. Run `BetterCurseForge.exe`.
|
||||||
|
3. Click **Patch CurseForge**.
|
||||||
|
4. Launch CurseForge Launcher—your modded client is ready to use!
|
||||||
|
|
||||||
|
### Command-Line Interface (CLI)
|
||||||
|
|
||||||
|
`BetterCurseForge.exe` can also be run directly from PowerShell or Command Prompt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check current patch and watcher status
|
||||||
|
.\BetterCurseForge.exe status
|
||||||
|
|
||||||
|
# Apply BetterCurseForge patch
|
||||||
|
.\BetterCurseForge.exe patch
|
||||||
|
|
||||||
|
# Restore original vanilla CurseForge
|
||||||
|
.\BetterCurseForge.exe unpatch
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 How Auto-Repatch Works
|
||||||
|
|
||||||
|
Whenever CurseForge downloads an update, the Overwolf installer replaces `resources/app.asar` with a vanilla archive and restarts `CurseForge.exe`. BetterCurseForge guarantees you never lose your patch:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[CurseForge Launcher Update] -->|Installer downloads update| B[CurseForge Quitting / Replacing Files]
|
||||||
|
B -->|Hook & Watcher Detect Update| C[Auto-Repatch Triggered]
|
||||||
|
C -->|Wait for Installer to Settle| D[Settle Verified]
|
||||||
|
D -->|Offline Extraction & Patch <3s| E[Dist & BetterCF Injected]
|
||||||
|
E -->|Recalculate ASAR SHA-256| F[Synchronize OWEASARSIG in CurseForge.exe]
|
||||||
|
F -->|Autonomous Relaunch| G[Modded CurseForge Launcher Ready!]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Permanent Daemon Installation**: On first patch, BetterCurseForge installs to `%LOCALAPPDATA%\BetterCurseForge` with an autostart registry entry in `HKCU\...\Run`.
|
||||||
|
2. **Proactive In-App Update Interceptor**: BetterCurseForge monitors updater states on app quit (`before-quit`) to initiate patching before the new launcher process initializes.
|
||||||
|
3. **Resilient Win32 Watcher Daemon**: A silent background process monitors CurseForge's installation directory with 0% CPU consumption. If an unpatched state is detected (e.g., after an update), it terminates the unpatched process, reapplies the patch, and relaunches the modded client.
|
||||||
|
4. **100% Offline Zero-Delay ASAR Engine**: Unpacking and repacking use bundled tools combined with CurseForge's built-in Node runtime (`ELECTRON_RUN_AS_NODE=1`), completing the entire repatch cycle in 3–4 seconds without requiring an internet connection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Themes & Plugins
|
||||||
|
|
||||||
|
User configurations, themes, and plugins are stored in your AppData directory:
|
||||||
|
```
|
||||||
|
%APPDATA%\CurseForge\BetterCurseForge\
|
||||||
|
├── themes/
|
||||||
|
│ ├── purple-night.css
|
||||||
|
│ └── purple-night.json
|
||||||
|
└── plugins/
|
||||||
|
└── compact-mode/
|
||||||
|
├── manifest.json
|
||||||
|
└── index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
You can open these folders anytime via the **Open Themes Folder** and **Open Plugins Folder** buttons in the BetterCurseForge GUI.
|
||||||
|
|
||||||
|
### Creating a Custom Theme
|
||||||
|
|
||||||
|
Create a CSS file and manifest in `%APPDATA%\CurseForge\BetterCurseForge\themes\my-theme.css`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "My Theme",
|
||||||
|
"description": "Custom color theme for CurseForge",
|
||||||
|
"author": "Your Name",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"css": "my-theme.css"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--surface-brand-brand: #10b981 !important;
|
||||||
|
--surface-brand-brand-hover: #059669 !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Building from Source
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Windows 10 / 11
|
||||||
|
- Python 3.10+
|
||||||
|
- Node.js 20+ (for docs)
|
||||||
|
|
||||||
|
### Build Instructions
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/your-username/BetterCurseForge.git
|
||||||
|
cd BetterCurseForge
|
||||||
|
|
||||||
|
# Build standalone executable with PyInstaller
|
||||||
|
python -m pip install pyinstaller
|
||||||
|
python -m PyInstaller BetterCurseForge.spec --noconfirm
|
||||||
|
|
||||||
|
# Output executable will be in:
|
||||||
|
# dist/BetterCurseForge.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running VitePress Documentation
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd docs
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the [MIT License](LICENSE).
|
||||||
|
CurseForge and Overwolf are trademarks of their respective owners. BetterCurseForge is an independent open-source project and is not affiliated with or endorsed by Overwolf or CurseForge.
|
||||||
310
bettercf/core/main-process.js
Normal file
310
bettercf/core/main-process.js
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
// BetterCurseForge — Main Process Hooks
|
||||||
|
// ======================================
|
||||||
|
// Runs in Electron's main process BEFORE CurseForge loads.
|
||||||
|
// Blocks ad network requests via session.webRequest.
|
||||||
|
// Manages themes, plugins, and settings IPC channels.
|
||||||
|
|
||||||
|
import { app, session, ipcMain, shell } from 'electron';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const USER_DATA = join(app.getPath('userData'), 'BetterCurseForge');
|
||||||
|
const THEMES_DIR = join(USER_DATA, 'themes');
|
||||||
|
const PLUGINS_DIR = join(USER_DATA, 'plugins');
|
||||||
|
const SETTINGS_FILE = join(USER_DATA, 'settings.json');
|
||||||
|
|
||||||
|
// Ensure user directories exist
|
||||||
|
[THEMES_DIR, PLUGINS_DIR].forEach(d => { try { mkdirSync(d, { recursive: true }); } catch {} });
|
||||||
|
|
||||||
|
// Seed built-in themes and plugins if user directories are empty
|
||||||
|
try {
|
||||||
|
const builtinThemes = join(__dirname, '..', 'themes');
|
||||||
|
if (existsSync(builtinThemes)) {
|
||||||
|
for (const f of readdirSync(builtinThemes)) {
|
||||||
|
try {
|
||||||
|
const src = join(builtinThemes, f);
|
||||||
|
const dst = join(THEMES_DIR, f);
|
||||||
|
if (!existsSync(dst)) {
|
||||||
|
writeFileSync(dst, readFileSync(src));
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// ── Settings ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let settings = {
|
||||||
|
adsBlocked: true,
|
||||||
|
enabledThemes: [],
|
||||||
|
enabledPlugins: [],
|
||||||
|
activeCustomTheme: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (existsSync(SETTINGS_FILE)) {
|
||||||
|
Object.assign(settings, JSON.parse(readFileSync(SETTINGS_FILE, 'utf-8')));
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
try { writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2)); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ad Blocking (Network Layer) ───────────────────────────────
|
||||||
|
|
||||||
|
const AD_URL_PATTERNS = [
|
||||||
|
'*://*.overwolf.com/ads/*',
|
||||||
|
'*://ads.overwolf.com/*',
|
||||||
|
'*://content.overwolf.com/ads/*',
|
||||||
|
'*://*.adrta.com/*',
|
||||||
|
'*://*.adnxs.com/*',
|
||||||
|
'*://*.doubleclick.net/*',
|
||||||
|
'*://*.googlesyndication.com/*',
|
||||||
|
'*://*.googleadservices.com/*',
|
||||||
|
'*://*.moatads.com/*',
|
||||||
|
'*://*.amazon-adsystem.com/*',
|
||||||
|
'*://*.adsrvr.org/*',
|
||||||
|
'*://*.adscorebr.com/*',
|
||||||
|
'*://*.serving-sys.com/*',
|
||||||
|
'*://*.adskeeper.co.uk/*',
|
||||||
|
];
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
if (!settings.adsBlocked) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ses = session.defaultSession;
|
||||||
|
ses.webRequest.onBeforeRequest({ urls: AD_URL_PATTERNS }, (details, callback) => {
|
||||||
|
callback({ cancel: true });
|
||||||
|
});
|
||||||
|
console.log('[BetterCF] Network ad blocking enabled.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[BetterCF] Failed to set up ad blocking:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Renderer Injection via app event ──────────────────────────
|
||||||
|
|
||||||
|
const rendererCode = (() => {
|
||||||
|
const rendererPath = join(__dirname, 'renderer.js');
|
||||||
|
if (existsSync(rendererPath)) {
|
||||||
|
return readFileSync(rendererPath, 'utf-8');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
app.on('browser-window-created', (_event, window) => {
|
||||||
|
if (!rendererCode) return;
|
||||||
|
|
||||||
|
window.webContents.on('did-finish-load', () => {
|
||||||
|
// Inject the IPC bridge first (since contextIsolation is false, this works)
|
||||||
|
const ipcBridge = `
|
||||||
|
if (!window.__BCF_IPC__) {
|
||||||
|
try {
|
||||||
|
const { ipcRenderer } = require('electron');
|
||||||
|
window.__BCF_IPC__ = {
|
||||||
|
invoke: (...args) => ipcRenderer.invoke(...args),
|
||||||
|
};
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
window.__BCF_CORE_PATH__ = ${JSON.stringify(__dirname)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
window.webContents.executeJavaScript(ipcBridge).then(() => {
|
||||||
|
return window.webContents.executeJavaScript(rendererCode);
|
||||||
|
}).then(() => {
|
||||||
|
console.log('[BetterCF] Renderer injected into window.');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[BetterCF] Renderer injection failed:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── IPC Channels ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
ipcMain.handle('bcf:themes:list', () => {
|
||||||
|
try {
|
||||||
|
if (!existsSync(THEMES_DIR)) return [];
|
||||||
|
const entries = readdirSync(THEMES_DIR, { withFileTypes: true });
|
||||||
|
const themes = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isFile() && entry.name.endsWith('.css')) {
|
||||||
|
const name = entry.name.replace('.css', '');
|
||||||
|
const css = readFileSync(join(THEMES_DIR, entry.name), 'utf-8');
|
||||||
|
const manifestPath = join(THEMES_DIR, `${name}.json`);
|
||||||
|
let manifest = {
|
||||||
|
name: name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
||||||
|
author: 'Custom',
|
||||||
|
description: '',
|
||||||
|
version: '1.0.0',
|
||||||
|
primaryColor: '#8b5cf6',
|
||||||
|
titleColor: '#a78bfa',
|
||||||
|
bodyColor: '#2e1065',
|
||||||
|
backgroundColor: '#161026',
|
||||||
|
};
|
||||||
|
if (existsSync(manifestPath)) {
|
||||||
|
try { Object.assign(manifest, JSON.parse(readFileSync(manifestPath, 'utf-8'))); } catch {}
|
||||||
|
}
|
||||||
|
themes.push({
|
||||||
|
...manifest,
|
||||||
|
fileName: entry.name,
|
||||||
|
css,
|
||||||
|
enabled: settings.enabledThemes.includes(entry.name) || settings.activeCustomTheme === entry.name,
|
||||||
|
});
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
const cssPath = join(THEMES_DIR, entry.name, 'theme.css');
|
||||||
|
const manifestPath = join(THEMES_DIR, entry.name, 'theme.json');
|
||||||
|
if (existsSync(cssPath)) {
|
||||||
|
const css = readFileSync(cssPath, 'utf-8');
|
||||||
|
let manifest = {
|
||||||
|
name: entry.name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
||||||
|
author: 'Custom',
|
||||||
|
description: '',
|
||||||
|
version: '1.0.0',
|
||||||
|
primaryColor: '#8b5cf6',
|
||||||
|
titleColor: '#a78bfa',
|
||||||
|
bodyColor: '#2e1065',
|
||||||
|
backgroundColor: '#161026',
|
||||||
|
};
|
||||||
|
if (existsSync(manifestPath)) {
|
||||||
|
try { Object.assign(manifest, JSON.parse(readFileSync(manifestPath, 'utf-8'))); } catch {}
|
||||||
|
}
|
||||||
|
themes.push({
|
||||||
|
...manifest,
|
||||||
|
fileName: entry.name,
|
||||||
|
css,
|
||||||
|
enabled: settings.enabledThemes.includes(entry.name) || settings.activeCustomTheme === entry.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return themes;
|
||||||
|
} catch { return []; }
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('bcf:plugins:list', () => {
|
||||||
|
try {
|
||||||
|
if (!existsSync(PLUGINS_DIR)) return [];
|
||||||
|
const entries = readdirSync(PLUGINS_DIR, { withFileTypes: true });
|
||||||
|
const plugins = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const manifestPath = join(PLUGINS_DIR, entry.name, 'manifest.json');
|
||||||
|
if (!existsSync(manifestPath)) continue;
|
||||||
|
try {
|
||||||
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
||||||
|
const indexPath = join(PLUGINS_DIR, entry.name, 'index.js');
|
||||||
|
const stylePath = join(PLUGINS_DIR, entry.name, 'styles.css');
|
||||||
|
plugins.push({
|
||||||
|
...manifest, dirName: entry.name,
|
||||||
|
hasScript: existsSync(indexPath), hasStyles: existsSync(stylePath),
|
||||||
|
enabled: settings.enabledPlugins.includes(entry.name),
|
||||||
|
script: existsSync(indexPath) ? readFileSync(indexPath, 'utf-8') : null,
|
||||||
|
styles: existsSync(stylePath) ? readFileSync(stylePath, 'utf-8') : null,
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return plugins;
|
||||||
|
} catch { return []; }
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('bcf:settings:get', () => ({ ...settings }));
|
||||||
|
ipcMain.handle('bcf:settings:set', (_, key, value) => {
|
||||||
|
settings[key] = value;
|
||||||
|
saveSettings();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('bcf:settings:toggle-theme', (_, fileName) => {
|
||||||
|
const idx = settings.enabledThemes.indexOf(fileName);
|
||||||
|
if (idx >= 0) settings.enabledThemes.splice(idx, 1);
|
||||||
|
else settings.enabledThemes.push(fileName);
|
||||||
|
saveSettings();
|
||||||
|
return settings.enabledThemes.includes(fileName);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('bcf:settings:toggle-plugin', (_, dirName) => {
|
||||||
|
const idx = settings.enabledPlugins.indexOf(dirName);
|
||||||
|
if (idx >= 0) settings.enabledPlugins.splice(idx, 1);
|
||||||
|
else settings.enabledPlugins.push(dirName);
|
||||||
|
saveSettings();
|
||||||
|
return settings.enabledPlugins.includes(dirName);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('bcf:open-themes-folder', async () => {
|
||||||
|
try {
|
||||||
|
await shell.openPath(THEMES_DIR);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('bcf:open-plugins-folder', async () => {
|
||||||
|
try {
|
||||||
|
await shell.openPath(PLUGINS_DIR);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('bcf:info', () => ({
|
||||||
|
version: '1.0.0',
|
||||||
|
bcfRoot: USER_DATA,
|
||||||
|
themesDir: THEMES_DIR,
|
||||||
|
pluginsDir: PLUGINS_DIR,
|
||||||
|
settingsFile: SETTINGS_FILE,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('[BetterCF] Main process hooks installed.');
|
||||||
|
|
||||||
|
// ── Auto-Repatch on Update Interceptor ────────────────────────
|
||||||
|
try {
|
||||||
|
const { spawn } = await import('node:child_process');
|
||||||
|
const patcherExe = join(process.env.LOCALAPPDATA || '', 'BetterCurseForge', 'BetterCurseForge.exe');
|
||||||
|
|
||||||
|
const onUpdateTriggered = () => {
|
||||||
|
try {
|
||||||
|
if (existsSync(patcherExe)) {
|
||||||
|
spawn(patcherExe, ['--wait-for-update', process.pid.toString()], {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore'
|
||||||
|
}).unref();
|
||||||
|
console.log('[BetterCF] Update pending. Spawned patcher --wait-for-update');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[BetterCF] Failed to spawn update patcher:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Check if curseforge-updater has a pending update when quitting
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
const pendingInstaller = join(process.env.LOCALAPPDATA || '', 'curseforge-updater', 'pending', 'setup.exe');
|
||||||
|
const directInstaller = join(process.env.LOCALAPPDATA || '', 'curseforge-updater', 'installer.exe');
|
||||||
|
if (existsSync(pendingInstaller) || existsSync(directInstaller)) {
|
||||||
|
onUpdateTriggered();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Ensure watcher daemon is running
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
if (existsSync(patcherExe)) {
|
||||||
|
spawn(patcherExe, ['--ensure-watcher'], {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore'
|
||||||
|
}).unref();
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, 4000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[BetterCF] Auto-repatch hook initialization error:', err);
|
||||||
|
}
|
||||||
79
bettercf/core/preload.js
Normal file
79
bettercf/core/preload.js
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// BetterCurseForge — Preload Script
|
||||||
|
// ==================================
|
||||||
|
// Injected into every BrowserWindow's preload phase.
|
||||||
|
// Chain-loads the original CurseForge preload, then injects the renderer runtime.
|
||||||
|
// Since contextIsolation is false, we have direct access to the renderer's JS context.
|
||||||
|
|
||||||
|
const { ipcRenderer } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// ── Find and load original preload ────────────────────────────
|
||||||
|
|
||||||
|
const args = process.argv;
|
||||||
|
let originalPreloadPath = null;
|
||||||
|
|
||||||
|
for (const arg of args) {
|
||||||
|
if (arg.startsWith('--bcf-original-preload=')) {
|
||||||
|
originalPreloadPath = arg.split('=')[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalPreloadPath && fs.existsSync(originalPreloadPath)) {
|
||||||
|
try {
|
||||||
|
require(originalPreloadPath);
|
||||||
|
console.log('[BetterCF] Original preload loaded:', originalPreloadPath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[BetterCF] Failed to load original preload:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Inject Renderer Runtime ───────────────────────────────────
|
||||||
|
|
||||||
|
const bcfInjected = args.some(a => a === '--bcf-injected=true');
|
||||||
|
|
||||||
|
if (bcfInjected) {
|
||||||
|
// Wait for DOM to be ready, then inject our renderer
|
||||||
|
const injectRenderer = () => {
|
||||||
|
const bcfCorePath = path.join(__dirname, 'bettercf', 'core');
|
||||||
|
const rendererPath = path.join(bcfCorePath, 'renderer.js');
|
||||||
|
|
||||||
|
if (!fs.existsSync(rendererPath)) {
|
||||||
|
console.warn('[BetterCF] renderer.js not found at', rendererPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read renderer code and inject via inline script
|
||||||
|
// CSP allows 'unsafe-inline' so this works
|
||||||
|
const rendererCode = fs.readFileSync(rendererPath, 'utf-8');
|
||||||
|
|
||||||
|
// Create the BetterCurseForge API on the window object
|
||||||
|
// This needs to bridge to the main process via IPC
|
||||||
|
const apiSetup = `
|
||||||
|
window.__BCF_IPC__ = {
|
||||||
|
invoke: (...args) => require('electron').ipcRenderer.invoke(...args),
|
||||||
|
};
|
||||||
|
window.__BCF_PRELOAD_PATH__ = ${JSON.stringify(bcfCorePath)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Inject API setup immediately
|
||||||
|
const apiScript = document.createElement('script');
|
||||||
|
apiScript.textContent = apiSetup;
|
||||||
|
(document.head || document.documentElement).appendChild(apiScript);
|
||||||
|
|
||||||
|
// Inject renderer after a short delay to ensure DOM is ready
|
||||||
|
const rendererScript = document.createElement('script');
|
||||||
|
rendererScript.type = 'module';
|
||||||
|
rendererScript.textContent = rendererCode;
|
||||||
|
(document.head || document.documentElement).appendChild(rendererScript);
|
||||||
|
|
||||||
|
console.log('[BetterCF] Renderer injected.');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', injectRenderer);
|
||||||
|
} else {
|
||||||
|
injectRenderer();
|
||||||
|
}
|
||||||
|
}
|
||||||
933
bettercf/core/renderer.js
Normal file
933
bettercf/core/renderer.js
Normal file
|
|
@ -0,0 +1,933 @@
|
||||||
|
// BetterCurseForge — Renderer Runtime
|
||||||
|
// ====================================
|
||||||
|
// Runs in the renderer process (the CurseForge UI).
|
||||||
|
// Handles ad removal, client themes in Settings -> Themes, plugins, and public API.
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (window.__BCF_RENDERER_LOADED__) {
|
||||||
|
console.log('[BetterCF] Renderer runtime already loaded.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.__BCF_RENDERER_LOADED__ = true;
|
||||||
|
|
||||||
|
// Fallback IPC bridge if not set by preload
|
||||||
|
if (!window.__BCF_IPC__) {
|
||||||
|
try {
|
||||||
|
const { ipcRenderer } = require('electron');
|
||||||
|
window.__BCF_IPC__ = {
|
||||||
|
invoke: (...args) => ipcRenderer.invoke(...args),
|
||||||
|
};
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
const ipc = window.__BCF_IPC__;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// AD REMOVAL
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const AdBlocker = {
|
||||||
|
_observer: null,
|
||||||
|
_style: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._style = document.createElement('style');
|
||||||
|
this._style.id = 'bcf-adblock';
|
||||||
|
this._style.textContent = `
|
||||||
|
/* ═══ BetterCurseForge Ad Blocker ═══ */
|
||||||
|
.curseforge-ad,
|
||||||
|
.ad-inner,
|
||||||
|
.overwolf-ad,
|
||||||
|
.affiliate-banner,
|
||||||
|
.swap-ads-location,
|
||||||
|
.subscribe-link,
|
||||||
|
.subscribe-link-small-resolution,
|
||||||
|
.subscribe-link-large-resolution,
|
||||||
|
.promo-banner {
|
||||||
|
display: none !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
max-width: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
position: absolute !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
owadview {
|
||||||
|
display: none !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class*="ad-container"],
|
||||||
|
[class*="ad-banner"],
|
||||||
|
[class*="high-impact-ad"],
|
||||||
|
[data-ad],
|
||||||
|
[data-ad-slot] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[style*="grid-template"][style*="ads"] {
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
grid-template-areas: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
(document.head || document.documentElement).appendChild(this._style);
|
||||||
|
|
||||||
|
this._observer = new MutationObserver((mutations) => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
for (const node of mutation.addedNodes) {
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
||||||
|
this._removeAds(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._observer.observe(document.documentElement, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => this._sweepAds(), 1000);
|
||||||
|
setTimeout(() => this._sweepAds(), 3000);
|
||||||
|
setTimeout(() => this._sweepAds(), 6000);
|
||||||
|
},
|
||||||
|
|
||||||
|
_removeAds(node) {
|
||||||
|
if (node.tagName?.toLowerCase() === 'owadview') {
|
||||||
|
const parent = node.parentElement;
|
||||||
|
node.remove();
|
||||||
|
if (parent && parent.children.length === 0) {
|
||||||
|
parent.style.display = 'none';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = node.className?.toString() || '';
|
||||||
|
if (/\b(ad-container|ad-banner|ads-wrapper|AdView|high-impact|display-ad)\b/i.test(classes)) {
|
||||||
|
node.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adChildren = node.querySelectorAll?.('owadview, [class*="ad-container"], [class*="AdView"], [class*="high-impact"]');
|
||||||
|
if (adChildren) {
|
||||||
|
for (const child of adChildren) {
|
||||||
|
child.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_sweepAds() {
|
||||||
|
const adSelectors = [
|
||||||
|
'.curseforge-ad',
|
||||||
|
'.ad-inner',
|
||||||
|
'.overwolf-ad',
|
||||||
|
'.affiliate-banner',
|
||||||
|
'.subscribe-link',
|
||||||
|
'.subscribe-link-small-resolution',
|
||||||
|
'.subscribe-link-large-resolution',
|
||||||
|
'.promo-banner',
|
||||||
|
'owadview',
|
||||||
|
];
|
||||||
|
const els = document.querySelectorAll(adSelectors.join(', '));
|
||||||
|
for (const el of els) {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT, null);
|
||||||
|
while (walker.nextNode()) {
|
||||||
|
const text = walker.currentNode.textContent?.trim();
|
||||||
|
if (text && /^Ads support authors/i.test(text)) {
|
||||||
|
let parent = walker.currentNode.parentElement;
|
||||||
|
while (parent && parent !== document.body) {
|
||||||
|
if (parent.children.length <= 2) {
|
||||||
|
parent.style.display = 'none';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// THEME SYSTEM
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const ThemeManager = {
|
||||||
|
_themes: [],
|
||||||
|
_activeThemeFileName: null,
|
||||||
|
_savedThemeFileName: null,
|
||||||
|
_styleElement: null,
|
||||||
|
_isPreviewing: false,
|
||||||
|
_isSuspendedForNative: false,
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this._injectBaseStyles();
|
||||||
|
|
||||||
|
// Read stored active theme preference
|
||||||
|
this._savedThemeFileName = localStorage.getItem('bcf_active_custom_theme') || null;
|
||||||
|
|
||||||
|
if (ipc) {
|
||||||
|
try {
|
||||||
|
const s = await ipc.invoke('bcf:settings:get');
|
||||||
|
if (s && s.activeCustomTheme !== undefined) {
|
||||||
|
this._savedThemeFileName = s.activeCustomTheme;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._activeThemeFileName = this._savedThemeFileName;
|
||||||
|
|
||||||
|
await this.loadThemes();
|
||||||
|
|
||||||
|
// If active theme is found, apply it
|
||||||
|
if (this._savedThemeFileName) {
|
||||||
|
const theme = this._themes.find(t => t.fileName === this._savedThemeFileName);
|
||||||
|
if (theme) {
|
||||||
|
this.applyTheme(theme, false);
|
||||||
|
} else {
|
||||||
|
// Stored theme file not found
|
||||||
|
this._savedThemeFileName = null;
|
||||||
|
this._activeThemeFileName = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[BetterCF] Theme manager ready (${this._themes.length} theme(s) found). Active: ${this._savedThemeFileName || 'None'}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
_injectBaseStyles() {
|
||||||
|
if (document.getElementById('bcf-custom-themes-style')) return;
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'bcf-custom-themes-style';
|
||||||
|
style.textContent = `
|
||||||
|
/* ═══ BetterCurseForge Settings Themes UI ═══ */
|
||||||
|
#bcf-custom-themes-section {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.bcf-themes-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.bcf-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 24px;
|
||||||
|
color: var(--color-text-title, currentColor);
|
||||||
|
}
|
||||||
|
.bcf-heading > span:first-child {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.bcf-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
color: #a78bfa;
|
||||||
|
background: rgba(139, 92, 246, 0.12);
|
||||||
|
border: 1px solid rgba(139, 92, 246, 0.3);
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
.bcf-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.bcf-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--color-text-primary, currentColor);
|
||||||
|
background: var(--surface-on-surface, rgba(255, 255, 255, 0.08));
|
||||||
|
border: 1px solid var(--border-tertiery, rgba(255, 255, 255, 0.15));
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
.bcf-btn svg {
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
}
|
||||||
|
.bcf-btn:hover {
|
||||||
|
background: var(--surface-on-surface-hover, rgba(255, 255, 255, 0.16));
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
color: var(--color-text-title-hover, #ffffff);
|
||||||
|
}
|
||||||
|
.bcf-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
.bcf-theme-thumb {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.bcf-theme-thumb .thumb-icon svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.bcf-theme-thumb.is-active .sprite-icon {
|
||||||
|
opacity: 1 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ Mutual Exclusivity ═══ */
|
||||||
|
/* When a BetterCF client theme is active, hide active checkmark & active indicator on native theme cards */
|
||||||
|
html.bcf-custom-theme-active .settings-themes-view .theme-thumb:not(.bcf-theme-thumb).is-active .sprite-icon {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
html.bcf-custom-theme-active .settings-themes-view .theme-thumb:not(.bcf-theme-thumb).is-active .thumb-icon {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
html.bcf-custom-theme-active .settings-themes-view .theme-thumb:not(.bcf-theme-thumb).is-active {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
/* Neutralize native skin styles and title bar */
|
||||||
|
html.bcf-custom-theme-active .title-bar,
|
||||||
|
html.bcf-custom-theme-active [class*="title-bar"],
|
||||||
|
html.bcf-custom-theme-active [class*="TitleBar"] {
|
||||||
|
background: #0d0818 !important;
|
||||||
|
--title-bar-background: #0d0818 !important;
|
||||||
|
}
|
||||||
|
html.bcf-custom-theme-active .pixelforge-skin,
|
||||||
|
html.bcf-custom-theme-active .lifeforge-skin,
|
||||||
|
html.bcf-custom-theme-active .fantasyforge-skin,
|
||||||
|
html.bcf-custom-theme-active .halloween-skin,
|
||||||
|
html.bcf-custom-theme-active .cf-light,
|
||||||
|
html.bcf-custom-theme-active .cf-light-colorblind {
|
||||||
|
--title-bar-background: #0d0818 !important;
|
||||||
|
--surface-brand-brand: #8b5cf6 !important;
|
||||||
|
--surface-brand-brand-hover: #9d74f7 !important;
|
||||||
|
--accent-font: var(--body-font, "Lato", sans-serif) !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
html.bcf-custom-theme-active .pixelforge-skin :not(.highlighted-project-container) .card:after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
(document.head || document.documentElement).appendChild(style);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadThemes() {
|
||||||
|
if (!ipc) return [];
|
||||||
|
try {
|
||||||
|
this._themes = await ipc.invoke('bcf:themes:list');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[BetterCF] Failed to load themes:', err);
|
||||||
|
this._themes = [];
|
||||||
|
}
|
||||||
|
return this._themes;
|
||||||
|
},
|
||||||
|
|
||||||
|
applyTheme(theme, save = true) {
|
||||||
|
if (!theme || !theme.css) return;
|
||||||
|
|
||||||
|
if (!this._styleElement) {
|
||||||
|
this._styleElement = document.createElement('style');
|
||||||
|
this._styleElement.id = 'bcf-custom-theme';
|
||||||
|
(document.head || document.documentElement).appendChild(this._styleElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._styleElement.disabled = false;
|
||||||
|
this._styleElement.textContent = theme.css;
|
||||||
|
this._styleElement.dataset.bcfTheme = theme.fileName;
|
||||||
|
this._activeThemeFileName = theme.fileName;
|
||||||
|
document.documentElement.classList.add('bcf-custom-theme-active');
|
||||||
|
|
||||||
|
const ALL_SKIN_CLASSES = [
|
||||||
|
'cf-light',
|
||||||
|
'cf-light-colorblind',
|
||||||
|
'pixelforge-skin',
|
||||||
|
'lifeforge-skin',
|
||||||
|
'fantasyforge-skin',
|
||||||
|
'halloween-skin',
|
||||||
|
];
|
||||||
|
const app = document.querySelector('.cf-app');
|
||||||
|
if (app) {
|
||||||
|
app.classList.remove(...ALL_SKIN_CLASSES);
|
||||||
|
if (!app.classList.contains('cf-classic')) {
|
||||||
|
app.classList.add('cf-classic');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (save) {
|
||||||
|
this._savedThemeFileName = theme.fileName;
|
||||||
|
this._isPreviewing = false;
|
||||||
|
try {
|
||||||
|
localStorage.setItem('bcf_active_custom_theme', theme.fileName);
|
||||||
|
ipc?.invoke?.('bcf:settings:set', 'activeCustomTheme', theme.fileName);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[BetterCF] Applied custom theme: ${theme.name} (${theme.fileName}) [save=${save}]`);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeTheme(save = true) {
|
||||||
|
if (this._styleElement) {
|
||||||
|
this._styleElement.remove();
|
||||||
|
this._styleElement = null;
|
||||||
|
}
|
||||||
|
this._activeThemeFileName = null;
|
||||||
|
document.documentElement.classList.remove('bcf-custom-theme-active');
|
||||||
|
|
||||||
|
if (save) {
|
||||||
|
this._savedThemeFileName = null;
|
||||||
|
this._isPreviewing = false;
|
||||||
|
try {
|
||||||
|
localStorage.removeItem('bcf_active_custom_theme');
|
||||||
|
ipc?.invoke?.('bcf:settings:set', 'activeCustomTheme', null);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[BetterCF] Reverted to native theme [save=${save}].`);
|
||||||
|
},
|
||||||
|
|
||||||
|
previewTheme(theme) {
|
||||||
|
if (!theme || !theme.css) return;
|
||||||
|
if (this._savedThemeFileName === theme.fileName) return;
|
||||||
|
this._isPreviewing = true;
|
||||||
|
this.applyTheme(theme, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelPreview() {
|
||||||
|
if (!this._isPreviewing) return;
|
||||||
|
this._isPreviewing = false;
|
||||||
|
if (this._savedThemeFileName) {
|
||||||
|
const saved = this._themes.find(t => t.fileName === this._savedThemeFileName);
|
||||||
|
if (saved) {
|
||||||
|
this.applyTheme(saved, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.removeTheme(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
suspendForNativePreview() {
|
||||||
|
if (this._isSuspendedForNative) return;
|
||||||
|
if (this._savedThemeFileName || this._activeThemeFileName) {
|
||||||
|
this._isSuspendedForNative = true;
|
||||||
|
if (this._styleElement) {
|
||||||
|
this._styleElement.disabled = true;
|
||||||
|
}
|
||||||
|
document.documentElement.classList.remove('bcf-custom-theme-active');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resumeFromNativePreview() {
|
||||||
|
if (!this._isSuspendedForNative) return;
|
||||||
|
this._isSuspendedForNative = false;
|
||||||
|
if (this._savedThemeFileName) {
|
||||||
|
if (this._styleElement) {
|
||||||
|
this._styleElement.disabled = false;
|
||||||
|
}
|
||||||
|
document.documentElement.classList.add('bcf-custom-theme-active');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getActiveThemeName() {
|
||||||
|
return this._savedThemeFileName;
|
||||||
|
},
|
||||||
|
|
||||||
|
getThemes() {
|
||||||
|
return [...this._themes];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// NATIVE SETTINGS -> THEMES HOOK & MUTUAL EXCLUSIVITY
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
let _isSwitchingThemeInternally = false;
|
||||||
|
|
||||||
|
function setupSettingsThemesHook() {
|
||||||
|
let isInjecting = false;
|
||||||
|
|
||||||
|
// Listen for clicks on native theme cards to ensure mutual exclusivity:
|
||||||
|
// If user clicks a native theme card, deactivate any client theme.
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (_isSwitchingThemeInternally) return;
|
||||||
|
const nativeThumb = e.target.closest('.theme-thumb:not(.bcf-theme-thumb)');
|
||||||
|
if (nativeThumb) {
|
||||||
|
ThemeManager._isSuspendedForNative = false;
|
||||||
|
if (ThemeManager.getActiveThemeName()) {
|
||||||
|
ThemeManager.removeTheme(true);
|
||||||
|
const grid = document.querySelector('#bcf-custom-themes-grid');
|
||||||
|
if (grid) {
|
||||||
|
grid.querySelectorAll('.bcf-theme-thumb').forEach(c => c.classList.remove('is-active'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// When hovering over native theme cards, suspend client theme so native hover preview works!
|
||||||
|
document.addEventListener('mouseover', (e) => {
|
||||||
|
const nativeThumb = e.target.closest('.theme-thumb:not(.bcf-theme-thumb)');
|
||||||
|
if (nativeThumb) {
|
||||||
|
ThemeManager.suspendForNativePreview();
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
document.addEventListener('mouseout', (e) => {
|
||||||
|
const nativeThumb = e.target.closest('.theme-thumb:not(.bcf-theme-thumb)');
|
||||||
|
if (nativeThumb) {
|
||||||
|
const toThumb = e.relatedTarget?.closest?.('.theme-thumb:not(.bcf-theme-thumb)');
|
||||||
|
if (!toThumb) {
|
||||||
|
ThemeManager.resumeFromNativePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
const checkAndInject = async () => {
|
||||||
|
const view = document.querySelector('.settings-themes-view');
|
||||||
|
if (!view) return;
|
||||||
|
|
||||||
|
if (view.querySelector('#bcf-custom-themes-section')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInjecting) return;
|
||||||
|
isInjecting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await renderCustomThemesSection(view);
|
||||||
|
} finally {
|
||||||
|
isInjecting = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
checkAndInject();
|
||||||
|
|
||||||
|
// Observe DOM mutations for when the user navigates to Settings -> Themes
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const view = document.querySelector('.settings-themes-view');
|
||||||
|
if (view && !view.querySelector('#bcf-custom-themes-section')) {
|
||||||
|
checkAndInject();
|
||||||
|
} else if (!view) {
|
||||||
|
ThemeManager.cancelPreview();
|
||||||
|
ThemeManager.resumeFromNativePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('hashchange', () => setTimeout(checkAndInject, 100));
|
||||||
|
window.addEventListener('popstate', () => setTimeout(checkAndInject, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str || '';
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderCustomThemesSection(view) {
|
||||||
|
if (view.querySelector('#bcf-custom-themes-section')) return;
|
||||||
|
|
||||||
|
const section = document.createElement('div');
|
||||||
|
section.className = 'settings-item-row';
|
||||||
|
section.id = 'bcf-custom-themes-section';
|
||||||
|
|
||||||
|
section.innerHTML = `
|
||||||
|
<div class="horizontal-divider"></div>
|
||||||
|
<div class="bcf-themes-header">
|
||||||
|
<h2 class="bcf-heading">
|
||||||
|
<span>Client Themes</span>
|
||||||
|
<span class="bcf-tag">BetterCurseForge</span>
|
||||||
|
</h2>
|
||||||
|
<div class="bcf-actions">
|
||||||
|
<button class="bcf-btn" id="bcf-open-folder-btn" type="button" title="Open themes folder">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M2 5C2 4.44772 2.44772 4 3 4H9.61257C10.043 4 10.4251 4.27543 10.5613 4.68377L11 6H2V5Z"/>
|
||||||
|
<path d="M2 8V19C2 19.5523 2.44772 20 3 20H21C21.5523 20 22 19.5523 22 19V9C22 8.44772 21.5523 8 21 8H2Z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Open Folder</span>
|
||||||
|
</button>
|
||||||
|
<button class="bcf-btn" id="bcf-refresh-themes-btn" type="button" title="Reload custom themes">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<g transform="translate(2, 2)">
|
||||||
|
<path d="M10 4C6.68629 4 4 6.68629 4 10H2C2 5.58172 5.58172 2 10 2C12.261 2 14.3029 2.93795 15.7579 4.44597L17.1314 2.94754C17.4396 2.61139 18 2.8294 18 3.2854V7.5C18 7.69203 17.8918 7.85877 17.7329 7.94253C17.738 7.96166 17.743 7.98082 17.748 8L13.6366 8C13.2016 8 12.9741 7.48284 13.268 7.16214L14.403 5.92399C13.3071 4.7407 11.74 4 10 4Z"/>
|
||||||
|
<path d="M10 16C13.3137 16 16 13.3137 16 10H18C18 14.4183 14.4183 18 10 18C7.73902 18 5.69711 17.062 4.24214 15.554L2.86858 17.0525C2.56044 17.3886 2 17.1706 2 16.7146V12.5C2 12.308 2.10825 12.1412 2.26705 12.0575C2.26198 12.0383 2.25697 12.0192 2.25204 12H6.36338C6.79843 12 7.02594 12.5172 6.73196 12.8379L5.59699 14.076C6.69293 15.2593 8.25997 16 10 16Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<span>Refresh</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="themes-list" id="bcf-custom-themes-grid"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Insert section right after existing settings-item-row or before upsell
|
||||||
|
const rows = view.querySelectorAll('.settings-item-row:not(#bcf-custom-themes-section)');
|
||||||
|
if (rows.length > 0) {
|
||||||
|
const lastRow = rows[rows.length - 1];
|
||||||
|
lastRow.after(section);
|
||||||
|
} else {
|
||||||
|
const upsell = view.querySelector('.upsell') || view.querySelector('[class*="upsell"]');
|
||||||
|
if (upsell) {
|
||||||
|
view.insertBefore(section, upsell);
|
||||||
|
} else {
|
||||||
|
view.appendChild(section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect buttons
|
||||||
|
section.querySelector('#bcf-open-folder-btn')?.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
ipc?.invoke?.('bcf:open-themes-folder');
|
||||||
|
});
|
||||||
|
|
||||||
|
section.querySelector('#bcf-refresh-themes-btn')?.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const btn = section.querySelector('#bcf-refresh-themes-btn');
|
||||||
|
if (btn) btn.style.opacity = '0.5';
|
||||||
|
await ThemeManager.loadThemes();
|
||||||
|
populateThemeCards(section);
|
||||||
|
if (btn) btn.style.opacity = '1';
|
||||||
|
});
|
||||||
|
|
||||||
|
populateThemeCards(section);
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateThemeCards(section) {
|
||||||
|
const grid = section.querySelector('#bcf-custom-themes-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
grid.innerHTML = '';
|
||||||
|
const themes = ThemeManager.getThemes();
|
||||||
|
const activeFileName = ThemeManager.getActiveThemeName();
|
||||||
|
|
||||||
|
if (themes.length === 0) {
|
||||||
|
const empty = document.createElement('div');
|
||||||
|
empty.className = 'bcf-empty-msg';
|
||||||
|
empty.style.cssText = 'color: var(--color-text-secondary, #71717a); font-size: 13px; padding: 10px 0;';
|
||||||
|
empty.textContent = 'No custom themes found in themes folder. Click "Open Folder" to add .css themes.';
|
||||||
|
grid.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const theme of themes) {
|
||||||
|
const isThisActive = activeFileName === theme.fileName;
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = `theme-thumb bcf-theme-thumb ${isThisActive ? 'is-active' : ''}`;
|
||||||
|
card.dataset.themeFile = theme.fileName;
|
||||||
|
|
||||||
|
const primary = theme.primaryColor || '#8b5cf6';
|
||||||
|
const title = theme.titleColor || '#a78bfa';
|
||||||
|
const body = theme.bodyColor || '#2e1065';
|
||||||
|
const bg = theme.backgroundColor || '#161026';
|
||||||
|
|
||||||
|
card.style.setProperty('--color-thumb-primary', primary);
|
||||||
|
card.style.setProperty('--color-thumb-title', title);
|
||||||
|
card.style.setProperty('--color-thumb-body', body);
|
||||||
|
card.style.setProperty('--color-thumb-name', '#f1f5f9');
|
||||||
|
card.style.setProperty('--color-thumb-icon-bg', primary);
|
||||||
|
card.style.setProperty('--color-thumb-check-color', '#ffffff');
|
||||||
|
card.style.backgroundColor = bg;
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="primary"></div>
|
||||||
|
<div class="title"></div>
|
||||||
|
<div class="body"></div>
|
||||||
|
<div class="name" title="${escapeHtml(theme.description || theme.name)}">${escapeHtml(theme.name)}</div>
|
||||||
|
<div class="thumb-icon">
|
||||||
|
<svg class="sprite-icon" aria-hidden="true" focusable="false">
|
||||||
|
<use href="../../assets/images/sprite.svg#icon-check"></use>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
card.addEventListener('mouseenter', () => {
|
||||||
|
ThemeManager.previewTheme(theme);
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('mouseleave', () => {
|
||||||
|
ThemeManager.cancelPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (ThemeManager.getActiveThemeName() === theme.fileName) {
|
||||||
|
// Toggle off -> revert to native theme
|
||||||
|
ThemeManager.removeTheme(true);
|
||||||
|
card.classList.remove('is-active');
|
||||||
|
} else {
|
||||||
|
// Activate this client theme
|
||||||
|
_isSwitchingThemeInternally = true;
|
||||||
|
try {
|
||||||
|
// Ensure CurseForge native theme switches to Classic Dark baseline so no other skin remains active
|
||||||
|
const classicCard = [...document.querySelectorAll('.settings-themes-view .theme-thumb:not(.bcf-theme-thumb)')]
|
||||||
|
.find(c => {
|
||||||
|
const txt = c.querySelector('.name')?.textContent?.trim().toLowerCase() || '';
|
||||||
|
return txt === 'curseforge classic' || (txt.includes('classic') && !txt.includes('colorblind'));
|
||||||
|
});
|
||||||
|
if (classicCard && !classicCard.classList.contains('is-active')) {
|
||||||
|
classicCard.click();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[BetterCF] Error switching base theme:', err);
|
||||||
|
} finally {
|
||||||
|
_isSwitchingThemeInternally = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeManager.applyTheme(theme, true);
|
||||||
|
grid.querySelectorAll('.bcf-theme-thumb').forEach(c => {
|
||||||
|
c.classList.toggle('is-active', c.dataset.themeFile === theme.fileName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// PLUGIN SYSTEM
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const PluginManager = {
|
||||||
|
_plugins: [],
|
||||||
|
_instances: new Map(),
|
||||||
|
_styleElements: new Map(),
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (!ipc) return;
|
||||||
|
try {
|
||||||
|
this._plugins = await ipc.invoke('bcf:plugins:list');
|
||||||
|
for (const plugin of this._plugins) {
|
||||||
|
if (plugin.enabled) {
|
||||||
|
this.enablePlugin(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[BetterCF] Plugin manager ready. ${this._plugins.length} plugin(s) found.`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[BetterCF] Failed to initialize plugins:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
enablePlugin(plugin) {
|
||||||
|
if (this._instances.has(plugin.dirName)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (plugin.styles) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = `bcf-plugin-style-${plugin.dirName}`;
|
||||||
|
style.textContent = plugin.styles;
|
||||||
|
(document.head || document.documentElement).appendChild(style);
|
||||||
|
this._styleElements.set(plugin.dirName, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.script) {
|
||||||
|
const instance = { enabled: true };
|
||||||
|
const pluginFn = new Function('BetterCurseForge', 'plugin', plugin.script);
|
||||||
|
pluginFn(window.BetterCurseForge, instance);
|
||||||
|
this._instances.set(plugin.dirName, instance);
|
||||||
|
console.log(`[BetterCF] Plugin loaded: ${plugin.name}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[BetterCF] Failed to load plugin ${plugin.name}:`, err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
disablePlugin(plugin) {
|
||||||
|
const instance = this._instances.get(plugin.dirName);
|
||||||
|
if (instance?.onDisable) {
|
||||||
|
try {
|
||||||
|
instance.onDisable();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[BetterCF] Error disabling plugin ${plugin.name}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._instances.delete(plugin.dirName);
|
||||||
|
|
||||||
|
const style = this._styleElements.get(plugin.dirName);
|
||||||
|
if (style) {
|
||||||
|
style.remove();
|
||||||
|
this._styleElements.delete(plugin.dirName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async togglePlugin(dirName) {
|
||||||
|
if (!ipc) return false;
|
||||||
|
const enabled = await ipc.invoke('bcf:settings:toggle-plugin', dirName);
|
||||||
|
const plugin = this._plugins.find(p => p.dirName === dirName);
|
||||||
|
if (!plugin) return false;
|
||||||
|
|
||||||
|
plugin.enabled = enabled;
|
||||||
|
if (enabled) {
|
||||||
|
this.enablePlugin(plugin);
|
||||||
|
} else {
|
||||||
|
this.disablePlugin(plugin);
|
||||||
|
}
|
||||||
|
return enabled;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPlugins() {
|
||||||
|
return [...this._plugins];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// PUBLIC API — window.BetterCurseForge
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const _injectedCSS = new Map();
|
||||||
|
let _cssCounter = 0;
|
||||||
|
const _eventHandlers = {};
|
||||||
|
|
||||||
|
window.BetterCurseForge = {
|
||||||
|
version: '1.0.0',
|
||||||
|
|
||||||
|
injectCSS(css) {
|
||||||
|
const id = `bcf-injected-${_cssCounter++}`;
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = id;
|
||||||
|
style.textContent = css;
|
||||||
|
(document.head || document.documentElement).appendChild(style);
|
||||||
|
_injectedCSS.set(id, style);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
style.remove();
|
||||||
|
_injectedCSS.delete(id);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
async get(key) {
|
||||||
|
if (!ipc) return null;
|
||||||
|
const all = await ipc.invoke('bcf:settings:get');
|
||||||
|
return key ? all[key] : all;
|
||||||
|
},
|
||||||
|
async set(key, value) {
|
||||||
|
if (!ipc) return false;
|
||||||
|
return ipc.invoke('bcf:settings:set', key, value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
patcher: {
|
||||||
|
_patches: [],
|
||||||
|
|
||||||
|
before(obj, method, callback) {
|
||||||
|
const original = obj[method];
|
||||||
|
obj[method] = function (...args) {
|
||||||
|
callback(args);
|
||||||
|
return original.apply(this, args);
|
||||||
|
};
|
||||||
|
const unpatch = () => { obj[method] = original; };
|
||||||
|
this._patches.push(unpatch);
|
||||||
|
return unpatch;
|
||||||
|
},
|
||||||
|
|
||||||
|
after(obj, method, callback) {
|
||||||
|
const original = obj[method];
|
||||||
|
obj[method] = function (...args) {
|
||||||
|
const result = original.apply(this, args);
|
||||||
|
callback(args, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const unpatch = () => { obj[method] = original; };
|
||||||
|
this._patches.push(unpatch);
|
||||||
|
return unpatch;
|
||||||
|
},
|
||||||
|
|
||||||
|
instead(obj, method, callback) {
|
||||||
|
const original = obj[method];
|
||||||
|
obj[method] = function (...args) {
|
||||||
|
return callback(args, original.bind(this));
|
||||||
|
};
|
||||||
|
const unpatch = () => { obj[method] = original; };
|
||||||
|
this._patches.push(unpatch);
|
||||||
|
return unpatch;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
on(event, callback) {
|
||||||
|
if (!_eventHandlers[event]) _eventHandlers[event] = [];
|
||||||
|
_eventHandlers[event].push(callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
off(event, callback) {
|
||||||
|
if (!_eventHandlers[event]) return;
|
||||||
|
_eventHandlers[event] = _eventHandlers[event].filter(cb => cb !== callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
emit(event, ...args) {
|
||||||
|
if (!_eventHandlers[event]) return;
|
||||||
|
for (const cb of _eventHandlers[event]) {
|
||||||
|
try { cb(...args); } catch (err) {
|
||||||
|
console.error(`[BetterCF] Event handler error (${event}):`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
themes: ThemeManager,
|
||||||
|
plugins: PluginManager,
|
||||||
|
ads: AdBlocker,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// INITIALIZATION
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
console.log('[BetterCF] Initializing renderer runtime...');
|
||||||
|
|
||||||
|
// 1. Block ads
|
||||||
|
AdBlocker.init();
|
||||||
|
|
||||||
|
// 2. Initialize theme manager
|
||||||
|
await ThemeManager.init();
|
||||||
|
|
||||||
|
// 3. Setup Native Settings -> Themes Hook
|
||||||
|
setupSettingsThemesHook();
|
||||||
|
|
||||||
|
// 4. Initialize plugins
|
||||||
|
await PluginManager.init();
|
||||||
|
|
||||||
|
// 5. Emit ready
|
||||||
|
window.BetterCurseForge.emit('ready');
|
||||||
|
|
||||||
|
console.log('[BetterCF] ✓ BetterCurseForge loaded successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => setTimeout(init, 300));
|
||||||
|
} else {
|
||||||
|
setTimeout(init, 300);
|
||||||
|
}
|
||||||
|
})();
|
||||||
65
bettercf/plugins/compact-mode/index.js
Normal file
65
bettercf/plugins/compact-mode/index.js
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
// Compact Mode Plugin for BetterCurseForge
|
||||||
|
// Reduces padding and spacing for a denser UI layout
|
||||||
|
|
||||||
|
const removeCSS = BetterCurseForge.injectCSS(`
|
||||||
|
/* Reduce card padding */
|
||||||
|
[class*="card"],
|
||||||
|
[class*="Card"] {
|
||||||
|
padding: 8px !important;
|
||||||
|
margin-bottom: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce list item spacing */
|
||||||
|
[class*="list-item"],
|
||||||
|
[class*="ListItem"],
|
||||||
|
[class*="mod-row"],
|
||||||
|
[class*="ModRow"] {
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce header sizes */
|
||||||
|
[class*="section-header"],
|
||||||
|
[class*="SectionHeader"] {
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce sidebar item padding */
|
||||||
|
[class*="sidebar"] [class*="item"],
|
||||||
|
[class*="Sidebar"] [class*="Item"],
|
||||||
|
nav a, nav button {
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce gap between elements */
|
||||||
|
[class*="grid"],
|
||||||
|
[class*="Grid"] {
|
||||||
|
gap: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smaller mod thumbnails */
|
||||||
|
[class*="thumbnail"],
|
||||||
|
[class*="Thumbnail"],
|
||||||
|
[class*="mod-icon"],
|
||||||
|
[class*="ModIcon"] {
|
||||||
|
width: 40px !important;
|
||||||
|
height: 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce page padding */
|
||||||
|
[class*="page-content"],
|
||||||
|
[class*="PageContent"],
|
||||||
|
[class*="content-area"],
|
||||||
|
[class*="ContentArea"] {
|
||||||
|
padding: 12px !important;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Plugin lifecycle
|
||||||
|
plugin.onDisable = () => {
|
||||||
|
removeCSS();
|
||||||
|
console.log('[Compact Mode] Disabled.');
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[Compact Mode] Enabled — UI is now more compact.');
|
||||||
6
bettercf/plugins/compact-mode/manifest.json
Normal file
6
bettercf/plugins/compact-mode/manifest.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "Compact Mode",
|
||||||
|
"author": "BetterCF Team",
|
||||||
|
"description": "Reduces padding and spacing throughout the UI for a more compact layout",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
215
bettercf/themes/purple-night.css
Normal file
215
bettercf/themes/purple-night.css
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
/* BetterCurseForge — Purple Night Theme */
|
||||||
|
/* A rich, sleek, fully-integrated dark purple theme for CurseForge Launcher */
|
||||||
|
|
||||||
|
:root,
|
||||||
|
.cf-app,
|
||||||
|
.base-skin,
|
||||||
|
.dark-skin,
|
||||||
|
.cf-classic,
|
||||||
|
.theme-purple-night {
|
||||||
|
/* ── Accent & Brand ── */
|
||||||
|
--surface-brand-brand: #8b5cf6 !important;
|
||||||
|
--surface-brand-brand-hover: #9d74f7 !important;
|
||||||
|
--color-primary: #8b5cf6 !important;
|
||||||
|
--color-primary-hover: #9d74f7 !important;
|
||||||
|
--color-primary-active: #7c3aed !important;
|
||||||
|
--color-accent: #a78bfa !important;
|
||||||
|
--color-highlight: #8b5cf6 !important;
|
||||||
|
--border-primary: #8b5cf6 !important;
|
||||||
|
--border-highlight: #8b5cf6 !important;
|
||||||
|
--selected-item-border: 3px solid #8b5cf6 !important;
|
||||||
|
|
||||||
|
/* ── Backgrounds & Surfaces ── */
|
||||||
|
--color-background: #0d0818 !important;
|
||||||
|
--surface-base: #07040d !important;
|
||||||
|
--surface-base-half: rgba(7, 4, 13, 0.5) !important;
|
||||||
|
--surface-primary: #110b20 !important;
|
||||||
|
--surface-secondary: #170e2b !important;
|
||||||
|
--surface-tertiery: #21153d !important;
|
||||||
|
--surface-tertiery-hover: #2c1c52 !important;
|
||||||
|
--surface-quartinery: #382468 !important;
|
||||||
|
--surface-on-surface: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
--surface-on-surface-hover: rgba(255, 255, 255, 0.14) !important;
|
||||||
|
|
||||||
|
/* ── Cards (Modpack / Project) ── */
|
||||||
|
--surface-card: #170e2b !important;
|
||||||
|
--surface-card-hover: #221540 !important;
|
||||||
|
--color-surface-card: #170e2b !important;
|
||||||
|
--color-surface-card-hover: #221540 !important;
|
||||||
|
--color-surface-card-active: #2b1b50 !important;
|
||||||
|
--color-surface-card-active-hover: #352163 !important;
|
||||||
|
--color-surface-card-disabled: #100a1e !important;
|
||||||
|
--color-surface-info-block: #21153d !important;
|
||||||
|
--color-surface-folder-view-item: #21153d !important;
|
||||||
|
|
||||||
|
/* ── Tables & Lists ── */
|
||||||
|
--color-surface-table: #170e2b !important;
|
||||||
|
--color-surface-table-hover: #21153d !important;
|
||||||
|
--color-surface-table-head: #1c1134 !important;
|
||||||
|
--color-surface-table-head-hover: #261747 !important;
|
||||||
|
|
||||||
|
/* ── Modals & Dialogs ── */
|
||||||
|
--color-surface-modal: #170e2b !important;
|
||||||
|
--color-surface-modal-footer: #110b20 !important;
|
||||||
|
--color-modal-border: #2c1c52 !important;
|
||||||
|
--color-modal-backdrop: rgba(7, 4, 13, 0.85) !important;
|
||||||
|
--color-modal-backdrop-low: rgba(7, 4, 13, 0.5) !important;
|
||||||
|
|
||||||
|
/* ── Title Bar ── */
|
||||||
|
--title-bar-background: linear-gradient(90deg, #110b20 0%, #1a1032 50%, #110b20 100%) !important;
|
||||||
|
--btn-window-action: #a78bfa !important;
|
||||||
|
--btn-window-action-hover: #2c1c52 !important;
|
||||||
|
|
||||||
|
/* ── Typography & Text ── */
|
||||||
|
--text-pure: #ffffff !important;
|
||||||
|
--text-primary: #f1f5f9 !important;
|
||||||
|
--color-text-title: #f8fafc !important;
|
||||||
|
--color-text-title-hover: #ffffff !important;
|
||||||
|
--color-text-primary: #e2e8f0 !important;
|
||||||
|
--color-text-body: #cbd5e1 !important;
|
||||||
|
--color-text-secondary: #94a3b8 !important;
|
||||||
|
--color-text-secondary-icon: #94a3b8 !important;
|
||||||
|
--color-text-dropdown: #e2e8f0 !important;
|
||||||
|
--color-text-tooltip: #ede9fe !important;
|
||||||
|
|
||||||
|
/* ── Buttons ── */
|
||||||
|
--btn-primary-text: #ffffff !important;
|
||||||
|
--btn-secondary-hover: rgba(139, 92, 246, 0.16) !important;
|
||||||
|
--btn-secondary-border: rgba(139, 92, 246, 0.35) !important;
|
||||||
|
--btn-clean-hover: #21153d !important;
|
||||||
|
--btn-tertiary-background: #21153d !important;
|
||||||
|
--btn-tertiary-hover-background: #2c1c52 !important;
|
||||||
|
--btn-tertiary-text: #f1f5f9 !important;
|
||||||
|
--btn-tertiary-hover-text: #ffffff !important;
|
||||||
|
|
||||||
|
/* ── Borders & Dividers ── */
|
||||||
|
--border-tertiery: rgba(139, 92, 246, 0.18) !important;
|
||||||
|
--color-divider-1: #21153d !important;
|
||||||
|
--color-divider-2: #2c1c52 !important;
|
||||||
|
|
||||||
|
/* ── Scrollbars ── */
|
||||||
|
--color-scrollbar-thumb: #3b2266 !important;
|
||||||
|
|
||||||
|
/* ── Theme Thumbnail Preview ── */
|
||||||
|
--color-thumb-primary: #8b5cf6 !important;
|
||||||
|
--color-thumb-title: #3b2266 !important;
|
||||||
|
--color-thumb-body: #241442 !important;
|
||||||
|
--color-thumb-name: #ffffff !important;
|
||||||
|
--color-thumb-icon-bg: #8b5cf6 !important;
|
||||||
|
--color-thumb-check-color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Element-Specific Polish ── */
|
||||||
|
|
||||||
|
/* Title bar */
|
||||||
|
.title-bar,
|
||||||
|
[class*="title-bar"],
|
||||||
|
[class*="TitleBar"] {
|
||||||
|
background: var(--title-bar-background) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidenav / left navigation */
|
||||||
|
.sidenav,
|
||||||
|
[class*="sidenav"] {
|
||||||
|
background-color: var(--surface-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar game icon active indicator bar */
|
||||||
|
.games-side-menu li a:after {
|
||||||
|
background-color: var(--surface-brand-brand) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User profile button & avatar border */
|
||||||
|
#user-login-menu .user-profile-button:before,
|
||||||
|
.user-profile-button:before {
|
||||||
|
border-color: var(--surface-brand-brand) !important;
|
||||||
|
}
|
||||||
|
#user-login-menu .user-profile-button:hover:before,
|
||||||
|
#user-login-menu.is-open .user-profile-button:before,
|
||||||
|
.user-profile-button:hover:before {
|
||||||
|
border-color: var(--surface-brand-brand-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar active tab indicator (settings, etc.) */
|
||||||
|
.system-side-menu ul.user-menu .user-menu-item > a.tab.is-active:before,
|
||||||
|
.system-side-menu ul.user-menu .user-menu-item > button.tab.is-active:before,
|
||||||
|
.system-side-menu ul.user-menu .user-menu-item > .subscription-tooltip > a.tab.is-active:before {
|
||||||
|
border-left-color: var(--surface-brand-brand) !important;
|
||||||
|
}
|
||||||
|
.system-side-menu ul.user-menu .user-menu-item > a.tab.is-active,
|
||||||
|
.system-side-menu ul.user-menu .user-menu-item > button.tab.is-active {
|
||||||
|
color: var(--surface-brand-brand) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active tab indicator and line in horizontal tabs */
|
||||||
|
.tabs-menu ul .line,
|
||||||
|
.tabs-menu ul li a:before,
|
||||||
|
.tabs-menu ul li .tab-control:before {
|
||||||
|
background-color: var(--surface-brand-brand) !important;
|
||||||
|
}
|
||||||
|
.tabs-menu ul li a.is-active,
|
||||||
|
.tabs-menu ul li.is-active a {
|
||||||
|
color: var(--color-text-title, #ffffff) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification badges and dots */
|
||||||
|
.highlight-indicator {
|
||||||
|
background-color: #8b5cf6 !important;
|
||||||
|
box-shadow: 0 0 6px rgba(139, 92, 246, 0.6) !important;
|
||||||
|
}
|
||||||
|
.badge-active:before,
|
||||||
|
.count-badge-wrapper .count-badge,
|
||||||
|
.popover-button button.btn-clean.btn-with-icon.has-badge:after {
|
||||||
|
background: var(--surface-brand-brand) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary buttons */
|
||||||
|
.btn-primary:not(.btn-secondary),
|
||||||
|
button[class*="primary"]:not([class*="secondary"]) {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:not(.btn-secondary):hover,
|
||||||
|
button[class*="primary"]:not([class*="secondary"]):hover {
|
||||||
|
background: linear-gradient(135deg, #9d74f7 0%, #8b5cf6 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Modpack card typography */
|
||||||
|
.tile-view-item .title,
|
||||||
|
.tile-view-item .title .name-container .mod-name {
|
||||||
|
color: var(--color-text-title, #ffffff) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-view-item .author {
|
||||||
|
color: var(--color-text-secondary, #94a3b8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modpack card version badge */
|
||||||
|
.tile-view-item .art .modpack-version:before {
|
||||||
|
background-color: rgba(23, 14, 43, 0.85) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-view-item .art .modpack-version {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection */
|
||||||
|
::selection {
|
||||||
|
background: rgba(139, 92, 246, 0.4) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbars */
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--surface-base);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-scrollbar-thumb);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #52318c;
|
||||||
|
}
|
||||||
10
bettercf/themes/purple-night.json
Normal file
10
bettercf/themes/purple-night.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "Purple Night",
|
||||||
|
"author": "BetterCF Team",
|
||||||
|
"description": "A sleek purple-tinted dark theme for CurseForge",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"primaryColor": "#8b5cf6",
|
||||||
|
"titleColor": "#a78bfa",
|
||||||
|
"bodyColor": "#2e1065",
|
||||||
|
"backgroundColor": "#130e20"
|
||||||
|
}
|
||||||
81
docs/.vitepress/config.ts
Normal file
81
docs/.vitepress/config.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { defineConfig } from 'vitepress';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
title: 'BetterCurseForge',
|
||||||
|
description: 'Client mod for CurseForge Launcher',
|
||||||
|
appearance: 'dark',
|
||||||
|
lastUpdated: true,
|
||||||
|
cleanUrls: true,
|
||||||
|
|
||||||
|
head: [
|
||||||
|
['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
|
||||||
|
['meta', { name: 'theme-color', content: '#9333ea' }],
|
||||||
|
['meta', { name: 'og:type', content: 'website' }],
|
||||||
|
['meta', { name: 'og:title', content: 'BetterCurseForge' }],
|
||||||
|
['meta', { name: 'og:description', content: 'Client mod for CurseForge Launcher — Ad blocking, custom themes, and powerful plugin API.' }],
|
||||||
|
],
|
||||||
|
|
||||||
|
themeConfig: {
|
||||||
|
siteTitle: 'BetterCurseForge',
|
||||||
|
|
||||||
|
nav: [
|
||||||
|
{ text: 'Guide', link: '/guide/installation' },
|
||||||
|
{ text: 'API', link: '/api/' },
|
||||||
|
{ text: 'GitHub', link: 'https://github.com/BetterCurseForge/BetterCurseForge' },
|
||||||
|
],
|
||||||
|
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
text: 'Getting Started',
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: 'Installation', link: '/guide/installation' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Themes',
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: 'Custom Themes', link: '/guide/themes' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Plugins',
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: 'Plugin Development', link: '/guide/plugins' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'API Reference',
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: 'Full API Reference', link: '/api/' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
socialLinks: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/BetterCurseForge/BetterCurseForge' },
|
||||||
|
],
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
message: 'Released under the MIT License.',
|
||||||
|
copyright: 'Copyright © 2026 BetterCurseForge Contributors',
|
||||||
|
},
|
||||||
|
|
||||||
|
search: {
|
||||||
|
provider: 'local',
|
||||||
|
},
|
||||||
|
|
||||||
|
outline: {
|
||||||
|
level: [2, 3],
|
||||||
|
label: 'On this page',
|
||||||
|
},
|
||||||
|
|
||||||
|
docFooter: {
|
||||||
|
prev: 'Previous page',
|
||||||
|
next: 'Next page',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
38
docs/.vitepress/theme/custom.css
Normal file
38
docs/.vitepress/theme/custom.css
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
/* BetterCurseForge Documentation Theme - Purple Accent */
|
||||||
|
:root {
|
||||||
|
--vp-c-brand-1: #9333ea;
|
||||||
|
--vp-c-brand-2: #a855f7;
|
||||||
|
--vp-c-brand-3: #7e22ce;
|
||||||
|
--vp-c-brand-soft: rgba(168, 85, 247, 0.14);
|
||||||
|
|
||||||
|
--vp-home-hero-name-color: transparent;
|
||||||
|
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, #a855f7 30%, #ec4899);
|
||||||
|
--vp-home-hero-image-background-image: linear-gradient(-45deg, #9333ea 30%, #ec4899);
|
||||||
|
--vp-home-hero-image-filter: blur(44px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--vp-c-brand-1: #a855f7;
|
||||||
|
--vp-c-brand-2: #c084fc;
|
||||||
|
--vp-c-brand-3: #9333ea;
|
||||||
|
--vp-c-brand-soft: rgba(168, 85, 247, 0.2);
|
||||||
|
|
||||||
|
--vp-home-hero-name-color: transparent;
|
||||||
|
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, #c084fc 25%, #f472b6);
|
||||||
|
--vp-home-hero-image-background-image: linear-gradient(-45deg, #a855f7 30%, #f472b6);
|
||||||
|
--vp-home-hero-image-filter: blur(56px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom styling enhancements */
|
||||||
|
.custom-block.tip {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc a {
|
||||||
|
text-decoration-color: var(--vp-c-brand-soft);
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc a:hover {
|
||||||
|
text-decoration-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
6
docs/.vitepress/theme/index.ts
Normal file
6
docs/.vitepress/theme/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import DefaultTheme from 'vitepress/theme';
|
||||||
|
import './custom.css';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: DefaultTheme,
|
||||||
|
};
|
||||||
261
docs/api/index.md
Normal file
261
docs/api/index.md
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
# API Reference
|
||||||
|
|
||||||
|
BetterCurseForge exposes a global runtime object on `window.BetterCurseForge` within the CurseForge Electron renderer process. This API allows themes, plugins, and developer scripts to inject styles, monkey-patch methods, manage settings, and listen to application events.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TypeScript Definition
|
||||||
|
|
||||||
|
For TypeScript development and autocompletion:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
BetterCurseForge: BetterCurseForgeAPI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BetterCurseForgeAPI {
|
||||||
|
/** Active BetterCurseForge runtime version */
|
||||||
|
readonly version: string;
|
||||||
|
|
||||||
|
/** Injects CSS dynamically into <head>. Returns an unmount callback. */
|
||||||
|
injectCSS(css: string): () => void;
|
||||||
|
|
||||||
|
/** Persistent settings storage */
|
||||||
|
settings: {
|
||||||
|
get(key?: string): Promise<any>;
|
||||||
|
set(key: string, value: any): Promise<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Runtime method interception and monkey-patching */
|
||||||
|
patcher: {
|
||||||
|
before(obj: any, method: string, callback: (args: any[]) => void): () => void;
|
||||||
|
after(obj: any, method: string, callback: (args: any[], result: any) => any): () => void;
|
||||||
|
instead(obj: any, method: string, callback: (args: any[], original: Function) => any): () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Event bus */
|
||||||
|
on(event: string, callback: (...args: any[]) => void): void;
|
||||||
|
off(event: string, callback: (...args: any[]) => void): void;
|
||||||
|
emit(event: string, ...args: any[]): void;
|
||||||
|
|
||||||
|
/** Internal subsystem managers */
|
||||||
|
themes: ThemeManager;
|
||||||
|
plugins: PluginManager;
|
||||||
|
ads: AdBlocker;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core API
|
||||||
|
|
||||||
|
### `BetterCurseForge.version`
|
||||||
|
|
||||||
|
- **Type**: `string`
|
||||||
|
- **Description**: The currently running BetterCurseForge runtime version.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
console.log(BetterCurseForge.version); // "1.0.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `BetterCurseForge.injectCSS(css)`
|
||||||
|
|
||||||
|
Dynamically injects CSS rules into the document `<head>`.
|
||||||
|
|
||||||
|
- **Parameters**:
|
||||||
|
- `css` *(string)*: Raw CSS string to inject.
|
||||||
|
- **Returns**: `() => void` — Function that removes the injected `<style>` tag when called.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Inject styles
|
||||||
|
const uninject = BetterCurseForge.injectCSS(`
|
||||||
|
.some-container {
|
||||||
|
background-color: #1a102f !important;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Later, remove the styles
|
||||||
|
uninject();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Settings API (`BetterCurseForge.settings`)
|
||||||
|
|
||||||
|
Manages persistent configuration stored in `%APPDATA%\CurseForge\BetterCurseForge\settings.json`.
|
||||||
|
|
||||||
|
### `settings.get(key?)`
|
||||||
|
|
||||||
|
Retrieves a setting by key, or returns the entire settings object if no key is specified.
|
||||||
|
|
||||||
|
- **Parameters**:
|
||||||
|
- `key` *(string, optional)*: Setting key name.
|
||||||
|
- **Returns**: `Promise<any>` — Value of the setting, or `null`.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Retrieve a specific value
|
||||||
|
const activeTheme = await BetterCurseForge.settings.get('activeCustomTheme');
|
||||||
|
|
||||||
|
// Retrieve all settings
|
||||||
|
const allSettings = await BetterCurseForge.settings.get();
|
||||||
|
```
|
||||||
|
|
||||||
|
### `settings.set(key, value)`
|
||||||
|
|
||||||
|
Stores a setting value and writes it asynchronously to disk.
|
||||||
|
|
||||||
|
- **Parameters**:
|
||||||
|
- `key` *(string)*: Setting key name.
|
||||||
|
- `value` *(any)*: JSON-serializable value.
|
||||||
|
- **Returns**: `Promise<boolean>` — `true` if saved successfully.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await BetterCurseForge.settings.set('myPlugin.fontSize', 14);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Patcher API (`BetterCurseForge.patcher`)
|
||||||
|
|
||||||
|
Provides monkey-patching utilities to inspect, modify, or replace methods on JavaScript objects and modules safely.
|
||||||
|
|
||||||
|
### `patcher.before(obj, method, callback)`
|
||||||
|
|
||||||
|
Executes `callback(args)` **before** the target method is invoked.
|
||||||
|
|
||||||
|
- **Parameters**:
|
||||||
|
- `obj` *(object)*: Target object.
|
||||||
|
- `method` *(string)*: Method name on `obj`.
|
||||||
|
- `callback` *(Function)*: Function receiving arguments: `(args) => void`.
|
||||||
|
- **Returns**: `() => void` — Unpatch function that restores the original method.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const unpatch = BetterCurseForge.patcher.before(console, 'warn', (args) => {
|
||||||
|
console.log('[Warn Interceptor]', ...args);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Revert patch:
|
||||||
|
unpatch();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `patcher.after(obj, method, callback)`
|
||||||
|
|
||||||
|
Executes `callback(args, result)` **after** the target method runs. You can return a new value to replace the method's return value.
|
||||||
|
|
||||||
|
- **Parameters**:
|
||||||
|
- `obj` *(object)*: Target object.
|
||||||
|
- `method` *(string)*: Method name on `obj`.
|
||||||
|
- `callback` *(Function)*: `(args, result) => any`.
|
||||||
|
- **Returns**: `() => void` — Unpatch function.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const unpatch = BetterCurseForge.patcher.after(service, 'getTitle', (args, result) => {
|
||||||
|
return `Prefix: ${result}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
unpatch();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `patcher.instead(obj, method, callback)`
|
||||||
|
|
||||||
|
Completely overrides the target method. Receives both the incoming arguments and a reference to the original method.
|
||||||
|
|
||||||
|
- **Parameters**:
|
||||||
|
- `obj` *(object)*: Target object.
|
||||||
|
- `method` *(string)*: Method name on `obj`.
|
||||||
|
- `callback` *(Function)*: `(args, originalFn) => any`.
|
||||||
|
- **Returns**: `() => void` — Unpatch function.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const unpatch = BetterCurseForge.patcher.instead(target, 'openExternalUrl', (args, originalFn) => {
|
||||||
|
const url = args[0];
|
||||||
|
if (url.startsWith('https://safe-domain.com')) {
|
||||||
|
return originalFn(...args);
|
||||||
|
}
|
||||||
|
console.warn('Blocked external URL navigation:', url);
|
||||||
|
});
|
||||||
|
|
||||||
|
unpatch();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Event System (`BetterCurseForge.on / off / emit`)
|
||||||
|
|
||||||
|
BetterCurseForge includes an event bus for inter-plugin and lifecycle communication.
|
||||||
|
|
||||||
|
### `BetterCurseForge.on(event, callback)`
|
||||||
|
Subscribes a listener to an event.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const onReady = () => console.log('BetterCurseForge is ready!');
|
||||||
|
BetterCurseForge.on('ready', onReady);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `BetterCurseForge.off(event, callback)`
|
||||||
|
Unsubscribes a listener.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
BetterCurseForge.off('ready', onReady);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `BetterCurseForge.emit(event, ...args)`
|
||||||
|
Emits an event to all registered listeners.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
BetterCurseForge.emit('custom-event', { detail: 123 });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Subsystem Managers
|
||||||
|
|
||||||
|
### `BetterCurseForge.themes` (`ThemeManager`)
|
||||||
|
Provides programmatic control over custom themes:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get all discovered themes
|
||||||
|
const themes = BetterCurseForge.themes.getThemes();
|
||||||
|
|
||||||
|
// Get active theme filename (or null)
|
||||||
|
const currentTheme = BetterCurseForge.themes.getActiveThemeName();
|
||||||
|
|
||||||
|
// Preview a theme temporarily on screen
|
||||||
|
BetterCurseForge.themes.previewTheme(themes[0]);
|
||||||
|
|
||||||
|
// Revert preview back to active theme
|
||||||
|
BetterCurseForge.themes.cancelPreview();
|
||||||
|
|
||||||
|
// Apply and save a theme permanently
|
||||||
|
BetterCurseForge.themes.applyTheme(themes[0], true);
|
||||||
|
|
||||||
|
// Remove custom theme and revert to native CurseForge skin
|
||||||
|
BetterCurseForge.themes.removeTheme(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `BetterCurseForge.plugins` (`PluginManager`)
|
||||||
|
Provides programmatic control over plugins:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get all installed plugins
|
||||||
|
const plugins = BetterCurseForge.plugins.getPlugins();
|
||||||
|
|
||||||
|
// Toggle a plugin by folder name
|
||||||
|
await BetterCurseForge.plugins.togglePlugin('compact-mode');
|
||||||
|
```
|
||||||
|
|
||||||
|
### `BetterCurseForge.ads` (`AdBlocker`)
|
||||||
|
Internal ad blocker runtime:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Contains zero-ad CSS rules and ad container cleanup handlers
|
||||||
|
console.log(BetterCurseForge.ads);
|
||||||
|
```
|
||||||
179
docs/guide/installation.md
Normal file
179
docs/guide/installation.md
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
# Installation & Usage
|
||||||
|
|
||||||
|
This guide covers installing, managing, and uninstalling **BetterCurseForge** using either the desktop GUI application or the command-line interface.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before using BetterCurseForge, make sure you have:
|
||||||
|
|
||||||
|
- **Windows 10 or Windows 11** (64-bit)
|
||||||
|
- **CurseForge Launcher** installed (standalone or Overwolf edition)
|
||||||
|
- CurseForge closed before running the patcher
|
||||||
|
|
||||||
|
::: tip SAFE & 100% REVERSIBLE
|
||||||
|
Before modifying any files, BetterCurseForge creates an untouched vanilla backup at `resources/app.asar.vanilla.bak`. You can cleanly revert to stock CurseForge at any time with a single click.
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Method 1: Using the Desktop GUI (Recommended)
|
||||||
|
|
||||||
|
BetterCurseForge comes as a standalone desktop executable with a sleek, dark-themed graphical interface.
|
||||||
|
|
||||||
|
### Step 1: Download `BetterCurseForge.exe`
|
||||||
|
|
||||||
|
Download the latest executable from the official releases:
|
||||||
|
|
||||||
|
- [Download `BetterCurseForge.exe`](https://github.com/BetterCurseForge/BetterCurseForge/releases/latest)
|
||||||
|
|
||||||
|
Place it in any convenient folder (e.g., Downloads, Tools, or desktop).
|
||||||
|
|
||||||
|
### Step 2: Open BetterCurseForge
|
||||||
|
|
||||||
|
Double-click `BetterCurseForge.exe` to launch the application.
|
||||||
|
|
||||||
|
{width=64 style="display:block;margin:1rem auto;"}
|
||||||
|
|
||||||
|
The application automatically:
|
||||||
|
- Detects your CurseForge installation folder from the Windows Registry.
|
||||||
|
- Checks whether your client is currently vanilla or patched.
|
||||||
|
- Displays the installed CurseForge version and installation directory.
|
||||||
|
|
||||||
|
### Step 3: Click "Patch CurseForge"
|
||||||
|
|
||||||
|
1. Click the **Patch CurseForge** button.
|
||||||
|
2. The patcher will close any running CurseForge processes, create a vanilla backup, apply all enhancements (ad removal, theme engine, premium unlocks, and plugin loader), update the executable integrity signature, and repack the application.
|
||||||
|
3. You will see live output in the log panel confirming:
|
||||||
|
```text
|
||||||
|
[+] Found CurseForge at: C:\Users\Username\AppData\Local\Programs\CurseForge Windows
|
||||||
|
[+] Disabled React ad component (eV -> return null)
|
||||||
|
[+] Disabled swap ads button
|
||||||
|
[+] Unlocked all native premium themes (Settings route)
|
||||||
|
[+] Unlocked all native premium themes (Popup route)
|
||||||
|
[+] Injected zero-ad and theme CSS rules into desktop.css
|
||||||
|
[+] Bundled BetterCurseForge runtime into asar
|
||||||
|
[+] Updated CurseForge.exe integrity signature
|
||||||
|
[*] CurseForge successfully patched! Ads removed, themes & plugins enabled.
|
||||||
|
```
|
||||||
|
4. Click **Launch CurseForge** or open your usual desktop shortcut—you're done!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Method 2: Command-Line Interface (CLI)
|
||||||
|
|
||||||
|
For automated workflows, scripting, or headless environments, `BetterCurseForge.exe` provides a comprehensive CLI:
|
||||||
|
|
||||||
|
### Patching via Terminal
|
||||||
|
|
||||||
|
Open PowerShell or Command Prompt in the folder containing `BetterCurseForge.exe`:
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```powershell [PowerShell]
|
||||||
|
.\BetterCurseForge.exe patch
|
||||||
|
```
|
||||||
|
|
||||||
|
```cmd [Command Prompt]
|
||||||
|
BetterCurseForge.exe patch
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Checking Status
|
||||||
|
|
||||||
|
Verify whether your client is patched:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\BetterCurseForge.exe status
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```text
|
||||||
|
CurseForge Status: PATCHED (BetterCurseForge v1.0.0)
|
||||||
|
Path: C:\Users\Username\AppData\Local\Programs\CurseForge Windows
|
||||||
|
ASAR Hash: Verified
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unpatching
|
||||||
|
|
||||||
|
Restore your vanilla CurseForge client:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\BetterCurseForge.exe unpatch
|
||||||
|
```
|
||||||
|
|
||||||
|
This restores `resources/app.asar` from the vanilla backup and re-verifies original executable signatures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auto-Repatch: Surviving CurseForge Updates
|
||||||
|
|
||||||
|
When the official CurseForge Launcher downloads an automatic update, it overwrites `app.asar` with a new version, temporarily removing client modifications.
|
||||||
|
|
||||||
|
BetterCurseForge solves this with **Auto-Repatch**:
|
||||||
|
|
||||||
|
### Enabling Auto-Repatch in the GUI
|
||||||
|
- In the BetterCurseForge desktop application, simply toggle the **Auto-Repatch on Update** checkbox.
|
||||||
|
|
||||||
|
### Enabling Auto-Repatch via CLI
|
||||||
|
```powershell
|
||||||
|
.\BetterCurseForge.exe --auto-repatch enable
|
||||||
|
```
|
||||||
|
|
||||||
|
### How Auto-Repatch Works
|
||||||
|
1. Registers a lightweight Windows background task.
|
||||||
|
2. Detects whenever CurseForge's files are modified or updated by Overwolf/CurseForge.
|
||||||
|
3. Automatically closes the client, creates a new backup of the updated version, applies the BetterCurseForge patches, updates the binary signature, and re-launches your modded client silently.
|
||||||
|
4. To disable anytime:
|
||||||
|
```powershell
|
||||||
|
.\BetterCurseForge.exe --auto-repatch disable
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How It Works Under the Hood
|
||||||
|
|
||||||
|
Modern versions of CurseForge include **ASAR Code Integrity Verification** enforced directly inside `CurseForge.exe`:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["BetterCurseForge.exe"] --> B["Locate CurseForge Windows Installation"]
|
||||||
|
B --> C["Extract app.asar & Create app.asar.vanilla.bak"]
|
||||||
|
C --> D["Disable React Ad Components & Swap Ads"]
|
||||||
|
D --> E["Unlock Native Premium Themes (Pixelforge, Lifeforge, etc.)"]
|
||||||
|
E --> F["Inject Zero-Ad & Responsive CSS into desktop.css"]
|
||||||
|
F --> G["Bundle BetterCurseForge Core Runtime into asar"]
|
||||||
|
G --> H["Repack app.asar & Calculate SHA256 Header Hash"]
|
||||||
|
H --> I["Update CurseForge.exe ASAR Integrity Signature"]
|
||||||
|
I --> J["Launch Modded CurseForge with Full Permissions"]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **AST & Component Patching**: Replaces the React ad rendering module (`eV -> return null`) to ensure ad frames are never mounted in the virtual DOM.
|
||||||
|
2. **Premium Skin Unlock**: Patches the client-side plan validation routes so all 4 native CurseForge skins (*Pixelforge*, *Lifeforge*, *Fantasyforge*, *Spookyforge*) can be freely selected in **Settings -> Themes**.
|
||||||
|
3. **ASAR Signature Patching**: Calculates the cryptographic SHA256 hash of the modified ASAR archive header and writes the new signature into `CurseForge.exe`, ensuring Electron boots without security errors or corruption alerts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting & FAQ
|
||||||
|
|
||||||
|
::: details How do I verify BetterCurseForge is running?
|
||||||
|
1. Open CurseForge.
|
||||||
|
2. Notice the total absence of banner ads and video sponsor frames.
|
||||||
|
3. Navigate to **Settings -> Themes**: you will see the **Client Themes** section with *Purple Night* available, as well as unlocked *Premium* skins (*Pixelforge*, *Lifeforge*, etc.).
|
||||||
|
4. Press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd> to open DevTools. The console will display:
|
||||||
|
```text
|
||||||
|
[BetterCF] Initializing renderer runtime...
|
||||||
|
[BetterCF] Theme manager ready (1 theme(s) found). Active: purple-night.css
|
||||||
|
[BetterCF] ✓ BetterCurseForge loaded successfully!
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: details Antivirus false-positive warning
|
||||||
|
Because `BetterCurseForge.exe` inspects running processes (`CurseForge.exe`), accesses the Windows Registry to locate install paths, and updates executable hashes, some heuristic scanners may flag it. BetterCurseForge is completely open source and safe. You can inspect the entire Python codebase in `patcher/` and build it yourself.
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: details Permission denied / EPERM error
|
||||||
|
If your CurseForge launcher is installed under `C:\Program Files`, modifying files requires Administrator permissions. Right-click `BetterCurseForge.exe` and select **Run as Administrator**.
|
||||||
|
:::
|
||||||
215
docs/guide/plugins.md
Normal file
215
docs/guide/plugins.md
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
# Writing Plugins
|
||||||
|
|
||||||
|
BetterCurseForge features a lightweight plugin system that allows you to extend CurseForge Launcher functionality, manipulate the DOM, hook runtime methods, and persist configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugin Directory Structure
|
||||||
|
|
||||||
|
Plugins are stored in the BetterCurseForge user directory:
|
||||||
|
|
||||||
|
```text
|
||||||
|
%APPDATA%\CurseForge\BetterCurseForge\plugins\
|
||||||
|
```
|
||||||
|
|
||||||
|
Each plugin lives in its own dedicated subfolder:
|
||||||
|
|
||||||
|
```text
|
||||||
|
plugins/
|
||||||
|
└── compact-mode/
|
||||||
|
├── manifest.json # Required: Plugin metadata
|
||||||
|
├── index.js # Required: Entry script
|
||||||
|
└── styles.css # Optional: Companion stylesheet
|
||||||
|
```
|
||||||
|
|
||||||
|
You can open the plugins folder directly from the BetterCurseForge desktop GUI by clicking **Open Plugins Folder**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugin Manifest (`manifest.json`)
|
||||||
|
|
||||||
|
The `manifest.json` file declares your plugin's metadata:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "compact-mode",
|
||||||
|
"name": "Compact Mode",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Reduces padding across mod lists, sidebar items, and modpack details for high-density browsing.",
|
||||||
|
"author": "ModderDev",
|
||||||
|
"main": "index.js",
|
||||||
|
"styles": "styles.css"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manifest Fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `name` | `string` | **Yes** | Display name shown in the management UI. |
|
||||||
|
| `version` | `string` | **Yes** | Semantic version string (e.g. `1.0.0`). |
|
||||||
|
| `description` | `string` | No | Brief summary of what the plugin accomplishes. |
|
||||||
|
| `author` | `string` | No | Author name or handle. |
|
||||||
|
| `main` | `string` | No | Entry JavaScript file. Defaults to `index.js`. |
|
||||||
|
| `styles` | `string` | No | Companion stylesheet injected automatically on enable. Defaults to `styles.css`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugin Lifecycle & Execution
|
||||||
|
|
||||||
|
Plugin scripts are executed when enabled, receiving the `BetterCurseForge` global API and a `plugin` context object:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// index.js
|
||||||
|
// BetterCurseForge passes (BetterCurseForge, plugin)
|
||||||
|
const { injectCSS, patcher, settings, on, off } = BetterCurseForge;
|
||||||
|
|
||||||
|
console.log('[MyPlugin] Plugin initialized!');
|
||||||
|
|
||||||
|
// Attach an onDisable cleanup handler:
|
||||||
|
plugin.onDisable = () => {
|
||||||
|
console.log('[MyPlugin] Plugin disabled! Cleaning up...');
|
||||||
|
// Remove any custom DOM elements, event listeners, or intervals here
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip AUTOMATIC STYLE INJECTION & REMOVAL
|
||||||
|
If your plugin folder contains a `styles.css` (or file declared in `"styles"` in `manifest.json`), BetterCurseForge automatically injects it into `<head>` when your plugin is enabled and removes it when disabled! You do not need to manually inject or clean up companion stylesheets.
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BetterCurseForge Plugin APIs
|
||||||
|
|
||||||
|
### 1. `BetterCurseForge.injectCSS(css)`
|
||||||
|
Injects custom CSS dynamically into `<head>`. Returns a cleanup function that unmounts the style tag:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const removeStyles = BetterCurseForge.injectCSS(`
|
||||||
|
.addon-row { padding: 4px 8px !important; }
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Later in cleanup:
|
||||||
|
plugin.onDisable = () => {
|
||||||
|
removeStyles();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `BetterCurseForge.settings`
|
||||||
|
Stores and retrieves persistent settings saved to disk (`settings.json`):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Retrieve a stored setting (or null if not set)
|
||||||
|
const isCustomFont = await BetterCurseForge.settings.get('compactMode.customFont');
|
||||||
|
|
||||||
|
// Store a setting
|
||||||
|
await BetterCurseForge.settings.set('compactMode.density', 'ultra');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. `BetterCurseForge.patcher`
|
||||||
|
Safe method monkey-patching for objects and functions:
|
||||||
|
|
||||||
|
#### `patcher.before(object, methodName, callback)`
|
||||||
|
Runs your callback before the target method executes. Receives `(args)`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const unpatch = BetterCurseForge.patcher.before(console, 'error', (args) => {
|
||||||
|
console.warn('[Intercepted Console Error]', ...args);
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.onDisable = () => unpatch();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `patcher.after(object, methodName, callback)`
|
||||||
|
Runs after the target method completes. Receives `(args, result)`. You can return a modified value:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const unpatch = BetterCurseForge.patcher.after(targetObj, 'getItemTitle', (args, result) => {
|
||||||
|
return `⚡ ${result}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.onDisable = () => unpatch();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `patcher.instead(object, methodName, callback)`
|
||||||
|
Completely supersedes the target method. Receives `(args, originalMethod)`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const unpatch = BetterCurseForge.patcher.instead(targetObj, 'fetchData', (args, originalFn) => {
|
||||||
|
console.log('Intercepting fetchData call...');
|
||||||
|
return originalFn(...args);
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.onDisable = () => unpatch();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. `BetterCurseForge.on(event, callback)` / `.off()` / `.emit()`
|
||||||
|
Application-wide event emitter:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const onReady = () => console.log('CurseForge UI is fully loaded!');
|
||||||
|
|
||||||
|
BetterCurseForge.on('ready', onReady);
|
||||||
|
|
||||||
|
plugin.onDisable = () => {
|
||||||
|
BetterCurseForge.off('ready', onReady);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Example: Compact Mode Plugin
|
||||||
|
|
||||||
|
This plugin adds a high-density layout mode to CurseForge.
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```json [manifest.json]
|
||||||
|
{
|
||||||
|
"id": "compact-mode",
|
||||||
|
"name": "Compact Mode",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Reduces margins and padding for high-density modpack browsing.",
|
||||||
|
"author": "BetterCurseForge Team",
|
||||||
|
"main": "index.js",
|
||||||
|
"styles": "styles.css"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript [index.js]
|
||||||
|
// Add compact layout class to the document root
|
||||||
|
document.documentElement.classList.add('bcf-compact-layout');
|
||||||
|
|
||||||
|
console.log('[Compact Mode] Activated');
|
||||||
|
|
||||||
|
// Clean up when toggled off
|
||||||
|
plugin.onDisable = () => {
|
||||||
|
document.documentElement.classList.remove('bcf-compact-layout');
|
||||||
|
console.log('[Compact Mode] Deactivated');
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```css [styles.css]
|
||||||
|
/* Automatically injected when plugin is enabled */
|
||||||
|
.bcf-compact-layout .tile-view-item {
|
||||||
|
width: 150px !important;
|
||||||
|
height: 240px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bcf-compact-layout .action-button-container {
|
||||||
|
height: 48px !important;
|
||||||
|
margin: 6px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bcf-compact-layout .details-title {
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
248
docs/guide/themes.md
Normal file
248
docs/guide/themes.md
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
# Custom Themes & Theming Guide
|
||||||
|
|
||||||
|
BetterCurseForge features a native, live-reloading theming engine. Themes allow you to completely restyle the CurseForge Launcher using standard CSS and theme manifests, fully integrated into the official user interface.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## In-App Theme Selector
|
||||||
|
|
||||||
|
BetterCurseForge embeds client themes directly into CurseForge's native **Settings -> Themes** menu.
|
||||||
|
|
||||||
|
### How to Access Client Themes
|
||||||
|
1. Open CurseForge Launcher.
|
||||||
|
2. Click the **Settings** gear icon in the bottom-left corner.
|
||||||
|
3. Select **Themes** from the settings sidebar.
|
||||||
|
4. Scroll down past the *Classic*, *Colorblind*, and *Premium* categories to find the **Client Themes** section:
|
||||||
|
|
||||||
|
{width=48 style="display:block;margin:1rem auto;"}
|
||||||
|
|
||||||
|
### Native Features in Settings -> Themes
|
||||||
|
- **Live Hover Preview**: Hovering your cursor over any theme card immediately previews the theme across the entire launcher in real time. Move your cursor away to revert, or click to apply.
|
||||||
|
- **Mutual Exclusivity**: Selecting a client theme seamlessly deselects native skins, and vice versa. Only one theme is active at a time.
|
||||||
|
- **"Open Folder" Button**: Directly opens your themes folder in Windows Explorer so you can drop in new stylesheets.
|
||||||
|
- **"Refresh" Button**: Reloads all installed custom themes from disk instantly without needing to restart CurseForge.
|
||||||
|
- **Native Premium Themes Unlocked**: All official CurseForge Premium themes (*Pixelforge*, *Lifeforge*, *Fantasyforge*, and *Spookyforge*) are permanently unlocked and free to use!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Theme Folder Location
|
||||||
|
|
||||||
|
Themes are stored in your user application data directory:
|
||||||
|
|
||||||
|
```text
|
||||||
|
%APPDATA%\CurseForge\BetterCurseForge\themes\
|
||||||
|
```
|
||||||
|
|
||||||
|
Full path:
|
||||||
|
```text
|
||||||
|
C:\Users\<YourUsername>\AppData\Roaming\CurseForge\BetterCurseForge\themes\
|
||||||
|
```
|
||||||
|
|
||||||
|
You can open this folder anytime by clicking the **Open Folder** button in **Settings -> Themes**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Theme File Structure
|
||||||
|
|
||||||
|
BetterCurseForge supports two convenient formats:
|
||||||
|
|
||||||
|
### 1. Single-File Theme (Recommended)
|
||||||
|
Place a `.css` file in the `themes/` directory, with an optional accompanying `.json` manifest:
|
||||||
|
|
||||||
|
```text
|
||||||
|
themes/
|
||||||
|
├── purple-night.css
|
||||||
|
└── purple-night.json (optional metadata & preview colors)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Folder-Based Theme
|
||||||
|
Create a folder inside `themes/` with a `theme.css` and `theme.json`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
themes/
|
||||||
|
└── emerald-dark/
|
||||||
|
├── theme.json
|
||||||
|
├── theme.css
|
||||||
|
└── background.webp (optional custom assets)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Theme Manifest Format (`theme.json`)
|
||||||
|
|
||||||
|
A `.json` manifest provides metadata and defines the miniature preview card colors shown in the **Settings -> Themes** menu:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Emerald Dark",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Deep obsidian tones with vivid emerald green accents.",
|
||||||
|
"author": "ModderDev",
|
||||||
|
"primaryColor": "#10b981",
|
||||||
|
"titleColor": "#064e3b",
|
||||||
|
"bodyColor": "#022c22",
|
||||||
|
"backgroundColor": "#051c14"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manifest Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `name` | `string` | Display name shown on the theme card and in settings. |
|
||||||
|
| `version` | `string` | Semantic version string (e.g. `1.0.0`). |
|
||||||
|
| `description` | `string` | Short tooltip description displayed when hovering the title. |
|
||||||
|
| `author` | `string` | Author name or handle. |
|
||||||
|
| `primaryColor` | `string` | Hex color for the primary square icon in the thumbnail card. |
|
||||||
|
| `titleColor` | `string` | Hex color for the top bar in the thumbnail card. |
|
||||||
|
| `bodyColor` | `string` | Hex color for the lower text bar in the thumbnail card. |
|
||||||
|
| `backgroundColor`| `string` | Hex background color for the thumbnail card itself. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CurseForge CSS Variables Reference
|
||||||
|
|
||||||
|
CurseForge is styled using CSS Custom Properties (variables). Overriding these variables allows you to theme 95% of the application cleanly without touching individual class names.
|
||||||
|
|
||||||
|
### Brand & Accents
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `--surface-brand-brand` | Primary brand accent (active tabs, primary buttons, indicators). |
|
||||||
|
| `--surface-brand-brand-hover` | Hover state for brand-colored elements. |
|
||||||
|
| `--color-primary` | Main accent color. |
|
||||||
|
| `--color-accent` | Secondary highlight color. |
|
||||||
|
| `--selected-item-border` | Border style for selected items and active inputs. |
|
||||||
|
|
||||||
|
### Surfaces & Backgrounds
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `--color-background` | Main application background (content views, modpack library). |
|
||||||
|
| `--surface-base` | Darkest background layer (scroll tracks, container bases). |
|
||||||
|
| `--surface-primary` | Elevated primary surface (sidebars, sub-panels). |
|
||||||
|
| `--surface-secondary` | Secondary surface layer (navigation menus, dialog panels). |
|
||||||
|
| `--surface-tertiery` | Tertiary surface layer (mod cards, list row containers). |
|
||||||
|
| `--surface-on-surface` | Subtle translucent overlay for buttons (`rgba(...)`). |
|
||||||
|
| `--surface-on-surface-hover` | Translucent hover overlay for buttons. |
|
||||||
|
|
||||||
|
### Modpack & Library Cards
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `--surface-card` | Modpack / project card base background. |
|
||||||
|
| `--surface-card-hover` | Background color when hovering over modpack cards. |
|
||||||
|
| `--color-surface-card-active` | Active or clicked state for card items. |
|
||||||
|
|
||||||
|
### Typography & Text
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `--text-pure` | Pure white text (`#ffffff`). |
|
||||||
|
| `--text-primary` | Primary readable text color. |
|
||||||
|
| `--color-text-title` | Headings, modpack titles, section names. |
|
||||||
|
| `--color-text-body` | Standard body and description text. |
|
||||||
|
| `--color-text-secondary` | Subtitles, authors, version numbers. |
|
||||||
|
|
||||||
|
### Title Bar & Navigation
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `--title-bar-background` | Background color or gradient for the top window title bar. |
|
||||||
|
| `--btn-window-action` | Minimize, maximize, and close button icons. |
|
||||||
|
| `--color-scrollbar-thumb` | Scrollbar thumb handle color. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Theme: Emerald Night
|
||||||
|
|
||||||
|
Here is a complete, working theme stylesheet you can save as `emerald-night.css`:
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```css [emerald-night.css]
|
||||||
|
/* ==========================================================================
|
||||||
|
BetterCurseForge Theme: Emerald Night
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
:root,
|
||||||
|
.cf-app,
|
||||||
|
.base-skin,
|
||||||
|
.dark-skin,
|
||||||
|
.cf-classic {
|
||||||
|
/* ── Accent & Brand ── */
|
||||||
|
--surface-brand-brand: #10b981 !important;
|
||||||
|
--surface-brand-brand-hover: #34d399 !important;
|
||||||
|
--color-primary: #10b981 !important;
|
||||||
|
--color-primary-hover: #34d399 !important;
|
||||||
|
--color-primary-active: #059669 !important;
|
||||||
|
--color-accent: #6ee7b7 !important;
|
||||||
|
--selected-item-border: 2px solid #10b981 !important;
|
||||||
|
|
||||||
|
/* ── Backgrounds & Surfaces ── */
|
||||||
|
--color-background: #06140f !important;
|
||||||
|
--surface-base: #030b08 !important;
|
||||||
|
--surface-primary: #091c16 !important;
|
||||||
|
--surface-secondary: #0d271f !important;
|
||||||
|
--surface-tertiery: #13352b !important;
|
||||||
|
--surface-tertiery-hover: #194639 !important;
|
||||||
|
|
||||||
|
/* ── Modpack & Library Cards ── */
|
||||||
|
--surface-card: #0d271f !important;
|
||||||
|
--surface-card-hover: #14382d !important;
|
||||||
|
|
||||||
|
/* ── Typography & Text ── */
|
||||||
|
--text-pure: #ffffff !important;
|
||||||
|
--text-primary: #f0fdf4 !important;
|
||||||
|
--color-text-title: #ffffff !important;
|
||||||
|
--color-text-body: #d1fae5 !important;
|
||||||
|
--color-text-secondary: #6ee7b7 !important;
|
||||||
|
|
||||||
|
/* ── Title Bar ── */
|
||||||
|
--title-bar-background: #091c16 !important;
|
||||||
|
--btn-window-action: #6ee7b7 !important;
|
||||||
|
|
||||||
|
/* ── Scrollbars ── */
|
||||||
|
--color-scrollbar-thumb: #13352b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary Action Buttons */
|
||||||
|
.btn-primary:not(.btn-secondary),
|
||||||
|
button[class*="primary"]:not([class*="secondary"]) {
|
||||||
|
background: #10b981 !important;
|
||||||
|
color: #030b08 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:not(.btn-secondary):hover,
|
||||||
|
button[class*="primary"]:not([class*="secondary"]):hover {
|
||||||
|
background: #34d399 !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json [emerald-night.json]
|
||||||
|
{
|
||||||
|
"name": "Emerald Night",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Deep emerald dark theme for CurseForge Launcher.",
|
||||||
|
"author": "BetterCurseForge",
|
||||||
|
"primaryColor": "#10b981",
|
||||||
|
"titleColor": "#0d271f",
|
||||||
|
"bodyColor": "#06140f",
|
||||||
|
"backgroundColor": "#091c16"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tips for Inspecting Elements
|
||||||
|
|
||||||
|
Because CurseForge runs on Electron and React, you can inspect any UI element using Chromium DevTools:
|
||||||
|
|
||||||
|
1. Press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd> in CurseForge to open DevTools.
|
||||||
|
2. Press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>C</kbd> to activate the element inspector.
|
||||||
|
3. Click on any button, banner, header, or list item to reveal its computed styles and classes in the **Elements** panel.
|
||||||
|
4. Test CSS changes live in the **Styles** tab before adding them to your theme file.
|
||||||
|
5. In **Settings -> Themes**, click **Refresh** to instantly apply your modified theme stylesheet!
|
||||||
67
docs/index.md
Normal file
67
docs/index.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
---
|
||||||
|
layout: home
|
||||||
|
|
||||||
|
hero:
|
||||||
|
name: "BetterCurseForge"
|
||||||
|
text: "Client mod for CurseForge Launcher"
|
||||||
|
tagline: "Ad-free experience, custom themes, unlocked premium skins, and plugins."
|
||||||
|
actions:
|
||||||
|
- theme: brand
|
||||||
|
text: Get Started
|
||||||
|
link: /guide/installation
|
||||||
|
- theme: alt
|
||||||
|
text: View on GitHub
|
||||||
|
link: https://github.com/BetterCurseForge/BetterCurseForge
|
||||||
|
|
||||||
|
features:
|
||||||
|
- icon: 🚫
|
||||||
|
title: Complete Ad Removal
|
||||||
|
details: Multi-layer elimination (React component neutralization, zero-ad CSS rules, and network blocking) removes all video ads, banners, and empty containers.
|
||||||
|
- icon: 🎨
|
||||||
|
title: Client Themes & Free Skins
|
||||||
|
details: Integrated directly into Settings -> Themes with live hover previews, theme folder shortcuts, and all native CurseForge Premium themes unlocked for free.
|
||||||
|
- icon: 🔄
|
||||||
|
title: Auto-Repatch
|
||||||
|
details: Survives launcher updates effortlessly. The automated background task detects CurseForge updates and silently re-patches the client.
|
||||||
|
- icon: 🖥️
|
||||||
|
title: Modern GUI & CLI
|
||||||
|
details: Includes an intuitive dark-themed desktop application with one-click actions, as well as a full command-line interface for power users.
|
||||||
|
- icon: 🧩
|
||||||
|
title: Plugin System
|
||||||
|
details: Extend launcher capabilities with lightweight JavaScript plugins using our runtime monkey-patcher and persistent settings API.
|
||||||
|
- icon: 🛡️
|
||||||
|
title: Safe & 100% Reversible
|
||||||
|
details: Automatically creates a clean vanilla backup (`app.asar.vanilla.bak`). You can restore stock CurseForge at any time with a single click.
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="quickstart-section" style="margin-top: 3rem; text-align: center;">
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Download `BetterCurseForge.exe` and patch in one click:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch the desktop GUI:
|
||||||
|
BetterCurseForge.exe
|
||||||
|
|
||||||
|
# Or patch directly from your terminal:
|
||||||
|
BetterCurseForge.exe patch
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="features-overview" style="margin-top: 2rem;">
|
||||||
|
|
||||||
|
## Why BetterCurseForge?
|
||||||
|
|
||||||
|
CurseForge is the premier platform for Minecraft modpacks, World of Warcraft addons, and game mods, but the official desktop app is burdened with intrusive ad containers, locked premium themes, and heavy telemetry.
|
||||||
|
|
||||||
|
**BetterCurseForge** restores a clean, fast, and customizable experience:
|
||||||
|
|
||||||
|
- **Seamless Native UI**: Client themes appear natively under **Settings -> Themes** alongside official themes, complete with real-time live hover previews.
|
||||||
|
- **Unlocked Premium Themes**: Access all native CurseForge Premium themes (*Pixelforge*, *Lifeforge*, *Fantasyforge*, *Spookyforge*) without a subscription.
|
||||||
|
- **Zero-Ad Layouts**: Ad columns and sidebars are eliminated from the layout, letting modpack grids expand to fill your entire display.
|
||||||
|
- **Automated Update Resistance**: Never worry about CurseForge updates breaking your setup—**Auto-Repatch** automatically detects updates and re-applies the patch in seconds.
|
||||||
|
- **One-Click Reversible**: Easily unpatch anytime via the GUI or `BetterCurseForge.exe unpatch` to restore vanilla CurseForge.
|
||||||
|
|
||||||
|
</div>
|
||||||
2519
docs/package-lock.json
generated
Normal file
2519
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
13
docs/package.json
Normal file
13
docs/package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "bettercurseforge-docs",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vitepress dev",
|
||||||
|
"build": "vitepress build",
|
||||||
|
"preview": "vitepress preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitepress": "^1.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
docs/public/logo.svg
Normal file
20
docs/public/logo.svg
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="purpleGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#c084fc" />
|
||||||
|
<stop offset="50%" stop-color="#9333ea" />
|
||||||
|
<stop offset="100%" stop-color="#6b21a8" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="blur" />
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background rounded rect -->
|
||||||
|
<rect x="5" y="5" width="90" height="90" rx="22" fill="#14141d" stroke="url(#purpleGrad)" stroke-width="3" />
|
||||||
|
|
||||||
|
<!-- Stylized Flame / Anvil / BCF Monogram -->
|
||||||
|
<path d="M 50 18 C 55 30 65 36 68 46 C 72 58 64 74 50 78 C 36 74 28 58 32 46 C 35 36 45 30 50 18 Z" fill="url(#purpleGrad)" filter="url(#glow)" />
|
||||||
|
<path d="M 50 38 C 53 45 58 48 59 54 C 61 60 57 68 50 70 C 43 68 39 60 41 54 C 42 48 47 45 50 38 Z" fill="#ffffff" opacity="0.9" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1 KiB |
979
patcher/patch_core.py
Normal file
979
patcher/patch_core.py
Normal file
|
|
@ -0,0 +1,979 @@
|
||||||
|
"""
|
||||||
|
BetterCurseForge — Core Patching Logic
|
||||||
|
=======================================
|
||||||
|
Direct ASAR modification + OWEASARSIG header hash synchronization.
|
||||||
|
Works on all versions of CurseForge (ow-electron).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
import shutil
|
||||||
|
import hashlib
|
||||||
|
import winreg
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
RUN_KEY_PATH = r"Software\Microsoft\Windows\CurrentVersion\Run"
|
||||||
|
WATCHER_VALUE_NAME = "BetterCurseForgeWatcher"
|
||||||
|
WATCHER_MUTEX_NAME = "BetterCurseForgeWatcherMutex"
|
||||||
|
PERMANENT_DIR = Path(os.environ.get("LOCALAPPDATA", "")) / "BetterCurseForge"
|
||||||
|
PERMANENT_EXE = PERMANENT_DIR / "BetterCurseForge.exe"
|
||||||
|
PERMANENT_BCF = PERMANENT_DIR / "bettercf"
|
||||||
|
PERMANENT_VENDOR = PERMANENT_DIR / "vendor"
|
||||||
|
|
||||||
|
def get_curseforge_dir() -> Path | None:
|
||||||
|
# 1. Registry lookup
|
||||||
|
try:
|
||||||
|
key = winreg.OpenKey(
|
||||||
|
winreg.HKEY_CURRENT_USER,
|
||||||
|
r"Software\Microsoft\Windows\CurrentVersion\Uninstall",
|
||||||
|
)
|
||||||
|
i = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
subkey_name = winreg.EnumKey(key, i)
|
||||||
|
i += 1
|
||||||
|
try:
|
||||||
|
subkey = winreg.OpenKey(key, subkey_name)
|
||||||
|
display_name, _ = winreg.QueryValueEx(subkey, "DisplayName")
|
||||||
|
if "CurseForge" in display_name:
|
||||||
|
uninstall_str, _ = winreg.QueryValueEx(subkey, "UninstallString")
|
||||||
|
path_str = uninstall_str.strip('"').split('" ')[0].strip('"')
|
||||||
|
install_dir = Path(path_str).parent
|
||||||
|
if install_dir.exists() and (install_dir / "CurseForge.exe").exists():
|
||||||
|
winreg.CloseKey(subkey)
|
||||||
|
winreg.CloseKey(key)
|
||||||
|
return install_dir
|
||||||
|
winreg.CloseKey(subkey)
|
||||||
|
except (FileNotFoundError, OSError):
|
||||||
|
pass
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
winreg.CloseKey(key)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2. LocalAppData fallback
|
||||||
|
appdata = os.environ.get("LOCALAPPDATA", "")
|
||||||
|
for p in [
|
||||||
|
Path(appdata) / "Programs" / "CurseForge Windows",
|
||||||
|
Path(appdata) / "CurseForge",
|
||||||
|
]:
|
||||||
|
if p.exists() and (p / "CurseForge.exe").exists():
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
def kill_curseforge():
|
||||||
|
try:
|
||||||
|
subprocess.run(["taskkill", "/f", "/im", "CurseForge.exe"], capture_output=True, timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def calc_asar_header_hash(asar_path: Path) -> str:
|
||||||
|
with open(asar_path, "rb") as f:
|
||||||
|
f.seek(12)
|
||||||
|
u4 = struct.unpack("<I", f.read(4))[0]
|
||||||
|
header_bytes = f.read(u4)
|
||||||
|
return hashlib.sha256(header_bytes).hexdigest()
|
||||||
|
|
||||||
|
def read_exe_hash(exe_path: Path) -> str | None:
|
||||||
|
try:
|
||||||
|
with open(exe_path, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
sig = "OWEASARSIG".encode("utf-16le")
|
||||||
|
idx = data.rfind(sig)
|
||||||
|
if idx == -1:
|
||||||
|
idx = data.rfind(b"OWEASARSIG")
|
||||||
|
if idx == -1:
|
||||||
|
return None
|
||||||
|
val_idx = data.find(b'"value":"', idx)
|
||||||
|
if val_idx == -1:
|
||||||
|
return None
|
||||||
|
hash_start = val_idx + 9
|
||||||
|
return data[hash_start:hash_start+64].decode("ascii", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_exe_hash(exe_path: Path, new_hash: str) -> bool:
|
||||||
|
with open(exe_path, "rb") as f:
|
||||||
|
data = bytearray(f.read())
|
||||||
|
sig = "OWEASARSIG".encode("utf-16le")
|
||||||
|
idx = data.rfind(sig)
|
||||||
|
if idx == -1:
|
||||||
|
idx = data.rfind(b"OWEASARSIG")
|
||||||
|
if idx == -1:
|
||||||
|
raise ValueError("OWEASARSIG signature block not found in CurseForge.exe")
|
||||||
|
val_idx = data.find(b'"value":"', idx)
|
||||||
|
if val_idx == -1:
|
||||||
|
raise ValueError('"value":" field not found in OWEASARSIG resource')
|
||||||
|
hash_start = val_idx + 9
|
||||||
|
old_hash = data[hash_start:hash_start+64].decode("ascii", errors="replace")
|
||||||
|
data[hash_start:hash_start+64] = new_hash.encode("ascii")
|
||||||
|
with open(exe_path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_user_data_dir() -> Path:
|
||||||
|
appdata = Path(os.environ.get("APPDATA", ""))
|
||||||
|
bcf_dir = appdata / "CurseForge" / "BetterCurseForge"
|
||||||
|
(bcf_dir / "themes").mkdir(parents=True, exist_ok=True)
|
||||||
|
(bcf_dir / "plugins").mkdir(parents=True, exist_ok=True)
|
||||||
|
return bcf_dir
|
||||||
|
|
||||||
|
def ensure_sample_content(bcf_src: Path):
|
||||||
|
user_dir = get_user_data_dir()
|
||||||
|
themes_src = bcf_src / "themes"
|
||||||
|
plugins_src = bcf_src / "plugins"
|
||||||
|
|
||||||
|
if themes_src.exists():
|
||||||
|
for item in themes_src.iterdir():
|
||||||
|
dest = user_dir / "themes" / item.name
|
||||||
|
if item.is_file():
|
||||||
|
if not dest.exists() or item.name.startswith("purple-night"):
|
||||||
|
shutil.copy2(item, dest)
|
||||||
|
elif item.is_dir() and not dest.exists():
|
||||||
|
shutil.copytree(item, dest)
|
||||||
|
|
||||||
|
if plugins_src.exists():
|
||||||
|
for item in plugins_src.iterdir():
|
||||||
|
dest = user_dir / "plugins" / item.name
|
||||||
|
if not dest.exists():
|
||||||
|
if item.is_file():
|
||||||
|
shutil.copy2(item, dest)
|
||||||
|
elif item.is_dir():
|
||||||
|
shutil.copytree(item, dest)
|
||||||
|
|
||||||
|
def find_bettercf_source() -> Path | None:
|
||||||
|
candidates = [
|
||||||
|
Path(getattr(sys, "_MEIPASS", "")) / "bettercf" if hasattr(sys, "_MEIPASS") else None,
|
||||||
|
Path(sys.executable).parent / "bettercf" if getattr(sys, "frozen", False) else None,
|
||||||
|
PERMANENT_BCF,
|
||||||
|
Path(__file__).parent / "bettercf",
|
||||||
|
Path(__file__).parent.parent / "bettercf",
|
||||||
|
Path.cwd() / "bettercf",
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if c and c.exists() and c.is_dir():
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_vendor_dir() -> Path | None:
|
||||||
|
candidates = [
|
||||||
|
Path(getattr(sys, "_MEIPASS", "")) / "vendor" if hasattr(sys, "_MEIPASS") else None,
|
||||||
|
Path(sys.executable).parent / "vendor" if getattr(sys, "frozen", False) else None,
|
||||||
|
PERMANENT_VENDOR,
|
||||||
|
Path(__file__).parent / "vendor",
|
||||||
|
Path(__file__).parent.parent / "patcher" / "vendor",
|
||||||
|
Path.cwd() / "patcher" / "vendor",
|
||||||
|
Path.cwd() / "vendor",
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if c and c.exists() and (c / "asar" / "node_modules" / "@electron" / "asar" / "bin" / "asar.mjs").exists():
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run_asar_tool(args: list[str], cf_dir: Path | None = None) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Executes asar pack/extract offline using bundled @electron/asar with either
|
||||||
|
CurseForge's internal Node v24 runtime or system node.
|
||||||
|
Falls back to npx only if offline runners fail.
|
||||||
|
"""
|
||||||
|
vendor_dir = find_vendor_dir()
|
||||||
|
if vendor_dir:
|
||||||
|
asar_mjs = vendor_dir / "asar" / "node_modules" / "@electron" / "asar" / "bin" / "asar.mjs"
|
||||||
|
node_modules = vendor_dir / "asar" / "node_modules"
|
||||||
|
|
||||||
|
runners = []
|
||||||
|
# Runner 1: CurseForge.exe itself (built-in Node v24, 100% offline & compatible)
|
||||||
|
if cf_dir and (cf_dir / "CurseForge.exe").exists():
|
||||||
|
runners.append((str(cf_dir / "CurseForge.exe"), True))
|
||||||
|
# Runner 2: System Node.js
|
||||||
|
sys_node = shutil.which("node")
|
||||||
|
if sys_node:
|
||||||
|
runners.append((sys_node, False))
|
||||||
|
# Runner 3: Vanilla backup of CurseForge.exe
|
||||||
|
if cf_dir and (cf_dir / "CurseForge.exe.vanilla").exists():
|
||||||
|
runners.append((str(cf_dir / "CurseForge.exe.vanilla"), True))
|
||||||
|
|
||||||
|
for runner_bin, is_electron in runners:
|
||||||
|
try:
|
||||||
|
env = os.environ.copy()
|
||||||
|
if is_electron:
|
||||||
|
env["ELECTRON_RUN_AS_NODE"] = "1"
|
||||||
|
env["NODE_PATH"] = str(node_modules)
|
||||||
|
cmd = [runner_bin, str(asar_mjs)] + args
|
||||||
|
creation_flags = 0x08000000 if os.name == "nt" else 0 # CREATE_NO_WINDOW
|
||||||
|
r = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
creationflags=creation_flags,
|
||||||
|
timeout=45
|
||||||
|
)
|
||||||
|
if r.returncode == 0:
|
||||||
|
return (True, r.stdout.strip())
|
||||||
|
else:
|
||||||
|
err_msg = r.stderr.strip() or r.stdout.strip()
|
||||||
|
except Exception as e:
|
||||||
|
err_msg = str(e)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fallback to npx if vendor runner was unavailable
|
||||||
|
try:
|
||||||
|
cmd_npx = ["npx", "--yes", "@electron/asar"] + args
|
||||||
|
r = subprocess.run(
|
||||||
|
cmd_npx,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
shell=True,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
if r.returncode == 0:
|
||||||
|
return (True, r.stdout.strip())
|
||||||
|
return (False, r.stderr.strip() or r.stdout.strip())
|
||||||
|
except Exception as e:
|
||||||
|
return (False, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def is_asar_patched(asar_path: Path) -> bool:
|
||||||
|
if not asar_path.exists():
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
with open(asar_path, "rb") as f:
|
||||||
|
f.seek(12)
|
||||||
|
u4 = struct.unpack("<I", f.read(4))[0]
|
||||||
|
header = f.read(u4).decode("utf-8", errors="replace")
|
||||||
|
return "bettercf" in header
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_curseforge_patched(cf_dir: Path) -> bool:
|
||||||
|
asar_path = cf_dir / "resources" / "app.asar"
|
||||||
|
return is_asar_patched(asar_path)
|
||||||
|
|
||||||
|
def patch_curseforge(log_callback=print) -> bool:
|
||||||
|
cf_dir = get_curseforge_dir()
|
||||||
|
if not cf_dir:
|
||||||
|
log_callback("CurseForge installation directory not found!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
resources_dir = cf_dir / "resources"
|
||||||
|
asar_path = resources_dir / "app.asar"
|
||||||
|
exe_path = cf_dir / "CurseForge.exe"
|
||||||
|
|
||||||
|
if not asar_path.exists() or not exe_path.exists():
|
||||||
|
log_callback("CurseForge resources/app.asar or CurseForge.exe missing!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
asar_bak = resources_dir / "app.asar.vanilla"
|
||||||
|
exe_bak = cf_dir / "CurseForge.exe.vanilla"
|
||||||
|
|
||||||
|
log_callback(f"Found CurseForge at: {cf_dir}")
|
||||||
|
kill_curseforge()
|
||||||
|
|
||||||
|
# If CurseForge just updated (clean vanilla files from Overwolf), refresh vanilla backups
|
||||||
|
try:
|
||||||
|
current_exe_sig = read_exe_hash(exe_path)
|
||||||
|
current_asar_hash = calc_asar_header_hash(asar_path)
|
||||||
|
if current_exe_sig == current_asar_hash and not is_curseforge_patched(cf_dir):
|
||||||
|
# Clean Overwolf update detected! Refresh vanilla backups to match new version
|
||||||
|
shutil.copy2(asar_path, asar_bak)
|
||||||
|
shutil.copy2(exe_path, exe_bak)
|
||||||
|
log_callback(f"[+] Updated vanilla backup to latest CurseForge version")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not asar_bak.exists() or is_asar_patched(asar_bak):
|
||||||
|
if not is_asar_patched(asar_path):
|
||||||
|
log_callback("Creating vanilla backup of app.asar...")
|
||||||
|
shutil.copy2(asar_path, asar_bak)
|
||||||
|
if not exe_bak.exists():
|
||||||
|
log_callback("Creating vanilla backup of CurseForge.exe...")
|
||||||
|
shutil.copy2(exe_path, exe_bak)
|
||||||
|
|
||||||
|
# Clean old unused resources/app folder if present
|
||||||
|
old_app = resources_dir / "app"
|
||||||
|
if old_app.exists():
|
||||||
|
try:
|
||||||
|
shutil.rmtree(old_app)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Extract asar
|
||||||
|
temp_extract = resources_dir / "bettercf_extract_temp"
|
||||||
|
if temp_extract.exists():
|
||||||
|
shutil.rmtree(temp_extract, ignore_errors=True)
|
||||||
|
|
||||||
|
# Always extract asar_path directly (so @electron/asar finds app.asar.unpacked)
|
||||||
|
source_asar = asar_path
|
||||||
|
|
||||||
|
log_callback(f"Extracting {'vanilla backup' if source_asar == asar_bak else 'app.asar'}...")
|
||||||
|
ok, err = run_asar_tool(["extract", str(source_asar), str(temp_extract)], cf_dir=cf_dir)
|
||||||
|
if not ok:
|
||||||
|
log_callback(f"Failed to extract asar: {err}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Zero-Ad & Themes Patch in desktop.js
|
||||||
|
desktop_js = temp_extract / "dist" / "desktop" / "desktop.js"
|
||||||
|
if desktop_js.exists():
|
||||||
|
content = desktop_js.read_text(encoding="utf-8")
|
||||||
|
# Clean up any legacy or duplicate __orig_eV replacements
|
||||||
|
while "let __orig_eV=e=>null;" in content:
|
||||||
|
content = content.replace("let __orig_eV=e=>null;", "")
|
||||||
|
if "eV=e=>null;let __orig_eV=e=>{" in content:
|
||||||
|
content = content.replace("eV=e=>null;let __orig_eV=e=>{", "eV=e=>{")
|
||||||
|
|
||||||
|
# Idempotently disable React ad component (eV -> return null)
|
||||||
|
if "/*BCF_AD_OFF*/" not in content and "eV=e=>{let" in content:
|
||||||
|
content = content.replace("eV=e=>{let", "/*BCF_AD_OFF*/eV=e=>{return null;let", 1)
|
||||||
|
log_callback("[+] Disabled React ad component (eV -> return null)")
|
||||||
|
if "showSwapAdsButton:T" in content:
|
||||||
|
content = content.replace("showSwapAdsButton:T", "showSwapAdsButton:!1")
|
||||||
|
log_callback("[+] Disabled swap ads button")
|
||||||
|
if "R=e=>!!e.forceUnlock||!e.isPremium||!!M" in content:
|
||||||
|
content = content.replace("R=e=>!!e.forceUnlock||!e.isPremium||!!M", "R=e=>!0")
|
||||||
|
log_callback("[+] Unlocked all native premium themes (Settings route)")
|
||||||
|
if "P=e=>!!e.forceUnlock||!e.isPremium||f" in content:
|
||||||
|
content = content.replace("P=e=>!!e.forceUnlock||!e.isPremium||f", "P=e=>!0")
|
||||||
|
log_callback("[+] Unlocked all native premium themes (Popup route)")
|
||||||
|
if "return!r?.forceUnlock&&r?.isPremium&&!t?u.Sx.Classic:n" in content:
|
||||||
|
content = content.replace("return!r?.forceUnlock&&r?.isPremium&&!t?u.Sx.Classic:n", "return n")
|
||||||
|
log_callback("[+] Unlocked premium theme state validation")
|
||||||
|
if "function iy(e){return e.subscription.plan?.active}" in content:
|
||||||
|
content = content.replace("function iy(e){return e.subscription.plan?.active}", "function iy(e){return !0}")
|
||||||
|
log_callback("[+] Unlocked subscription plan status for themes")
|
||||||
|
desktop_js.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
# 2. Zero-Ad & Settings Themes CSS in desktop.css
|
||||||
|
desktop_css = temp_extract / "dist" / "desktop" / "desktop.css"
|
||||||
|
if desktop_css.exists():
|
||||||
|
css_rules = """
|
||||||
|
/* ═══ BetterCurseForge Zero-Ad Rules ═══ */
|
||||||
|
.curseforge-ad,
|
||||||
|
.ad-inner,
|
||||||
|
.overwolf-ad,
|
||||||
|
.swap-ads-location,
|
||||||
|
owadview,
|
||||||
|
[class*="-skin-aside"],
|
||||||
|
.promo-banner,
|
||||||
|
.subscribe-link,
|
||||||
|
.subscribe-link-small-resolution,
|
||||||
|
.subscribe-link-large-resolution,
|
||||||
|
[data-ad],
|
||||||
|
[data-ad-slot] {
|
||||||
|
display: none !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
max-width: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ BetterCurseForge Settings Themes UI ═══ */
|
||||||
|
#bcf-custom-themes-section {
|
||||||
|
margin-top: 12px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.bcf-themes-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.bcf-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 24px;
|
||||||
|
color: var(--color-text-title, currentColor);
|
||||||
|
}
|
||||||
|
.bcf-heading > span:first-child {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.bcf-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
color: #a78bfa;
|
||||||
|
background: rgba(139, 92, 246, 0.12);
|
||||||
|
border: 1px solid rgba(139, 92, 246, 0.3);
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
.bcf-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.bcf-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--color-text-primary, currentColor);
|
||||||
|
background: var(--surface-on-surface, rgba(255, 255, 255, 0.08));
|
||||||
|
border: 1px solid var(--border-tertiery, rgba(255, 255, 255, 0.15));
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
.bcf-btn svg {
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
}
|
||||||
|
.bcf-btn:hover {
|
||||||
|
background: var(--surface-on-surface-hover, rgba(255, 255, 255, 0.16));
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
color: var(--color-text-title-hover, #ffffff);
|
||||||
|
}
|
||||||
|
.bcf-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
.bcf-theme-thumb {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.bcf-theme-thumb .thumb-icon svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.bcf-theme-thumb.is-active .sprite-icon {
|
||||||
|
opacity: 1 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mutual exclusivity: hide checkmark on native theme cards when a client theme is active */
|
||||||
|
html.bcf-custom-theme-active .settings-themes-view .theme-thumb:not(.bcf-theme-thumb).is-active .sprite-icon {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
html.bcf-custom-theme-active .settings-themes-view .theme-thumb:not(.bcf-theme-thumb).is-active .thumb-icon {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
html.bcf-custom-theme-active .settings-themes-view .theme-thumb:not(.bcf-theme-thumb).is-active {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
/* Neutralize native skin styles and title bar */
|
||||||
|
html.bcf-custom-theme-active .title-bar,
|
||||||
|
html.bcf-custom-theme-active [class*="title-bar"],
|
||||||
|
html.bcf-custom-theme-active [class*="TitleBar"] {
|
||||||
|
background: #0d0818 !important;
|
||||||
|
--title-bar-background: #0d0818 !important;
|
||||||
|
}
|
||||||
|
html.bcf-custom-theme-active .pixelforge-skin,
|
||||||
|
html.bcf-custom-theme-active .lifeforge-skin,
|
||||||
|
html.bcf-custom-theme-active .fantasyforge-skin,
|
||||||
|
html.bcf-custom-theme-active .halloween-skin,
|
||||||
|
html.bcf-custom-theme-active .cf-light,
|
||||||
|
html.bcf-custom-theme-active .cf-light-colorblind {
|
||||||
|
--title-bar-background: #0d0818 !important;
|
||||||
|
--surface-brand-brand: #8b5cf6 !important;
|
||||||
|
--surface-brand-brand-hover: #9d74f7 !important;
|
||||||
|
--accent-font: var(--body-font, "Lato", sans-serif) !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
html.bcf-custom-theme-active .pixelforge-skin :not(.highlighted-project-container) .card:after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
css_text = desktop_css.read_text(encoding="utf-8")
|
||||||
|
if "/* ═══ BetterCurseForge Zero-Ad Rules ═══ */" not in css_text:
|
||||||
|
desktop_css.write_text(css_text + "\n" + css_rules, encoding="utf-8")
|
||||||
|
log_callback("[+] Injected zero-ad and theme CSS rules into desktop.css")
|
||||||
|
|
||||||
|
# 3. Copy BetterCF runtime into asar
|
||||||
|
bcf_src = find_bettercf_source()
|
||||||
|
if bcf_src:
|
||||||
|
ensure_sample_content(bcf_src)
|
||||||
|
bcf_dest = temp_extract / "dist" / "bettercf"
|
||||||
|
if bcf_dest.exists():
|
||||||
|
shutil.rmtree(bcf_dest)
|
||||||
|
shutil.copytree(bcf_src, bcf_dest)
|
||||||
|
log_callback("[+] Bundled BetterCurseForge runtime into asar")
|
||||||
|
|
||||||
|
# 4. Inject main process hooks in background.js
|
||||||
|
bg_js = temp_extract / "dist" / "background" / "background.js"
|
||||||
|
if bg_js.exists():
|
||||||
|
bg_content = bg_js.read_text(encoding="utf-8")
|
||||||
|
if "bettercf/core/main-process.js" not in bg_content:
|
||||||
|
bg_inject = "import '../bettercf/core/main-process.js';\n"
|
||||||
|
bg_js.write_text(bg_inject + bg_content, encoding="utf-8")
|
||||||
|
log_callback("[+] Injected main-process hooks into background.js")
|
||||||
|
|
||||||
|
# 5. Inject renderer script into desktop.html
|
||||||
|
desktop_html = temp_extract / "dist" / "desktop" / "desktop.html"
|
||||||
|
if desktop_html.exists():
|
||||||
|
html_content = desktop_html.read_text(encoding="utf-8")
|
||||||
|
if "bettercf/core/renderer.js" not in html_content:
|
||||||
|
bcf_script = '<script src="../bettercf/core/renderer.js"></script>'
|
||||||
|
html_content = html_content.replace("</head>", f"{bcf_script}</head>")
|
||||||
|
desktop_html.write_text(html_content, encoding="utf-8")
|
||||||
|
log_callback("[+] Injected renderer script into desktop.html")
|
||||||
|
|
||||||
|
# 6. Inject preload hook in preload.js
|
||||||
|
preload_js = temp_extract / "dist" / "preload" / "preload.js"
|
||||||
|
if preload_js.exists():
|
||||||
|
preload_content = preload_js.read_text(encoding="utf-8")
|
||||||
|
if "__BCF_IPC__" not in preload_content:
|
||||||
|
preload_hook = """
|
||||||
|
// ── BetterCurseForge Preload Hook ──
|
||||||
|
try {
|
||||||
|
const { ipcRenderer } = require('electron');
|
||||||
|
window.__BCF_IPC__ = {
|
||||||
|
invoke: (...args) => ipcRenderer.invoke(...args),
|
||||||
|
};
|
||||||
|
} catch (e) {}
|
||||||
|
"""
|
||||||
|
preload_js.write_text(preload_content + "\n" + preload_hook, encoding="utf-8")
|
||||||
|
log_callback("[+] Injected renderer loader into preload.js")
|
||||||
|
|
||||||
|
# 6. Repack ASAR
|
||||||
|
log_callback("Repacking app.asar...")
|
||||||
|
ok, err = run_asar_tool(["pack", str(temp_extract), str(asar_path)], cf_dir=cf_dir)
|
||||||
|
if not ok:
|
||||||
|
log_callback(f"Failed to pack asar: {err}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 7. Compute new hash and update CurseForge.exe
|
||||||
|
new_hash = calc_asar_header_hash(asar_path)
|
||||||
|
log_callback(f"New ASAR header hash: {new_hash}")
|
||||||
|
# Always restore clean vanilla exe before patching hash
|
||||||
|
shutil.copy2(exe_bak, exe_path)
|
||||||
|
update_exe_hash(exe_path, new_hash)
|
||||||
|
log_callback("[+] Updated CurseForge.exe integrity signature")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if temp_extract.exists():
|
||||||
|
shutil.rmtree(temp_extract, ignore_errors=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ensure_permanent_installation()
|
||||||
|
set_autostart_watcher(True)
|
||||||
|
start_background_watcher()
|
||||||
|
log_callback("[+] Auto-repatch watcher active and persistent across updates.")
|
||||||
|
except Exception as e:
|
||||||
|
log_callback(f"[!] Note: Could not auto-start watcher: {e}")
|
||||||
|
|
||||||
|
log_callback("[*] CurseForge successfully patched! Ads removed, themes & plugins enabled.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unpatch_curseforge(log_callback=print) -> bool:
|
||||||
|
cf_dir = get_curseforge_dir()
|
||||||
|
if not cf_dir:
|
||||||
|
log_callback("CurseForge not found.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
kill_curseforge()
|
||||||
|
resources_dir = cf_dir / "resources"
|
||||||
|
asar_path = resources_dir / "app.asar"
|
||||||
|
exe_path = cf_dir / "CurseForge.exe"
|
||||||
|
asar_bak = resources_dir / "app.asar.vanilla"
|
||||||
|
exe_bak = cf_dir / "CurseForge.exe.vanilla"
|
||||||
|
|
||||||
|
restored = False
|
||||||
|
if asar_bak.exists():
|
||||||
|
shutil.copy2(asar_bak, asar_path)
|
||||||
|
log_callback("[+] Restored vanilla app.asar")
|
||||||
|
restored = True
|
||||||
|
if exe_bak.exists():
|
||||||
|
shutil.copy2(exe_bak, exe_path)
|
||||||
|
log_callback("[+] Restored vanilla CurseForge.exe")
|
||||||
|
restored = True
|
||||||
|
|
||||||
|
old_app = resources_dir / "app"
|
||||||
|
if old_app.exists():
|
||||||
|
shutil.rmtree(old_app, ignore_errors=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
set_autostart_watcher(False)
|
||||||
|
stop_background_watcher()
|
||||||
|
log_callback("[-] Auto-repatch watcher deactivated.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if restored:
|
||||||
|
log_callback("CurseForge successfully returned to vanilla.")
|
||||||
|
else:
|
||||||
|
log_callback("Vanilla backups not found. Reinstall CurseForge to restore completely.")
|
||||||
|
return restored
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# AUTO-REPATCH WATCHER & PERSISTENCE ENGINE
|
||||||
|
def is_curseforge_running() -> bool:
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(
|
||||||
|
["tasklist", "/FI", "IMAGENAME eq CurseForge.exe", "/NH"],
|
||||||
|
text=True, stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
return "CurseForge.exe" in out
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_updater_running() -> bool:
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(
|
||||||
|
["tasklist", "/FO", "CSV", "/NH"],
|
||||||
|
text=True, stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
for line in out.splitlines():
|
||||||
|
line = line.strip().lower()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = [p.strip('"') for p in line.split('","')]
|
||||||
|
if parts:
|
||||||
|
pname = parts[0]
|
||||||
|
if pname in ("setup.exe", "installer.exe") or ("curseforge" in pname and "updater" in pname):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ensure_permanent_installation() -> Path:
|
||||||
|
"""
|
||||||
|
Ensures BetterCurseForge executable, bettercf runtime, and vendor tools are permanently
|
||||||
|
installed to %LOCALAPPDATA%\\BetterCurseForge so auto-repatch never breaks.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
PERMANENT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
src_bcf = find_bettercf_source()
|
||||||
|
if src_bcf and src_bcf.exists():
|
||||||
|
if PERMANENT_BCF.resolve() != src_bcf.resolve():
|
||||||
|
if PERMANENT_BCF.exists():
|
||||||
|
shutil.rmtree(PERMANENT_BCF, ignore_errors=True)
|
||||||
|
shutil.copytree(src_bcf, PERMANENT_BCF)
|
||||||
|
|
||||||
|
src_vendor = find_vendor_dir()
|
||||||
|
if src_vendor and src_vendor.exists():
|
||||||
|
if PERMANENT_VENDOR.resolve() != src_vendor.resolve():
|
||||||
|
if PERMANENT_VENDOR.exists():
|
||||||
|
shutil.rmtree(PERMANENT_VENDOR, ignore_errors=True)
|
||||||
|
shutil.copytree(src_vendor, PERMANENT_VENDOR)
|
||||||
|
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
curr_exe = Path(sys.executable).resolve()
|
||||||
|
if curr_exe != PERMANENT_EXE.resolve():
|
||||||
|
shutil.copy2(curr_exe, PERMANENT_EXE)
|
||||||
|
else:
|
||||||
|
dist_exe = Path(__file__).parent.parent / "dist" / "BetterCurseForge.exe"
|
||||||
|
if dist_exe.exists():
|
||||||
|
shutil.copy2(dist_exe, PERMANENT_EXE)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return PERMANENT_EXE if PERMANENT_EXE.exists() else Path(sys.executable)
|
||||||
|
|
||||||
|
def is_autostart_watcher_enabled() -> bool:
|
||||||
|
try:
|
||||||
|
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY_PATH, 0, winreg.KEY_READ)
|
||||||
|
try:
|
||||||
|
val, _ = winreg.QueryValueEx(key, WATCHER_VALUE_NAME)
|
||||||
|
winreg.CloseKey(key)
|
||||||
|
return bool(val)
|
||||||
|
except FileNotFoundError:
|
||||||
|
winreg.CloseKey(key)
|
||||||
|
return False
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_autostart_watcher(enable: bool) -> bool:
|
||||||
|
try:
|
||||||
|
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY_PATH, 0, winreg.KEY_SET_VALUE)
|
||||||
|
if enable:
|
||||||
|
perm_exe = ensure_permanent_installation()
|
||||||
|
if perm_exe.suffix.lower() == ".exe":
|
||||||
|
cmd = f'"{perm_exe.resolve()}" --watch'
|
||||||
|
else:
|
||||||
|
python_exe = Path(sys.executable).parent / "pythonw.exe"
|
||||||
|
if not python_exe.exists():
|
||||||
|
python_exe = Path(sys.executable)
|
||||||
|
cmd = f'"{python_exe.resolve()}" "{Path(__file__).parent.resolve() / "patcher_gui.py"}" --watch'
|
||||||
|
|
||||||
|
winreg.SetValueEx(key, WATCHER_VALUE_NAME, 0, winreg.REG_SZ, cmd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["schtasks", "/Create", "/TN", "BetterCurseForgeWatcher",
|
||||||
|
"/TR", cmd, "/SC", "ONLOGON", "/RL", "LIMITED", "/F"],
|
||||||
|
capture_output=True, timeout=5
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
winreg.DeleteValue(key, WATCHER_VALUE_NAME)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
subprocess.run(["schtasks", "/Delete", "/TN", "BetterCurseForgeWatcher", "/F"],
|
||||||
|
capture_output=True, timeout=5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
winreg.CloseKey(key)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_watcher_running() -> bool:
|
||||||
|
import ctypes
|
||||||
|
kernel32 = ctypes.windll.kernel32
|
||||||
|
kernel32.OpenMutexW.restype = ctypes.c_void_p
|
||||||
|
kernel32.OpenMutexW.argtypes = [ctypes.c_uint, ctypes.c_bool, ctypes.c_wchar_p]
|
||||||
|
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||||
|
SYNCHRONIZE = 0x00100000
|
||||||
|
h = kernel32.OpenMutexW(SYNCHRONIZE, False, WATCHER_MUTEX_NAME)
|
||||||
|
if h:
|
||||||
|
kernel32.CloseHandle(h)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start_background_watcher():
|
||||||
|
if is_watcher_running():
|
||||||
|
return
|
||||||
|
perm_exe = ensure_permanent_installation()
|
||||||
|
if perm_exe.exists() and perm_exe.suffix.lower() == ".exe":
|
||||||
|
cmd_str = f'"{perm_exe.resolve()}" --watch'
|
||||||
|
elif getattr(sys, "frozen", False):
|
||||||
|
cmd_str = f'"{Path(sys.executable).resolve()}" --watch'
|
||||||
|
else:
|
||||||
|
python_exe = Path(sys.executable).parent / "pythonw.exe"
|
||||||
|
if not python_exe.exists():
|
||||||
|
python_exe = Path(sys.executable)
|
||||||
|
cmd_str = f'"{python_exe.resolve()}" "{Path(__file__).parent.resolve() / "patcher_gui.py"}" --watch'
|
||||||
|
|
||||||
|
try:
|
||||||
|
ps_cmd = f"Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{{CommandLine = '{cmd_str}'}}"
|
||||||
|
subprocess.run(["powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", ps_cmd],
|
||||||
|
capture_output=True, timeout=5)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
creation_flags = 0x08000000 | 0x00000200
|
||||||
|
subprocess.Popen(f'start "" {cmd_str}', shell=True, creationflags=creation_flags)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop_background_watcher():
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["powershell", "-NoProfile", "-Command",
|
||||||
|
"Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*--watch*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"],
|
||||||
|
capture_output=True, timeout=10
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def wait_for_files_settled(cf_dir: Path, timeout: float = 90.0, log_fn=None) -> bool:
|
||||||
|
"""
|
||||||
|
Waits for the CurseForge updater/installer to finish writing files.
|
||||||
|
Ensures resources/app.asar is present, fully written, and readable.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
start = time.time()
|
||||||
|
asar_path = cf_dir / "resources" / "app.asar"
|
||||||
|
last_size = -1
|
||||||
|
stable_checks = 0
|
||||||
|
|
||||||
|
while time.time() - start < timeout:
|
||||||
|
time.sleep(1.5)
|
||||||
|
|
||||||
|
if is_updater_running():
|
||||||
|
stable_checks = 0
|
||||||
|
if log_fn:
|
||||||
|
log_fn("CurseForge installer is currently running... waiting for completion.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if asar_path.exists():
|
||||||
|
try:
|
||||||
|
asar_size = asar_path.stat().st_size
|
||||||
|
if asar_size > 5_000_000:
|
||||||
|
if asar_size == last_size:
|
||||||
|
stable_checks += 1
|
||||||
|
if stable_checks >= 2:
|
||||||
|
try:
|
||||||
|
with open(asar_path, "rb") as f:
|
||||||
|
magic = f.read(4)
|
||||||
|
if len(magic) == 4:
|
||||||
|
if log_fn:
|
||||||
|
log_fn(f"Installer finished. app.asar settled ({asar_size // 1048576}MB).")
|
||||||
|
return True
|
||||||
|
except (PermissionError, OSError):
|
||||||
|
stable_checks = 0
|
||||||
|
else:
|
||||||
|
stable_checks = 0
|
||||||
|
last_size = asar_size
|
||||||
|
if log_fn:
|
||||||
|
log_fn(f"Waiting for app.asar to settle... ({asar_size // 1048576}MB)")
|
||||||
|
except (OSError, PermissionError):
|
||||||
|
stable_checks = 0
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wait_for_update_and_repatch(pid_to_wait: int | None = None, log_fn=None) -> bool:
|
||||||
|
"""
|
||||||
|
Invoked when CurseForge is quitting to apply an update.
|
||||||
|
Waits for the old CurseForge instance to exit, waits for the installer to finish,
|
||||||
|
and immediately repatches before/as CurseForge restarts.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
if log_fn is None:
|
||||||
|
log_dir = get_user_data_dir()
|
||||||
|
watch_log = log_dir / "watcher.log"
|
||||||
|
def log_fn(msg):
|
||||||
|
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
try:
|
||||||
|
with open(watch_log, "a", encoding="utf-8") as f:
|
||||||
|
f.write(f"[{ts}] [OnUpdateHook] {msg}\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log_fn(f"Wait-for-update triggered (PID={pid_to_wait})")
|
||||||
|
cf_dir = get_curseforge_dir()
|
||||||
|
if not cf_dir:
|
||||||
|
log_fn("CurseForge directory not found.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if pid_to_wait:
|
||||||
|
log_fn(f"Waiting for PID {pid_to_wait} to terminate...")
|
||||||
|
for _ in range(30):
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(
|
||||||
|
["tasklist", "/FI", f"PID eq {pid_to_wait}", "/NH"],
|
||||||
|
text=True, stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
if str(pid_to_wait) not in out:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
settled = wait_for_files_settled(cf_dir, timeout=90.0, log_fn=log_fn)
|
||||||
|
if not settled:
|
||||||
|
log_fn("Timed out waiting for installer to settle.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
was_running = is_curseforge_running()
|
||||||
|
if was_running:
|
||||||
|
log_fn("CurseForge launched unpatched by installer. Terminating to apply patch...")
|
||||||
|
kill_curseforge()
|
||||||
|
time.sleep(1.5)
|
||||||
|
|
||||||
|
success = False
|
||||||
|
for attempt in range(1, 4):
|
||||||
|
success = patch_curseforge(log_callback=log_fn)
|
||||||
|
if success:
|
||||||
|
break
|
||||||
|
log_fn(f"Patch attempt {attempt} failed, retrying in 2 seconds...")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
log_fn("★ CurseForge successfully re-patched after update!")
|
||||||
|
log_fn("Launching modded CurseForge...")
|
||||||
|
subprocess.Popen([str(cf_dir / "CurseForge.exe")])
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log_fn("Failed to re-patch CurseForge after update.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_watcher_loop():
|
||||||
|
"""
|
||||||
|
Resilient Win32 filesystem watcher with kernel event signaling + 3s heartbeat fallback.
|
||||||
|
0% CPU consumption during idle.
|
||||||
|
Instantly and reliably re-patches CurseForge when updated.
|
||||||
|
"""
|
||||||
|
import ctypes
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
log_dir = get_user_data_dir()
|
||||||
|
watch_log = log_dir / "watcher.log"
|
||||||
|
|
||||||
|
def wlog(msg):
|
||||||
|
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
try:
|
||||||
|
with open(watch_log, "a", encoding="utf-8") as f:
|
||||||
|
f.write(f"[{ts}] {msg}\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
kernel32 = ctypes.windll.kernel32
|
||||||
|
kernel32.CreateMutexW.restype = ctypes.c_void_p
|
||||||
|
kernel32.CreateMutexW.argtypes = [ctypes.c_void_p, ctypes.c_bool, ctypes.c_wchar_p]
|
||||||
|
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||||
|
|
||||||
|
mutex = kernel32.CreateMutexW(None, True, WATCHER_MUTEX_NAME)
|
||||||
|
if kernel32.GetLastError() == 183: # ERROR_ALREADY_EXISTS
|
||||||
|
wlog("Watcher already running (mutex exists). Exiting duplicate.")
|
||||||
|
return
|
||||||
|
|
||||||
|
wlog("Watcher started.")
|
||||||
|
cf_dir = get_curseforge_dir()
|
||||||
|
if not cf_dir:
|
||||||
|
wlog("CurseForge directory not found. Watcher exiting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
def check_and_repatch(trigger_reason: str):
|
||||||
|
if not is_curseforge_patched(cf_dir):
|
||||||
|
wlog(f"CurseForge unpatched state detected ({trigger_reason}). Waiting for files to settle...")
|
||||||
|
settled = wait_for_files_settled(cf_dir, timeout=90.0, log_fn=wlog)
|
||||||
|
if settled:
|
||||||
|
was_running = is_curseforge_running()
|
||||||
|
if was_running:
|
||||||
|
wlog("CurseForge is currently running unpatched. Closing it to apply BetterCurseForge patch...")
|
||||||
|
kill_curseforge()
|
||||||
|
time.sleep(1.5)
|
||||||
|
|
||||||
|
wlog("Applying BetterCurseForge patch...")
|
||||||
|
success = False
|
||||||
|
for attempt in range(1, 4):
|
||||||
|
success = patch_curseforge(log_callback=wlog)
|
||||||
|
if success:
|
||||||
|
break
|
||||||
|
wlog(f"Patch attempt {attempt} failed, retrying in 2 seconds...")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
wlog("★ CurseForge successfully re-patched after update!")
|
||||||
|
if was_running:
|
||||||
|
wlog("Relaunching modded CurseForge...")
|
||||||
|
subprocess.Popen([str(cf_dir / "CurseForge.exe")])
|
||||||
|
else:
|
||||||
|
wlog("Failed to re-patch CurseForge after update.")
|
||||||
|
else:
|
||||||
|
wlog("Timed out waiting for installer files to settle.")
|
||||||
|
|
||||||
|
check_and_repatch("Startup Check")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(3)
|
||||||
|
try:
|
||||||
|
check_and_repatch("Heartbeat Check")
|
||||||
|
except Exception as e:
|
||||||
|
wlog(f"Error during check_and_repatch: {e}")
|
||||||
|
finally:
|
||||||
|
if mutex:
|
||||||
|
kernel32.CloseHandle(mutex)
|
||||||
|
wlog("Watcher stopped.")
|
||||||
|
|
||||||
63
patcher/patcher.py
Normal file
63
patcher/patcher.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
BetterCurseForge Patcher — CLI
|
||||||
|
===============================
|
||||||
|
Command-line interface for patching CurseForge Launcher.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
if os.name == "nt":
|
||||||
|
if sys.stdout is not None:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
if sys.stderr is not None:
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import patch_core
|
||||||
|
except ImportError:
|
||||||
|
import patch_core
|
||||||
|
|
||||||
|
VERSION = "1.0.0"
|
||||||
|
|
||||||
|
BANNER = r"""
|
||||||
|
╔══════════════════════════════════════════╗
|
||||||
|
║ ⬡ BetterCurseForge Patcher ⬡ ║
|
||||||
|
║ v{ver} ║
|
||||||
|
╚══════════════════════════════════════════╝
|
||||||
|
""".format(ver=VERSION)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(BANNER)
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python patcher.py [patch | unpatch | status | watch]")
|
||||||
|
print("For GUI, run without arguments or run patcher_gui.py")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd = sys.argv[1].lower()
|
||||||
|
if cmd in ("watch", "--watch"):
|
||||||
|
patch_core.run_watcher_loop()
|
||||||
|
elif cmd == "--wait-for-update":
|
||||||
|
pid = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else None
|
||||||
|
patch_core.wait_for_update_and_repatch(pid)
|
||||||
|
elif cmd == "--ensure-watcher":
|
||||||
|
if not patch_core.is_watcher_running():
|
||||||
|
patch_core.start_background_watcher()
|
||||||
|
elif cmd == "patch":
|
||||||
|
patch_core.patch_curseforge()
|
||||||
|
elif cmd == "unpatch":
|
||||||
|
patch_core.unpatch_curseforge()
|
||||||
|
elif cmd == "status":
|
||||||
|
cf = patch_core.get_curseforge_dir()
|
||||||
|
if not cf:
|
||||||
|
print("CurseForge installation not found.")
|
||||||
|
else:
|
||||||
|
print(f"CurseForge directory: {cf}")
|
||||||
|
patched = patch_core.is_curseforge_patched(cf)
|
||||||
|
print(f"Patch status: {'PATCHED' if patched else 'NOT PATCHED'}")
|
||||||
|
print(f"Auto-repatch watcher: {'ACTIVE' if patch_core.is_watcher_running() else 'INACTIVE'}")
|
||||||
|
else:
|
||||||
|
print(f"Unknown command: {cmd}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
427
patcher/patcher_gui.py
Normal file
427
patcher/patcher_gui.py
Normal file
|
|
@ -0,0 +1,427 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
BetterCurseForge — Graphical Patcher & Management Suite
|
||||||
|
========================================================
|
||||||
|
Single executable GUI for patching CurseForge, managing themes/plugins,
|
||||||
|
and providing zero-timer automatic re-patching after updates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
if os.name == "nt":
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1].lower() not in ("--watch", "--wait-for-update", "--ensure-watcher"):
|
||||||
|
import ctypes
|
||||||
|
ctypes.windll.kernel32.AttachConsole(-1)
|
||||||
|
if sys.stdout is not None:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
if sys.stderr is not None:
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox, scrolledtext
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Import patch core logic
|
||||||
|
try:
|
||||||
|
from . import patch_core
|
||||||
|
except ImportError:
|
||||||
|
import patch_core
|
||||||
|
|
||||||
|
VERSION = "1.0.0"
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# COLOR PALETTE (Modern Discord / Vencord Dark Theme)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
COLORS = {
|
||||||
|
"bg": "#121214",
|
||||||
|
"bg_card": "#18191c",
|
||||||
|
"bg_elevated": "#202225",
|
||||||
|
"bg_input": "#0e0f10",
|
||||||
|
"purple": "#5865f2",
|
||||||
|
"purple_hover": "#4752c4",
|
||||||
|
"purple_dark": "#3c45a5",
|
||||||
|
"purple_glow": "#7983f5",
|
||||||
|
"green": "#57f287",
|
||||||
|
"green_dark": "#237e44",
|
||||||
|
"red": "#ed4245",
|
||||||
|
"yellow": "#fee75c",
|
||||||
|
"text": "#f2f3f5",
|
||||||
|
"text_dim": "#949ba4",
|
||||||
|
"text_muted": "#5c6067",
|
||||||
|
"border": "#2b2d31",
|
||||||
|
"border_focus": "#5865f2",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# GUI APPLICATION
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class BetterCFApp:
|
||||||
|
def __init__(self):
|
||||||
|
self.root = tk.Tk()
|
||||||
|
self.root.title("BetterCurseForge Patcher")
|
||||||
|
self.root.geometry("780x560")
|
||||||
|
self.root.minsize(720, 500)
|
||||||
|
self.root.configure(bg=COLORS["bg"])
|
||||||
|
|
||||||
|
self.install_dir = None
|
||||||
|
self.is_working = False
|
||||||
|
|
||||||
|
self._configure_styles()
|
||||||
|
self._build_ui()
|
||||||
|
self._refresh_status()
|
||||||
|
|
||||||
|
def _configure_styles(self):
|
||||||
|
style = ttk.Style()
|
||||||
|
style.theme_use("clam")
|
||||||
|
style.configure(".", background=COLORS["bg"], foreground=COLORS["text"], font=("Segoe UI", 10))
|
||||||
|
style.configure("TFrame", background=COLORS["bg"])
|
||||||
|
style.configure("Card.TFrame", background=COLORS["bg_card"])
|
||||||
|
style.configure("CardTitle.TLabel", font=("Segoe UI", 11, "bold"), foreground=COLORS["text"])
|
||||||
|
style.configure("StatusKey.TLabel", font=("Segoe UI", 9), foreground=COLORS["text_dim"])
|
||||||
|
style.configure("StatusVal.TLabel", font=("Segoe UI", 9, "bold"), foreground=COLORS["text"])
|
||||||
|
style.configure("Patched.TLabel", font=("Segoe UI", 9, "bold"), foreground=COLORS["green"])
|
||||||
|
style.configure("NotPatched.TLabel", font=("Segoe UI", 9, "bold"), foreground=COLORS["red"])
|
||||||
|
style.configure("Error.TLabel", font=("Segoe UI", 9, "bold"), foreground=COLORS["red"])
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
# Header
|
||||||
|
header = tk.Frame(self.root, bg=COLORS["bg_card"], padx=20, pady=12)
|
||||||
|
header.pack(fill="x")
|
||||||
|
|
||||||
|
title_frame = tk.Frame(header, bg=COLORS["bg_card"])
|
||||||
|
title_frame.pack(side="left")
|
||||||
|
|
||||||
|
title_lbl = tk.Label(
|
||||||
|
title_frame, text="⬡ BetterCurseForge", font=("Segoe UI", 16, "bold"),
|
||||||
|
bg=COLORS["bg_card"], fg=COLORS["purple_glow"]
|
||||||
|
)
|
||||||
|
title_lbl.pack(side="left")
|
||||||
|
|
||||||
|
ver_lbl = tk.Label(
|
||||||
|
title_frame, text=f"v{VERSION}", font=("Segoe UI", 9),
|
||||||
|
bg=COLORS["bg_card"], fg=COLORS["text_dim"]
|
||||||
|
)
|
||||||
|
ver_lbl.pack(side="left", padx=(8, 0), pady=(4, 0))
|
||||||
|
|
||||||
|
sub_lbl = tk.Label(
|
||||||
|
header, text="Ad-free launcher with custom themes and plugins", font=("Segoe UI", 9),
|
||||||
|
bg=COLORS["bg_card"], fg=COLORS["text_dim"]
|
||||||
|
)
|
||||||
|
sub_lbl.pack(side="left", padx=(16, 0), pady=(4, 0))
|
||||||
|
|
||||||
|
# Main Layout (2 Columns)
|
||||||
|
main = ttk.Frame(self.root, padding="14")
|
||||||
|
main.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
left = ttk.Frame(main)
|
||||||
|
left.pack(side="left", fill="both", expand=False, padx=(0, 10))
|
||||||
|
|
||||||
|
# ── Status Card ──
|
||||||
|
status_card = tk.Frame(left, bg=COLORS["bg_card"], highlightbackground=COLORS["border"],
|
||||||
|
highlightthickness=1, padx=14, pady=12)
|
||||||
|
status_card.pack(fill="x", pady=(0, 10))
|
||||||
|
|
||||||
|
ttk.Label(status_card, text="📋 Launcher Status", style="CardTitle.TLabel").pack(anchor="w")
|
||||||
|
tk.Frame(status_card, height=1, bg=COLORS["border"]).pack(fill="x", pady=(6, 8))
|
||||||
|
|
||||||
|
self.status_grid = tk.Frame(status_card, bg=COLORS["bg_card"])
|
||||||
|
self.status_grid.pack(fill="x")
|
||||||
|
|
||||||
|
self._status_labels = {}
|
||||||
|
for i, (key, label) in enumerate([
|
||||||
|
("install", "Install Path"),
|
||||||
|
("status", "Patch Status"),
|
||||||
|
("watcher", "Auto-Repatch"),
|
||||||
|
("themes", "Themes"),
|
||||||
|
("plugins", "Plugins"),
|
||||||
|
]):
|
||||||
|
ttk.Label(self.status_grid, text=label, style="StatusKey.TLabel").grid(
|
||||||
|
row=i, column=0, sticky="w", pady=2, padx=(0, 12))
|
||||||
|
lbl = ttk.Label(self.status_grid, text="—", style="StatusVal.TLabel")
|
||||||
|
lbl.grid(row=i, column=1, sticky="w", pady=2)
|
||||||
|
self._status_labels[key] = lbl
|
||||||
|
|
||||||
|
# ── Action Buttons ──
|
||||||
|
btn_frame = tk.Frame(left, bg=COLORS["bg"])
|
||||||
|
btn_frame.pack(fill="x")
|
||||||
|
|
||||||
|
self.patch_btn = tk.Button(
|
||||||
|
btn_frame, text="⚡ Patch CurseForge", font=("Segoe UI", 11, "bold"),
|
||||||
|
bg=COLORS["purple"], fg="#ffffff", activebackground=COLORS["purple_hover"],
|
||||||
|
activeforeground="#ffffff", relief="flat", cursor="hand2", pady=9,
|
||||||
|
command=self._on_patch,
|
||||||
|
)
|
||||||
|
self.patch_btn.pack(fill="x", pady=(0, 5))
|
||||||
|
|
||||||
|
self.unpatch_btn = tk.Button(
|
||||||
|
btn_frame, text="🔄 Restore Vanilla", font=("Segoe UI", 10),
|
||||||
|
bg=COLORS["bg_elevated"], fg=COLORS["text_dim"], activebackground=COLORS["border"],
|
||||||
|
activeforeground=COLORS["text"], relief="flat", cursor="hand2", pady=7,
|
||||||
|
command=self._on_unpatch,
|
||||||
|
)
|
||||||
|
self.unpatch_btn.pack(fill="x", pady=(0, 5))
|
||||||
|
|
||||||
|
self.watcher_btn = tk.Button(
|
||||||
|
btn_frame, text="🛡️ Auto-Repatch on Update", font=("Segoe UI", 10),
|
||||||
|
bg=COLORS["bg_elevated"], fg=COLORS["text_dim"], activebackground=COLORS["border"],
|
||||||
|
activeforeground=COLORS["text"], relief="flat", cursor="hand2", pady=7,
|
||||||
|
command=self._on_toggle_watcher,
|
||||||
|
)
|
||||||
|
self.watcher_btn.pack(fill="x", pady=(0, 8))
|
||||||
|
|
||||||
|
# Secondary Actions
|
||||||
|
sub_row = tk.Frame(btn_frame, bg=COLORS["bg"])
|
||||||
|
sub_row.pack(fill="x")
|
||||||
|
|
||||||
|
self.launch_btn = tk.Button(
|
||||||
|
sub_row, text="🚀 Launch", font=("Segoe UI", 9),
|
||||||
|
bg=COLORS["bg_elevated"], fg=COLORS["text_dim"], activebackground=COLORS["border"],
|
||||||
|
activeforeground=COLORS["text"], relief="flat", cursor="hand2", padx=10, pady=5,
|
||||||
|
command=self._on_launch,
|
||||||
|
)
|
||||||
|
self.launch_btn.pack(side="left", padx=(0, 4), fill="x", expand=True)
|
||||||
|
|
||||||
|
self.themes_btn = tk.Button(
|
||||||
|
sub_row, text="🎨 Themes", font=("Segoe UI", 9),
|
||||||
|
bg=COLORS["bg_elevated"], fg=COLORS["text_dim"], activebackground=COLORS["border"],
|
||||||
|
activeforeground=COLORS["text"], relief="flat", cursor="hand2", padx=8, pady=5,
|
||||||
|
command=self._on_open_themes,
|
||||||
|
)
|
||||||
|
self.themes_btn.pack(side="left", padx=(0, 4), fill="x", expand=True)
|
||||||
|
|
||||||
|
self.plugins_btn = tk.Button(
|
||||||
|
sub_row, text="🔌 Plugins", font=("Segoe UI", 9),
|
||||||
|
bg=COLORS["bg_elevated"], fg=COLORS["text_dim"], activebackground=COLORS["border"],
|
||||||
|
activeforeground=COLORS["text"], relief="flat", cursor="hand2", padx=8, pady=5,
|
||||||
|
command=self._on_open_plugins,
|
||||||
|
)
|
||||||
|
self.plugins_btn.pack(side="left", fill="x", expand=True)
|
||||||
|
|
||||||
|
# ── Right Column: Logs ──
|
||||||
|
right = ttk.Frame(main)
|
||||||
|
right.pack(side="right", fill="both", expand=True)
|
||||||
|
|
||||||
|
log_card = tk.Frame(right, bg=COLORS["bg_card"], highlightbackground=COLORS["border"],
|
||||||
|
highlightthickness=1, padx=12, pady=10)
|
||||||
|
log_card.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
log_header = tk.Frame(log_card, bg=COLORS["bg_card"])
|
||||||
|
log_header.pack(fill="x", pady=(0, 6))
|
||||||
|
|
||||||
|
ttk.Label(log_header, text="📝 Activity Log", style="CardTitle.TLabel").pack(side="left")
|
||||||
|
clear_btn = tk.Button(
|
||||||
|
log_header, text="Clear", font=("Segoe UI", 8),
|
||||||
|
bg=COLORS["bg_card"], fg=COLORS["text_muted"], activebackground=COLORS["bg_card"],
|
||||||
|
activeforeground=COLORS["text"], relief="flat", cursor="hand2",
|
||||||
|
command=self._clear_log
|
||||||
|
)
|
||||||
|
clear_btn.pack(side="right")
|
||||||
|
|
||||||
|
self.log_text = scrolledtext.ScrolledText(
|
||||||
|
log_card, wrap="word", font=("Cascadia Code", 9), height=18,
|
||||||
|
bg=COLORS["bg_input"], fg=COLORS["text_dim"], insertbackground=COLORS["text"],
|
||||||
|
relief="flat", highlightbackground=COLORS["border"], highlightthickness=1,
|
||||||
|
padx=8, pady=8,
|
||||||
|
)
|
||||||
|
self.log_text.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
self.log_text.tag_configure("timestamp", foreground=COLORS["text_muted"])
|
||||||
|
self.log_text.tag_configure("info", foreground=COLORS["text"])
|
||||||
|
self.log_text.tag_configure("success", foreground=COLORS["green"])
|
||||||
|
self.log_text.tag_configure("error", foreground=COLORS["red"])
|
||||||
|
self.log_text.tag_configure("warn", foreground=COLORS["yellow"])
|
||||||
|
self.log_text.tag_configure("header", foreground=COLORS["purple_glow"], font=("Segoe UI", 9, "bold"))
|
||||||
|
self.log_text.configure(state="disabled")
|
||||||
|
|
||||||
|
# ── Logging ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def log(self, message: str, level: str = "info"):
|
||||||
|
ts = datetime.now().strftime("%H:%M:%S")
|
||||||
|
self.log_text.configure(state="normal")
|
||||||
|
self.log_text.insert("end", f"[{ts}] ", "timestamp")
|
||||||
|
|
||||||
|
tag = level
|
||||||
|
if message.startswith("[+]") or message.startswith("✓") or "success" in message.lower():
|
||||||
|
tag = "success"
|
||||||
|
elif message.startswith("[-]") or message.startswith("✗") or "fail" in message.lower() or "error" in message.lower():
|
||||||
|
tag = "error"
|
||||||
|
elif message.startswith("[!]") or "warn" in message.lower():
|
||||||
|
tag = "warn"
|
||||||
|
|
||||||
|
self.log_text.insert("end", f"{message}\n", tag)
|
||||||
|
self.log_text.see("end")
|
||||||
|
self.log_text.configure(state="disabled")
|
||||||
|
|
||||||
|
def _clear_log(self):
|
||||||
|
self.log_text.configure(state="normal")
|
||||||
|
self.log_text.delete("1.0", "end")
|
||||||
|
self.log_text.configure(state="disabled")
|
||||||
|
|
||||||
|
# ── Refresh Status ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _refresh_status(self):
|
||||||
|
self.install_dir = patch_core.get_curseforge_dir()
|
||||||
|
if not self.install_dir:
|
||||||
|
self._status_labels["install"].configure(text="Not Found", style="Error.TLabel")
|
||||||
|
self._status_labels["status"].configure(text="N/A", style="StatusVal.TLabel")
|
||||||
|
self.patch_btn.configure(state="disabled")
|
||||||
|
self.unpatch_btn.configure(state="disabled")
|
||||||
|
self.log("CurseForge installation not found!", "error")
|
||||||
|
return
|
||||||
|
|
||||||
|
path_str = str(self.install_dir)
|
||||||
|
if len(path_str) > 32:
|
||||||
|
path_str = "..." + path_str[-29:]
|
||||||
|
self._status_labels["install"].configure(text=path_str, style="StatusVal.TLabel")
|
||||||
|
|
||||||
|
is_patched = patch_core.is_curseforge_patched(self.install_dir)
|
||||||
|
if is_patched:
|
||||||
|
self._status_labels["status"].configure(text="✓ PATCHED", style="Patched.TLabel")
|
||||||
|
self.patch_btn.configure(text="🔁 Re-Patch CurseForge")
|
||||||
|
self.unpatch_btn.configure(state="normal")
|
||||||
|
else:
|
||||||
|
self._status_labels["status"].configure(text="✗ NOT PATCHED", style="NotPatched.TLabel")
|
||||||
|
self.patch_btn.configure(text="⚡ Patch CurseForge")
|
||||||
|
self.unpatch_btn.configure(state="disabled")
|
||||||
|
|
||||||
|
# Watcher status
|
||||||
|
autostart = patch_core.is_autostart_watcher_enabled()
|
||||||
|
running = patch_core.is_watcher_running()
|
||||||
|
if is_patched and not running and autostart:
|
||||||
|
patch_core.start_background_watcher()
|
||||||
|
running = patch_core.is_watcher_running()
|
||||||
|
|
||||||
|
if running:
|
||||||
|
self._status_labels["watcher"].configure(text="Active (Instant)", style="Patched.TLabel")
|
||||||
|
self.watcher_btn.configure(text="🛡️ Auto-Repatch: ON", bg=COLORS["green_dark"], fg="#ffffff")
|
||||||
|
elif autostart:
|
||||||
|
self._status_labels["watcher"].configure(text="Enabled (Idle)", style="Patched.TLabel")
|
||||||
|
self.watcher_btn.configure(text="🛡️ Auto-Repatch: ON", bg=COLORS["green_dark"], fg="#ffffff")
|
||||||
|
else:
|
||||||
|
self._status_labels["watcher"].configure(text="Inactive", style="StatusKey.TLabel")
|
||||||
|
self.watcher_btn.configure(text="🛡️ Enable Auto-Repatch", bg=COLORS["bg_elevated"], fg=COLORS["text_dim"])
|
||||||
|
|
||||||
|
# Themes & Plugins counts
|
||||||
|
user_dir = patch_core.get_user_data_dir()
|
||||||
|
n_themes = len(list((user_dir / "themes").glob("*.css")))
|
||||||
|
n_plugins = len([p for p in (user_dir / "plugins").iterdir() if p.is_dir()])
|
||||||
|
self._status_labels["themes"].configure(text=f"{n_themes} installed")
|
||||||
|
self._status_labels["plugins"].configure(text=f"{n_plugins} installed")
|
||||||
|
|
||||||
|
self.patch_btn.configure(state="normal")
|
||||||
|
|
||||||
|
# ── Actions ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _on_patch(self):
|
||||||
|
if self.is_working:
|
||||||
|
return
|
||||||
|
self.is_working = True
|
||||||
|
self.patch_btn.configure(state="disabled")
|
||||||
|
self.unpatch_btn.configure(state="disabled")
|
||||||
|
|
||||||
|
def task():
|
||||||
|
self.log("Starting patch process...", "header")
|
||||||
|
success = patch_core.patch_curseforge(log_callback=lambda msg: self.root.after(0, self.log, msg))
|
||||||
|
if success:
|
||||||
|
self.root.after(0, self.log, "[+] Auto-repatch active. Watcher running in background.", "success")
|
||||||
|
self.root.after(0, self._after_action)
|
||||||
|
|
||||||
|
threading.Thread(target=task, daemon=True).start()
|
||||||
|
|
||||||
|
def _on_unpatch(self):
|
||||||
|
if self.is_working:
|
||||||
|
return
|
||||||
|
self.is_working = True
|
||||||
|
self.patch_btn.configure(state="disabled")
|
||||||
|
self.unpatch_btn.configure(state="disabled")
|
||||||
|
|
||||||
|
def task():
|
||||||
|
self.log("Restoring vanilla CurseForge...", "header")
|
||||||
|
success = patch_core.unpatch_curseforge(log_callback=lambda msg: self.root.after(0, self.log, msg))
|
||||||
|
self.root.after(0, self._after_action)
|
||||||
|
|
||||||
|
threading.Thread(target=task, daemon=True).start()
|
||||||
|
|
||||||
|
def _after_action(self):
|
||||||
|
self.is_working = False
|
||||||
|
self._refresh_status()
|
||||||
|
|
||||||
|
def _on_toggle_watcher(self):
|
||||||
|
current = patch_core.is_autostart_watcher_enabled() or patch_core.is_watcher_running()
|
||||||
|
new_state = not current
|
||||||
|
patch_core.set_autostart_watcher(new_state)
|
||||||
|
if new_state:
|
||||||
|
patch_core.start_background_watcher()
|
||||||
|
self.log("[+] Auto-repatch enabled. Watching resources/ directory for updates.", "success")
|
||||||
|
else:
|
||||||
|
patch_core.stop_background_watcher()
|
||||||
|
self.log("[-] Auto-repatch disabled.", "warn")
|
||||||
|
self._refresh_status()
|
||||||
|
|
||||||
|
def _on_launch(self):
|
||||||
|
if not self.install_dir:
|
||||||
|
return
|
||||||
|
exe = self.install_dir / "CurseForge.exe"
|
||||||
|
if exe.exists():
|
||||||
|
self.log(f"Launching CurseForge: {exe}")
|
||||||
|
creation_flags = 0x00000008 | 0x00000200 # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
|
||||||
|
subprocess.Popen([str(exe)], cwd=str(self.install_dir), creationflags=creation_flags)
|
||||||
|
else:
|
||||||
|
self.log("CurseForge.exe not found!", "error")
|
||||||
|
|
||||||
|
def _on_open_themes(self):
|
||||||
|
user_dir = patch_core.get_user_data_dir()
|
||||||
|
themes_dir = user_dir / "themes"
|
||||||
|
os.startfile(str(themes_dir))
|
||||||
|
self.log(f"Opened themes folder: {themes_dir}")
|
||||||
|
|
||||||
|
def _on_open_plugins(self):
|
||||||
|
user_dir = patch_core.get_user_data_dir()
|
||||||
|
plugins_dir = user_dir / "plugins"
|
||||||
|
os.startfile(str(plugins_dir))
|
||||||
|
self.log(f"Opened plugins folder: {plugins_dir}")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.log(f"BetterCurseForge Patcher v{VERSION} ready.")
|
||||||
|
self.root.mainloop()
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# MAIN ENTRY POINT
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
arg = sys.argv[1].lower()
|
||||||
|
if arg == "--watch":
|
||||||
|
# Silent background watcher mode
|
||||||
|
patch_core.run_watcher_loop()
|
||||||
|
sys.exit(0)
|
||||||
|
elif arg == "--wait-for-update":
|
||||||
|
pid = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else None
|
||||||
|
patch_core.wait_for_update_and_repatch(pid)
|
||||||
|
sys.exit(0)
|
||||||
|
elif arg == "--ensure-watcher":
|
||||||
|
if not patch_core.is_watcher_running():
|
||||||
|
patch_core.start_background_watcher()
|
||||||
|
sys.exit(0)
|
||||||
|
elif arg == "patch":
|
||||||
|
patch_core.patch_curseforge()
|
||||||
|
sys.exit(0)
|
||||||
|
elif arg == "unpatch":
|
||||||
|
patch_core.unpatch_curseforge()
|
||||||
|
sys.exit(0)
|
||||||
|
elif arg == "status":
|
||||||
|
cf = patch_core.get_curseforge_dir()
|
||||||
|
print("CurseForge:", cf)
|
||||||
|
print("Patched:", patch_core.is_curseforge_patched(cf) if cf else False)
|
||||||
|
print("Watcher:", "RUNNING" if patch_core.is_watcher_running() else "STOPPED")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
app = BetterCFApp()
|
||||||
|
app.run()
|
||||||
16
patcher/vendor/asar/node_modules/.bin/asar
generated
vendored
Normal file
16
patcher/vendor/asar/node_modules/.bin/asar
generated
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../@electron/asar/bin/asar.mjs" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../@electron/asar/bin/asar.mjs" "$@"
|
||||||
|
fi
|
||||||
17
patcher/vendor/asar/node_modules/.bin/asar.cmd
generated
vendored
Normal file
17
patcher/vendor/asar/node_modules/.bin/asar.cmd
generated
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@electron\asar\bin\asar.mjs" %*
|
||||||
28
patcher/vendor/asar/node_modules/.bin/asar.ps1
generated
vendored
Normal file
28
patcher/vendor/asar/node_modules/.bin/asar.ps1
generated
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../@electron/asar/bin/asar.mjs" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../@electron/asar/bin/asar.mjs" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../@electron/asar/bin/asar.mjs" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../@electron/asar/bin/asar.mjs" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
110
patcher/vendor/asar/node_modules/.package-lock.json
generated
vendored
Normal file
110
patcher/vendor/asar/node_modules/.package-lock.json
generated
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
{
|
||||||
|
"name": "4b0e2640fe917ac8",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"node_modules/@electron/asar": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-6pI64z/tMSBkUTZLQFYhs2YiB7fS9sKA6gy7/2nH1Ju7GGwT32UPsj4Mxsws5NMmtR6C0zYdXVxEU/rGqJRCJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^13.0.2",
|
||||||
|
"minimatch": "^10.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"asar": "bin/asar.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||||
|
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/glob": {
|
||||||
|
"version": "13.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
|
||||||
|
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^10.2.2",
|
||||||
|
"minipass": "^7.1.3",
|
||||||
|
"path-scurry": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "11.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
|
||||||
|
"integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "10.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
|
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-scurry": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^11.0.0",
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
patcher/vendor/asar/node_modules/@electron/asar/LICENSE.md
generated
vendored
Normal file
20
patcher/vendor/asar/node_modules/@electron/asar/LICENSE.md
generated
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
Copyright (c) 2014 GitHub Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
233
patcher/vendor/asar/node_modules/@electron/asar/README.md
generated
vendored
Normal file
233
patcher/vendor/asar/node_modules/@electron/asar/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
# @electron/asar - Electron Archive
|
||||||
|
|
||||||
|
[](https://github.com/electron/asar/actions/workflows/test.yml)
|
||||||
|
[](https://npmjs.org/package/@electron/asar)
|
||||||
|
[](https://packages.electronjs.org/asar)
|
||||||
|
|
||||||
|
ASAR is a simple extensive archive format. It concatenates all files together without compression
|
||||||
|
(like [`tar`](https://www.gnu.org/software/tar/)) while having random access support.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Support random access
|
||||||
|
* Use JSON to store file information
|
||||||
|
* Very easy to write a parser
|
||||||
|
* Store the contents of duplicated files only once
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
This module requires Node 22.12.0 or later.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --engine-strict @electron/asar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ asar --help
|
||||||
|
|
||||||
|
Usage: asar [options] [command]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
|
||||||
|
pack|p <dir> <output>
|
||||||
|
create asar archive
|
||||||
|
|
||||||
|
list|l <archive>
|
||||||
|
list files of asar archive
|
||||||
|
|
||||||
|
extract-file|ef <archive> <filename>
|
||||||
|
extract one file from archive
|
||||||
|
|
||||||
|
extract|e <archive> <dest>
|
||||||
|
extract archive
|
||||||
|
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-h, --help output usage information
|
||||||
|
-V, --version output the version number
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Excluding multiple resources from being packed
|
||||||
|
|
||||||
|
Given:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app
|
||||||
|
(a) ├── x1
|
||||||
|
(b) ├── x2
|
||||||
|
(c) ├── y3
|
||||||
|
(d) │ ├── x1
|
||||||
|
(e) │ └── z1
|
||||||
|
(f) │ └── x2
|
||||||
|
(g) └── z4
|
||||||
|
(h) └── w1
|
||||||
|
```
|
||||||
|
|
||||||
|
Exclude: a, b
|
||||||
|
|
||||||
|
```bash
|
||||||
|
asar pack app app.asar --unpack-dir "{x1,x2}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Exclude: a, b, d, f
|
||||||
|
|
||||||
|
```bash
|
||||||
|
asar pack app app.asar --unpack-dir "**/{x1,x2}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Exclude: a, b, d, f, h
|
||||||
|
|
||||||
|
```bash
|
||||||
|
asar pack app app.asar --unpack-dir "{**/x1,**/x2,z4/w1}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Programmatic usage
|
||||||
|
|
||||||
|
For full API usage, see the [API documentation](https://packages.electronjs.org/asar).
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createPackage } from '@electron/asar';
|
||||||
|
|
||||||
|
const src = 'some/path/';
|
||||||
|
const dest = 'name.asar';
|
||||||
|
|
||||||
|
await createPackage(src, dest);
|
||||||
|
console.log('done.');
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that there is currently **no** error handling provided!
|
||||||
|
|
||||||
|
### Deduplication
|
||||||
|
|
||||||
|
Files with identical contents are stored once and shared: the first copy is
|
||||||
|
written into the archive and every other copy's header entry points at that same
|
||||||
|
`offset`. Nothing changes for readers — each file still has its own entry, size,
|
||||||
|
integrity hash, and executable bit — but archives with duplicated contents (a
|
||||||
|
common shape for bundled `node_modules`) get smaller and pack faster, since the
|
||||||
|
redundant bytes are never written.
|
||||||
|
|
||||||
|
Unpacked files (`unpack` / `unpackDir`) are always written out in full, because
|
||||||
|
they live on disk outside the archive.
|
||||||
|
|
||||||
|
### Transform
|
||||||
|
|
||||||
|
You can pass in a `transform` option, that is a function, which either returns
|
||||||
|
nothing, or a `stream.Transform`. The latter will be used on files that will be
|
||||||
|
in the `.asar` file to transform them (e.g. compress).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createPackageWithOptions } from '@electron/asar';
|
||||||
|
|
||||||
|
const src = 'some/path/';
|
||||||
|
const dest = 'name.asar';
|
||||||
|
|
||||||
|
function transform (filename) {
|
||||||
|
return new CustomTransformStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
await createPackageWithOptions(src, dest, { transform: transform });
|
||||||
|
console.log('done.');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
Asar uses [Pickle][pickle] to safely serialize binary value to file.
|
||||||
|
|
||||||
|
The format of asar is very flat:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
| UInt32: header_size | String: header | Bytes: file1 | ... | Bytes: file42 |
|
||||||
|
```
|
||||||
|
|
||||||
|
The `header_size` and `header` are serialized with [Pickle][pickle] class, and
|
||||||
|
`header_size`'s [Pickle][pickle] object is 8 bytes.
|
||||||
|
|
||||||
|
The `header` is a JSON string, and the `header_size` is the size of `header`'s
|
||||||
|
`Pickle` object.
|
||||||
|
|
||||||
|
Structure of `header` is something like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"files": {
|
||||||
|
"tmp": {
|
||||||
|
"files": {}
|
||||||
|
},
|
||||||
|
"usr" : {
|
||||||
|
"files": {
|
||||||
|
"bin": {
|
||||||
|
"files": {
|
||||||
|
"ls": {
|
||||||
|
"offset": "0",
|
||||||
|
"size": 100,
|
||||||
|
"executable": true,
|
||||||
|
"integrity": {
|
||||||
|
"algorithm": "SHA256",
|
||||||
|
"hash": "...",
|
||||||
|
"blockSize": 1024,
|
||||||
|
"blocks": ["...", "..."]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cd": {
|
||||||
|
"offset": "100",
|
||||||
|
"size": 100,
|
||||||
|
"executable": true,
|
||||||
|
"integrity": {
|
||||||
|
"algorithm": "SHA256",
|
||||||
|
"hash": "...",
|
||||||
|
"blockSize": 1024,
|
||||||
|
"blocks": ["...", "..."]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"etc": {
|
||||||
|
"files": {
|
||||||
|
"hosts": {
|
||||||
|
"offset": "200",
|
||||||
|
"size": 32,
|
||||||
|
"integrity": {
|
||||||
|
"algorithm": "SHA256",
|
||||||
|
"hash": "...",
|
||||||
|
"blockSize": 1024,
|
||||||
|
"blocks": ["...", "..."]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`offset` and `size` records the information to read the file from archive, the
|
||||||
|
`offset` starts from 0 so you have to manually add the size of `header_size` and
|
||||||
|
`header` to the `offset` to get the real offset of the file.
|
||||||
|
|
||||||
|
Files with identical contents share a single copy in the archive, so more than
|
||||||
|
one entry can point at the same `offset`.
|
||||||
|
|
||||||
|
`offset` is a UINT64 number represented in string, because there is no way to
|
||||||
|
precisely represent UINT64 in JavaScript `Number`. `size` is a JavaScript
|
||||||
|
`Number` that is no larger than `Number.MAX_SAFE_INTEGER`, which has a value of
|
||||||
|
`9007199254740991` and is about 8PB in size. We didn't store `size` in UINT64
|
||||||
|
because file size in Node.js is represented as `Number` and it is not safe to
|
||||||
|
convert `Number` to UINT64.
|
||||||
|
|
||||||
|
`integrity` is an object consisting of a few keys:
|
||||||
|
|
||||||
|
* A hashing `algorithm`, currently only `SHA256` is supported.
|
||||||
|
* A hex encoded `hash` value representing the hash of the entire file.
|
||||||
|
* An array of hex encoded hashes for the `blocks` of the file (i.e. for a blockSize of 4KB, this array contains the hash of every block if you split the file into N 4KB blocks).
|
||||||
|
* A integer value `blockSize` representing the size in bytes of each block in the `blocks` hashes above.
|
||||||
|
|
||||||
|
[pickle]: https://chromium.googlesource.com/chromium/src/+/main/base/pickle.h
|
||||||
182
patcher/vendor/asar/node_modules/@electron/asar/bin/asar.mjs
generated
vendored
Normal file
182
patcher/vendor/asar/node_modules/@electron/asar/bin/asar.mjs
generated
vendored
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import packageJSON from '../package.json' with { type: 'json' };
|
||||||
|
import { createPackageWithOptions, listPackage, extractFile, extractAll } from '../lib/asar.js';
|
||||||
|
import { parseArgs } from 'node:util';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const splitVersion = function (version) {
|
||||||
|
return version.split('.').map(function (part) {
|
||||||
|
return Number(part);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const requiredNodeVersion = splitVersion(packageJSON.engines.node.slice(2));
|
||||||
|
const actualNodeVersion = splitVersion(process.versions.node);
|
||||||
|
|
||||||
|
if (
|
||||||
|
actualNodeVersion[0] < requiredNodeVersion[0] ||
|
||||||
|
(actualNodeVersion[0] === requiredNodeVersion[0] && actualNodeVersion[1] < requiredNodeVersion[1])
|
||||||
|
) {
|
||||||
|
console.error('CANNOT RUN WITH NODE ' + process.versions.node);
|
||||||
|
console.error('asar requires Node ' + packageJSON.engines.node + '.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const commands = {
|
||||||
|
pack: {
|
||||||
|
aliases: ['p'],
|
||||||
|
usage: 'pack|p [options] <dir> <output>',
|
||||||
|
description: 'create asar archive',
|
||||||
|
args: ['dir', 'output'],
|
||||||
|
options: {
|
||||||
|
ordering: { type: 'string', description: 'path to a text file for ordering contents' },
|
||||||
|
unpack: { type: 'string', description: 'do not pack files matching glob <expression>' },
|
||||||
|
'unpack-dir': {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'do not pack dirs matching glob <expression> or starting with literal <expression>',
|
||||||
|
},
|
||||||
|
'exclude-hidden': { type: 'boolean', description: 'exclude hidden files' },
|
||||||
|
},
|
||||||
|
action: (positionals, values) => {
|
||||||
|
const [dir, output] = positionals;
|
||||||
|
const options = {
|
||||||
|
unpack: values.unpack,
|
||||||
|
unpackDir: values['unpack-dir'],
|
||||||
|
ordering: values.ordering,
|
||||||
|
dot: !values['exclude-hidden'],
|
||||||
|
};
|
||||||
|
return createPackageWithOptions(dir, output, options);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
aliases: ['l'],
|
||||||
|
usage: 'list|l [options] <archive>',
|
||||||
|
description: 'list files of asar archive',
|
||||||
|
args: ['archive'],
|
||||||
|
options: {
|
||||||
|
'is-pack': {
|
||||||
|
type: 'boolean',
|
||||||
|
short: 'i',
|
||||||
|
description: 'each file in the asar is pack or unpack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: (positionals, values) => {
|
||||||
|
const [archive] = positionals;
|
||||||
|
const files = listPackage(archive, { isPack: values['is-pack'] });
|
||||||
|
for (const i in files) {
|
||||||
|
console.log(files[i]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'extract-file': {
|
||||||
|
aliases: ['ef'],
|
||||||
|
usage: 'extract-file|ef <archive> <filename>',
|
||||||
|
description: 'extract one file from archive',
|
||||||
|
args: ['archive', 'filename'],
|
||||||
|
options: {},
|
||||||
|
action: (positionals) => {
|
||||||
|
const [archive, filename] = positionals;
|
||||||
|
fs.writeFileSync(path.basename(filename), extractFile(archive, filename));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extract: {
|
||||||
|
aliases: ['e'],
|
||||||
|
usage: 'extract|e <archive> <dest>',
|
||||||
|
description: 'extract archive',
|
||||||
|
args: ['archive', 'dest'],
|
||||||
|
options: {},
|
||||||
|
action: (positionals) => {
|
||||||
|
const [archive, dest] = positionals;
|
||||||
|
extractAll(archive, dest);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log('Usage: asar [options] [command]');
|
||||||
|
console.log();
|
||||||
|
console.log('Manipulate asar archive files');
|
||||||
|
console.log();
|
||||||
|
console.log('Options:');
|
||||||
|
console.log(' -V, --version output the version number');
|
||||||
|
console.log(' -h, --help display help for command');
|
||||||
|
console.log();
|
||||||
|
console.log('Commands:');
|
||||||
|
for (const [name, cmd] of Object.entries(commands)) {
|
||||||
|
const label = `${name}|${cmd.aliases[0]}`;
|
||||||
|
console.log(` ${label.padEnd(32)} ${cmd.description}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printCommandHelp(cmd) {
|
||||||
|
console.log(`Usage: asar ${cmd.usage}`);
|
||||||
|
console.log();
|
||||||
|
console.log(cmd.description);
|
||||||
|
console.log();
|
||||||
|
console.log('Options:');
|
||||||
|
for (const [opt, spec] of Object.entries(cmd.options)) {
|
||||||
|
const prefix = spec.short ? `-${spec.short}, ` : '';
|
||||||
|
const suffix = spec.type === 'string' ? ' <value>' : '';
|
||||||
|
const label = `${prefix}--${opt}${suffix}`;
|
||||||
|
console.log(` ${label.padEnd(32)} ${spec.description}`);
|
||||||
|
}
|
||||||
|
console.log(` ${'-h, --help'.padEnd(32)} display help for command`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
||||||
|
printHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args[0] === '--version' || args[0] === '-V') {
|
||||||
|
console.log('v' + packageJSON.version);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandName = args[0];
|
||||||
|
const commandArgs = args.slice(1);
|
||||||
|
|
||||||
|
let command = commands[commandName];
|
||||||
|
if (!command) {
|
||||||
|
command = Object.values(commands).find((cmd) => cmd.aliases.includes(commandName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
console.log("asar: '%s' is not an asar command. See 'asar --help'.", commandName);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let values, positionals;
|
||||||
|
try {
|
||||||
|
({ values, positionals } = parseArgs({
|
||||||
|
args: commandArgs,
|
||||||
|
options: {
|
||||||
|
...command.options,
|
||||||
|
help: { type: 'boolean', short: 'h' },
|
||||||
|
},
|
||||||
|
allowPositionals: true,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`error: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.help) {
|
||||||
|
printCommandHelp(command);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionals.length < command.args.length) {
|
||||||
|
const missing = command.args[positionals.length];
|
||||||
|
console.error(`error: missing required argument '${missing}'`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.resolve(command.action(positionals, values)).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
75
patcher/vendor/asar/node_modules/@electron/asar/lib/asar.d.ts
generated
vendored
Normal file
75
patcher/vendor/asar/node_modules/@electron/asar/lib/asar.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { FilesystemEntry } from './filesystem.js';
|
||||||
|
import { InputMetadata } from './disk.js';
|
||||||
|
import { CrawledFileType } from './crawlfs.js';
|
||||||
|
import { GlobOptionsWithFileTypesFalse } from 'glob';
|
||||||
|
export declare function createPackage(src: string, dest: string): Promise<void>;
|
||||||
|
export type CreateOptions = {
|
||||||
|
dot?: boolean;
|
||||||
|
globOptions?: GlobOptionsWithFileTypesFalse;
|
||||||
|
/**
|
||||||
|
* Path to a file containing the list of relative filepaths relative to `src` and the specific order they should be inserted into the asar.
|
||||||
|
* Formats allowed below:
|
||||||
|
* filepath
|
||||||
|
* : filepath
|
||||||
|
* <anything>:filepath
|
||||||
|
*/
|
||||||
|
ordering?: string;
|
||||||
|
pattern?: string;
|
||||||
|
transform?: (filePath: string) => NodeJS.ReadWriteStream | void;
|
||||||
|
unpack?: string;
|
||||||
|
unpackDir?: string;
|
||||||
|
};
|
||||||
|
export declare function createPackageWithOptions(src: string, dest: string, options: CreateOptions): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Create an ASAR archive from a list of filenames.
|
||||||
|
*
|
||||||
|
* @param src - Base path. All files are relative to this.
|
||||||
|
* @param dest - Archive filename (& path).
|
||||||
|
* @param filenames - List of filenames relative to src.
|
||||||
|
* @param [metadata] - Object with filenames as keys and {type='directory|file|link', stat: fs.stat} as values. (Optional)
|
||||||
|
* @param [options] - Options passed to `createPackageWithOptions`.
|
||||||
|
*/
|
||||||
|
export declare function createPackageFromFiles(src: string, dest: string, filenames: string[], metadata?: InputMetadata, options?: CreateOptions): Promise<void>;
|
||||||
|
export type AsarStream = {
|
||||||
|
/**
|
||||||
|
Relative path to the file or directory from within the archive
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
/**
|
||||||
|
Function that returns a read stream for a file.
|
||||||
|
Note: this is called multiple times per "file", so a new NodeJS.ReadableStream needs to be created each time
|
||||||
|
*/
|
||||||
|
streamGenerator: () => NodeJS.ReadableStream;
|
||||||
|
/**
|
||||||
|
Whether the file/link should be unpacked
|
||||||
|
*/
|
||||||
|
unpacked: boolean;
|
||||||
|
stat: CrawledFileType['stat'];
|
||||||
|
};
|
||||||
|
export type AsarDirectory = Pick<AsarStream, 'path' | 'unpacked'> & {
|
||||||
|
type: 'directory';
|
||||||
|
};
|
||||||
|
export type AsarSymlinkStream = AsarStream & {
|
||||||
|
type: 'link';
|
||||||
|
symlink: string;
|
||||||
|
};
|
||||||
|
export type AsarFileStream = AsarStream & {
|
||||||
|
type: 'file';
|
||||||
|
};
|
||||||
|
export type AsarStreamType = AsarDirectory | AsarFileStream | AsarSymlinkStream;
|
||||||
|
/**
|
||||||
|
* Create an ASAR archive from a list of streams.
|
||||||
|
*
|
||||||
|
* @param dest - Archive filename (& path).
|
||||||
|
* @param streams - List of streams to be piped in-memory into asar filesystem. Insertion order is preserved.
|
||||||
|
*/
|
||||||
|
export declare function createPackageFromStreams(dest: string, streams: AsarStreamType[]): Promise<void>;
|
||||||
|
export declare function statFile(archivePath: string, filename: string, followLinks?: boolean): FilesystemEntry;
|
||||||
|
export declare function getRawHeader(archivePath: string): import("./disk.js").ArchiveHeader;
|
||||||
|
export interface ListOptions {
|
||||||
|
isPack: boolean;
|
||||||
|
}
|
||||||
|
export declare function listPackage(archivePath: string, options: ListOptions): string[];
|
||||||
|
export declare function extractFile(archivePath: string, filename: string, followLinks?: boolean): Buffer<ArrayBuffer>;
|
||||||
|
export declare function extractAll(archivePath: string, dest: string): void;
|
||||||
|
export { uncacheAll, uncacheFilesystem as uncache, FileRecord, DirectoryRecord } from './disk.js';
|
||||||
312
patcher/vendor/asar/node_modules/@electron/asar/lib/asar.js
generated
vendored
Normal file
312
patcher/vendor/asar/node_modules/@electron/asar/lib/asar.js
generated
vendored
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
import path from 'node:path';
|
||||||
|
import { minimatch } from 'minimatch';
|
||||||
|
import { wrappedFs as fs } from './wrapped-fs.js';
|
||||||
|
import { Filesystem } from './filesystem.js';
|
||||||
|
import { ensureWithin } from './path-validation.js';
|
||||||
|
import { readArchiveHeaderSync, readFilesystemSync, readFileSync, streamFilesystem, writeFilesystem, } from './disk.js';
|
||||||
|
import { crawl as crawlFilesystem, determineFileType } from './crawlfs.js';
|
||||||
|
/**
|
||||||
|
* Whether a directory should be excluded from packing due to the `--unpack-dir" option.
|
||||||
|
*
|
||||||
|
* @param dirPath - directory path to check
|
||||||
|
* @param pattern - literal prefix [for backward compatibility] or glob pattern
|
||||||
|
* @param unpackDirs - Array of directory paths previously marked as unpacked
|
||||||
|
*/
|
||||||
|
function isUnpackedDir(dirPath, pattern, unpackDirs) {
|
||||||
|
if (dirPath.startsWith(pattern) || minimatch(dirPath, pattern)) {
|
||||||
|
unpackDirs.add(dirPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (const unpackDir of unpackDirs) {
|
||||||
|
if (dirPath.startsWith(unpackDir) && !path.relative(unpackDir, dirPath).startsWith('..')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function createPackage(src, dest) {
|
||||||
|
return createPackageWithOptions(src, dest, {});
|
||||||
|
}
|
||||||
|
export async function createPackageWithOptions(src, dest, options) {
|
||||||
|
const globOptions = options.globOptions ? options.globOptions : {};
|
||||||
|
globOptions.dot = options.dot === undefined ? true : options.dot;
|
||||||
|
const pattern = src + (options.pattern ? options.pattern : '/**/*');
|
||||||
|
const [filenames, metadata] = await crawlFilesystem(pattern, globOptions);
|
||||||
|
return createPackageFromFiles(src, dest, filenames, metadata, options);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create an ASAR archive from a list of filenames.
|
||||||
|
*
|
||||||
|
* @param src - Base path. All files are relative to this.
|
||||||
|
* @param dest - Archive filename (& path).
|
||||||
|
* @param filenames - List of filenames relative to src.
|
||||||
|
* @param [metadata] - Object with filenames as keys and {type='directory|file|link', stat: fs.stat} as values. (Optional)
|
||||||
|
* @param [options] - Options passed to `createPackageWithOptions`.
|
||||||
|
*/
|
||||||
|
export async function createPackageFromFiles(src, dest, filenames, metadata = {}, options = {}) {
|
||||||
|
src = path.normalize(src);
|
||||||
|
dest = path.normalize(dest);
|
||||||
|
filenames = filenames.map(function (filename) {
|
||||||
|
return path.normalize(filename);
|
||||||
|
});
|
||||||
|
const filesystem = new Filesystem(src);
|
||||||
|
const files = [];
|
||||||
|
const links = [];
|
||||||
|
const unpackDirs = new Set();
|
||||||
|
let filenamesSorted = [];
|
||||||
|
if (options.ordering) {
|
||||||
|
const orderingFiles = (await fs.readFile(options.ordering))
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => {
|
||||||
|
if (line.includes(':')) {
|
||||||
|
line = line.split(':').pop();
|
||||||
|
}
|
||||||
|
line = line.trim();
|
||||||
|
if (line.startsWith('/')) {
|
||||||
|
line = line.slice(1);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
const ordering = [];
|
||||||
|
for (const file of orderingFiles) {
|
||||||
|
const pathComponents = file.split(path.sep);
|
||||||
|
let str = src;
|
||||||
|
for (const pathComponent of pathComponents) {
|
||||||
|
str = path.join(str, pathComponent);
|
||||||
|
ordering.push(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let missing = 0;
|
||||||
|
const total = filenames.length;
|
||||||
|
const filenameSet = new Set(filenames);
|
||||||
|
const sortedSet = new Set();
|
||||||
|
for (const file of ordering) {
|
||||||
|
if (!sortedSet.has(file) && filenameSet.has(file)) {
|
||||||
|
filenamesSorted.push(file);
|
||||||
|
sortedSet.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const file of filenames) {
|
||||||
|
if (!sortedSet.has(file)) {
|
||||||
|
filenamesSorted.push(file);
|
||||||
|
sortedSet.add(file);
|
||||||
|
missing += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`Ordering file has ${((total - missing) / total) * 100}% coverage.`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
filenamesSorted = filenames;
|
||||||
|
}
|
||||||
|
const shouldUnpackPath = function (filename, relativePath, unpack, unpackDir) {
|
||||||
|
let shouldUnpack = false;
|
||||||
|
if (unpack) {
|
||||||
|
shouldUnpack = minimatch(filename, unpack, { matchBase: true });
|
||||||
|
}
|
||||||
|
if (!shouldUnpack && unpackDir) {
|
||||||
|
shouldUnpack = isUnpackedDir(relativePath, unpackDir, unpackDirs);
|
||||||
|
}
|
||||||
|
return shouldUnpack;
|
||||||
|
};
|
||||||
|
// Batch-resolve metadata for files missing it (parallelized)
|
||||||
|
const missingMetadata = filenamesSorted.filter((f) => !metadata[f]);
|
||||||
|
if (missingMetadata.length > 0) {
|
||||||
|
const resolved = await Promise.all(missingMetadata.map(async (filename) => {
|
||||||
|
const fileType = await determineFileType(filename);
|
||||||
|
if (!fileType) {
|
||||||
|
throw new Error('Unknown file type for file: ' + filename);
|
||||||
|
}
|
||||||
|
return [filename, fileType];
|
||||||
|
}));
|
||||||
|
for (const [filename, fileType] of resolved) {
|
||||||
|
metadata[filename] = fileType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Process files in original sorted order to preserve header key ordering
|
||||||
|
for (const filename of filenamesSorted) {
|
||||||
|
const file = metadata[filename];
|
||||||
|
let shouldUnpack;
|
||||||
|
switch (file.type) {
|
||||||
|
case 'directory':
|
||||||
|
shouldUnpack = shouldUnpackPath(filename, path.relative(src, filename), undefined, options.unpackDir);
|
||||||
|
filesystem.insertDirectory(filename, shouldUnpack);
|
||||||
|
break;
|
||||||
|
case 'file': {
|
||||||
|
shouldUnpack = shouldUnpackPath(filename, path.relative(src, path.dirname(filename)), options.unpack, options.unpackDir);
|
||||||
|
const duplicate = await filesystem.insertFile(filename, () => fs.createReadStream(filename), shouldUnpack, file, options);
|
||||||
|
files.push({ filename, unpack: shouldUnpack, duplicate });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'link':
|
||||||
|
shouldUnpack = shouldUnpackPath(filename, path.relative(src, filename), options.unpack, options.unpackDir);
|
||||||
|
links.push({ filename, unpack: shouldUnpack });
|
||||||
|
filesystem.insertLink(filename, shouldUnpack);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fs.mkdirp(path.dirname(dest));
|
||||||
|
return writeFilesystem(dest, filesystem, { files, links }, metadata);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create an ASAR archive from a list of streams.
|
||||||
|
*
|
||||||
|
* @param dest - Archive filename (& path).
|
||||||
|
* @param streams - List of streams to be piped in-memory into asar filesystem. Insertion order is preserved.
|
||||||
|
*/
|
||||||
|
export async function createPackageFromStreams(dest, streams) {
|
||||||
|
// We use an ambiguous root `src` since we're piping directly from a stream and the `filePath` for the stream is already relative to the src/root
|
||||||
|
const src = '.';
|
||||||
|
const filesystem = new Filesystem(src);
|
||||||
|
const files = [];
|
||||||
|
const links = [];
|
||||||
|
const handleFile = async function (stream) {
|
||||||
|
const { path: destinationPath, type } = stream;
|
||||||
|
const filename = path.normalize(destinationPath);
|
||||||
|
switch (type) {
|
||||||
|
case 'directory':
|
||||||
|
filesystem.insertDirectory(filename, stream.unpacked);
|
||||||
|
break;
|
||||||
|
case 'file': {
|
||||||
|
const duplicate = await filesystem.insertFile(filename, stream.streamGenerator, stream.unpacked, { type: 'file', stat: stream.stat },
|
||||||
|
// `filename` is the destination path inside the archive, not a path
|
||||||
|
// on disk, so integrity must be computed from the stream.
|
||||||
|
{ fromStream: true });
|
||||||
|
files.push({
|
||||||
|
filename,
|
||||||
|
streamGenerator: stream.streamGenerator,
|
||||||
|
link: undefined,
|
||||||
|
mode: stream.stat.mode,
|
||||||
|
unpack: stream.unpacked,
|
||||||
|
duplicate,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'link':
|
||||||
|
links.push({
|
||||||
|
filename,
|
||||||
|
streamGenerator: stream.streamGenerator,
|
||||||
|
link: stream.symlink,
|
||||||
|
mode: stream.stat.mode,
|
||||||
|
unpack: stream.unpacked,
|
||||||
|
});
|
||||||
|
filesystem.insertLink(filename, stream.unpacked, path.dirname(filename), stream.symlink, src);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
const insertsDone = async function () {
|
||||||
|
await fs.mkdirp(path.dirname(dest));
|
||||||
|
return streamFilesystem(dest, filesystem, { files, links });
|
||||||
|
};
|
||||||
|
const streamQueue = streams.slice();
|
||||||
|
const next = async function (stream) {
|
||||||
|
if (!stream) {
|
||||||
|
return insertsDone();
|
||||||
|
}
|
||||||
|
await handleFile(stream);
|
||||||
|
return next(streamQueue.shift());
|
||||||
|
};
|
||||||
|
return next(streamQueue.shift());
|
||||||
|
}
|
||||||
|
export function statFile(archivePath, filename, followLinks = true) {
|
||||||
|
const filesystem = readFilesystemSync(archivePath);
|
||||||
|
return filesystem.getFile(filename, followLinks);
|
||||||
|
}
|
||||||
|
export function getRawHeader(archivePath) {
|
||||||
|
return readArchiveHeaderSync(archivePath);
|
||||||
|
}
|
||||||
|
export function listPackage(archivePath, options) {
|
||||||
|
return readFilesystemSync(archivePath).listFiles(options);
|
||||||
|
}
|
||||||
|
export function extractFile(archivePath, filename, followLinks = true) {
|
||||||
|
const filesystem = readFilesystemSync(archivePath);
|
||||||
|
const fileInfo = filesystem.getFile(filename, followLinks);
|
||||||
|
if ('link' in fileInfo || 'files' in fileInfo) {
|
||||||
|
throw new Error('Expected to find file at: ' + filename + ' but found a directory or link');
|
||||||
|
}
|
||||||
|
return readFileSync(filesystem, filename, fileInfo);
|
||||||
|
}
|
||||||
|
export function extractAll(archivePath, dest) {
|
||||||
|
const filesystem = readFilesystemSync(archivePath);
|
||||||
|
const filenames = filesystem.listFiles();
|
||||||
|
// under windows just extract links as regular files
|
||||||
|
const followLinks = process.platform === 'win32';
|
||||||
|
// create destination directory
|
||||||
|
fs.mkdirpSync(dest);
|
||||||
|
// Read the entire data section at once — one syscall instead of one per file.
|
||||||
|
const headerSize = filesystem.getHeaderSize();
|
||||||
|
const archiveSize = fs.statSync(archivePath).size;
|
||||||
|
const dataStart = 8 + headerSize;
|
||||||
|
const dataSize = archiveSize - dataStart;
|
||||||
|
let dataBuf = null;
|
||||||
|
if (dataSize > 0) {
|
||||||
|
dataBuf = Buffer.alloc(dataSize);
|
||||||
|
const fd = fs.openSync(archivePath, 'r');
|
||||||
|
try {
|
||||||
|
fs.readSync(fd, dataBuf, 0, dataSize, dataStart);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
fs.closeSync(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const extractionErrors = [];
|
||||||
|
for (const fullPath of filenames) {
|
||||||
|
// Remove leading slash
|
||||||
|
const filename = fullPath.substr(1);
|
||||||
|
const destFilename = ensureWithin(dest, filename);
|
||||||
|
const file = filesystem.getFile(filename, followLinks);
|
||||||
|
if ('files' in file) {
|
||||||
|
// it's a directory, create it and continue with the next entry
|
||||||
|
fs.mkdirpSync(destFilename);
|
||||||
|
}
|
||||||
|
else if ('link' in file) {
|
||||||
|
// it's a symlink, create a symlink
|
||||||
|
const linkSrcPath = path.dirname(path.join(dest, file.link));
|
||||||
|
const linkDestPath = path.dirname(destFilename);
|
||||||
|
const relativePath = path.relative(linkDestPath, linkSrcPath);
|
||||||
|
// try to delete output file, because we can't overwrite a link
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(destFilename);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
const linkTo = path.join(relativePath, path.basename(file.link));
|
||||||
|
if (path.relative(dest, linkSrcPath).startsWith('..')) {
|
||||||
|
throw new Error(`${fullPath}: file "${file.link}" links out of the package to "${linkSrcPath}"`);
|
||||||
|
}
|
||||||
|
fs.symlinkSync(linkTo, destFilename);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// it's a file, try to extract it
|
||||||
|
try {
|
||||||
|
let content;
|
||||||
|
if (file.unpacked) {
|
||||||
|
content = fs.readFileSync(path.join(`${filesystem.getRootPath()}.unpacked`, filename));
|
||||||
|
}
|
||||||
|
else if (file.size <= 0) {
|
||||||
|
content = Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Slice from the pre-read data buffer — zero-copy view
|
||||||
|
const offset = parseInt(file.offset);
|
||||||
|
content = dataBuf.subarray(offset, offset + file.size);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(destFilename, content);
|
||||||
|
if (file.executable) {
|
||||||
|
fs.chmodSync(destFilename, '755');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
extractionErrors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (extractionErrors.length) {
|
||||||
|
throw new Error('Unable to extract some files:\n\n' +
|
||||||
|
extractionErrors.map((error) => error.stack).join('\n\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export { uncacheAll, uncacheFilesystem as uncache } from './disk.js';
|
||||||
|
//# sourceMappingURL=asar.js.map
|
||||||
1
patcher/vendor/asar/node_modules/@electron/asar/lib/asar.js.map
generated
vendored
Normal file
1
patcher/vendor/asar/node_modules/@electron/asar/lib/asar.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
13
patcher/vendor/asar/node_modules/@electron/asar/lib/crawlfs.d.ts
generated
vendored
Normal file
13
patcher/vendor/asar/node_modules/@electron/asar/lib/crawlfs.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { GlobOptionsWithFileTypesFalse } from 'glob';
|
||||||
|
import { Stats } from 'node:fs';
|
||||||
|
export type CrawledFileType = {
|
||||||
|
type: 'file' | 'directory' | 'link';
|
||||||
|
stat: Pick<Stats, 'mode' | 'size'>;
|
||||||
|
transformed?: {
|
||||||
|
path: string;
|
||||||
|
stat: Stats;
|
||||||
|
};
|
||||||
|
cachedBuffer?: Buffer;
|
||||||
|
};
|
||||||
|
export declare function determineFileType(filename: string): Promise<CrawledFileType | null>;
|
||||||
|
export declare function crawl(dir: string, options: GlobOptionsWithFileTypesFalse): Promise<readonly [string[], Record<string, CrawledFileType>]>;
|
||||||
58
patcher/vendor/asar/node_modules/@electron/asar/lib/crawlfs.js
generated
vendored
Normal file
58
patcher/vendor/asar/node_modules/@electron/asar/lib/crawlfs.js
generated
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { glob } from 'glob';
|
||||||
|
import { wrappedFs as fs } from './wrapped-fs.js';
|
||||||
|
import path from 'node:path';
|
||||||
|
export async function determineFileType(filename) {
|
||||||
|
const stat = await fs.lstat(filename);
|
||||||
|
if (stat.isFile()) {
|
||||||
|
return { type: 'file', stat };
|
||||||
|
}
|
||||||
|
else if (stat.isDirectory()) {
|
||||||
|
return { type: 'directory', stat };
|
||||||
|
}
|
||||||
|
else if (stat.isSymbolicLink()) {
|
||||||
|
return { type: 'link', stat };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
export async function crawl(dir, options) {
|
||||||
|
const metadata = {};
|
||||||
|
// TODO replace with `fs.glob`
|
||||||
|
const crawled = await glob(dir, {
|
||||||
|
windowsPathsNoEscape: true,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
const results = await Promise.all(crawled.sort().map(async (filename) => [filename, await determineFileType(filename)]));
|
||||||
|
const links = [];
|
||||||
|
const linkSet = new Set();
|
||||||
|
const filenames = results
|
||||||
|
.map(([filename, type]) => {
|
||||||
|
if (type) {
|
||||||
|
metadata[filename] = type;
|
||||||
|
if (type.type === 'link') {
|
||||||
|
links.push(filename);
|
||||||
|
linkSet.add(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filename;
|
||||||
|
})
|
||||||
|
.filter((filename) => {
|
||||||
|
if (links.length === 0)
|
||||||
|
return true;
|
||||||
|
// Newer glob can return files inside symlinked directories, to avoid
|
||||||
|
// those appearing in archives we need to manually exclude them here
|
||||||
|
const isExactLink = linkSet.has(filename);
|
||||||
|
for (const link of links) {
|
||||||
|
if (isExactLink && filename === link)
|
||||||
|
continue;
|
||||||
|
if (filename.startsWith(link)) {
|
||||||
|
// symlink may point outside the directory: https://github.com/electron/asar/issues/303
|
||||||
|
const relativePath = path.relative(link, path.dirname(filename));
|
||||||
|
if (!relativePath.startsWith('..'))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return [filenames, metadata];
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=crawlfs.js.map
|
||||||
1
patcher/vendor/asar/node_modules/@electron/asar/lib/crawlfs.js.map
generated
vendored
Normal file
1
patcher/vendor/asar/node_modules/@electron/asar/lib/crawlfs.js.map
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"crawlfs.js","sourceRoot":"","sources":["../src/crawlfs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAiC,MAAM,MAAM,CAAC;AAE3D,OAAO,EAAE,SAAS,IAAI,EAAE,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,IAAI,MAAM,WAAW,CAAC;AAY7B,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAgB;IACtD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;QAClB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAChC,CAAC;SAAM,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;QAC9B,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IACrC,CAAC;SAAM,IAAI,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;QACjC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAChC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,GAAW,EAAE,OAAsC;IAC7E,MAAM,QAAQ,GAAoC,EAAE,CAAC;IACrD,8BAA8B;IAC9B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,EAAE;QAC9B,oBAAoB,EAAE,IAAI;QAC1B,GAAG,OAAO;KACX,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAU,CAAC,CAC/F,CAAC;IACF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,SAAS,GAAG,OAAO;SACtB,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE;QACxB,IAAI,IAAI,EAAE,CAAC;YACT,QAAQ,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;YAC1B,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBACzB,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACrB,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE;QACnB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACpC,qEAAqE;QACrE,oEAAoE;QACpE,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,WAAW,IAAI,QAAQ,KAAK,IAAI;gBAAE,SAAS;YAC/C,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC9B,uFAAuF;gBACvF,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACjE,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC;oBAAE,OAAO,KAAK,CAAC;YACnD,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IACL,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAU,CAAC;AACxC,CAAC"}
|
||||||
50
patcher/vendor/asar/node_modules/@electron/asar/lib/disk.d.ts
generated
vendored
Normal file
50
patcher/vendor/asar/node_modules/@electron/asar/lib/disk.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { Filesystem, FilesystemFileEntry } from './filesystem.js';
|
||||||
|
import { CrawledFileType } from './crawlfs.js';
|
||||||
|
import { Stats } from 'node:fs';
|
||||||
|
export declare function validateHeader(header: unknown): void;
|
||||||
|
export type InputMetadata = {
|
||||||
|
[property: string]: CrawledFileType;
|
||||||
|
};
|
||||||
|
export type BasicFilesArray = {
|
||||||
|
filename: string;
|
||||||
|
unpack: boolean;
|
||||||
|
/** Contents shared with an earlier file, already written to the archive */
|
||||||
|
duplicate?: boolean;
|
||||||
|
}[];
|
||||||
|
export type BasicStreamArray = {
|
||||||
|
filename: string;
|
||||||
|
streamGenerator: () => NodeJS.ReadableStream;
|
||||||
|
mode: Stats['mode'];
|
||||||
|
unpack: boolean;
|
||||||
|
link: string | undefined;
|
||||||
|
/** Contents shared with an earlier file, already written to the archive */
|
||||||
|
duplicate?: boolean;
|
||||||
|
}[];
|
||||||
|
export type FilesystemFilesAndLinks<T extends BasicFilesArray | BasicStreamArray> = {
|
||||||
|
files: T;
|
||||||
|
links: T;
|
||||||
|
};
|
||||||
|
export declare function writeFilesystem(dest: string, filesystem: Filesystem, lists: FilesystemFilesAndLinks<BasicFilesArray>, metadata: InputMetadata): Promise<void>;
|
||||||
|
export declare function streamFilesystem(dest: string, filesystem: Filesystem, lists: FilesystemFilesAndLinks<BasicStreamArray>): Promise<void>;
|
||||||
|
export interface FileRecord extends FilesystemFileEntry {
|
||||||
|
integrity: {
|
||||||
|
hash: string;
|
||||||
|
algorithm: 'SHA256';
|
||||||
|
blocks: string[];
|
||||||
|
blockSize: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export type DirectoryRecord = {
|
||||||
|
files: Record<string, DirectoryRecord | FileRecord>;
|
||||||
|
};
|
||||||
|
export type ArchiveHeader = {
|
||||||
|
header: DirectoryRecord;
|
||||||
|
headerString: string;
|
||||||
|
headerSize: number;
|
||||||
|
};
|
||||||
|
export declare function readArchiveHeaderSync(archivePath: string): ArchiveHeader;
|
||||||
|
export declare function readFilesystemSync(archivePath: string): Filesystem;
|
||||||
|
export declare function uncacheFilesystem(archivePath: string): boolean;
|
||||||
|
export declare function uncacheAll(): void;
|
||||||
|
export declare function readFileSync(filesystem: Filesystem, filename: string, info: FilesystemFileEntry): Buffer<ArrayBuffer>;
|
||||||
|
export declare function readFileWithFd(fd: number, filesystem: Filesystem, filename: string, info: FilesystemFileEntry): Buffer<ArrayBuffer>;
|
||||||
340
patcher/vendor/asar/node_modules/@electron/asar/lib/disk.js
generated
vendored
Normal file
340
patcher/vendor/asar/node_modules/@electron/asar/lib/disk.js
generated
vendored
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
import path from 'node:path';
|
||||||
|
import { wrappedFs as fs } from './wrapped-fs.js';
|
||||||
|
import { Pickle } from './pickle.js';
|
||||||
|
import { Filesystem } from './filesystem.js';
|
||||||
|
import { ensureWithin } from './path-validation.js';
|
||||||
|
import stream from 'node:stream/promises';
|
||||||
|
let filesystemCache = Object.create(null);
|
||||||
|
class HeaderValidationError extends Error {
|
||||||
|
constructor(path, message) {
|
||||||
|
super(`Invalid archive header at "${path}": ${message}`);
|
||||||
|
this.name = 'HeaderValidationError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function validateFileEntry(entry, entryPath) {
|
||||||
|
if (typeof entry.offset !== 'string') {
|
||||||
|
throw new HeaderValidationError(entryPath, `"offset" must be a string, got ${typeof entry.offset}`);
|
||||||
|
}
|
||||||
|
if (!/^\d+$/.test(entry.offset)) {
|
||||||
|
throw new HeaderValidationError(entryPath, `"offset" must be a numeric string, got "${entry.offset}"`);
|
||||||
|
}
|
||||||
|
if (typeof entry.size !== 'number' || !Number.isFinite(entry.size) || entry.size < 0) {
|
||||||
|
throw new HeaderValidationError(entryPath, `"size" must be a non-negative number, got ${JSON.stringify(entry.size)}`);
|
||||||
|
}
|
||||||
|
if (entry.unpacked !== undefined && typeof entry.unpacked !== 'boolean') {
|
||||||
|
throw new HeaderValidationError(entryPath, `"unpacked" must be a boolean, got ${typeof entry.unpacked}`);
|
||||||
|
}
|
||||||
|
if (entry.executable !== undefined && typeof entry.executable !== 'boolean') {
|
||||||
|
throw new HeaderValidationError(entryPath, `"executable" must be a boolean, got ${typeof entry.executable}`);
|
||||||
|
}
|
||||||
|
if (entry.integrity !== undefined) {
|
||||||
|
validateIntegrity(entry.integrity, entryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function validateUnpackedFileEntry(entry, entryPath) {
|
||||||
|
if (typeof entry.size !== 'number' || !Number.isFinite(entry.size) || entry.size < 0) {
|
||||||
|
throw new HeaderValidationError(entryPath, `"size" must be a non-negative number, got ${JSON.stringify(entry.size)}`);
|
||||||
|
}
|
||||||
|
if (entry.unpacked !== true) {
|
||||||
|
throw new HeaderValidationError(entryPath, `"unpacked" must be true for unpacked file entries`);
|
||||||
|
}
|
||||||
|
if (entry.executable !== undefined && typeof entry.executable !== 'boolean') {
|
||||||
|
throw new HeaderValidationError(entryPath, `"executable" must be a boolean, got ${typeof entry.executable}`);
|
||||||
|
}
|
||||||
|
if (entry.integrity !== undefined) {
|
||||||
|
validateIntegrity(entry.integrity, entryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function validateIntegrity(integrity, entryPath) {
|
||||||
|
if (typeof integrity !== 'object' || integrity === null || Array.isArray(integrity)) {
|
||||||
|
throw new HeaderValidationError(entryPath, `"integrity" must be an object`);
|
||||||
|
}
|
||||||
|
const rec = integrity;
|
||||||
|
if (typeof rec.algorithm !== 'string') {
|
||||||
|
throw new HeaderValidationError(entryPath, `"integrity.algorithm" must be a string`);
|
||||||
|
}
|
||||||
|
if (typeof rec.hash !== 'string') {
|
||||||
|
throw new HeaderValidationError(entryPath, `"integrity.hash" must be a string`);
|
||||||
|
}
|
||||||
|
if (typeof rec.blockSize !== 'number' || !Number.isFinite(rec.blockSize) || rec.blockSize <= 0) {
|
||||||
|
throw new HeaderValidationError(entryPath, `"integrity.blockSize" must be a positive number`);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(rec.blocks)) {
|
||||||
|
throw new HeaderValidationError(entryPath, `"integrity.blocks" must be an array`);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < rec.blocks.length; i++) {
|
||||||
|
if (typeof rec.blocks[i] !== 'string') {
|
||||||
|
throw new HeaderValidationError(entryPath, `"integrity.blocks[${i}]" must be a string`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function validateLinkEntry(entry, entryPath) {
|
||||||
|
if (typeof entry.link !== 'string') {
|
||||||
|
throw new HeaderValidationError(entryPath, `"link" must be a string, got ${typeof entry.link}`);
|
||||||
|
}
|
||||||
|
if (entry.link.length === 0) {
|
||||||
|
throw new HeaderValidationError(entryPath, `"link" must not be empty`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function validateDirectoryEntry(entry, entryPath) {
|
||||||
|
if (typeof entry.files !== 'object' || entry.files === null || Array.isArray(entry.files)) {
|
||||||
|
throw new HeaderValidationError(entryPath, `"files" must be a plain object`);
|
||||||
|
}
|
||||||
|
const files = entry.files;
|
||||||
|
for (const [name, child] of Object.entries(files)) {
|
||||||
|
if (name.includes('/') || name.includes('\\') || name === '.' || name === '..') {
|
||||||
|
throw new HeaderValidationError(entryPath, `invalid entry name "${name}"`);
|
||||||
|
}
|
||||||
|
const childPath = entryPath === '/' ? `/${name}` : `${entryPath}/${name}`;
|
||||||
|
validateHeaderEntry(child, childPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function validateHeaderEntry(entry, entryPath) {
|
||||||
|
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
||||||
|
throw new HeaderValidationError(entryPath, 'entry must be an object');
|
||||||
|
}
|
||||||
|
const rec = entry;
|
||||||
|
if ('link' in rec) {
|
||||||
|
validateLinkEntry(rec, entryPath);
|
||||||
|
}
|
||||||
|
else if ('files' in rec) {
|
||||||
|
validateDirectoryEntry(rec, entryPath);
|
||||||
|
}
|
||||||
|
else if ('offset' in rec) {
|
||||||
|
validateFileEntry(rec, entryPath);
|
||||||
|
}
|
||||||
|
else if ('unpacked' in rec && rec.unpacked === true && 'size' in rec) {
|
||||||
|
validateUnpackedFileEntry(rec, entryPath);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new HeaderValidationError(entryPath, 'entry must be a directory (with "files"), a file (with "offset" or "unpacked"), or a link (with "link")');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function validateHeader(header) {
|
||||||
|
if (typeof header !== 'object' || header === null || Array.isArray(header)) {
|
||||||
|
throw new HeaderValidationError('/', 'header must be an object');
|
||||||
|
}
|
||||||
|
const rec = header;
|
||||||
|
if (!('files' in rec)) {
|
||||||
|
throw new HeaderValidationError('/', 'root header must be a directory with a "files" property');
|
||||||
|
}
|
||||||
|
validateDirectoryEntry(rec, '/');
|
||||||
|
}
|
||||||
|
async function copyFile(dest, src, filename) {
|
||||||
|
const srcFile = path.join(src, filename);
|
||||||
|
const targetFile = path.join(dest, filename);
|
||||||
|
const [content, stats] = await Promise.all([
|
||||||
|
fs.readFile(srcFile),
|
||||||
|
fs.stat(srcFile),
|
||||||
|
fs.mkdirp(path.dirname(targetFile)),
|
||||||
|
]);
|
||||||
|
return fs.writeFile(targetFile, content, { mode: stats.mode });
|
||||||
|
}
|
||||||
|
async function streamTransformedFile(stream, outStream) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.pipe(outStream, { end: false });
|
||||||
|
stream.on('error', reject);
|
||||||
|
stream.on('end', () => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const writeFileListToStream = async function (dest, filesystem, out, lists, metadata) {
|
||||||
|
const { files, links } = lists;
|
||||||
|
// Batch cached buffers into a single write to avoid per-file write syscalls
|
||||||
|
let pendingBuffers = [];
|
||||||
|
const flushPendingBuffers = async () => {
|
||||||
|
if (pendingBuffers.length === 0)
|
||||||
|
return;
|
||||||
|
const combined = pendingBuffers.length === 1 ? pendingBuffers[0] : Buffer.concat(pendingBuffers);
|
||||||
|
pendingBuffers = [];
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
out.write(combined, (err) => (err ? reject(err) : resolve()));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.duplicate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (file.unpack) {
|
||||||
|
await flushPendingBuffers();
|
||||||
|
const filename = path.relative(filesystem.getRootPath(), file.filename);
|
||||||
|
await copyFile(`${dest}.unpacked`, filesystem.getRootPath(), filename);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const fileMeta = metadata[file.filename];
|
||||||
|
if (fileMeta.cachedBuffer) {
|
||||||
|
pendingBuffers.push(fileMeta.cachedBuffer);
|
||||||
|
fileMeta.cachedBuffer = undefined;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await flushPendingBuffers();
|
||||||
|
const transformed = fileMeta.transformed;
|
||||||
|
const stream = fs.createReadStream(transformed ? transformed.path : file.filename);
|
||||||
|
await streamTransformedFile(stream, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await flushPendingBuffers();
|
||||||
|
for (const file of links.filter((f) => f.unpack)) {
|
||||||
|
const filename = path.relative(filesystem.getRootPath(), file.filename);
|
||||||
|
const link = await fs.readlink(file.filename);
|
||||||
|
await createSymlink(dest, filename, link);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
out.on('error', reject);
|
||||||
|
out.end(() => resolve());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export async function writeFilesystem(dest, filesystem, lists, metadata) {
|
||||||
|
const out = await createFilesystemWriteStream(filesystem, dest);
|
||||||
|
return writeFileListToStream(dest, filesystem, out, lists, metadata);
|
||||||
|
}
|
||||||
|
export async function streamFilesystem(dest, filesystem, lists) {
|
||||||
|
const out = await createFilesystemWriteStream(filesystem, dest);
|
||||||
|
const { files, links } = lists;
|
||||||
|
for await (const file of files) {
|
||||||
|
if (file.duplicate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// the file should not be packed into archive
|
||||||
|
if (file.unpack) {
|
||||||
|
const targetFile = path.join(`${dest}.unpacked`, file.filename);
|
||||||
|
await fs.mkdirp(path.dirname(targetFile));
|
||||||
|
const writeStream = fs.createWriteStream(targetFile, { mode: file.mode });
|
||||||
|
await stream.pipeline(file.streamGenerator(), writeStream);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await streamTransformedFile(file.streamGenerator(), out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const file of links.filter((f) => f.unpack && f.link)) {
|
||||||
|
// the symlink needs to be recreated outside in .unpacked
|
||||||
|
await createSymlink(dest, file.filename, file.link);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
out.on('error', reject);
|
||||||
|
out.end(() => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function readArchiveHeaderSync(archivePath) {
|
||||||
|
const fd = fs.openSync(archivePath, 'r');
|
||||||
|
let size;
|
||||||
|
let headerBuf;
|
||||||
|
try {
|
||||||
|
const archiveSize = fs.fstatSync(fd).size;
|
||||||
|
const sizeBuf = Buffer.alloc(8);
|
||||||
|
if (fs.readSync(fd, sizeBuf, 0, 8, null) !== 8) {
|
||||||
|
throw new Error('Unable to read header size');
|
||||||
|
}
|
||||||
|
const sizePickle = Pickle.createFromBuffer(sizeBuf);
|
||||||
|
size = sizePickle.createIterator().readUInt32();
|
||||||
|
if (size < 0 || size > archiveSize - 8) {
|
||||||
|
throw new Error(`Header size ${size} exceeds archive size ${archiveSize}. The archive is corrupted.`);
|
||||||
|
}
|
||||||
|
headerBuf = Buffer.alloc(size);
|
||||||
|
if (fs.readSync(fd, headerBuf, 0, size, null) !== size) {
|
||||||
|
throw new Error('Unable to read header');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
fs.closeSync(fd);
|
||||||
|
}
|
||||||
|
const headerPickle = Pickle.createFromBuffer(headerBuf);
|
||||||
|
const header = headerPickle.createIterator().readString();
|
||||||
|
const parsedHeader = JSON.parse(header);
|
||||||
|
validateHeader(parsedHeader);
|
||||||
|
return { headerString: header, header: parsedHeader, headerSize: size };
|
||||||
|
}
|
||||||
|
export function readFilesystemSync(archivePath) {
|
||||||
|
if (!filesystemCache[archivePath]) {
|
||||||
|
const header = readArchiveHeaderSync(archivePath);
|
||||||
|
const filesystem = new Filesystem(archivePath);
|
||||||
|
filesystem.setHeader(header.header, header.headerSize);
|
||||||
|
filesystemCache[archivePath] = filesystem;
|
||||||
|
}
|
||||||
|
return filesystemCache[archivePath];
|
||||||
|
}
|
||||||
|
export function uncacheFilesystem(archivePath) {
|
||||||
|
if (filesystemCache[archivePath]) {
|
||||||
|
filesystemCache[archivePath] = undefined;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
export function uncacheAll() {
|
||||||
|
filesystemCache = {};
|
||||||
|
}
|
||||||
|
export function readFileSync(filesystem, filename, info) {
|
||||||
|
if (info.size <= 0) {
|
||||||
|
return Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
if (info.unpacked) {
|
||||||
|
// it's an unpacked file, copy it.
|
||||||
|
const unpackedDir = `${filesystem.getRootPath()}.unpacked`;
|
||||||
|
return fs.readFileSync(ensureWithin(unpackedDir, filename));
|
||||||
|
}
|
||||||
|
// Node throws an exception when reading 0 bytes into a 0-size buffer,
|
||||||
|
// so we short-circuit the read in this case.
|
||||||
|
const fd = fs.openSync(filesystem.getRootPath(), 'r');
|
||||||
|
try {
|
||||||
|
const fileOffset = parseInt(info.offset);
|
||||||
|
if (Number.isNaN(fileOffset) || fileOffset < 0 || !Number.isSafeInteger(fileOffset)) {
|
||||||
|
throw new Error(`Invalid file offset in archive header: ${info.offset}`);
|
||||||
|
}
|
||||||
|
const offset = 8 + filesystem.getHeaderSize() + fileOffset;
|
||||||
|
if (!Number.isSafeInteger(offset)) {
|
||||||
|
throw new Error(`Computed offset exceeds safe integer range`);
|
||||||
|
}
|
||||||
|
const archiveSize = fs.fstatSync(fd).size;
|
||||||
|
if (offset < 0 || offset + info.size > archiveSize) {
|
||||||
|
throw new Error(`File entry extends beyond archive boundary (offset=${offset}, size=${info.size}, archiveSize=${archiveSize})`);
|
||||||
|
}
|
||||||
|
const buffer = Buffer.alloc(info.size);
|
||||||
|
fs.readSync(fd, buffer, 0, info.size, offset);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
fs.closeSync(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function readFileWithFd(fd, filesystem, filename, info) {
|
||||||
|
let buffer = Buffer.alloc(info.size);
|
||||||
|
if (info.size <= 0) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
if (info.unpacked) {
|
||||||
|
const unpackedDir = `${filesystem.getRootPath()}.unpacked`;
|
||||||
|
buffer = fs.readFileSync(ensureWithin(unpackedDir, filename));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const offset = 8 + filesystem.getHeaderSize() + parseInt(info.offset);
|
||||||
|
fs.readSync(fd, buffer, 0, info.size, offset);
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
async function createFilesystemWriteStream(filesystem, dest) {
|
||||||
|
const headerPickle = Pickle.createEmpty();
|
||||||
|
headerPickle.writeString(JSON.stringify(filesystem.getHeader()));
|
||||||
|
const headerBuf = headerPickle.toBuffer();
|
||||||
|
const sizePickle = Pickle.createEmpty();
|
||||||
|
sizePickle.writeUInt32(headerBuf.length);
|
||||||
|
const sizeBuf = sizePickle.toBuffer();
|
||||||
|
const out = fs.createWriteStream(dest);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
out.on('error', reject);
|
||||||
|
out.write(sizeBuf);
|
||||||
|
return out.write(headerBuf, () => resolve());
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
async function createSymlink(dest, filepath, link) {
|
||||||
|
// if symlink is within subdirectories, then we need to recreate dir structure
|
||||||
|
await fs.mkdirp(path.join(`${dest}.unpacked`, path.dirname(filepath)));
|
||||||
|
// create symlink within unpacked dir
|
||||||
|
await fs.symlink(link, path.join(`${dest}.unpacked`, filepath)).catch(async (error) => {
|
||||||
|
if (error.code === 'EPERM' && error.syscall === 'symlink') {
|
||||||
|
throw new Error('Could not create symlinks for unpacked assets. On Windows, consider activating Developer Mode to allow non-admin users to create symlinks by following the instructions at https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development.');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=disk.js.map
|
||||||
1
patcher/vendor/asar/node_modules/@electron/asar/lib/disk.js.map
generated
vendored
Normal file
1
patcher/vendor/asar/node_modules/@electron/asar/lib/disk.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
62
patcher/vendor/asar/node_modules/@electron/asar/lib/filesystem.d.ts
generated
vendored
Normal file
62
patcher/vendor/asar/node_modules/@electron/asar/lib/filesystem.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { FileIntegrity } from './integrity.js';
|
||||||
|
import { CrawledFileType } from './crawlfs.js';
|
||||||
|
export type EntryMetadata = {
|
||||||
|
unpacked?: boolean;
|
||||||
|
};
|
||||||
|
export type FilesystemDirectoryEntry = {
|
||||||
|
files: Record<string, FilesystemEntry>;
|
||||||
|
} & EntryMetadata;
|
||||||
|
export type FilesystemFileEntry = {
|
||||||
|
unpacked: boolean;
|
||||||
|
executable: boolean;
|
||||||
|
offset: string;
|
||||||
|
size: number;
|
||||||
|
integrity: FileIntegrity;
|
||||||
|
} & EntryMetadata;
|
||||||
|
export type FilesystemLinkEntry = {
|
||||||
|
link: string;
|
||||||
|
} & EntryMetadata;
|
||||||
|
export type FilesystemEntry = FilesystemDirectoryEntry | FilesystemFileEntry | FilesystemLinkEntry;
|
||||||
|
export declare class Filesystem {
|
||||||
|
private src;
|
||||||
|
private header;
|
||||||
|
private headerSize;
|
||||||
|
private offset;
|
||||||
|
private contentOffsets;
|
||||||
|
constructor(src: string);
|
||||||
|
getRootPath(): string;
|
||||||
|
getHeader(): FilesystemEntry;
|
||||||
|
getHeaderSize(): number;
|
||||||
|
setHeader(header: FilesystemEntry, headerSize: number): void;
|
||||||
|
searchNodeFromDirectory(p: string): FilesystemEntry;
|
||||||
|
searchNodeFromPath(p: string): FilesystemEntry;
|
||||||
|
insertDirectory(p: string, shouldUnpack: boolean): Record<string, FilesystemEntry>;
|
||||||
|
insertFile(p: string, streamGenerator: () => NodeJS.ReadableStream, shouldUnpack: boolean, file: CrawledFileType, options?: {
|
||||||
|
transform?: (filePath: string) => NodeJS.ReadWriteStream | void;
|
||||||
|
/**
|
||||||
|
* Set when the file content comes from a stream rather than a file on
|
||||||
|
* disk at `p`. In that case `p` is only the destination path inside the
|
||||||
|
* archive, so integrity must be computed from the stream.
|
||||||
|
*/
|
||||||
|
fromStream?: boolean;
|
||||||
|
}): Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* Fills in a file entry and reserves the region of the archive that holds its
|
||||||
|
* contents, growing the archive by `size` bytes. When a file with identical
|
||||||
|
* contents was already inserted, the entry instead points at the offset of those
|
||||||
|
* contents and the archive is left unchanged — the caller must then skip writing
|
||||||
|
* the contents.
|
||||||
|
*
|
||||||
|
* @returns whether the contents were already stored by an earlier file
|
||||||
|
*/
|
||||||
|
private storeFileEntry;
|
||||||
|
private insertFileAsync;
|
||||||
|
insertLink(p: string, shouldUnpack: boolean, parentPath?: string, symlink?: string, // /var/tmp => /private/var
|
||||||
|
src?: string): string;
|
||||||
|
private resolveLink;
|
||||||
|
listFiles(options?: {
|
||||||
|
isPack: boolean;
|
||||||
|
}): string[];
|
||||||
|
getNode(p: string, followLinks?: boolean, depth?: number, visited?: Set<string>): FilesystemEntry;
|
||||||
|
getFile(p: string, followLinks?: boolean, depth?: number, visited?: Set<string>): FilesystemEntry;
|
||||||
|
}
|
||||||
256
patcher/vendor/asar/node_modules/@electron/asar/lib/filesystem.js
generated
vendored
Normal file
256
patcher/vendor/asar/node_modules/@electron/asar/lib/filesystem.js
generated
vendored
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import stream from 'node:stream/promises';
|
||||||
|
import { getFileIntegrity, getFileIntegrityFromBuffer } from './integrity.js';
|
||||||
|
import { wrappedFs as fs } from './wrapped-fs.js';
|
||||||
|
const UINT32_MAX = 2 ** 32 - 1;
|
||||||
|
const SYMLINK_MAX_DEPTH = 40; // matches Linux SYMLOOP_MAX
|
||||||
|
// Files smaller than this use buffer-based integrity hashing (avoids stream overhead).
|
||||||
|
// Files larger than this use streaming to avoid holding large buffers in memory.
|
||||||
|
const BUFFER_HASH_THRESHOLD = 2 * 1024 * 1024; // 2MB
|
||||||
|
export class Filesystem {
|
||||||
|
src;
|
||||||
|
header;
|
||||||
|
headerSize;
|
||||||
|
offset;
|
||||||
|
// SHA-256 of contents already stored => the offset those contents live at
|
||||||
|
contentOffsets;
|
||||||
|
constructor(src) {
|
||||||
|
this.src = path.resolve(src);
|
||||||
|
this.header = { files: Object.create(null) };
|
||||||
|
this.headerSize = 0;
|
||||||
|
this.offset = BigInt(0);
|
||||||
|
this.contentOffsets = new Map();
|
||||||
|
}
|
||||||
|
getRootPath() {
|
||||||
|
return this.src;
|
||||||
|
}
|
||||||
|
getHeader() {
|
||||||
|
return this.header;
|
||||||
|
}
|
||||||
|
getHeaderSize() {
|
||||||
|
return this.headerSize;
|
||||||
|
}
|
||||||
|
setHeader(header, headerSize) {
|
||||||
|
this.header = header;
|
||||||
|
this.headerSize = headerSize;
|
||||||
|
}
|
||||||
|
searchNodeFromDirectory(p) {
|
||||||
|
let json = this.header;
|
||||||
|
const dirs = p.split(path.sep);
|
||||||
|
for (const dir of dirs) {
|
||||||
|
if (dir !== '.') {
|
||||||
|
if ('files' in json) {
|
||||||
|
if (!json.files[dir]) {
|
||||||
|
json.files[dir] = { files: Object.create(null) };
|
||||||
|
}
|
||||||
|
json = json.files[dir];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error('Unexpected directory state while traversing: ' + p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
searchNodeFromPath(p) {
|
||||||
|
p = path.relative(this.src, p);
|
||||||
|
if (!p) {
|
||||||
|
return this.header;
|
||||||
|
}
|
||||||
|
const name = path.basename(p);
|
||||||
|
const node = this.searchNodeFromDirectory(path.dirname(p));
|
||||||
|
if (!node.files) {
|
||||||
|
node.files = Object.create(null);
|
||||||
|
}
|
||||||
|
if (!node.files[name]) {
|
||||||
|
node.files[name] = Object.create(null);
|
||||||
|
}
|
||||||
|
return node.files[name];
|
||||||
|
}
|
||||||
|
insertDirectory(p, shouldUnpack) {
|
||||||
|
const node = this.searchNodeFromPath(p);
|
||||||
|
if (shouldUnpack) {
|
||||||
|
node.unpacked = shouldUnpack;
|
||||||
|
}
|
||||||
|
node.files = node.files || Object.create(null);
|
||||||
|
return node.files;
|
||||||
|
}
|
||||||
|
insertFile(p, streamGenerator, shouldUnpack, file, options = {}) {
|
||||||
|
const dirNode = this.searchNodeFromPath(path.dirname(p));
|
||||||
|
const node = this.searchNodeFromPath(p);
|
||||||
|
if (shouldUnpack || dirNode.unpacked) {
|
||||||
|
node.size = file.stat.size;
|
||||||
|
node.unpacked = true;
|
||||||
|
// Unpacked files are copied out of the archive as standalone files, so every
|
||||||
|
// copy is written even when their contents are identical.
|
||||||
|
return getFileIntegrity(streamGenerator()).then((integrity) => {
|
||||||
|
node.integrity = integrity;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const transformed = options.transform && options.transform(p);
|
||||||
|
if (transformed) {
|
||||||
|
return this.insertFileAsync(p, streamGenerator, file, node, transformed);
|
||||||
|
}
|
||||||
|
const size = file.stat.size;
|
||||||
|
// JavaScript cannot precisely present integers >= UINT32_MAX.
|
||||||
|
if (size > UINT32_MAX) {
|
||||||
|
throw new Error(`${p}: file size can not be larger than 4.2GB`);
|
||||||
|
}
|
||||||
|
const executable = process.platform !== 'win32' && (file.stat.mode & 0o100) !== 0;
|
||||||
|
if (!options.fromStream && size <= BUFFER_HASH_THRESHOLD) {
|
||||||
|
// Fully synchronous fast path — no Promise, no stream, no microtask yield
|
||||||
|
try {
|
||||||
|
const fileBuffer = fs.readFileSync(p);
|
||||||
|
// Don't trust a buffer that doesn't match the size recorded in the header;
|
||||||
|
// hash the stream instead.
|
||||||
|
if (fileBuffer.length === size) {
|
||||||
|
const integrity = getFileIntegrityFromBuffer(fileBuffer);
|
||||||
|
const duplicate = this.storeFileEntry(node, size, executable, integrity);
|
||||||
|
if (!duplicate) {
|
||||||
|
file.cachedBuffer = fileBuffer;
|
||||||
|
}
|
||||||
|
return Promise.resolve(duplicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Fall through to stream path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getFileIntegrity(streamGenerator()).then((integrity) => this.storeFileEntry(node, size, executable, integrity));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Fills in a file entry and reserves the region of the archive that holds its
|
||||||
|
* contents, growing the archive by `size` bytes. When a file with identical
|
||||||
|
* contents was already inserted, the entry instead points at the offset of those
|
||||||
|
* contents and the archive is left unchanged — the caller must then skip writing
|
||||||
|
* the contents.
|
||||||
|
*
|
||||||
|
* @returns whether the contents were already stored by an earlier file
|
||||||
|
*/
|
||||||
|
storeFileEntry(node, size, executable, integrity) {
|
||||||
|
const sharedOffset = this.contentOffsets.get(integrity.hash);
|
||||||
|
node.size = size;
|
||||||
|
node.offset = sharedOffset ?? this.offset.toString();
|
||||||
|
if (executable) {
|
||||||
|
node.executable = true;
|
||||||
|
}
|
||||||
|
node.integrity = integrity;
|
||||||
|
if (sharedOffset !== undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.contentOffsets.set(integrity.hash, node.offset);
|
||||||
|
this.offset += BigInt(size);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
async insertFileAsync(p, streamGenerator, file, node, transformed) {
|
||||||
|
const tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), 'asar-'));
|
||||||
|
const tmpfile = path.join(tmpdir, path.basename(p));
|
||||||
|
const out = fs.createWriteStream(tmpfile);
|
||||||
|
await stream.pipeline(streamGenerator(), transformed, out);
|
||||||
|
file.transformed = {
|
||||||
|
path: tmpfile,
|
||||||
|
stat: await fs.lstat(tmpfile),
|
||||||
|
};
|
||||||
|
const size = file.transformed.stat.size;
|
||||||
|
if (size > UINT32_MAX) {
|
||||||
|
throw new Error(`${p}: file size can not be larger than 4.2GB`);
|
||||||
|
}
|
||||||
|
// Integrity must be computed over the transformed bytes that are actually
|
||||||
|
// stored in the archive, not the original (pre-transform) source bytes.
|
||||||
|
const integrity = await getFileIntegrity(fs.createReadStream(file.transformed.path));
|
||||||
|
const executable = process.platform !== 'win32' && (file.stat.mode & 0o100) !== 0;
|
||||||
|
const duplicate = this.storeFileEntry(node, size, executable, integrity);
|
||||||
|
if (duplicate) {
|
||||||
|
// Nothing will read these transformed bytes again, so don't leave them behind
|
||||||
|
file.transformed = undefined;
|
||||||
|
fs.rmSync(tmpdir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
return duplicate;
|
||||||
|
}
|
||||||
|
insertLink(p, shouldUnpack, parentPath = fs.realpathSync(path.dirname(p)), symlink = fs.readlinkSync(p), // /var/tmp => /private/var
|
||||||
|
src = fs.realpathSync(this.src)) {
|
||||||
|
const link = this.resolveLink(src, parentPath, symlink);
|
||||||
|
if (path.isAbsolute(link) || path.normalize(link).startsWith('..')) {
|
||||||
|
throw new Error(`${p}: file "${link}" links out of the package`);
|
||||||
|
}
|
||||||
|
const node = this.searchNodeFromPath(p);
|
||||||
|
const dirNode = this.searchNodeFromPath(path.dirname(p));
|
||||||
|
if (shouldUnpack || dirNode.unpacked) {
|
||||||
|
node.unpacked = true;
|
||||||
|
}
|
||||||
|
node.link = link;
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
resolveLink(src, parentPath, symlink) {
|
||||||
|
// Use path.resolve (not path.join) so that an absolute symlink target is
|
||||||
|
// honored as-is instead of being concatenated onto parentPath. With join,
|
||||||
|
// an absolute target's leading separator is swallowed, producing a broken
|
||||||
|
// relative link for in-package targets and silently bypassing the
|
||||||
|
// out-of-package guard for targets outside the package. resolve handles
|
||||||
|
// both absolute and relative targets through a single code path.
|
||||||
|
const target = path.resolve(parentPath, symlink);
|
||||||
|
const link = path.relative(src, target);
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
listFiles(options) {
|
||||||
|
const files = [];
|
||||||
|
const fillFilesFromMetadata = function (basePath, metadata) {
|
||||||
|
if (!('files' in metadata)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [childPath, childMetadata] of Object.entries(metadata.files)) {
|
||||||
|
const fullPath = path.join(basePath, childPath);
|
||||||
|
const packState = 'unpacked' in childMetadata && childMetadata.unpacked ? 'unpack' : 'pack ';
|
||||||
|
files.push(options && options.isPack ? `${packState} : ${fullPath}` : fullPath);
|
||||||
|
fillFilesFromMetadata(fullPath, childMetadata);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fillFilesFromMetadata('/', this.header);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
getNode(p, followLinks = true, depth = 0, visited = new Set()) {
|
||||||
|
const node = this.searchNodeFromDirectory(path.dirname(p));
|
||||||
|
const name = path.basename(p);
|
||||||
|
if ('link' in node && followLinks) {
|
||||||
|
const resolvedPath = path.join(node.link, name);
|
||||||
|
if (visited.has(resolvedPath)) {
|
||||||
|
throw new Error(`"${p}": circular symlink detected at "${resolvedPath}"`);
|
||||||
|
}
|
||||||
|
if (depth >= SYMLINK_MAX_DEPTH) {
|
||||||
|
throw new Error(`"${p}": too many levels of symbolic links (>${SYMLINK_MAX_DEPTH})`);
|
||||||
|
}
|
||||||
|
visited.add(resolvedPath);
|
||||||
|
return this.getNode(resolvedPath, followLinks, depth + 1, visited);
|
||||||
|
}
|
||||||
|
if (name) {
|
||||||
|
return node.files[name];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getFile(p, followLinks = true, depth = 0, visited = new Set()) {
|
||||||
|
const info = this.getNode(p, followLinks, depth, visited);
|
||||||
|
if (!info) {
|
||||||
|
throw new Error(`"${p}" was not found in this archive`);
|
||||||
|
}
|
||||||
|
// if followLinks is false we don't resolve symlinks
|
||||||
|
if ('link' in info && followLinks) {
|
||||||
|
const link = info.link;
|
||||||
|
if (visited.has(link)) {
|
||||||
|
throw new Error(`"${p}": circular symlink detected at "${link}"`);
|
||||||
|
}
|
||||||
|
if (depth >= SYMLINK_MAX_DEPTH) {
|
||||||
|
throw new Error(`"${p}": too many levels of symbolic links (>${SYMLINK_MAX_DEPTH})`);
|
||||||
|
}
|
||||||
|
visited.add(link);
|
||||||
|
return this.getFile(link, followLinks, depth + 1, visited);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=filesystem.js.map
|
||||||
1
patcher/vendor/asar/node_modules/@electron/asar/lib/filesystem.js.map
generated
vendored
Normal file
1
patcher/vendor/asar/node_modules/@electron/asar/lib/filesystem.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
8
patcher/vendor/asar/node_modules/@electron/asar/lib/integrity.d.ts
generated
vendored
Normal file
8
patcher/vendor/asar/node_modules/@electron/asar/lib/integrity.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export type FileIntegrity = {
|
||||||
|
algorithm: 'SHA256';
|
||||||
|
hash: string;
|
||||||
|
blockSize: number;
|
||||||
|
blocks: string[];
|
||||||
|
};
|
||||||
|
export declare function getFileIntegrity(inputFileStream: NodeJS.ReadableStream): Promise<FileIntegrity>;
|
||||||
|
export declare function getFileIntegrityFromBuffer(data: Buffer): FileIntegrity;
|
||||||
58
patcher/vendor/asar/node_modules/@electron/asar/lib/integrity.js
generated
vendored
Normal file
58
patcher/vendor/asar/node_modules/@electron/asar/lib/integrity.js
generated
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import stream from 'node:stream';
|
||||||
|
import streamPromises from 'node:stream/promises';
|
||||||
|
const ALGORITHM = 'SHA256';
|
||||||
|
// 4MB default block size
|
||||||
|
const BLOCK_SIZE = 4 * 1024 * 1024;
|
||||||
|
export async function getFileIntegrity(inputFileStream) {
|
||||||
|
const fileHash = crypto.createHash(ALGORITHM);
|
||||||
|
const blockHashes = [];
|
||||||
|
let blockHash = crypto.createHash(ALGORITHM);
|
||||||
|
let currentBlockSize = 0;
|
||||||
|
await streamPromises.pipeline(inputFileStream, new stream.PassThrough({
|
||||||
|
decodeStrings: false,
|
||||||
|
transform(_chunk, encoding, callback) {
|
||||||
|
fileHash.update(_chunk);
|
||||||
|
let offset = 0;
|
||||||
|
while (offset < _chunk.byteLength) {
|
||||||
|
const remaining = BLOCK_SIZE - currentBlockSize;
|
||||||
|
const end = Math.min(offset + remaining, _chunk.byteLength);
|
||||||
|
const slice = offset === 0 && end === _chunk.byteLength ? _chunk : _chunk.subarray(offset, end);
|
||||||
|
blockHash.update(slice);
|
||||||
|
currentBlockSize += end - offset;
|
||||||
|
if (currentBlockSize === BLOCK_SIZE) {
|
||||||
|
blockHashes.push(blockHash.digest('hex'));
|
||||||
|
blockHash = crypto.createHash(ALGORITHM);
|
||||||
|
currentBlockSize = 0;
|
||||||
|
}
|
||||||
|
offset = end;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
flush(callback) {
|
||||||
|
if (currentBlockSize > 0 || blockHashes.length === 0) {
|
||||||
|
blockHashes.push(blockHash.digest('hex'));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
algorithm: ALGORITHM,
|
||||||
|
hash: fileHash.digest('hex'),
|
||||||
|
blockSize: BLOCK_SIZE,
|
||||||
|
blocks: blockHashes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function getFileIntegrityFromBuffer(data) {
|
||||||
|
const hash = crypto.createHash(ALGORITHM).update(data).digest('hex');
|
||||||
|
const blocks = [];
|
||||||
|
for (let offset = 0; offset < data.length; offset += BLOCK_SIZE) {
|
||||||
|
const end = Math.min(offset + BLOCK_SIZE, data.length);
|
||||||
|
blocks.push(crypto.createHash(ALGORITHM).update(data.subarray(offset, end)).digest('hex'));
|
||||||
|
}
|
||||||
|
if (data.length === 0) {
|
||||||
|
blocks.push(crypto.createHash(ALGORITHM).update(data).digest('hex'));
|
||||||
|
}
|
||||||
|
return { algorithm: ALGORITHM, hash, blockSize: BLOCK_SIZE, blocks };
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=integrity.js.map
|
||||||
1
patcher/vendor/asar/node_modules/@electron/asar/lib/integrity.js.map
generated
vendored
Normal file
1
patcher/vendor/asar/node_modules/@electron/asar/lib/integrity.js.map
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"integrity.js","sourceRoot":"","sources":["../src/integrity.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,cAAc,MAAM,sBAAsB,CAAC;AAElD,MAAM,SAAS,GAAG,QAAQ,CAAC;AAC3B,yBAAyB;AACzB,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AASnC,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,eAAsC;IAEtC,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAE9C,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,IAAI,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAC7C,IAAI,gBAAgB,GAAG,CAAC,CAAC;IAEzB,MAAM,cAAc,CAAC,QAAQ,CAC3B,eAAe,EACf,IAAI,MAAM,CAAC,WAAW,CAAC;QACrB,aAAa,EAAE,KAAK;QACpB,SAAS,CAAC,MAAc,EAAE,QAAQ,EAAE,QAAQ;YAC1C,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAExB,IAAI,MAAM,GAAG,CAAC,CAAC;YACf,OAAO,MAAM,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;gBAClC,MAAM,SAAS,GAAG,UAAU,GAAG,gBAAgB,CAAC;gBAChD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,SAAS,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;gBAC5D,MAAM,KAAK,GACT,MAAM,KAAK,CAAC,IAAI,GAAG,KAAK,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;gBACpF,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACxB,gBAAgB,IAAI,GAAG,GAAG,MAAM,CAAC;gBACjC,IAAI,gBAAgB,KAAK,UAAU,EAAE,CAAC;oBACpC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;oBAC1C,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;oBACzC,gBAAgB,GAAG,CAAC,CAAC;gBACvB,CAAC;gBACD,MAAM,GAAG,GAAG,CAAC;YACf,CAAC;YACD,QAAQ,EAAE,CAAC;QACb,CAAC;QACD,KAAK,CAAC,QAAQ;YACZ,IAAI,gBAAgB,GAAG,CAAC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACrD,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC5C,CAAC;YACD,QAAQ,EAAE,CAAC;QACb,CAAC;KACF,CAAC,CACH,CAAC;IAEF,OAAO;QACL,SAAS,EAAE,SAAS;QACpB,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC;QAC5B,SAAS,EAAE,UAAU;QACrB,MAAM,EAAE,WAAW;KACpB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,IAAY;IACrD,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACrE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,MAAM,IAAI,UAAU,EAAE,CAAC;QAChE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACvD,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7F,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;AACvE,CAAC"}
|
||||||
6
patcher/vendor/asar/node_modules/@electron/asar/lib/path-validation.d.ts
generated
vendored
Normal file
6
patcher/vendor/asar/node_modules/@electron/asar/lib/path-validation.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Validates that a resolved child path is strictly within a container directory.
|
||||||
|
* Uses path.resolve (not realpath) so it works before paths exist on disk.
|
||||||
|
* Throws if the resolved path escapes the container via traversal sequences.
|
||||||
|
*/
|
||||||
|
export declare function ensureWithin(container: string, filePath: string): string;
|
||||||
16
patcher/vendor/asar/node_modules/@electron/asar/lib/path-validation.js
generated
vendored
Normal file
16
patcher/vendor/asar/node_modules/@electron/asar/lib/path-validation.js
generated
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import path from 'node:path';
|
||||||
|
/**
|
||||||
|
* Validates that a resolved child path is strictly within a container directory.
|
||||||
|
* Uses path.resolve (not realpath) so it works before paths exist on disk.
|
||||||
|
* Throws if the resolved path escapes the container via traversal sequences.
|
||||||
|
*/
|
||||||
|
export function ensureWithin(container, filePath) {
|
||||||
|
const resolvedContainer = path.resolve(container);
|
||||||
|
const resolvedPath = path.resolve(resolvedContainer, filePath);
|
||||||
|
if (!resolvedPath.startsWith(resolvedContainer + path.sep) &&
|
||||||
|
resolvedPath !== resolvedContainer) {
|
||||||
|
throw new Error(`Path "${filePath}" resolves to "${resolvedPath}" which is outside "${resolvedContainer}"`);
|
||||||
|
}
|
||||||
|
return resolvedPath;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=path-validation.js.map
|
||||||
1
patcher/vendor/asar/node_modules/@electron/asar/lib/path-validation.js.map
generated
vendored
Normal file
1
patcher/vendor/asar/node_modules/@electron/asar/lib/path-validation.js.map
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"path-validation.js","sourceRoot":"","sources":["../src/path-validation.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,SAAiB,EAAE,QAAgB;IAC9D,MAAM,iBAAiB,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAClD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;IAC/D,IACE,CAAC,YAAY,CAAC,UAAU,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC;QACtD,YAAY,KAAK,iBAAiB,EAClC,CAAC;QACD,MAAM,IAAI,KAAK,CACb,SAAS,QAAQ,kBAAkB,YAAY,uBAAuB,iBAAiB,GAAG,CAC3F,CAAC;IACJ,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC"}
|
||||||
46
patcher/vendor/asar/node_modules/@electron/asar/lib/pickle.d.ts
generated
vendored
Normal file
46
patcher/vendor/asar/node_modules/@electron/asar/lib/pickle.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
declare class PickleIterator {
|
||||||
|
private payload;
|
||||||
|
private payloadOffset;
|
||||||
|
private readIndex;
|
||||||
|
private endIndex;
|
||||||
|
constructor(pickle: Pickle);
|
||||||
|
readBool(): boolean;
|
||||||
|
readInt(): number;
|
||||||
|
readUInt32(): number;
|
||||||
|
readInt64(): bigint;
|
||||||
|
readUInt64(): bigint;
|
||||||
|
readFloat(): number;
|
||||||
|
readDouble(): number;
|
||||||
|
readString(): string;
|
||||||
|
readBytes(length: number): Buffer;
|
||||||
|
readBytes<R, F extends (...args: any[]) => R>(length: number, method: F): R;
|
||||||
|
getReadPayloadOffsetAndAdvance(length: number): number;
|
||||||
|
advance(size: number): void;
|
||||||
|
}
|
||||||
|
export declare class Pickle {
|
||||||
|
private header;
|
||||||
|
private headerSize;
|
||||||
|
private capacityAfterHeader;
|
||||||
|
private writeOffset;
|
||||||
|
private constructor();
|
||||||
|
static createEmpty(): Pickle;
|
||||||
|
static createFromBuffer(buffer: Buffer): Pickle;
|
||||||
|
getHeader(): Buffer<ArrayBufferLike>;
|
||||||
|
getHeaderSize(): number;
|
||||||
|
createIterator(): PickleIterator;
|
||||||
|
toBuffer(): Buffer<ArrayBuffer>;
|
||||||
|
writeBool(value: boolean): boolean;
|
||||||
|
writeInt(value: number): boolean;
|
||||||
|
writeUInt32(value: number): boolean;
|
||||||
|
writeInt64(value: number): boolean;
|
||||||
|
writeUInt64(value: number): boolean;
|
||||||
|
writeFloat(value: number): boolean;
|
||||||
|
writeDouble(value: number): boolean;
|
||||||
|
writeString(value: string): boolean;
|
||||||
|
setPayloadSize(payloadSize: number): number;
|
||||||
|
getPayloadSize(): number;
|
||||||
|
writeBytes(data: string, length: number, method?: undefined): boolean;
|
||||||
|
writeBytes(data: number | bigint, length: number, method: Function): boolean;
|
||||||
|
resize(newCapacity: number): void;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
205
patcher/vendor/asar/node_modules/@electron/asar/lib/pickle.js
generated
vendored
Normal file
205
patcher/vendor/asar/node_modules/@electron/asar/lib/pickle.js
generated
vendored
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
// sizeof(T).
|
||||||
|
const SIZE_INT32 = 4;
|
||||||
|
const SIZE_UINT32 = 4;
|
||||||
|
const SIZE_INT64 = 8;
|
||||||
|
const SIZE_UINT64 = 8;
|
||||||
|
const SIZE_FLOAT = 4;
|
||||||
|
const SIZE_DOUBLE = 8;
|
||||||
|
// The allocation granularity of the payload.
|
||||||
|
const PAYLOAD_UNIT = 64;
|
||||||
|
// Largest JS number.
|
||||||
|
const CAPACITY_READ_ONLY = 9007199254740992;
|
||||||
|
// Aligns 'i' by rounding it up to the next multiple of 'alignment'.
|
||||||
|
const alignInt = function (i, alignment) {
|
||||||
|
return i + ((alignment - (i % alignment)) % alignment);
|
||||||
|
};
|
||||||
|
// PickleIterator reads data from a Pickle. The Pickle object must remain valid
|
||||||
|
// while the PickleIterator object is in use.
|
||||||
|
class PickleIterator {
|
||||||
|
payload;
|
||||||
|
payloadOffset;
|
||||||
|
readIndex;
|
||||||
|
endIndex;
|
||||||
|
constructor(pickle) {
|
||||||
|
this.payload = pickle.getHeader();
|
||||||
|
this.payloadOffset = pickle.getHeaderSize();
|
||||||
|
this.readIndex = 0;
|
||||||
|
this.endIndex = pickle.getPayloadSize();
|
||||||
|
}
|
||||||
|
readBool() {
|
||||||
|
return this.readInt() !== 0;
|
||||||
|
}
|
||||||
|
readInt() {
|
||||||
|
return this.readBytes(SIZE_INT32, Buffer.prototype.readInt32LE);
|
||||||
|
}
|
||||||
|
readUInt32() {
|
||||||
|
return this.readBytes(SIZE_UINT32, Buffer.prototype.readUInt32LE);
|
||||||
|
}
|
||||||
|
readInt64() {
|
||||||
|
return this.readBytes(SIZE_INT64, Buffer.prototype.readBigInt64LE);
|
||||||
|
}
|
||||||
|
readUInt64() {
|
||||||
|
return this.readBytes(SIZE_UINT64, Buffer.prototype.readBigUInt64LE);
|
||||||
|
}
|
||||||
|
readFloat() {
|
||||||
|
return this.readBytes(SIZE_FLOAT, Buffer.prototype.readFloatLE);
|
||||||
|
}
|
||||||
|
readDouble() {
|
||||||
|
return this.readBytes(SIZE_DOUBLE, Buffer.prototype.readDoubleLE);
|
||||||
|
}
|
||||||
|
readString() {
|
||||||
|
return this.readBytes(this.readInt()).toString();
|
||||||
|
}
|
||||||
|
readBytes(length, method) {
|
||||||
|
const readPayloadOffset = this.getReadPayloadOffsetAndAdvance(length);
|
||||||
|
if (method != null) {
|
||||||
|
return method.call(this.payload, readPayloadOffset, length);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return this.payload.slice(readPayloadOffset, readPayloadOffset + length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getReadPayloadOffsetAndAdvance(length) {
|
||||||
|
if (length > this.endIndex - this.readIndex) {
|
||||||
|
this.readIndex = this.endIndex;
|
||||||
|
throw new Error('Failed to read data with length of ' + length);
|
||||||
|
}
|
||||||
|
const readPayloadOffset = this.payloadOffset + this.readIndex;
|
||||||
|
this.advance(length);
|
||||||
|
return readPayloadOffset;
|
||||||
|
}
|
||||||
|
advance(size) {
|
||||||
|
const alignedSize = alignInt(size, SIZE_UINT32);
|
||||||
|
if (this.endIndex - this.readIndex < alignedSize) {
|
||||||
|
this.readIndex = this.endIndex;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.readIndex += alignedSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This class provides facilities for basic binary value packing and unpacking.
|
||||||
|
//
|
||||||
|
// The Pickle class supports appending primitive values (ints, strings, etc.)
|
||||||
|
// to a pickle instance. The Pickle instance grows its internal memory buffer
|
||||||
|
// dynamically to hold the sequence of primitive values. The internal memory
|
||||||
|
// buffer is exposed as the "data" of the Pickle. This "data" can be passed
|
||||||
|
// to a Pickle object to initialize it for reading.
|
||||||
|
//
|
||||||
|
// When reading from a Pickle object, it is important for the consumer to know
|
||||||
|
// what value types to read and in what order to read them as the Pickle does
|
||||||
|
// not keep track of the type of data written to it.
|
||||||
|
//
|
||||||
|
// The Pickle's data has a header which contains the size of the Pickle's
|
||||||
|
// payload. It can optionally support additional space in the header. That
|
||||||
|
// space is controlled by the header_size parameter passed to the Pickle
|
||||||
|
// constructor.
|
||||||
|
export class Pickle {
|
||||||
|
header;
|
||||||
|
headerSize;
|
||||||
|
capacityAfterHeader;
|
||||||
|
writeOffset;
|
||||||
|
constructor(buffer) {
|
||||||
|
if (buffer) {
|
||||||
|
this.header = buffer;
|
||||||
|
this.headerSize = buffer.length - this.getPayloadSize();
|
||||||
|
this.capacityAfterHeader = CAPACITY_READ_ONLY;
|
||||||
|
this.writeOffset = 0;
|
||||||
|
if (this.headerSize > buffer.length) {
|
||||||
|
this.headerSize = 0;
|
||||||
|
}
|
||||||
|
if (this.headerSize !== alignInt(this.headerSize, SIZE_UINT32)) {
|
||||||
|
this.headerSize = 0;
|
||||||
|
}
|
||||||
|
if (this.headerSize === 0) {
|
||||||
|
this.header = Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.header = Buffer.alloc(0);
|
||||||
|
this.headerSize = SIZE_UINT32;
|
||||||
|
this.capacityAfterHeader = 0;
|
||||||
|
this.writeOffset = 0;
|
||||||
|
this.resize(PAYLOAD_UNIT);
|
||||||
|
this.setPayloadSize(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static createEmpty() {
|
||||||
|
return new Pickle();
|
||||||
|
}
|
||||||
|
static createFromBuffer(buffer) {
|
||||||
|
return new Pickle(buffer);
|
||||||
|
}
|
||||||
|
getHeader() {
|
||||||
|
return this.header;
|
||||||
|
}
|
||||||
|
getHeaderSize() {
|
||||||
|
return this.headerSize;
|
||||||
|
}
|
||||||
|
createIterator() {
|
||||||
|
return new PickleIterator(this);
|
||||||
|
}
|
||||||
|
toBuffer() {
|
||||||
|
return this.header.slice(0, this.headerSize + this.getPayloadSize());
|
||||||
|
}
|
||||||
|
writeBool(value) {
|
||||||
|
return this.writeInt(value ? 1 : 0);
|
||||||
|
}
|
||||||
|
writeInt(value) {
|
||||||
|
return this.writeBytes(value, SIZE_INT32, Buffer.prototype.writeInt32LE);
|
||||||
|
}
|
||||||
|
writeUInt32(value) {
|
||||||
|
return this.writeBytes(value, SIZE_UINT32, Buffer.prototype.writeUInt32LE);
|
||||||
|
}
|
||||||
|
writeInt64(value) {
|
||||||
|
return this.writeBytes(BigInt(value), SIZE_INT64, Buffer.prototype.writeBigInt64LE);
|
||||||
|
}
|
||||||
|
writeUInt64(value) {
|
||||||
|
return this.writeBytes(BigInt(value), SIZE_UINT64, Buffer.prototype.writeBigUInt64LE);
|
||||||
|
}
|
||||||
|
writeFloat(value) {
|
||||||
|
return this.writeBytes(value, SIZE_FLOAT, Buffer.prototype.writeFloatLE);
|
||||||
|
}
|
||||||
|
writeDouble(value) {
|
||||||
|
return this.writeBytes(value, SIZE_DOUBLE, Buffer.prototype.writeDoubleLE);
|
||||||
|
}
|
||||||
|
writeString(value) {
|
||||||
|
const length = Buffer.byteLength(value, 'utf8');
|
||||||
|
if (!this.writeInt(length)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.writeBytes(value, length);
|
||||||
|
}
|
||||||
|
setPayloadSize(payloadSize) {
|
||||||
|
return this.header.writeUInt32LE(payloadSize, 0);
|
||||||
|
}
|
||||||
|
getPayloadSize() {
|
||||||
|
return this.header.readUInt32LE(0);
|
||||||
|
}
|
||||||
|
writeBytes(data, length, method) {
|
||||||
|
const dataLength = alignInt(length, SIZE_UINT32);
|
||||||
|
const newSize = this.writeOffset + dataLength;
|
||||||
|
if (newSize > this.capacityAfterHeader) {
|
||||||
|
this.resize(Math.max(this.capacityAfterHeader * 2, newSize));
|
||||||
|
}
|
||||||
|
if (method) {
|
||||||
|
method.call(this.header, data, this.headerSize + this.writeOffset);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.header.write(data, this.headerSize + this.writeOffset, length);
|
||||||
|
}
|
||||||
|
const endOffset = this.headerSize + this.writeOffset + length;
|
||||||
|
this.header.fill(0, endOffset, endOffset + dataLength - length);
|
||||||
|
this.setPayloadSize(newSize);
|
||||||
|
this.writeOffset = newSize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
resize(newCapacity) {
|
||||||
|
newCapacity = alignInt(newCapacity, PAYLOAD_UNIT);
|
||||||
|
const newHeader = Buffer.alloc(this.headerSize + newCapacity);
|
||||||
|
this.header.copy(newHeader, 0, 0, this.headerSize + this.writeOffset);
|
||||||
|
this.header = newHeader;
|
||||||
|
this.capacityAfterHeader = newCapacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=pickle.js.map
|
||||||
1
patcher/vendor/asar/node_modules/@electron/asar/lib/pickle.js.map
generated
vendored
Normal file
1
patcher/vendor/asar/node_modules/@electron/asar/lib/pickle.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
13
patcher/vendor/asar/node_modules/@electron/asar/lib/wrapped-fs.d.ts
generated
vendored
Normal file
13
patcher/vendor/asar/node_modules/@electron/asar/lib/wrapped-fs.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
type AsarFS = typeof import('fs') & {
|
||||||
|
mkdirp(dir: string): Promise<void>;
|
||||||
|
mkdirpSync(dir: string): void;
|
||||||
|
lstat: (typeof import('fs'))['promises']['lstat'];
|
||||||
|
mkdtemp: (typeof import('fs'))['promises']['mkdtemp'];
|
||||||
|
readFile: (typeof import('fs'))['promises']['readFile'];
|
||||||
|
stat: (typeof import('fs'))['promises']['stat'];
|
||||||
|
writeFile: (typeof import('fs'))['promises']['writeFile'];
|
||||||
|
symlink: (typeof import('fs'))['promises']['symlink'];
|
||||||
|
readlink: (typeof import('fs'))['promises']['readlink'];
|
||||||
|
};
|
||||||
|
export declare const wrappedFs: AsarFS;
|
||||||
|
export {};
|
||||||
23
patcher/vendor/asar/node_modules/@electron/asar/lib/wrapped-fs.js
generated
vendored
Normal file
23
patcher/vendor/asar/node_modules/@electron/asar/lib/wrapped-fs.js
generated
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const fs = 'electron' in process.versions ? require('original-fs') : require('node:fs');
|
||||||
|
const promisifiedMethods = [
|
||||||
|
'lstat',
|
||||||
|
'mkdtemp',
|
||||||
|
'readFile',
|
||||||
|
'stat',
|
||||||
|
'writeFile',
|
||||||
|
'symlink',
|
||||||
|
'readlink',
|
||||||
|
];
|
||||||
|
export const wrappedFs = Object.keys(fs).reduce((accum, method) => {
|
||||||
|
return {
|
||||||
|
...accum,
|
||||||
|
[method]: promisifiedMethods.includes(method) ? fs.promises[method] : fs[method],
|
||||||
|
};
|
||||||
|
}, {
|
||||||
|
// To make it more like fs-extra
|
||||||
|
mkdirp: (dir) => fs.promises.mkdir(dir, { recursive: true }),
|
||||||
|
mkdirpSync: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=wrapped-fs.js.map
|
||||||
1
patcher/vendor/asar/node_modules/@electron/asar/lib/wrapped-fs.js.map
generated
vendored
Normal file
1
patcher/vendor/asar/node_modules/@electron/asar/lib/wrapped-fs.js.map
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"wrapped-fs.js","sourceRoot":"","sources":["../src/wrapped-fs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,EAAE,GAAG,UAAU,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAExF,MAAM,kBAAkB,GAAG;IACzB,OAAO;IACP,SAAS;IACT,UAAU;IACV,MAAM;IACN,WAAW;IACX,SAAS;IACT,UAAU;CACX,CAAC;AAcF,MAAM,CAAC,MAAM,SAAS,GAAW,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CACrD,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;IAChB,OAAO;QACL,GAAG,KAAK;QACR,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;KACjF,CAAC;AACJ,CAAC,EACD;IACE,gCAAgC;IAChC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAC5D,UAAU,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;CAClD,CACZ,CAAC"}
|
||||||
78
patcher/vendor/asar/node_modules/@electron/asar/package.json
generated
vendored
Normal file
78
patcher/vendor/asar/node_modules/@electron/asar/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
{
|
||||||
|
"name": "@electron/asar",
|
||||||
|
"description": "Creating Electron app packages",
|
||||||
|
"version": "4.3.1",
|
||||||
|
"type": "module",
|
||||||
|
"exports": "./lib/asar.js",
|
||||||
|
"types": "./lib/asar.d.ts",
|
||||||
|
"bin": "./bin/asar.mjs",
|
||||||
|
"files": [
|
||||||
|
"bin",
|
||||||
|
"lib"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://github.com/electron/asar",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/electron/asar.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/electron/asar/issues"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"provenance": true
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"build:docs": "typedoc src/asar.ts",
|
||||||
|
"test": "yarn lint && yarn vitest run && xvfb-maybe electron node_modules/vitest/vitest.mjs run",
|
||||||
|
"test:typecheck": "cd test && tsc",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"lint": "oxfmt --check . && oxlint",
|
||||||
|
"lint:fix": "oxfmt --write . && oxlint --fix",
|
||||||
|
"benchmark": "yarn build && tsx benchmark/run.ts",
|
||||||
|
"benchmark:quick": "yarn build && tsx benchmark/run.ts small",
|
||||||
|
"prepack": "yarn build",
|
||||||
|
"prepare": "husky"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^13.0.2",
|
||||||
|
"minimatch": "^10.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node22": "^22.0.1",
|
||||||
|
"@types/node": "^22.12.0",
|
||||||
|
"electron": "^43.0.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^17.4.1",
|
||||||
|
"oxfmt": "^0.44.0",
|
||||||
|
"oxlint": "^1.59.0",
|
||||||
|
"oxlint-tsgolint": "^0.20.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typedoc": "~0.28.0",
|
||||||
|
"typescript": "^6.0.2",
|
||||||
|
"vite": "^8.0.16",
|
||||||
|
"vitest": "^5.0.1",
|
||||||
|
"xvfb-maybe": "^0.2.1"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"fsevents": "npm:@electron/fsevents@2.3.3-fork"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f",
|
||||||
|
"dependenciesMeta": {
|
||||||
|
"electron": {
|
||||||
|
"built": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{json,md}": "oxfmt --write",
|
||||||
|
"*.{js,ts,cjs,mjs,cts,mts}": [
|
||||||
|
"oxlint --fix",
|
||||||
|
"oxfmt --write"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
23
patcher/vendor/asar/node_modules/balanced-match/LICENSE.md
generated
vendored
Normal file
23
patcher/vendor/asar/node_modules/balanced-match/LICENSE.md
generated
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
(MIT)
|
||||||
|
|
||||||
|
Original code Copyright Julian Gruber <julian@juliangruber.com>
|
||||||
|
|
||||||
|
Port to TypeScript Copyright Isaac Z. Schlueter <i@izs.me>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
57
patcher/vendor/asar/node_modules/balanced-match/README.md
generated
vendored
Normal file
57
patcher/vendor/asar/node_modules/balanced-match/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# balanced-match
|
||||||
|
|
||||||
|
Match balanced string pairs, like `{` and `}` or `<b>` and
|
||||||
|
`</b>`. Supports regular expressions as well!
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Get the first matching pair of braces:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { balanced } from 'balanced-match'
|
||||||
|
|
||||||
|
console.log(balanced('{', '}', 'pre{in{nested}}post'))
|
||||||
|
console.log(balanced('{', '}', 'pre{first}between{second}post'))
|
||||||
|
console.log(
|
||||||
|
balanced(/\s+\{\s+/, /\s+\}\s+/, 'pre { in{nest} } post'),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The matches are:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ node example.js
|
||||||
|
{ start: 3, end: 14, pre: 'pre', body: 'in{nested}', post: 'post' }
|
||||||
|
{ start: 3,
|
||||||
|
end: 9,
|
||||||
|
pre: 'pre',
|
||||||
|
body: 'first',
|
||||||
|
post: 'between{second}post' }
|
||||||
|
{ start: 3, end: 17, pre: 'pre', body: 'in{nest}', post: 'post' }
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### const m = balanced(a, b, str)
|
||||||
|
|
||||||
|
For the first non-nested matching pair of `a` and `b` in `str`, return an
|
||||||
|
object with those keys:
|
||||||
|
|
||||||
|
- **start** the index of the first match of `a`
|
||||||
|
- **end** the index of the matching `b`
|
||||||
|
- **pre** the preamble, `a` and `b` not included
|
||||||
|
- **body** the match, `a` and `b` not included
|
||||||
|
- **post** the postscript, `a` and `b` not included
|
||||||
|
|
||||||
|
If there's no match, `undefined` will be returned.
|
||||||
|
|
||||||
|
If the `str` contains more `a` than `b` / there are unmatched pairs, the first match that was closed will be used. For example, `{{a}` will match `['{', 'a', '']` and `{a}}` will match `['', 'a', '}']`.
|
||||||
|
|
||||||
|
### const r = balanced.range(a, b, str)
|
||||||
|
|
||||||
|
For the first non-nested matching pair of `a` and `b` in `str`, return an
|
||||||
|
array with indexes: `[ <a index>, <b index> ]`.
|
||||||
|
|
||||||
|
If there's no match, `undefined` will be returned.
|
||||||
|
|
||||||
|
If the `str` contains more `a` than `b` / there are unmatched pairs, the first match that was closed will be used. For example, `{{a}` will match `[ 1, 3 ]` and `{a}}` will match `[0, 2]`.
|
||||||
68
patcher/vendor/asar/node_modules/balanced-match/package.json
generated
vendored
Normal file
68
patcher/vendor/asar/node_modules/balanced-match/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
"name": "balanced-match",
|
||||||
|
"description": "Match balanced character pairs, like \"{\" and \"}\"",
|
||||||
|
"version": "4.0.4",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/juliangruber/balanced-match.git"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"preversion": "npm test",
|
||||||
|
"postversion": "npm publish",
|
||||||
|
"prepublishOnly": "git push origin --follow-tags",
|
||||||
|
"prepare": "tshy",
|
||||||
|
"pretest": "npm run prepare",
|
||||||
|
"presnap": "npm run prepare",
|
||||||
|
"test": "tap",
|
||||||
|
"snap": "tap",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"benchmark": "node benchmark/index.js",
|
||||||
|
"typedoc": "typedoc --tsconfig .tshy/esm.json ./src/*.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/brace-expansion": "^1.1.2",
|
||||||
|
"@types/node": "^25.2.1",
|
||||||
|
"mkdirp": "^3.0.1",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"tap": "^21.6.2",
|
||||||
|
"tshy": "^3.0.2",
|
||||||
|
"typedoc": "^0.28.5"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"match",
|
||||||
|
"regexp",
|
||||||
|
"test",
|
||||||
|
"balanced",
|
||||||
|
"parse"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"tshy": {
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/commonjs/index.js",
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"module": "./dist/esm/index.js"
|
||||||
|
}
|
||||||
23
patcher/vendor/asar/node_modules/brace-expansion/LICENSE
generated
vendored
Normal file
23
patcher/vendor/asar/node_modules/brace-expansion/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright Julian Gruber <julian@juliangruber.com>
|
||||||
|
|
||||||
|
TypeScript port Copyright Isaac Z. Schlueter <i@izs.me>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
94
patcher/vendor/asar/node_modules/brace-expansion/README.md
generated
vendored
Normal file
94
patcher/vendor/asar/node_modules/brace-expansion/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
# brace-expansion
|
||||||
|
|
||||||
|
[Brace expansion](https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html),
|
||||||
|
as known from sh/bash, in JavaScript.
|
||||||
|
|
||||||
|
[](https://github.com/juliangruber/brace-expansion/actions/workflows/ci.yml)
|
||||||
|
[](https://www.npmjs.org/package/brace-expansion)
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { expand } from 'brace-expansion'
|
||||||
|
|
||||||
|
expand('file-{a,b,c}.jpg')
|
||||||
|
// => ['file-a.jpg', 'file-b.jpg', 'file-c.jpg']
|
||||||
|
|
||||||
|
expand('-v{,,}')
|
||||||
|
// => ['-v', '-v', '-v']
|
||||||
|
|
||||||
|
expand('file{0..2}.jpg')
|
||||||
|
// => ['file0.jpg', 'file1.jpg', 'file2.jpg']
|
||||||
|
|
||||||
|
expand('file-{a..c}.jpg')
|
||||||
|
// => ['file-a.jpg', 'file-b.jpg', 'file-c.jpg']
|
||||||
|
|
||||||
|
expand('file{2..0}.jpg')
|
||||||
|
// => ['file2.jpg', 'file1.jpg', 'file0.jpg']
|
||||||
|
|
||||||
|
expand('file{0..4..2}.jpg')
|
||||||
|
// => ['file0.jpg', 'file2.jpg', 'file4.jpg']
|
||||||
|
|
||||||
|
expand('file-{a..e..2}.jpg')
|
||||||
|
// => ['file-a.jpg', 'file-c.jpg', 'file-e.jpg']
|
||||||
|
|
||||||
|
expand('file{00..10..5}.jpg')
|
||||||
|
// => ['file00.jpg', 'file05.jpg', 'file10.jpg']
|
||||||
|
|
||||||
|
expand('{{A..C},{a..c}}')
|
||||||
|
// => ['A', 'B', 'C', 'a', 'b', 'c']
|
||||||
|
|
||||||
|
expand('ppp{,config,oe{,conf}}')
|
||||||
|
// => ['ppp', 'pppconfig', 'pppoe', 'pppoeconf']
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { expand } from 'brace-expansion'
|
||||||
|
```
|
||||||
|
|
||||||
|
### const expanded = expand(str, [options])
|
||||||
|
|
||||||
|
Return an array of all possible and valid expansions of `str`. If
|
||||||
|
none are found, `[str]` is returned.
|
||||||
|
|
||||||
|
The `options` object can provide a `max` value to cap the number
|
||||||
|
of expansions allowed. This is limited to `100_000` by default,
|
||||||
|
to prevent DoS attacks.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const expansions = expand('{1..100}'.repeat(5), {
|
||||||
|
max: 100,
|
||||||
|
})
|
||||||
|
// expansions.length will be 100, not 100^5
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid expansions are:
|
||||||
|
|
||||||
|
```js
|
||||||
|
;/^(.*,)+(.+)?$/
|
||||||
|
// {a,b,...}
|
||||||
|
```
|
||||||
|
|
||||||
|
A comma separated list of options, like `{a,b}` or `{a,{b,c}}` or `{,a,}`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
;/^-?\d+\.\.-?\d+(\.\.-?\d+)?$/
|
||||||
|
// {x..y[..incr]}
|
||||||
|
```
|
||||||
|
|
||||||
|
A numeric sequence from `x` to `y` inclusive, with optional increment.
|
||||||
|
If `x` or `y` start with a leading `0`, all the numbers will be padded
|
||||||
|
to have equal length. Negative numbers and backwards iteration work too.
|
||||||
|
|
||||||
|
```js
|
||||||
|
;/^-?\d+\.\.-?\d+(\.\.-?\d+)?$/
|
||||||
|
// {x..y[..incr]}
|
||||||
|
```
|
||||||
|
|
||||||
|
An alphabetic sequence from `x` to `y` inclusive, with optional increment.
|
||||||
|
`x` and `y` must be exactly one character, and if given, `incr` must be a
|
||||||
|
number.
|
||||||
|
|
||||||
|
For compatibility reasons, the string `${` is not eligible for brace expansion.
|
||||||
64
patcher/vendor/asar/node_modules/brace-expansion/package.json
generated
vendored
Normal file
64
patcher/vendor/asar/node_modules/brace-expansion/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"name": "brace-expansion",
|
||||||
|
"description": "Brace expansion as known from sh/bash",
|
||||||
|
"version": "5.0.6",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"preversion": "npm test",
|
||||||
|
"postversion": "npm publish",
|
||||||
|
"prepublishOnly": "git push origin --follow-tags",
|
||||||
|
"prepare": "tshy",
|
||||||
|
"pretest": "npm run prepare",
|
||||||
|
"presnap": "npm run prepare",
|
||||||
|
"test": "tap",
|
||||||
|
"snap": "tap",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"benchmark": "node benchmark/index.js",
|
||||||
|
"typedoc": "typedoc --tsconfig .tshy/esm.json ./src/*.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/brace-expansion": "^1.1.2",
|
||||||
|
"@types/node": "^25.2.1",
|
||||||
|
"mkdirp": "^3.0.1",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"tap": "^21.6.2",
|
||||||
|
"tshy": "^3.0.2",
|
||||||
|
"typedoc": "^0.28.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"tshy": {
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/commonjs/index.js",
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"module": "./dist/esm/index.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+ssh://git@github.com/juliangruber/brace-expansion.git"
|
||||||
|
}
|
||||||
|
}
|
||||||
63
patcher/vendor/asar/node_modules/glob/LICENSE.md
generated
vendored
Normal file
63
patcher/vendor/asar/node_modules/glob/LICENSE.md
generated
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
All packages under `src/` are licensed according to the terms in
|
||||||
|
their respective `LICENSE` or `LICENSE.md` files.
|
||||||
|
|
||||||
|
The remainder of this project is licensed under the Blue Oak
|
||||||
|
Model License, as follows:
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
# Blue Oak Model License
|
||||||
|
|
||||||
|
Version 1.0.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This license gives everyone as much permission to work with
|
||||||
|
this software as possible, while protecting contributors
|
||||||
|
from liability.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
In order to receive this license, you must agree to its
|
||||||
|
rules. The rules of this license are both obligations
|
||||||
|
under that agreement and conditions to your license.
|
||||||
|
You must not do anything with this software that triggers
|
||||||
|
a rule that you cannot or will not follow.
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
Each contributor licenses you to do everything with this
|
||||||
|
software that would otherwise infringe that contributor's
|
||||||
|
copyright in it.
|
||||||
|
|
||||||
|
## Notices
|
||||||
|
|
||||||
|
You must ensure that everyone who gets a copy of
|
||||||
|
any part of this software from you, with or without
|
||||||
|
changes, also gets the text of this license or a link to
|
||||||
|
<https://blueoakcouncil.org/license/1.0.0>.
|
||||||
|
|
||||||
|
## Excuse
|
||||||
|
|
||||||
|
If anyone notifies you in writing that you have not
|
||||||
|
complied with [Notices](#notices), you can keep your
|
||||||
|
license by taking all practical steps to comply within 30
|
||||||
|
days after the notice. If you do not do so, your license
|
||||||
|
ends immediately.
|
||||||
|
|
||||||
|
## Patent
|
||||||
|
|
||||||
|
Each contributor licenses you to do everything with this
|
||||||
|
software that would otherwise infringe any patent claims
|
||||||
|
they can license or become able to license.
|
||||||
|
|
||||||
|
## Reliability
|
||||||
|
|
||||||
|
No contributor can revoke this license.
|
||||||
|
|
||||||
|
## No Liability
|
||||||
|
|
||||||
|
***As far as the law allows, this software comes as is,
|
||||||
|
without any warranty or condition, and no contributor
|
||||||
|
will be liable to anyone for any damages related to this
|
||||||
|
software or this license, under any kind of legal claim.***
|
||||||
1203
patcher/vendor/asar/node_modules/glob/README.md
generated
vendored
Normal file
1203
patcher/vendor/asar/node_modules/glob/README.md
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
98
patcher/vendor/asar/node_modules/glob/package.json
generated
vendored
Normal file
98
patcher/vendor/asar/node_modules/glob/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
{
|
||||||
|
"author": "Isaac Z. Schlueter <i@izs.me> (https://blog.izs.me/)",
|
||||||
|
"name": "glob",
|
||||||
|
"description": "the most correct and second fastest glob implementation in JavaScript",
|
||||||
|
"version": "13.0.6",
|
||||||
|
"type": "module",
|
||||||
|
"tshy": {
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
"./raw": "./src/index.ts",
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.min.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/index.min.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/commonjs/index.min.js",
|
||||||
|
"module": "./dist/esm/index.min.js",
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
"./raw": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.min.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/index.min.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@github.com:isaacs/node-glob.git"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"preversion": "npm test",
|
||||||
|
"postversion": "npm publish",
|
||||||
|
"prepublishOnly": "npm run benchclean; git push origin --follow-tags",
|
||||||
|
"prepare": "tshy && bash scripts/build.sh",
|
||||||
|
"pretest": "npm run prepare",
|
||||||
|
"presnap": "npm run prepare",
|
||||||
|
"test": "tap",
|
||||||
|
"snap": "tap",
|
||||||
|
"format": "prettier --write . --log-level warn",
|
||||||
|
"typedoc": "typedoc",
|
||||||
|
"profclean": "rm -f v8.log profile.txt",
|
||||||
|
"test-regen": "npm run profclean && TEST_REGEN=1 node --no-warnings --loader ts-node/esm test/00-setup.ts",
|
||||||
|
"prebench": "npm run prepare",
|
||||||
|
"bench": "bash benchmark.sh",
|
||||||
|
"preprof": "npm run prepare",
|
||||||
|
"prof": "bash prof.sh",
|
||||||
|
"benchclean": "node benchclean.cjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^10.2.2",
|
||||||
|
"minipass": "^7.1.3",
|
||||||
|
"path-scurry": "^2.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.3.0",
|
||||||
|
"esbuild": "^0.27.3",
|
||||||
|
"memfs": "^4.50.0",
|
||||||
|
"mkdirp": "^3.0.1",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"rimraf": "^6.1.3",
|
||||||
|
"tap": "^21.6.1",
|
||||||
|
"tshy": "^3.3.2",
|
||||||
|
"typedoc": "^0.28.17"
|
||||||
|
},
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
patcher/vendor/asar/node_modules/lru-cache/LICENSE.md
generated
vendored
Normal file
55
patcher/vendor/asar/node_modules/lru-cache/LICENSE.md
generated
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Blue Oak Model License
|
||||||
|
|
||||||
|
Version 1.0.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This license gives everyone as much permission to work with
|
||||||
|
this software as possible, while protecting contributors
|
||||||
|
from liability.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
In order to receive this license, you must agree to its
|
||||||
|
rules. The rules of this license are both obligations
|
||||||
|
under that agreement and conditions to your license.
|
||||||
|
You must not do anything with this software that triggers
|
||||||
|
a rule that you cannot or will not follow.
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
Each contributor licenses you to do everything with this
|
||||||
|
software that would otherwise infringe that contributor's
|
||||||
|
copyright in it.
|
||||||
|
|
||||||
|
## Notices
|
||||||
|
|
||||||
|
You must ensure that everyone who gets a copy of
|
||||||
|
any part of this software from you, with or without
|
||||||
|
changes, also gets the text of this license or a link to
|
||||||
|
<https://blueoakcouncil.org/license/1.0.0>.
|
||||||
|
|
||||||
|
## Excuse
|
||||||
|
|
||||||
|
If anyone notifies you in writing that you have not
|
||||||
|
complied with [Notices](#notices), you can keep your
|
||||||
|
license by taking all practical steps to comply within 30
|
||||||
|
days after the notice. If you do not do so, your license
|
||||||
|
ends immediately.
|
||||||
|
|
||||||
|
## Patent
|
||||||
|
|
||||||
|
Each contributor licenses you to do everything with this
|
||||||
|
software that would otherwise infringe any patent claims
|
||||||
|
they can license or become able to license.
|
||||||
|
|
||||||
|
## Reliability
|
||||||
|
|
||||||
|
No contributor can revoke this license.
|
||||||
|
|
||||||
|
## No Liability
|
||||||
|
|
||||||
|
***As far as the law allows, this software comes as is,
|
||||||
|
without any warranty or condition, and no contributor
|
||||||
|
will be liable to anyone for any damages related to this
|
||||||
|
software or this license, under any kind of legal claim.***
|
||||||
469
patcher/vendor/asar/node_modules/lru-cache/README.md
generated
vendored
Normal file
469
patcher/vendor/asar/node_modules/lru-cache/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,469 @@
|
||||||
|
# lru-cache
|
||||||
|
|
||||||
|
A cache object that deletes the least-recently-used items.
|
||||||
|
|
||||||
|
Specify a max number of the most recently used items that you
|
||||||
|
want to keep, and this cache will keep that many of the most
|
||||||
|
recently accessed items.
|
||||||
|
|
||||||
|
This is not primarily a TTL cache, and does not make strong TTL
|
||||||
|
guarantees. There is no preemptive pruning of expired items by
|
||||||
|
default, but you _may_ set a TTL on the cache or on a single
|
||||||
|
`set`. If you do so, it will treat expired items as missing, and
|
||||||
|
delete them when fetched. If you are more interested in TTL
|
||||||
|
caching than LRU caching, check out
|
||||||
|
[@isaacs/ttlcache](http://npm.im/@isaacs/ttlcache).
|
||||||
|
|
||||||
|
As of version 7, this is one of the most performant LRU
|
||||||
|
implementations available in JavaScript, and supports a wide
|
||||||
|
diversity of use cases. However, note that using some of the
|
||||||
|
features will necessarily impact performance, by causing the
|
||||||
|
cache to have to do more work. See the "Performance" section
|
||||||
|
below.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install lru-cache --save
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
// hybrid module, either works
|
||||||
|
import { LRUCache } from 'lru-cache'
|
||||||
|
// or:
|
||||||
|
const { LRUCache } = require('lru-cache')
|
||||||
|
// or in minified form for web browsers:
|
||||||
|
import { LRUCache } from 'http://unpkg.com/lru-cache@9/dist/mjs/index.min.mjs'
|
||||||
|
|
||||||
|
// At least one of 'max', 'ttl', or 'maxSize' is required, to prevent
|
||||||
|
// unsafe unbounded storage.
|
||||||
|
//
|
||||||
|
// In most cases, it's best to specify a max for performance, so all
|
||||||
|
// the required memory allocation is done up-front.
|
||||||
|
//
|
||||||
|
// All the other options are optional, see the sections below for
|
||||||
|
// documentation on what each one does. Most of them can be
|
||||||
|
// overridden for specific items in get()/set()
|
||||||
|
const options = {
|
||||||
|
max: 500,
|
||||||
|
|
||||||
|
// for use with tracking overall storage size
|
||||||
|
maxSize: 5000,
|
||||||
|
sizeCalculation: (value, key) => {
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
|
||||||
|
// for use when you need to clean up something when objects
|
||||||
|
// are evicted from the cache
|
||||||
|
dispose: (value, key, reason) => {
|
||||||
|
freeFromMemoryOrWhatever(value)
|
||||||
|
},
|
||||||
|
|
||||||
|
// for use when you need to know that an item is being inserted
|
||||||
|
// note that this does NOT allow you to prevent the insertion,
|
||||||
|
// it just allows you to know about it.
|
||||||
|
onInsert: (value, key, reason) => {
|
||||||
|
logInsertionOrWhatever(key, value)
|
||||||
|
},
|
||||||
|
|
||||||
|
// how long to live in ms
|
||||||
|
ttl: 1000 * 60 * 5,
|
||||||
|
|
||||||
|
// return stale items before removing from cache?
|
||||||
|
allowStale: false,
|
||||||
|
|
||||||
|
updateAgeOnGet: false,
|
||||||
|
updateAgeOnHas: false,
|
||||||
|
|
||||||
|
// async method to use for cache.fetch(), for
|
||||||
|
// stale-while-revalidate type of behavior
|
||||||
|
fetchMethod: async (key, staleValue, { options, signal, context }) => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = new LRUCache(options)
|
||||||
|
|
||||||
|
cache.set('key', 'value')
|
||||||
|
cache.get('key') // "value"
|
||||||
|
|
||||||
|
// non-string keys ARE fully supported
|
||||||
|
// but note that it must be THE SAME object, not
|
||||||
|
// just a JSON-equivalent object.
|
||||||
|
var someObject = { a: 1 }
|
||||||
|
cache.set(someObject, 'a value')
|
||||||
|
// Object keys are not toString()-ed
|
||||||
|
cache.set('[object Object]', 'a different value')
|
||||||
|
assert.equal(cache.get(someObject), 'a value')
|
||||||
|
// A similar object with same keys/values won't work,
|
||||||
|
// because it's a different object identity
|
||||||
|
assert.equal(cache.get({ a: 1 }), undefined)
|
||||||
|
|
||||||
|
cache.clear() // empty the cache
|
||||||
|
```
|
||||||
|
|
||||||
|
If you put more stuff in the cache, then less recently used items
|
||||||
|
will fall out. That's what an LRU cache is.
|
||||||
|
|
||||||
|
For full description of the API and all options, please see [the
|
||||||
|
LRUCache typedocs](https://isaacs.github.io/node-lru-cache/)
|
||||||
|
|
||||||
|
## Storage Bounds Safety
|
||||||
|
|
||||||
|
This implementation aims to be as flexible as possible, within
|
||||||
|
the limits of safe memory consumption and optimal performance.
|
||||||
|
|
||||||
|
At initial object creation, storage is allocated for `max` items.
|
||||||
|
If `max` is set to zero, then some performance is lost, and item
|
||||||
|
count is unbounded. Either `maxSize` or `ttl` _must_ be set if
|
||||||
|
`max` is not specified.
|
||||||
|
|
||||||
|
If `maxSize` is set, then this creates a safe limit on the
|
||||||
|
maximum storage consumed, but without the performance benefits of
|
||||||
|
pre-allocation. When `maxSize` is set, every item _must_ provide
|
||||||
|
a size, either via the `sizeCalculation` method provided to the
|
||||||
|
constructor, or via a `size` or `sizeCalculation` option provided
|
||||||
|
to `cache.set()`. The size of every item _must_ be a positive
|
||||||
|
integer.
|
||||||
|
|
||||||
|
If neither `max` nor `maxSize` are set, then `ttl` tracking must
|
||||||
|
be enabled. Note that, even when tracking item `ttl`, items are
|
||||||
|
_not_ preemptively deleted when they become stale, unless
|
||||||
|
`ttlAutopurge` is enabled. Instead, they are only purged the
|
||||||
|
next time the key is requested. Thus, if `ttlAutopurge`, `max`,
|
||||||
|
and `maxSize` are all not set, then the cache will potentially
|
||||||
|
grow unbounded.
|
||||||
|
|
||||||
|
In this case, a warning is printed to standard error. Future
|
||||||
|
versions may require the use of `ttlAutopurge` if `max` and
|
||||||
|
`maxSize` are not specified.
|
||||||
|
|
||||||
|
If you truly wish to use a cache that is bound _only_ by TTL
|
||||||
|
expiration, consider using a `Map` object, and calling
|
||||||
|
`setTimeout` to delete entries when they expire. It will perform
|
||||||
|
much better than an LRU cache.
|
||||||
|
|
||||||
|
Here is an implementation you may use, under the same
|
||||||
|
[license](./LICENSE) as this package:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// a storage-unbounded ttl cache that is not an lru-cache
|
||||||
|
const cache = {
|
||||||
|
data: new Map(),
|
||||||
|
timers: new Map(),
|
||||||
|
set: (k, v, ttl) => {
|
||||||
|
if (cache.timers.has(k)) {
|
||||||
|
clearTimeout(cache.timers.get(k))
|
||||||
|
}
|
||||||
|
cache.timers.set(
|
||||||
|
k,
|
||||||
|
setTimeout(() => cache.delete(k), ttl),
|
||||||
|
)
|
||||||
|
cache.data.set(k, v)
|
||||||
|
},
|
||||||
|
get: k => cache.data.get(k),
|
||||||
|
has: k => cache.data.has(k),
|
||||||
|
delete: k => {
|
||||||
|
if (cache.timers.has(k)) {
|
||||||
|
clearTimeout(cache.timers.get(k))
|
||||||
|
}
|
||||||
|
cache.timers.delete(k)
|
||||||
|
return cache.data.delete(k)
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
cache.data.clear()
|
||||||
|
for (const v of cache.timers.values()) {
|
||||||
|
clearTimeout(v)
|
||||||
|
}
|
||||||
|
cache.timers.clear()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If that isn't to your liking, check out
|
||||||
|
[@isaacs/ttlcache](http://npm.im/@isaacs/ttlcache).
|
||||||
|
|
||||||
|
## Storing Undefined Values
|
||||||
|
|
||||||
|
This cache never stores undefined values, as `undefined` is used
|
||||||
|
internally in a few places to indicate that a key is not in the
|
||||||
|
cache.
|
||||||
|
|
||||||
|
You may call `cache.set(key, undefined)`, but this is just
|
||||||
|
an alias for `cache.delete(key)`. Note that this has the effect
|
||||||
|
that `cache.has(key)` will return _false_ after setting it to
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
```js
|
||||||
|
cache.set(myKey, undefined)
|
||||||
|
cache.has(myKey) // false!
|
||||||
|
```
|
||||||
|
|
||||||
|
If you need to track `undefined` values, and still note that the
|
||||||
|
key is in the cache, an easy workaround is to use a sigil object
|
||||||
|
of your own.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { LRUCache } from 'lru-cache'
|
||||||
|
const undefinedValue = Symbol('undefined')
|
||||||
|
const cache = new LRUCache(...)
|
||||||
|
const mySet = (key, value) =>
|
||||||
|
cache.set(key, value === undefined ? undefinedValue : value)
|
||||||
|
const myGet = (key, value) => {
|
||||||
|
const v = cache.get(key)
|
||||||
|
return v === undefinedValue ? undefined : v
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tracing and Observability
|
||||||
|
|
||||||
|
Most methods can accept a `status` option, which is an
|
||||||
|
[`LRUCache.Status`](https://isaacs.github.io/node-lru-cache/interfaces/LRUCache.LRUCache.Status.html)
|
||||||
|
object that will be decorated along the operation with
|
||||||
|
indications about what was done and why.
|
||||||
|
|
||||||
|
Additionally, this library is instrumented using the
|
||||||
|
[`node:diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html)
|
||||||
|
module on Node and other platforms that support it. In order to
|
||||||
|
get diagnostics metrics, listen on the
|
||||||
|
`channel('lru-cache:metrics')`. To get Tracing Channel traces,
|
||||||
|
subscribe to the `tracingChannel('lru-cache')`. The
|
||||||
|
[`LRUCache.Status`](https://isaacs.github.io/node-lru-cache/interfaces/LRUCache.LRUCache.Status.html)
|
||||||
|
objects will be provided as the message context to those channel
|
||||||
|
listeners.
|
||||||
|
|
||||||
|
For example, you could do the following to get comprehensive
|
||||||
|
information about every LRUCache instance in your application:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { tracingChannel, subscribe } from 'node:diagnostics_channel'
|
||||||
|
|
||||||
|
subscribe('lru-cache:metrics', (message, name) => {
|
||||||
|
// name will always be 'lru-cache:metrics'
|
||||||
|
// message will be the LRUCache.Status object for whatever
|
||||||
|
// synchronous operation was performed.
|
||||||
|
console.error('LRUCache Metrics', message)
|
||||||
|
})
|
||||||
|
|
||||||
|
tracingChannel('lru-cache').subscribe({
|
||||||
|
start: status => {
|
||||||
|
// a traced operation is starting
|
||||||
|
},
|
||||||
|
asyncStart: status => {
|
||||||
|
// an async traced operation is starting
|
||||||
|
},
|
||||||
|
asyncEnd: status => {
|
||||||
|
// an async traced operation is ending
|
||||||
|
}
|
||||||
|
error: status => {
|
||||||
|
// a traced operation failed
|
||||||
|
},
|
||||||
|
end: status => {
|
||||||
|
// a traced operation is complete
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The async `cache.fetch()` and `cache.forceFetch` methods are
|
||||||
|
covered by `tracingChannels`. All the other operations are
|
||||||
|
covered by the `lru-cache:metrics` channel, because they are
|
||||||
|
strictly synchronous, and thus don't have an asynchronous
|
||||||
|
lifecycle to track.
|
||||||
|
|
||||||
|
Note that using `status` objects or using
|
||||||
|
`node:diagnostics_channel` listeners _will_ impose a modest
|
||||||
|
performance penalty. Creating data objects is not ever free; do
|
||||||
|
not believe anyone who tells you otherwise. But it is as small as
|
||||||
|
possible.
|
||||||
|
|
||||||
|
### Platform Compatibility Caveat
|
||||||
|
|
||||||
|
Not all platforms support the `node:diagnostics_channel` module.
|
||||||
|
Currently, this is only available in Node, Bun, and Deno, and
|
||||||
|
some edge computing platforms that provide a Node compatibility
|
||||||
|
layer.
|
||||||
|
|
||||||
|
To work around this, if you are loading in a non-Node
|
||||||
|
environment, the package.json exports will direct your module
|
||||||
|
loader to pull in a version that starts out with a dummy
|
||||||
|
implementation, then does a conditional dynamic `import` of the
|
||||||
|
`node:diagnostics_channel` module, and then swaps out those
|
||||||
|
dummy objects with the real thing if it succeeds. This means that
|
||||||
|
cache metrics and tracing channels started in the first load-time
|
||||||
|
tick of your application will _not_ be covered, except in
|
||||||
|
environments that load using the `require` import
|
||||||
|
condition, or both the `node` and `esm` import conditions
|
||||||
|
together.
|
||||||
|
|
||||||
|
Top-level await _could_ be used to remove this caveat, but that
|
||||||
|
feature is dead on arrival, unfortunately. See
|
||||||
|
[#397](https://github.com/isaacs/node-lru-cache/issues/397) and
|
||||||
|
[#398](https://github.com/isaacs/node-lru-cache/issues/398) for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
As of April 2026, version 11 of this library is one of the most
|
||||||
|
performant LRU cache implementations in JavaScript.
|
||||||
|
|
||||||
|
Benchmarks can be extremely difficult to get right. In
|
||||||
|
particular, the performance of set/get/delete operations on
|
||||||
|
objects will vary _wildly_ depending on the type of key used. V8
|
||||||
|
is highly optimized for objects with keys that are short strings,
|
||||||
|
especially integer numeric strings. Thus any benchmark which
|
||||||
|
tests _solely_ using numbers as keys will tend to find that an
|
||||||
|
object-based approach performs the best.
|
||||||
|
|
||||||
|
Note that coercing _anything_ to strings to use as object keys is
|
||||||
|
unsafe, unless you can be 100% certain that no other type of
|
||||||
|
value will be used. For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const myCache = {}
|
||||||
|
const set = (k, v) => (myCache[k] = v)
|
||||||
|
const get = k => myCache[k]
|
||||||
|
|
||||||
|
set({}, 'please hang onto this for me')
|
||||||
|
set('[object Object]', 'oopsie')
|
||||||
|
```
|
||||||
|
|
||||||
|
Also beware of "Just So" stories regarding performance. Garbage
|
||||||
|
collection of large (especially: deep) object graphs can be
|
||||||
|
incredibly costly, with several "tipping points" where it
|
||||||
|
increases exponentially. As a result, putting that off until
|
||||||
|
later can make it much worse, and less predictable. If a library
|
||||||
|
performs well, but only in a scenario where the object graph is
|
||||||
|
kept shallow, then that won't help you if you are using large
|
||||||
|
objects as keys.
|
||||||
|
|
||||||
|
In general, when attempting to use a library to improve
|
||||||
|
performance (such as a cache like this one), it's best to choose
|
||||||
|
an option that will perform well in the sorts of scenarios where
|
||||||
|
you'll actually use it.
|
||||||
|
|
||||||
|
This library is optimized for repeated gets and minimizing
|
||||||
|
eviction time, since that is the expected need of a LRU. Set
|
||||||
|
operations are somewhat slower on average than a few other
|
||||||
|
options, in part because of that optimization. It is assumed
|
||||||
|
that you'll be caching some costly operation, ideally as rarely
|
||||||
|
as possible, so optimizing set over get would be unwise.
|
||||||
|
|
||||||
|
If performance matters to you:
|
||||||
|
|
||||||
|
1. If it's at all possible to use small integer values as keys,
|
||||||
|
and you can guarantee that no other types of values will be
|
||||||
|
used as keys, then do that, and use a cache such as
|
||||||
|
[lru-fast](https://npmjs.com/package/lru-fast), or
|
||||||
|
[mnemonist's
|
||||||
|
LRUCache](https://yomguithereal.github.io/mnemonist/lru-cache)
|
||||||
|
which uses an Object as its data store.
|
||||||
|
|
||||||
|
2. Failing that, if you can use short non-numeric strings (ie,
|
||||||
|
less than 256 characters) as your keys, and you do not need
|
||||||
|
any of the other features of this library, use [mnemonist's
|
||||||
|
LRUCache](https://yomguithereal.github.io/mnemonist/lru-cache).
|
||||||
|
|
||||||
|
3. If the types of your keys will be anything else, especially
|
||||||
|
long strings, strings that look like floats, objects, or some
|
||||||
|
mix of types, or if you aren't sure, then this library will
|
||||||
|
work well for you.
|
||||||
|
|
||||||
|
If you do not need the features that this library provides
|
||||||
|
(like asynchronous fetching, a variety of TTL staleness
|
||||||
|
options, and so on), then [mnemonist's
|
||||||
|
LRUMap](https://yomguithereal.github.io/mnemonist/lru-map) is
|
||||||
|
also a very good option, and just slightly faster than this
|
||||||
|
module (since it does considerably less).
|
||||||
|
|
||||||
|
4. Do not use a `dispose` function, size tracking, or especially
|
||||||
|
ttl behavior or observability features, unless absolutely
|
||||||
|
needed. These features are convenient, and necessary in some
|
||||||
|
use cases, and every attempt has been made to make the
|
||||||
|
performance impact minimal, but it isn't nothing.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
When writing tests that involve TTL-related functionality, note
|
||||||
|
that this module creates an internal reference to the global
|
||||||
|
`performance` or `Date` objects at import time. If you import it
|
||||||
|
statically at the top level, those references cannot be mocked or
|
||||||
|
overridden in your test environment.
|
||||||
|
|
||||||
|
To avoid this, dynamically import the package within your tests
|
||||||
|
so that the references are captured after your mocks are applied.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ❌ Not recommended
|
||||||
|
import { LRUCache } from 'lru-cache'
|
||||||
|
// mocking timers, e.g. jest.useFakeTimers()
|
||||||
|
|
||||||
|
// ✅ Recommended for TTL tests
|
||||||
|
// mocking timers, e.g. jest.useFakeTimers()
|
||||||
|
const { LRUCache } = await import('lru-cache')
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures that your mocked timers or time sources are
|
||||||
|
respected when testing TTL behavior.
|
||||||
|
|
||||||
|
Additionally, you can pass in a `perf` option when creating your
|
||||||
|
LRUCache instance. This option accepts any object with a `now`
|
||||||
|
method that returns a number.
|
||||||
|
|
||||||
|
For example, this would be a very bare-bones time-mocking system
|
||||||
|
you could use in your tests, without any particular test
|
||||||
|
framework:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { LRUCache } from 'lru-cache'
|
||||||
|
|
||||||
|
let myClockTime = 0
|
||||||
|
|
||||||
|
const cache = new LRUCache<string>({
|
||||||
|
max: 10,
|
||||||
|
ttl: 1000,
|
||||||
|
perf: {
|
||||||
|
now: () => myClockTime,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// run tests, updating myClockTime as needed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Breaking Changes in Version 7
|
||||||
|
|
||||||
|
This library changed to a different algorithm and internal data
|
||||||
|
structure in version 7, yielding significantly better
|
||||||
|
performance, albeit with some subtle changes as a result.
|
||||||
|
|
||||||
|
If you were relying on the internals of LRUCache in version 6 or
|
||||||
|
before, it probably will not work in version 7 and above.
|
||||||
|
|
||||||
|
## Breaking Changes in Version 8
|
||||||
|
|
||||||
|
- The `fetchContext` option was renamed to `context`, and may no
|
||||||
|
longer be set on the cache instance itself.
|
||||||
|
- Rewritten in TypeScript, so pretty much all the types moved
|
||||||
|
around a lot.
|
||||||
|
- The AbortController/AbortSignal polyfill was removed. For this
|
||||||
|
reason, **Node version 16.14.0 or higher is now required**.
|
||||||
|
- Internal properties were moved to actual private class
|
||||||
|
properties.
|
||||||
|
- Keys and values must not be `null` or `undefined`.
|
||||||
|
- Minified export available at `'lru-cache/min'`, for both CJS
|
||||||
|
and MJS builds.
|
||||||
|
|
||||||
|
## Breaking Changes in Version 9
|
||||||
|
|
||||||
|
- Named export only, no default export.
|
||||||
|
- AbortController polyfill returned, albeit with a warning when
|
||||||
|
used.
|
||||||
|
|
||||||
|
## Breaking Changes in Version 10
|
||||||
|
|
||||||
|
- `cache.fetch()` return type is now `Promise<V | undefined>`
|
||||||
|
instead of `Promise<V | void>`. This is an irrelevant change
|
||||||
|
practically speaking, but can require changes for TypeScript
|
||||||
|
users.
|
||||||
|
|
||||||
|
For more info, see the [change log](CHANGELOG.md).
|
||||||
154
patcher/vendor/asar/node_modules/lru-cache/package.json
generated
vendored
Normal file
154
patcher/vendor/asar/node_modules/lru-cache/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
{
|
||||||
|
"name": "lru-cache",
|
||||||
|
"description": "A cache object that deletes the least-recently-used items.",
|
||||||
|
"version": "11.3.6",
|
||||||
|
"author": "Isaac Z. Schlueter <i@izs.me>",
|
||||||
|
"keywords": [
|
||||||
|
"mru",
|
||||||
|
"lru",
|
||||||
|
"cache"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run prepare",
|
||||||
|
"prepare": "tshy && bash scripts/build.sh",
|
||||||
|
"pretest": "npm run prepare",
|
||||||
|
"presnap": "npm run prepare",
|
||||||
|
"test": "tap",
|
||||||
|
"snap": "tap",
|
||||||
|
"preversion": "npm test",
|
||||||
|
"postversion": "npm publish",
|
||||||
|
"prepublishOnly": "git push origin --follow-tags",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"typedoc": "typedoc --tsconfig ./.tshy/esm.json ./src/*.ts",
|
||||||
|
"benchmark-results-typedoc": "bash scripts/benchmark-results-typedoc.sh",
|
||||||
|
"prebenchmark": "npm run prepare",
|
||||||
|
"benchmark": "make -C benchmark",
|
||||||
|
"preprofile": "npm run prepare",
|
||||||
|
"profile": "make -C benchmark profile",
|
||||||
|
"lint": "oxlint --fix src test",
|
||||||
|
"postsnap": "npm run lint",
|
||||||
|
"postlint": "npm run format"
|
||||||
|
},
|
||||||
|
"main": "./dist/commonjs/index.min.js",
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"tshy": {
|
||||||
|
"esmDialects": [
|
||||||
|
"browser",
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"commonjsDialects": [
|
||||||
|
"browser",
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
"./raw": "./src/index.ts",
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"browser": {
|
||||||
|
"types": "./dist/esm/browser/index.d.ts",
|
||||||
|
"default": "./dist/esm/browser/index.min.js"
|
||||||
|
},
|
||||||
|
"node": {
|
||||||
|
"types": "./dist/esm/node/index.d.ts",
|
||||||
|
"default": "./dist/esm/node/index.min.js"
|
||||||
|
},
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.min.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"browser": {
|
||||||
|
"types": "./dist/commonjs/browser/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/browser/index.min.js"
|
||||||
|
},
|
||||||
|
"node": {
|
||||||
|
"types": "./dist/commonjs/node/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/node/index.min.js"
|
||||||
|
},
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/index.min.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selfLink": false
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+ssh://git@github.com/isaacs/node-lru-cache.git"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"benchmark": "^2.1.4",
|
||||||
|
"esbuild": "^0.25.9",
|
||||||
|
"marked": "^4.2.12",
|
||||||
|
"mkdirp": "^3.0.1",
|
||||||
|
"oxlint": "^1.58.0",
|
||||||
|
"oxlint-tsgolint": "^0.19.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"tap": "^21.7.2",
|
||||||
|
"tshy": "^4.1.1",
|
||||||
|
"typedoc": "^0.28.18"
|
||||||
|
},
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./raw": {
|
||||||
|
"import": {
|
||||||
|
"browser": {
|
||||||
|
"types": "./dist/esm/browser/index.d.ts",
|
||||||
|
"default": "./dist/esm/browser/index.js"
|
||||||
|
},
|
||||||
|
"node": {
|
||||||
|
"types": "./dist/esm/node/index.d.ts",
|
||||||
|
"default": "./dist/esm/node/index.js"
|
||||||
|
},
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"browser": {
|
||||||
|
"types": "./dist/commonjs/browser/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/browser/index.js"
|
||||||
|
},
|
||||||
|
"node": {
|
||||||
|
"types": "./dist/commonjs/node/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/node/index.js"
|
||||||
|
},
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"browser": {
|
||||||
|
"types": "./dist/esm/browser/index.d.ts",
|
||||||
|
"default": "./dist/esm/browser/index.min.js"
|
||||||
|
},
|
||||||
|
"node": {
|
||||||
|
"types": "./dist/esm/node/index.d.ts",
|
||||||
|
"default": "./dist/esm/node/index.min.js"
|
||||||
|
},
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.min.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"browser": {
|
||||||
|
"types": "./dist/commonjs/browser/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/browser/index.min.js"
|
||||||
|
},
|
||||||
|
"node": {
|
||||||
|
"types": "./dist/commonjs/node/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/node/index.min.js"
|
||||||
|
},
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/index.min.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"module": "./dist/esm/index.min.js"
|
||||||
|
}
|
||||||
55
patcher/vendor/asar/node_modules/minimatch/LICENSE.md
generated
vendored
Normal file
55
patcher/vendor/asar/node_modules/minimatch/LICENSE.md
generated
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Blue Oak Model License
|
||||||
|
|
||||||
|
Version 1.0.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This license gives everyone as much permission to work with
|
||||||
|
this software as possible, while protecting contributors
|
||||||
|
from liability.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
In order to receive this license, you must agree to its
|
||||||
|
rules. The rules of this license are both obligations
|
||||||
|
under that agreement and conditions to your license.
|
||||||
|
You must not do anything with this software that triggers
|
||||||
|
a rule that you cannot or will not follow.
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
Each contributor licenses you to do everything with this
|
||||||
|
software that would otherwise infringe that contributor's
|
||||||
|
copyright in it.
|
||||||
|
|
||||||
|
## Notices
|
||||||
|
|
||||||
|
You must ensure that everyone who gets a copy of
|
||||||
|
any part of this software from you, with or without
|
||||||
|
changes, also gets the text of this license or a link to
|
||||||
|
<https://blueoakcouncil.org/license/1.0.0>.
|
||||||
|
|
||||||
|
## Excuse
|
||||||
|
|
||||||
|
If anyone notifies you in writing that you have not
|
||||||
|
complied with [Notices](#notices), you can keep your
|
||||||
|
license by taking all practical steps to comply within 30
|
||||||
|
days after the notice. If you do not do so, your license
|
||||||
|
ends immediately.
|
||||||
|
|
||||||
|
## Patent
|
||||||
|
|
||||||
|
Each contributor licenses you to do everything with this
|
||||||
|
software that would otherwise infringe any patent claims
|
||||||
|
they can license or become able to license.
|
||||||
|
|
||||||
|
## Reliability
|
||||||
|
|
||||||
|
No contributor can revoke this license.
|
||||||
|
|
||||||
|
## No Liability
|
||||||
|
|
||||||
|
**_As far as the law allows, this software comes as is,
|
||||||
|
without any warranty or condition, and no contributor
|
||||||
|
will be liable to anyone for any damages related to this
|
||||||
|
software or this license, under any kind of legal claim._**
|
||||||
528
patcher/vendor/asar/node_modules/minimatch/README.md
generated
vendored
Normal file
528
patcher/vendor/asar/node_modules/minimatch/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,528 @@
|
||||||
|
# minimatch
|
||||||
|
|
||||||
|
A minimal matching utility.
|
||||||
|
|
||||||
|
This is the matching library used internally by npm.
|
||||||
|
|
||||||
|
It works by converting glob expressions into JavaScript `RegExp`
|
||||||
|
objects.
|
||||||
|
|
||||||
|
## Important Security Consideration!
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This library uses JavaScript regular expressions. Please read
|
||||||
|
> the following warning carefully, and be thoughtful about what
|
||||||
|
> you provide to this library in production systems.
|
||||||
|
|
||||||
|
_Any_ library in JavaScript that deals with matching string
|
||||||
|
patterns using regular expressions will be subject to
|
||||||
|
[ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)
|
||||||
|
if the pattern is generated using untrusted input.
|
||||||
|
|
||||||
|
Efforts have been made to mitigate risk as much as is feasible in
|
||||||
|
such a library, providing maximum recursion depths and so forth,
|
||||||
|
but these measures can only ultimately protect against accidents,
|
||||||
|
not malice. A dedicated attacker can _always_ find patterns that
|
||||||
|
cannot be defended against by a bash-compatible glob pattern
|
||||||
|
matching system that uses JavaScript regular expressions.
|
||||||
|
|
||||||
|
To be extremely clear:
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> **If you create a system where you take user input, and use
|
||||||
|
> that input as the source of a Regular Expression pattern, in
|
||||||
|
> this or any extant glob matcher in JavaScript, you will be
|
||||||
|
> pwned.**
|
||||||
|
|
||||||
|
A future version of this library _may_ use a different matching
|
||||||
|
algorithm which does not exhibit backtracking problems. If and
|
||||||
|
when that happens, it will likely be a sweeping change, and those
|
||||||
|
improvements will **not** be backported to legacy versions.
|
||||||
|
|
||||||
|
In the near term, it is not reasonable to continue to play
|
||||||
|
whack-a-mole with security advisories, and so any future ReDoS
|
||||||
|
reports will be considered "working as intended", and resolved
|
||||||
|
entirely by this warning.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
// hybrid module, load with require() or import
|
||||||
|
import { minimatch } from 'minimatch'
|
||||||
|
// or:
|
||||||
|
const { minimatch } = require('minimatch')
|
||||||
|
|
||||||
|
minimatch('bar.foo', '*.foo') // true!
|
||||||
|
minimatch('bar.foo', '*.bar') // false!
|
||||||
|
minimatch('bar.foo', '*.+(bar|foo)', { debug: true }) // true, and noisy!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Supports these glob features:
|
||||||
|
|
||||||
|
- Brace Expansion
|
||||||
|
- Extended glob matching
|
||||||
|
- "Globstar" `**` matching
|
||||||
|
- [Posix character
|
||||||
|
classes](https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html),
|
||||||
|
like `[[:alpha:]]`, supporting the full range of Unicode
|
||||||
|
characters. For example, `[[:alpha:]]` will match against
|
||||||
|
`'é'`, though `[a-zA-Z]` will not. Collating symbol and set
|
||||||
|
matching is not supported, so `[[=e=]]` will _not_ match `'é'`
|
||||||
|
and `[[.ch.]]` will not match `'ch'` in locales where `ch` is
|
||||||
|
considered a single character.
|
||||||
|
|
||||||
|
See:
|
||||||
|
|
||||||
|
- `man sh`
|
||||||
|
- `man bash` [Pattern
|
||||||
|
Matching](https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html)
|
||||||
|
- `man 3 fnmatch`
|
||||||
|
- `man 5 gitignore`
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
**Please only use forward-slashes in glob expressions.**
|
||||||
|
|
||||||
|
Though windows uses either `/` or `\` as its path separator, only `/`
|
||||||
|
characters are used by this glob implementation. You must use
|
||||||
|
forward-slashes **only** in glob expressions. Back-slashes in patterns
|
||||||
|
will always be interpreted as escape characters, not path separators.
|
||||||
|
|
||||||
|
Note that `\` or `/` _will_ be interpreted as path separators in paths on
|
||||||
|
Windows, and will match against `/` in glob expressions.
|
||||||
|
|
||||||
|
So just always use `/` in patterns.
|
||||||
|
|
||||||
|
### UNC Paths
|
||||||
|
|
||||||
|
On Windows, UNC paths like `//?/c:/...` or
|
||||||
|
`//ComputerName/Share/...` are handled specially.
|
||||||
|
|
||||||
|
- Patterns starting with a double-slash followed by some
|
||||||
|
non-slash characters will preserve their double-slash. As a
|
||||||
|
result, a pattern like `//*` will match `//x`, but not `/x`.
|
||||||
|
- Patterns staring with `//?/<drive letter>:` will _not_ treat
|
||||||
|
the `?` as a wildcard character. Instead, it will be treated
|
||||||
|
as a normal string.
|
||||||
|
- Patterns starting with `//?/<drive letter>:/...` will match
|
||||||
|
file paths starting with `<drive letter>:/...`, and vice versa,
|
||||||
|
as if the `//?/` was not present. This behavior only is
|
||||||
|
present when the drive letters are a case-insensitive match to
|
||||||
|
one another. The remaining portions of the path/pattern are
|
||||||
|
compared case sensitively, unless `nocase:true` is set.
|
||||||
|
|
||||||
|
Note that specifying a UNC path using `\` characters as path
|
||||||
|
separators is always allowed in the file path argument, but only
|
||||||
|
allowed in the pattern argument when `windowsPathsNoEscape: true`
|
||||||
|
is set in the options.
|
||||||
|
|
||||||
|
## Minimatch Class
|
||||||
|
|
||||||
|
Create a minimatch object by instantiating the `minimatch.Minimatch` class.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var Minimatch = require('minimatch').Minimatch
|
||||||
|
var mm = new Minimatch(pattern, options)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
- `pattern` The original pattern the minimatch object represents.
|
||||||
|
- `options` The options supplied to the constructor.
|
||||||
|
- `set` A 2-dimensional array of regexp or string expressions.
|
||||||
|
Each row in the
|
||||||
|
array corresponds to a brace-expanded pattern. Each item in the row
|
||||||
|
corresponds to a single path-part. For example, the pattern
|
||||||
|
`{a,b/c}/d` would expand to a set of patterns like:
|
||||||
|
|
||||||
|
[ [ a, d ]
|
||||||
|
, [ b, c, d ] ]
|
||||||
|
|
||||||
|
If a portion of the pattern doesn't have any "magic" in it
|
||||||
|
(that is, it's something like `"foo"` rather than `fo*o?`), then it
|
||||||
|
will be left as a string rather than converted to a regular
|
||||||
|
expression.
|
||||||
|
|
||||||
|
- `regexp` Created by the `makeRe` method. A single regular expression
|
||||||
|
expressing the entire pattern. This is useful in cases where you wish
|
||||||
|
to use the pattern somewhat like `fnmatch(3)` with `FNM_PATH` enabled.
|
||||||
|
- `negate` True if the pattern is negated.
|
||||||
|
- `comment` True if the pattern is a comment.
|
||||||
|
- `empty` True if the pattern is `""`.
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- `makeRe()` Generate the `regexp` member if necessary, and return it.
|
||||||
|
Will return `false` if the pattern is invalid.
|
||||||
|
- `match(fname)` Return true if the filename matches the pattern, or
|
||||||
|
false otherwise.
|
||||||
|
- `matchOne(fileArray, patternArray, partial)` Take a `/`-split
|
||||||
|
filename, and match it against a single row in the `regExpSet`. This
|
||||||
|
method is mainly for internal use, but is exposed so that it can be
|
||||||
|
used by a glob-walker that needs to avoid excessive filesystem calls.
|
||||||
|
- `hasMagic()` Returns true if the parsed pattern contains any
|
||||||
|
magic characters. Returns false if all comparator parts are
|
||||||
|
string literals. If the `magicalBraces` option is set on the
|
||||||
|
constructor, then it will consider brace expansions which are
|
||||||
|
not otherwise magical to be magic. If not set, then a pattern
|
||||||
|
like `a{b,c}d` will return `false`, because neither `abd` nor
|
||||||
|
`acd` contain any special glob characters.
|
||||||
|
|
||||||
|
This does **not** mean that the pattern string can be used as a
|
||||||
|
literal filename, as it may contain magic glob characters that
|
||||||
|
are escaped. For example, the pattern `\\*` or `[*]` would not
|
||||||
|
be considered to have magic, as the matching portion parses to
|
||||||
|
the literal string `'*'` and would match a path named `'*'`,
|
||||||
|
not `'\\*'` or `'[*]'`. The `minimatch.unescape()` method may
|
||||||
|
be used to remove escape characters.
|
||||||
|
|
||||||
|
All other methods are internal, and will be called as necessary.
|
||||||
|
|
||||||
|
### minimatch(path, pattern, options)
|
||||||
|
|
||||||
|
Main export. Tests a path against the pattern using the options.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var isJS = minimatch(file, '*.js', { matchBase: true })
|
||||||
|
```
|
||||||
|
|
||||||
|
### minimatch.filter(pattern, options)
|
||||||
|
|
||||||
|
Returns a function that tests its
|
||||||
|
supplied argument, suitable for use with `Array.filter`. Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var javascripts = fileList.filter(
|
||||||
|
minimatch.filter('*.js', { matchBase: true }),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### minimatch.escape(pattern, options = {})
|
||||||
|
|
||||||
|
Escape all magic characters in a glob pattern, so that it will
|
||||||
|
only ever match literal strings.
|
||||||
|
|
||||||
|
If the `windowsPathsNoEscape` option is used, then characters are
|
||||||
|
escaped by wrapping in `[]`, because a magic character wrapped in
|
||||||
|
a character class can only be satisfied by that exact character.
|
||||||
|
|
||||||
|
Slashes (and backslashes in `windowsPathsNoEscape` mode) cannot
|
||||||
|
be escaped or unescaped.
|
||||||
|
|
||||||
|
### minimatch.unescape(pattern, options = {})
|
||||||
|
|
||||||
|
Un-escape a glob string that may contain some escaped characters.
|
||||||
|
|
||||||
|
If the `windowsPathsNoEscape` option is used, then square-brace
|
||||||
|
escapes are removed, but not backslash escapes. For example, it
|
||||||
|
will turn the string `'[*]'` into `*`, but it will not turn
|
||||||
|
`'\\*'` into `'*'`, because `\` is a path separator in
|
||||||
|
`windowsPathsNoEscape` mode.
|
||||||
|
|
||||||
|
When `windowsPathsNoEscape` is not set, then both brace escapes
|
||||||
|
and backslash escapes are removed.
|
||||||
|
|
||||||
|
Slashes (and backslashes in `windowsPathsNoEscape` mode) cannot
|
||||||
|
be escaped or unescaped.
|
||||||
|
|
||||||
|
### minimatch.match(list, pattern, options)
|
||||||
|
|
||||||
|
Match against the list of
|
||||||
|
files, in the style of fnmatch or glob. If nothing is matched, and
|
||||||
|
options.nonull is set, then return a list containing the pattern itself.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var javascripts = minimatch.match(fileList, '*.js', { matchBase: true })
|
||||||
|
```
|
||||||
|
|
||||||
|
### minimatch.makeRe(pattern, options)
|
||||||
|
|
||||||
|
Make a regular expression object from the pattern.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
All options are `false` by default.
|
||||||
|
|
||||||
|
### debug
|
||||||
|
|
||||||
|
Dump a ton of stuff to stderr.
|
||||||
|
|
||||||
|
### nobrace
|
||||||
|
|
||||||
|
Do not expand `{a,b}` and `{1..3}` brace sets.
|
||||||
|
|
||||||
|
### noglobstar
|
||||||
|
|
||||||
|
Disable `**` matching against multiple folder names.
|
||||||
|
|
||||||
|
### dot
|
||||||
|
|
||||||
|
Allow patterns to match filenames starting with a period, even if
|
||||||
|
the pattern does not explicitly have a period in that spot.
|
||||||
|
|
||||||
|
Note that by default, `a/**/b` will **not** match `a/.d/b`, unless `dot`
|
||||||
|
is set.
|
||||||
|
|
||||||
|
### noext
|
||||||
|
|
||||||
|
Disable "extglob" style patterns like `+(a|b)`.
|
||||||
|
|
||||||
|
### nocase
|
||||||
|
|
||||||
|
Perform a case-insensitive match.
|
||||||
|
|
||||||
|
### nocaseMagicOnly
|
||||||
|
|
||||||
|
When used with `{nocase: true}`, create regular expressions that
|
||||||
|
are case-insensitive, but leave string match portions untouched.
|
||||||
|
Has no effect when used without `{nocase: true}`.
|
||||||
|
|
||||||
|
Useful when some other form of case-insensitive matching is used,
|
||||||
|
or if the original string representation is useful in some other
|
||||||
|
way.
|
||||||
|
|
||||||
|
### nonull
|
||||||
|
|
||||||
|
When a match is not found by `minimatch.match`, return a list containing
|
||||||
|
the pattern itself if this option is set. When not set, an empty list
|
||||||
|
is returned if there are no matches.
|
||||||
|
|
||||||
|
### magicalBraces
|
||||||
|
|
||||||
|
This only affects the results of the `Minimatch.hasMagic` method.
|
||||||
|
|
||||||
|
If the pattern contains brace expansions, such as `a{b,c}d`, but
|
||||||
|
no other magic characters, then the `Minimatch.hasMagic()` method
|
||||||
|
will return `false` by default. When this option set, it will
|
||||||
|
return `true` for brace expansion as well as other magic glob
|
||||||
|
characters.
|
||||||
|
|
||||||
|
### matchBase
|
||||||
|
|
||||||
|
If set, then patterns without slashes will be matched
|
||||||
|
against the basename of the path if it contains slashes. For example,
|
||||||
|
`a?b` would match the path `/xyz/123/acb`, but not `/xyz/acb/123`.
|
||||||
|
|
||||||
|
### nocomment
|
||||||
|
|
||||||
|
Suppress the behavior of treating `#` at the start of a pattern as a
|
||||||
|
comment.
|
||||||
|
|
||||||
|
### nonegate
|
||||||
|
|
||||||
|
Suppress the behavior of treating a leading `!` character as negation.
|
||||||
|
|
||||||
|
### flipNegate
|
||||||
|
|
||||||
|
Returns from negate expressions the same as if they were not negated.
|
||||||
|
(Ie, true on a hit, false on a miss.)
|
||||||
|
|
||||||
|
### partial
|
||||||
|
|
||||||
|
Compare a partial path to a pattern. As long as the parts of the path that
|
||||||
|
are present are not contradicted by the pattern, it will be treated as a
|
||||||
|
match. This is useful in applications where you're walking through a
|
||||||
|
folder structure, and don't yet have the full path, but want to ensure that
|
||||||
|
you do not walk down paths that can never be a match.
|
||||||
|
|
||||||
|
For example,
|
||||||
|
|
||||||
|
```js
|
||||||
|
minimatch('/a/b', '/a/*/c/d', { partial: true }) // true, might be /a/b/c/d
|
||||||
|
minimatch('/a/b', '/**/d', { partial: true }) // true, might be /a/b/.../d
|
||||||
|
minimatch('/x/y/z', '/a/**/z', { partial: true }) // false, because x !== a
|
||||||
|
```
|
||||||
|
|
||||||
|
### windowsPathsNoEscape
|
||||||
|
|
||||||
|
Use `\\` as a path separator _only_, and _never_ as an escape
|
||||||
|
character. If set, all `\\` characters are replaced with `/` in
|
||||||
|
the pattern. Note that this makes it **impossible** to match
|
||||||
|
against paths containing literal glob pattern characters, but
|
||||||
|
allows matching with patterns constructed using `path.join()` and
|
||||||
|
`path.resolve()` on Windows platforms, mimicking the (buggy!)
|
||||||
|
behavior of earlier versions on Windows. Please use with
|
||||||
|
caution, and be mindful of [the caveat about Windows
|
||||||
|
paths](#windows).
|
||||||
|
|
||||||
|
For legacy reasons, this is also set if
|
||||||
|
`options.allowWindowsEscape` is set to the exact value `false`.
|
||||||
|
|
||||||
|
### windowsNoMagicRoot
|
||||||
|
|
||||||
|
When a pattern starts with a UNC path or drive letter, and in
|
||||||
|
`nocase:true` mode, do not convert the root portions of the
|
||||||
|
pattern into a case-insensitive regular expression, and instead
|
||||||
|
leave them as strings.
|
||||||
|
|
||||||
|
This is the default when the platform is `win32` and
|
||||||
|
`nocase:true` is set.
|
||||||
|
|
||||||
|
### preserveMultipleSlashes
|
||||||
|
|
||||||
|
By default, multiple `/` characters (other than the leading `//`
|
||||||
|
in a UNC path, see "UNC Paths" above) are treated as a single
|
||||||
|
`/`.
|
||||||
|
|
||||||
|
That is, a pattern like `a///b` will match the file path `a/b`.
|
||||||
|
|
||||||
|
Set `preserveMultipleSlashes: true` to suppress this behavior.
|
||||||
|
|
||||||
|
### optimizationLevel
|
||||||
|
|
||||||
|
A number indicating the level of optimization that should be done
|
||||||
|
to the pattern prior to parsing and using it for matches.
|
||||||
|
|
||||||
|
Globstar parts `**` are always converted to `*` when `noglobstar`
|
||||||
|
is set, and multiple adjacent `**` parts are converted into a
|
||||||
|
single `**` (ie, `a/**/**/b` will be treated as `a/**/b`, as this
|
||||||
|
is equivalent in all cases).
|
||||||
|
|
||||||
|
- `0` - Make no further changes. In this mode, `.` and `..` are
|
||||||
|
maintained in the pattern, meaning that they must also appear
|
||||||
|
in the same position in the test path string. Eg, a pattern
|
||||||
|
like `a/*/../c` will match the string `a/b/../c` but not the
|
||||||
|
string `a/c`.
|
||||||
|
- `1` - (default) Remove cases where a double-dot `..` follows a
|
||||||
|
pattern portion that is not `**`, `.`, `..`, or empty `''`. For
|
||||||
|
example, the pattern `./a/b/../*` is converted to `./a/*`, and
|
||||||
|
so it will match the path string `./a/c`, but not the path
|
||||||
|
string `./a/b/../c`. Dots and empty path portions in the
|
||||||
|
pattern are preserved.
|
||||||
|
- `2` (or higher) - Much more aggressive optimizations, suitable
|
||||||
|
for use with file-walking cases:
|
||||||
|
- Remove cases where a double-dot `..` follows a pattern
|
||||||
|
portion that is not `**`, `.`, or empty `''`. Remove empty
|
||||||
|
and `.` portions of the pattern, where safe to do so (ie,
|
||||||
|
anywhere other than the last position, the first position, or
|
||||||
|
the second position in a pattern starting with `/`, as this
|
||||||
|
may indicate a UNC path on Windows).
|
||||||
|
- Convert patterns containing `<pre>/**/../<p>/<rest>` into the
|
||||||
|
equivalent `<pre>/{..,**}/<p>/<rest>`, where `<p>` is a
|
||||||
|
a pattern portion other than `.`, `..`, `**`, or empty
|
||||||
|
`''`.
|
||||||
|
- Dedupe patterns where a `**` portion is present in one and
|
||||||
|
omitted in another, and it is not the final path portion, and
|
||||||
|
they are otherwise equivalent. So `{a/**/b,a/b}` becomes
|
||||||
|
`a/**/b`, because `**` matches against an empty path portion.
|
||||||
|
- Dedupe patterns where a `*` portion is present in one, and a
|
||||||
|
non-dot pattern other than `**`, `.`, `..`, or `''` is in the
|
||||||
|
same position in the other. So `a/{*,x}/b` becomes `a/*/b`,
|
||||||
|
because `*` can match against `x`.
|
||||||
|
|
||||||
|
While these optimizations improve the performance of
|
||||||
|
file-walking use cases such as [glob](http://npm.im/glob) (ie,
|
||||||
|
the reason this module exists), there are cases where it will
|
||||||
|
fail to match a literal string that would have been matched in
|
||||||
|
optimization level 1 or 0.
|
||||||
|
|
||||||
|
Specifically, while the `Minimatch.match()` method will
|
||||||
|
optimize the file path string in the same ways, resulting in
|
||||||
|
the same matches, it will fail when tested with the regular
|
||||||
|
expression provided by `Minimatch.makeRe()`, unless the path
|
||||||
|
string is first processed with
|
||||||
|
`minimatch.levelTwoFileOptimize()` or similar.
|
||||||
|
|
||||||
|
### platform
|
||||||
|
|
||||||
|
When set to `win32`, this will trigger all windows-specific
|
||||||
|
behaviors (special handling for UNC paths, and treating `\` as
|
||||||
|
separators in file paths for comparison.)
|
||||||
|
|
||||||
|
Defaults to the value of `process.platform`.
|
||||||
|
|
||||||
|
### maxGlobstarRecursion
|
||||||
|
|
||||||
|
Max number of non-adjacent `**` patterns to recursively walk
|
||||||
|
down.
|
||||||
|
|
||||||
|
The default of `200` is almost certainly high enough for most
|
||||||
|
purposes, and can handle absurdly excessive patterns.
|
||||||
|
|
||||||
|
If the limit is exceeded (which would require very excessively
|
||||||
|
long patterns and paths containing lots of `**` patterns!), then
|
||||||
|
it is treated as non-matching, even if the path would normally
|
||||||
|
match the pattern provided.
|
||||||
|
|
||||||
|
That is, this is an intentional false negative, deemed an
|
||||||
|
acceptable break in correctness for security and performance.
|
||||||
|
|
||||||
|
### maxExtglobRecursion
|
||||||
|
|
||||||
|
Max depth to traverse for nested extglobs like `*(a|b|c)`
|
||||||
|
|
||||||
|
Default is 2, which is quite low, but any higher value swiftly
|
||||||
|
results in punishing performance impacts. Note that this is _not_
|
||||||
|
relevant when the globstar types can be safely coalesced into a
|
||||||
|
single set.
|
||||||
|
|
||||||
|
For example, `*(a|@(b|c)|d)` would be flattened into
|
||||||
|
`*(a|b|c|d)`. Thus, many common extglobs will retain good
|
||||||
|
performance and never hit this limit, even if they are
|
||||||
|
excessively deep and complicated.
|
||||||
|
|
||||||
|
If the limit is hit, then the extglob characters are simply not
|
||||||
|
parsed, and the pattern effectively switches into `noextglob:
|
||||||
|
true` mode for the contents of that nested sub-pattern. This will
|
||||||
|
typically _not_ result in a match, but is considered a valid
|
||||||
|
trade-off for security and performance.
|
||||||
|
|
||||||
|
## Comparisons to other fnmatch/glob implementations
|
||||||
|
|
||||||
|
While strict compliance with the existing standards is a
|
||||||
|
worthwhile goal, some discrepancies exist between minimatch and
|
||||||
|
other implementations. Some are intentional, and some are
|
||||||
|
unavoidable.
|
||||||
|
|
||||||
|
If the pattern starts with a `!` character, then it is negated. Set the
|
||||||
|
`nonegate` flag to suppress this behavior, and treat leading `!`
|
||||||
|
characters normally. This is perhaps relevant if you wish to start the
|
||||||
|
pattern with a negative extglob pattern like `!(a|B)`. Multiple `!`
|
||||||
|
characters at the start of a pattern will negate the pattern multiple
|
||||||
|
times.
|
||||||
|
|
||||||
|
If a pattern starts with `#`, then it is treated as a comment, and
|
||||||
|
will not match anything. Use `\#` to match a literal `#` at the
|
||||||
|
start of a line, or set the `nocomment` flag to suppress this behavior.
|
||||||
|
|
||||||
|
The double-star character `**` is supported by default, unless the
|
||||||
|
`noglobstar` flag is set. This is supported in the manner of bsdglob
|
||||||
|
and bash 4.1, where `**` only has special significance if it is the only
|
||||||
|
thing in a path part. That is, `a/**/b` will match `a/x/y/b`, but
|
||||||
|
`a/**b` will not.
|
||||||
|
|
||||||
|
If an escaped pattern has no matches, and the `nonull` flag is set,
|
||||||
|
then minimatch.match returns the pattern as-provided, rather than
|
||||||
|
interpreting the character escapes. For example,
|
||||||
|
`minimatch.match([], "\\*a\\?")` will return `"\\*a\\?"` rather than
|
||||||
|
`"*a?"`. This is akin to setting the `nullglob` option in bash, except
|
||||||
|
that it does not resolve escaped pattern characters.
|
||||||
|
|
||||||
|
If brace expansion is not disabled, then it is performed before any
|
||||||
|
other interpretation of the glob pattern. Thus, a pattern like
|
||||||
|
`+(a|{b),c)}`, which would not be valid in bash or zsh, is expanded
|
||||||
|
**first** into the set of `+(a|b)` and `+(a|c)`, and those patterns are
|
||||||
|
checked for validity. Since those two are valid, matching proceeds.
|
||||||
|
|
||||||
|
Negated extglob patterns are handled as closely as possible to
|
||||||
|
Bash semantics, but there are some cases with negative extglobs
|
||||||
|
which are exceedingly difficult to express in a JavaScript
|
||||||
|
regular expression. In particular the negated pattern
|
||||||
|
`<start>!(<pattern>*|)*` will in bash match anything that does
|
||||||
|
not start with `<start><pattern>`. However,
|
||||||
|
`<start>!(<pattern>*)*` _will_ match paths starting with
|
||||||
|
`<start><pattern>`, because the empty string can match against
|
||||||
|
the negated portion. In this library, `<start>!(<pattern>*|)*`
|
||||||
|
will _not_ match any pattern starting with `<start>`, due to a
|
||||||
|
difference in precisely which patterns are considered "greedy" in
|
||||||
|
Regular Expressions vs bash path expansion. This may be fixable,
|
||||||
|
but not without incurring some complexity and performance costs,
|
||||||
|
and the trade-off seems to not be worth pursuing.
|
||||||
|
|
||||||
|
Note that `fnmatch(3)` in libc is an extremely naive string comparison
|
||||||
|
matcher, which does not do anything special for slashes. This library is
|
||||||
|
designed to be used in glob searching and file walkers, and so it does do
|
||||||
|
special things with `/`. Thus, `foo*` will not match `foo/bar` in this
|
||||||
|
library, even though it would in `fnmatch(3)`.
|
||||||
73
patcher/vendor/asar/node_modules/minimatch/package.json
generated
vendored
Normal file
73
patcher/vendor/asar/node_modules/minimatch/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
{
|
||||||
|
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
|
||||||
|
"name": "minimatch",
|
||||||
|
"description": "a glob matcher in javascript",
|
||||||
|
"version": "10.2.5",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@github.com:isaacs/minimatch"
|
||||||
|
},
|
||||||
|
"main": "./dist/commonjs/index.js",
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"preversion": "npm test",
|
||||||
|
"postversion": "npm publish",
|
||||||
|
"prepublishOnly": "git push origin --follow-tags",
|
||||||
|
"prepare": "tshy",
|
||||||
|
"pretest": "npm run prepare",
|
||||||
|
"presnap": "npm run prepare",
|
||||||
|
"test": "tap",
|
||||||
|
"snap": "tap",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"benchmark": "node benchmark/index.js",
|
||||||
|
"typedoc": "typedoc --tsconfig .tshy/esm.json ./src/*.ts",
|
||||||
|
"lint": "oxlint --fix src test",
|
||||||
|
"postsnap": "npm run lint",
|
||||||
|
"postlint": "npm run format"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.5.0",
|
||||||
|
"mkdirp": "^3.0.1",
|
||||||
|
"oxlint": "^1.57.0",
|
||||||
|
"oxlint-tsgolint": "^0.18.1",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"tap": "^21.6.2",
|
||||||
|
"tshy": "^4.0.0",
|
||||||
|
"typedoc": "^0.28.18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
},
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"tshy": {
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"selfLink": false
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"module": "./dist/esm/index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
patcher/vendor/asar/node_modules/minipass/LICENSE.md
generated
vendored
Normal file
55
patcher/vendor/asar/node_modules/minipass/LICENSE.md
generated
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Blue Oak Model License
|
||||||
|
|
||||||
|
Version 1.0.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This license gives everyone as much permission to work with
|
||||||
|
this software as possible, while protecting contributors
|
||||||
|
from liability.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
In order to receive this license, you must agree to its
|
||||||
|
rules. The rules of this license are both obligations
|
||||||
|
under that agreement and conditions to your license.
|
||||||
|
You must not do anything with this software that triggers
|
||||||
|
a rule that you cannot or will not follow.
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
Each contributor licenses you to do everything with this
|
||||||
|
software that would otherwise infringe that contributor's
|
||||||
|
copyright in it.
|
||||||
|
|
||||||
|
## Notices
|
||||||
|
|
||||||
|
You must ensure that everyone who gets a copy of
|
||||||
|
any part of this software from you, with or without
|
||||||
|
changes, also gets the text of this license or a link to
|
||||||
|
<https://blueoakcouncil.org/license/1.0.0>.
|
||||||
|
|
||||||
|
## Excuse
|
||||||
|
|
||||||
|
If anyone notifies you in writing that you have not
|
||||||
|
complied with [Notices](#notices), you can keep your
|
||||||
|
license by taking all practical steps to comply within 30
|
||||||
|
days after the notice. If you do not do so, your license
|
||||||
|
ends immediately.
|
||||||
|
|
||||||
|
## Patent
|
||||||
|
|
||||||
|
Each contributor licenses you to do everything with this
|
||||||
|
software that would otherwise infringe any patent claims
|
||||||
|
they can license or become able to license.
|
||||||
|
|
||||||
|
## Reliability
|
||||||
|
|
||||||
|
No contributor can revoke this license.
|
||||||
|
|
||||||
|
## No Liability
|
||||||
|
|
||||||
|
***As far as the law allows, this software comes as is,
|
||||||
|
without any warranty or condition, and no contributor
|
||||||
|
will be liable to anyone for any damages related to this
|
||||||
|
software or this license, under any kind of legal claim.***
|
||||||
825
patcher/vendor/asar/node_modules/minipass/README.md
generated
vendored
Normal file
825
patcher/vendor/asar/node_modules/minipass/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,825 @@
|
||||||
|
# minipass
|
||||||
|
|
||||||
|
A _very_ minimal implementation of a [PassThrough
|
||||||
|
stream](https://nodejs.org/api/stream.html#stream_class_stream_passthrough)
|
||||||
|
|
||||||
|
[It's very
|
||||||
|
fast](https://docs.google.com/spreadsheets/d/1K_HR5oh3r80b8WVMWCPPjfuWXUgfkmhlX7FGI6JJ8tY/edit?usp=sharing)
|
||||||
|
for objects, strings, and buffers.
|
||||||
|
|
||||||
|
Supports `pipe()`ing (including multi-`pipe()` and backpressure
|
||||||
|
transmission), buffering data until either a `data` event handler
|
||||||
|
or `pipe()` is added (so you don't lose the first chunk), and
|
||||||
|
most other cases where PassThrough is a good idea.
|
||||||
|
|
||||||
|
There is a `read()` method, but it's much more efficient to
|
||||||
|
consume data from this stream via `'data'` events or by calling
|
||||||
|
`pipe()` into some other stream. Calling `read()` requires the
|
||||||
|
buffer to be flattened in some cases, which requires copying
|
||||||
|
memory.
|
||||||
|
|
||||||
|
If you set `objectMode: true` in the options, then whatever is
|
||||||
|
written will be emitted. Otherwise, it'll do a minimal amount of
|
||||||
|
Buffer copying to ensure proper Streams semantics when `read(n)`
|
||||||
|
is called.
|
||||||
|
|
||||||
|
`objectMode` can only be set at instantiation. Attempting to
|
||||||
|
write something other than a String or Buffer without having set
|
||||||
|
`objectMode` in the options will throw an error.
|
||||||
|
|
||||||
|
This is not a `through` or `through2` stream. It doesn't
|
||||||
|
transform the data, it just passes it right through. If you want
|
||||||
|
to transform the data, extend the class, and override the
|
||||||
|
`write()` method. Once you're done transforming the data however
|
||||||
|
you want, call `super.write()` with the transform output.
|
||||||
|
|
||||||
|
For some examples of streams that extend Minipass in various
|
||||||
|
ways, check out:
|
||||||
|
|
||||||
|
- [minizlib](http://npm.im/minizlib)
|
||||||
|
- [fs-minipass](http://npm.im/fs-minipass)
|
||||||
|
- [tar](http://npm.im/tar)
|
||||||
|
- [minipass-collect](http://npm.im/minipass-collect)
|
||||||
|
- [minipass-flush](http://npm.im/minipass-flush)
|
||||||
|
- [minipass-pipeline](http://npm.im/minipass-pipeline)
|
||||||
|
- [tap](http://npm.im/tap)
|
||||||
|
- [tap-parser](http://npm.im/tap-parser)
|
||||||
|
- [treport](http://npm.im/treport)
|
||||||
|
- [minipass-fetch](http://npm.im/minipass-fetch)
|
||||||
|
- [pacote](http://npm.im/pacote)
|
||||||
|
- [make-fetch-happen](http://npm.im/make-fetch-happen)
|
||||||
|
- [cacache](http://npm.im/cacache)
|
||||||
|
- [ssri](http://npm.im/ssri)
|
||||||
|
- [npm-registry-fetch](http://npm.im/npm-registry-fetch)
|
||||||
|
- [minipass-json-stream](http://npm.im/minipass-json-stream)
|
||||||
|
- [minipass-sized](http://npm.im/minipass-sized)
|
||||||
|
|
||||||
|
## Usage in TypeScript
|
||||||
|
|
||||||
|
The `Minipass` class takes three type template definitions:
|
||||||
|
|
||||||
|
- `RType` the type being read, which defaults to `Buffer`. If
|
||||||
|
`RType` is `string`, then the constructor _must_ get an options
|
||||||
|
object specifying either an `encoding` or `objectMode: true`.
|
||||||
|
If it's anything other than `string` or `Buffer`, then it
|
||||||
|
_must_ get an options object specifying `objectMode: true`.
|
||||||
|
- `WType` the type being written. If `RType` is `Buffer` or
|
||||||
|
`string`, then this defaults to `ContiguousData` (Buffer,
|
||||||
|
string, ArrayBuffer, or ArrayBufferView). Otherwise, it
|
||||||
|
defaults to `RType`.
|
||||||
|
- `Events` type mapping event names to the arguments emitted
|
||||||
|
with that event, which extends `Minipass.Events`.
|
||||||
|
|
||||||
|
To declare types for custom events in subclasses, extend the
|
||||||
|
third parameter with your own event signatures. For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Minipass } from 'minipass'
|
||||||
|
|
||||||
|
// a NDJSON stream that emits 'jsonError' when it can't stringify
|
||||||
|
export interface Events extends Minipass.Events {
|
||||||
|
jsonError: [e: Error]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NDJSONStream extends Minipass<string, any, Events> {
|
||||||
|
constructor() {
|
||||||
|
super({ objectMode: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// data is type `any` because that's WType
|
||||||
|
write(data, encoding, cb) {
|
||||||
|
try {
|
||||||
|
const json = JSON.stringify(data)
|
||||||
|
return super.write(json + '\n', encoding, cb)
|
||||||
|
} catch (er) {
|
||||||
|
if (!er instanceof Error) {
|
||||||
|
er = Object.assign(new Error('json stringify failed'), {
|
||||||
|
cause: er,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// trying to emit with something OTHER than an error will
|
||||||
|
// fail, because we declared the event arguments type.
|
||||||
|
this.emit('jsonError', er)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = new NDJSONStream()
|
||||||
|
s.on('jsonError', e => {
|
||||||
|
// here, TS knows that e is an Error
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Emitting/handling events that aren't declared in this way is
|
||||||
|
fine, but the arguments will be typed as `unknown`.
|
||||||
|
|
||||||
|
## Differences from Node.js Streams
|
||||||
|
|
||||||
|
There are several things that make Minipass streams different
|
||||||
|
from (and in some ways superior to) Node.js core streams.
|
||||||
|
|
||||||
|
Please read these caveats if you are familiar with node-core
|
||||||
|
streams and intend to use Minipass streams in your programs.
|
||||||
|
|
||||||
|
You can avoid most of these differences entirely (for a very
|
||||||
|
small performance penalty) by setting `{async: true}` in the
|
||||||
|
constructor options.
|
||||||
|
|
||||||
|
### Timing
|
||||||
|
|
||||||
|
Minipass streams are designed to support synchronous use-cases.
|
||||||
|
Thus, data is emitted as soon as it is available, always. It is
|
||||||
|
buffered until read, but no longer. Another way to look at it is
|
||||||
|
that Minipass streams are exactly as synchronous as the logic
|
||||||
|
that writes into them.
|
||||||
|
|
||||||
|
This can be surprising if your code relies on
|
||||||
|
`PassThrough.write()` always providing data on the next tick
|
||||||
|
rather than the current one, or being able to call `resume()` and
|
||||||
|
not have the entire buffer disappear immediately.
|
||||||
|
|
||||||
|
However, without this synchronicity guarantee, there would be no
|
||||||
|
way for Minipass to achieve the speeds it does, or support the
|
||||||
|
synchronous use cases that it does. Simply put, waiting takes
|
||||||
|
time.
|
||||||
|
|
||||||
|
This non-deferring approach makes Minipass streams much easier to
|
||||||
|
reason about, especially in the context of Promises and other
|
||||||
|
flow-control mechanisms.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// hybrid module, either works
|
||||||
|
import { Minipass } from 'minipass'
|
||||||
|
// or:
|
||||||
|
const { Minipass } = require('minipass')
|
||||||
|
|
||||||
|
const stream = new Minipass()
|
||||||
|
stream.on('data', () => console.log('data event'))
|
||||||
|
console.log('before write')
|
||||||
|
stream.write('hello')
|
||||||
|
console.log('after write')
|
||||||
|
// output:
|
||||||
|
// before write
|
||||||
|
// data event
|
||||||
|
// after write
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exception: Async Opt-In
|
||||||
|
|
||||||
|
If you wish to have a Minipass stream with behavior that more
|
||||||
|
closely mimics Node.js core streams, you can set the stream in
|
||||||
|
async mode either by setting `async: true` in the constructor
|
||||||
|
options, or by setting `stream.async = true` later on.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// hybrid module, either works
|
||||||
|
import { Minipass } from 'minipass'
|
||||||
|
// or:
|
||||||
|
const { Minipass } = require('minipass')
|
||||||
|
|
||||||
|
const asyncStream = new Minipass({ async: true })
|
||||||
|
asyncStream.on('data', () => console.log('data event'))
|
||||||
|
console.log('before write')
|
||||||
|
asyncStream.write('hello')
|
||||||
|
console.log('after write')
|
||||||
|
// output:
|
||||||
|
// before write
|
||||||
|
// after write
|
||||||
|
// data event <-- this is deferred until the next tick
|
||||||
|
```
|
||||||
|
|
||||||
|
Switching _out_ of async mode is unsafe, as it could cause data
|
||||||
|
corruption, and so is not enabled. Example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Minipass } from 'minipass'
|
||||||
|
const stream = new Minipass({ encoding: 'utf8' })
|
||||||
|
stream.on('data', chunk => console.log(chunk))
|
||||||
|
stream.async = true
|
||||||
|
console.log('before writes')
|
||||||
|
stream.write('hello')
|
||||||
|
setStreamSyncAgainSomehow(stream) // <-- this doesn't actually exist!
|
||||||
|
stream.write('world')
|
||||||
|
console.log('after writes')
|
||||||
|
// hypothetical output would be:
|
||||||
|
// before writes
|
||||||
|
// world
|
||||||
|
// after writes
|
||||||
|
// hello
|
||||||
|
// NOT GOOD!
|
||||||
|
```
|
||||||
|
|
||||||
|
To avoid this problem, once set into async mode, any attempt to
|
||||||
|
make the stream sync again will be ignored.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { Minipass } = require('minipass')
|
||||||
|
const stream = new Minipass({ encoding: 'utf8' })
|
||||||
|
stream.on('data', chunk => console.log(chunk))
|
||||||
|
stream.async = true
|
||||||
|
console.log('before writes')
|
||||||
|
stream.write('hello')
|
||||||
|
stream.async = false // <-- no-op, stream already async
|
||||||
|
stream.write('world')
|
||||||
|
console.log('after writes')
|
||||||
|
// actual output:
|
||||||
|
// before writes
|
||||||
|
// after writes
|
||||||
|
// hello
|
||||||
|
// world
|
||||||
|
```
|
||||||
|
|
||||||
|
### No High/Low Water Marks
|
||||||
|
|
||||||
|
Node.js core streams will optimistically fill up a buffer,
|
||||||
|
returning `true` on all writes until the limit is hit, even if
|
||||||
|
the data has nowhere to go. Then, they will not attempt to draw
|
||||||
|
more data in until the buffer size dips below a minimum value.
|
||||||
|
|
||||||
|
Minipass streams are much simpler. The `write()` method will
|
||||||
|
return `true` if the data has somewhere to go (which is to say,
|
||||||
|
given the timing guarantees, that the data is already there by
|
||||||
|
the time `write()` returns).
|
||||||
|
|
||||||
|
If the data has nowhere to go, then `write()` returns false, and
|
||||||
|
the data sits in a buffer, to be drained out immediately as soon
|
||||||
|
as anyone consumes it.
|
||||||
|
|
||||||
|
Since nothing is ever buffered unnecessarily, there is much less
|
||||||
|
copying data, and less bookkeeping about buffer capacity levels.
|
||||||
|
|
||||||
|
### Hazards of Buffering (or: Why Minipass Is So Fast)
|
||||||
|
|
||||||
|
Since data written to a Minipass stream is immediately written
|
||||||
|
all the way through the pipeline, and `write()` always returns
|
||||||
|
true/false based on whether the data was fully flushed,
|
||||||
|
backpressure is communicated immediately to the upstream caller.
|
||||||
|
This minimizes buffering.
|
||||||
|
|
||||||
|
Consider this case:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { PassThrough } = require('stream')
|
||||||
|
const p1 = new PassThrough({ highWaterMark: 1024 })
|
||||||
|
const p2 = new PassThrough({ highWaterMark: 1024 })
|
||||||
|
const p3 = new PassThrough({ highWaterMark: 1024 })
|
||||||
|
const p4 = new PassThrough({ highWaterMark: 1024 })
|
||||||
|
|
||||||
|
p1.pipe(p2).pipe(p3).pipe(p4)
|
||||||
|
p4.on('data', () => console.log('made it through'))
|
||||||
|
|
||||||
|
// this returns false and buffers, then writes to p2 on next tick (1)
|
||||||
|
// p2 returns false and buffers, pausing p1, then writes to p3 on next tick (2)
|
||||||
|
// p3 returns false and buffers, pausing p2, then writes to p4 on next tick (3)
|
||||||
|
// p4 returns false and buffers, pausing p3, then emits 'data' and 'drain'
|
||||||
|
// on next tick (4)
|
||||||
|
// p3 sees p4's 'drain' event, and calls resume(), emitting 'resume' and
|
||||||
|
// 'drain' on next tick (5)
|
||||||
|
// p2 sees p3's 'drain', calls resume(), emits 'resume' and 'drain' on next tick (6)
|
||||||
|
// p1 sees p2's 'drain', calls resume(), emits 'resume' and 'drain' on next
|
||||||
|
// tick (7)
|
||||||
|
|
||||||
|
p1.write(Buffer.alloc(2048)) // returns false
|
||||||
|
```
|
||||||
|
|
||||||
|
Along the way, the data was buffered and deferred at each stage,
|
||||||
|
and multiple event deferrals happened, for an unblocked pipeline
|
||||||
|
where it was perfectly safe to write all the way through!
|
||||||
|
|
||||||
|
Furthermore, setting a `highWaterMark` of `1024` might lead
|
||||||
|
someone reading the code to think an advisory maximum of 1KiB is
|
||||||
|
being set for the pipeline. However, the actual advisory
|
||||||
|
buffering level is the _sum_ of `highWaterMark` values, since
|
||||||
|
each one has its own bucket.
|
||||||
|
|
||||||
|
Consider the Minipass case:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const m1 = new Minipass()
|
||||||
|
const m2 = new Minipass()
|
||||||
|
const m3 = new Minipass()
|
||||||
|
const m4 = new Minipass()
|
||||||
|
|
||||||
|
m1.pipe(m2).pipe(m3).pipe(m4)
|
||||||
|
m4.on('data', () => console.log('made it through'))
|
||||||
|
|
||||||
|
// m1 is flowing, so it writes the data to m2 immediately
|
||||||
|
// m2 is flowing, so it writes the data to m3 immediately
|
||||||
|
// m3 is flowing, so it writes the data to m4 immediately
|
||||||
|
// m4 is flowing, so it fires the 'data' event immediately, returns true
|
||||||
|
// m4's write returned true, so m3 is still flowing, returns true
|
||||||
|
// m3's write returned true, so m2 is still flowing, returns true
|
||||||
|
// m2's write returned true, so m1 is still flowing, returns true
|
||||||
|
// No event deferrals or buffering along the way!
|
||||||
|
|
||||||
|
m1.write(Buffer.alloc(2048)) // returns true
|
||||||
|
```
|
||||||
|
|
||||||
|
It is extremely unlikely that you _don't_ want to buffer any data
|
||||||
|
written, or _ever_ buffer data that can be flushed all the way
|
||||||
|
through. Neither node-core streams nor Minipass ever fail to
|
||||||
|
buffer written data, but node-core streams do a lot of
|
||||||
|
unnecessary buffering and pausing.
|
||||||
|
|
||||||
|
As always, the faster implementation is the one that does less
|
||||||
|
stuff and waits less time to do it.
|
||||||
|
|
||||||
|
### Immediately emit `end` for empty streams (when not paused)
|
||||||
|
|
||||||
|
If a stream is not paused, and `end()` is called before writing
|
||||||
|
any data into it, then it will emit `end` immediately.
|
||||||
|
|
||||||
|
If you have logic that occurs on the `end` event which you don't
|
||||||
|
want to potentially happen immediately (for example, closing file
|
||||||
|
descriptors, moving on to the next entry in an archive parse
|
||||||
|
stream, etc.) then be sure to call `stream.pause()` on creation,
|
||||||
|
and then `stream.resume()` once you are ready to respond to the
|
||||||
|
`end` event.
|
||||||
|
|
||||||
|
However, this is _usually_ not a problem because:
|
||||||
|
|
||||||
|
### Emit `end` When Asked
|
||||||
|
|
||||||
|
One hazard of immediately emitting `'end'` is that you may not
|
||||||
|
yet have had a chance to add a listener. In order to avoid this
|
||||||
|
hazard, Minipass streams safely re-emit the `'end'` event if a
|
||||||
|
new listener is added after `'end'` has been emitted.
|
||||||
|
|
||||||
|
Ie, if you do `stream.on('end', someFunction)`, and the stream
|
||||||
|
has already emitted `end`, then it will call the handler right
|
||||||
|
away. (You can think of this somewhat like attaching a new
|
||||||
|
`.then(fn)` to a previously-resolved Promise.)
|
||||||
|
|
||||||
|
To prevent calling handlers multiple times who would not expect
|
||||||
|
multiple ends to occur, all listeners are removed from the
|
||||||
|
`'end'` event whenever it is emitted.
|
||||||
|
|
||||||
|
### Emit `error` When Asked
|
||||||
|
|
||||||
|
The most recent error object passed to the `'error'` event is
|
||||||
|
stored on the stream. If a new `'error'` event handler is added,
|
||||||
|
and an error was previously emitted, then the event handler will
|
||||||
|
be called immediately (or on `process.nextTick` in the case of
|
||||||
|
async streams).
|
||||||
|
|
||||||
|
This makes it much more difficult to end up trying to interact
|
||||||
|
with a broken stream, if the error handler is added after an
|
||||||
|
error was previously emitted.
|
||||||
|
|
||||||
|
### Impact of "immediate flow" on Tee-streams
|
||||||
|
|
||||||
|
A "tee stream" is a stream piping to multiple destinations:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const tee = new Minipass()
|
||||||
|
t.pipe(dest1)
|
||||||
|
t.pipe(dest2)
|
||||||
|
t.write('foo') // goes to both destinations
|
||||||
|
```
|
||||||
|
|
||||||
|
Since Minipass streams _immediately_ process any pending data
|
||||||
|
through the pipeline when a new pipe destination is added, this
|
||||||
|
can have surprising effects, especially when a stream comes in
|
||||||
|
from some other function and may or may not have data in its
|
||||||
|
buffer.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// WARNING! WILL LOSE DATA!
|
||||||
|
const src = new Minipass()
|
||||||
|
src.write('foo')
|
||||||
|
src.pipe(dest1) // 'foo' chunk flows to dest1 immediately, and is gone
|
||||||
|
src.pipe(dest2) // gets nothing!
|
||||||
|
```
|
||||||
|
|
||||||
|
One solution is to create a dedicated tee-stream junction that
|
||||||
|
pipes to both locations, and then pipe to _that_ instead.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Safe example: tee to both places
|
||||||
|
const src = new Minipass()
|
||||||
|
src.write('foo')
|
||||||
|
const tee = new Minipass()
|
||||||
|
tee.pipe(dest1)
|
||||||
|
tee.pipe(dest2)
|
||||||
|
src.pipe(tee) // tee gets 'foo', pipes to both locations
|
||||||
|
```
|
||||||
|
|
||||||
|
The same caveat applies to `on('data')` event listeners. The
|
||||||
|
first one added will _immediately_ receive all of the data,
|
||||||
|
leaving nothing for the second:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// WARNING! WILL LOSE DATA!
|
||||||
|
const src = new Minipass()
|
||||||
|
src.write('foo')
|
||||||
|
src.on('data', handler1) // receives 'foo' right away
|
||||||
|
src.on('data', handler2) // nothing to see here!
|
||||||
|
```
|
||||||
|
|
||||||
|
Using a dedicated tee-stream can be used in this case as well:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Safe example: tee to both data handlers
|
||||||
|
const src = new Minipass()
|
||||||
|
src.write('foo')
|
||||||
|
const tee = new Minipass()
|
||||||
|
tee.on('data', handler1)
|
||||||
|
tee.on('data', handler2)
|
||||||
|
src.pipe(tee)
|
||||||
|
```
|
||||||
|
|
||||||
|
All of the hazards in this section are avoided by setting `{
|
||||||
|
async: true }` in the Minipass constructor, or by setting
|
||||||
|
`stream.async = true` afterwards. Note that this does add some
|
||||||
|
overhead, so should only be done in cases where you are willing
|
||||||
|
to lose a bit of performance in order to avoid having to refactor
|
||||||
|
program logic.
|
||||||
|
|
||||||
|
## USAGE
|
||||||
|
|
||||||
|
It's a stream! Use it like a stream and it'll most likely do what
|
||||||
|
you want.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Minipass } from 'minipass'
|
||||||
|
const mp = new Minipass(options) // options is optional
|
||||||
|
mp.write('foo')
|
||||||
|
mp.pipe(someOtherStream)
|
||||||
|
mp.end('bar')
|
||||||
|
```
|
||||||
|
|
||||||
|
### OPTIONS
|
||||||
|
|
||||||
|
- `encoding` How would you like the data coming _out_ of the
|
||||||
|
stream to be encoded? Accepts any values that can be passed to
|
||||||
|
`Buffer.toString()`.
|
||||||
|
- `objectMode` Emit data exactly as it comes in. This will be
|
||||||
|
flipped on by default if you write() something other than a
|
||||||
|
string or Buffer at any point. Setting `objectMode: true` will
|
||||||
|
prevent setting any encoding value.
|
||||||
|
- `async` Defaults to `false`. Set to `true` to defer data
|
||||||
|
emission until next tick. This reduces performance slightly,
|
||||||
|
but makes Minipass streams use timing behavior closer to Node
|
||||||
|
core streams. See [Timing](#timing) for more details.
|
||||||
|
- `signal` An `AbortSignal` that will cause the stream to unhook
|
||||||
|
itself from everything and become as inert as possible. Note
|
||||||
|
that providing a `signal` parameter will make `'error'` events
|
||||||
|
no longer throw if they are unhandled, but they will still be
|
||||||
|
emitted to handlers if any are attached.
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
Implements the user-facing portions of Node.js's `Readable` and
|
||||||
|
`Writable` streams.
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- `write(chunk, [encoding], [callback])` - Put data in. (Note
|
||||||
|
that, in the base Minipass class, the same data will come out.)
|
||||||
|
Returns `false` if the stream will buffer the next write, or
|
||||||
|
true if it's still in "flowing" mode.
|
||||||
|
- `end([chunk, [encoding]], [callback])` - Signal that you have
|
||||||
|
no more data to write. This will queue an `end` event to be
|
||||||
|
fired when all the data has been consumed.
|
||||||
|
- `pause()` - No more data for a while, please. This also
|
||||||
|
prevents `end` from being emitted for empty streams until the
|
||||||
|
stream is resumed.
|
||||||
|
- `resume()` - Resume the stream. If there's data in the buffer,
|
||||||
|
it is all discarded. Any buffered events are immediately
|
||||||
|
emitted.
|
||||||
|
- `pipe(dest)` - Send all output to the stream provided. When
|
||||||
|
data is emitted, it is immediately written to any and all pipe
|
||||||
|
destinations. (Or written on next tick in `async` mode.)
|
||||||
|
- `unpipe(dest)` - Stop piping to the destination stream. This is
|
||||||
|
immediate, meaning that any asynchronously queued data will
|
||||||
|
_not_ make it to the destination when running in `async` mode.
|
||||||
|
- `options.end` - Boolean, end the destination stream when the
|
||||||
|
source stream ends. Default `true`.
|
||||||
|
- `options.proxyErrors` - Boolean, proxy `error` events from
|
||||||
|
the source stream to the destination stream. Note that errors
|
||||||
|
are _not_ proxied after the pipeline terminates, either due
|
||||||
|
to the source emitting `'end'` or manually unpiping with
|
||||||
|
`src.unpipe(dest)`. Default `false`.
|
||||||
|
- `on(ev, fn)`, `emit(ev, fn)` - Minipass streams are
|
||||||
|
EventEmitters. Some events are given special treatment,
|
||||||
|
however. (See below under "events".)
|
||||||
|
- `promise()` - Returns a Promise that resolves when the stream
|
||||||
|
emits `end`, or rejects if the stream emits `error`.
|
||||||
|
- `collect()` - Return a Promise that resolves on `end` with an
|
||||||
|
array containing each chunk of data that was emitted, or
|
||||||
|
rejects if the stream emits `error`. Note that this consumes
|
||||||
|
the stream data.
|
||||||
|
- `concat()` - Same as `collect()`, but concatenates the data
|
||||||
|
into a single Buffer object. Will reject the returned promise
|
||||||
|
if the stream is in objectMode, or if it goes into objectMode
|
||||||
|
by the end of the data.
|
||||||
|
- `read(n)` - Consume `n` bytes of data out of the buffer. If `n`
|
||||||
|
is not provided, then consume all of it. If `n` bytes are not
|
||||||
|
available, then it returns null. **Note** consuming streams in
|
||||||
|
this way is less efficient, and can lead to unnecessary Buffer
|
||||||
|
copying.
|
||||||
|
- `destroy([er])` - Destroy the stream. If an error is provided,
|
||||||
|
then an `'error'` event is emitted. If the stream has a
|
||||||
|
`close()` method, and has not emitted a `'close'` event yet,
|
||||||
|
then `stream.close()` will be called. Any Promises returned by
|
||||||
|
`.promise()`, `.collect()` or `.concat()` will be rejected.
|
||||||
|
After being destroyed, writing to the stream will emit an
|
||||||
|
error. No more data will be emitted if the stream is destroyed,
|
||||||
|
even if it was previously buffered.
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
- `bufferLength` Read-only. Total number of bytes buffered, or in
|
||||||
|
the case of objectMode, the total number of objects.
|
||||||
|
- `encoding` Read-only. The encoding that has been set.
|
||||||
|
- `flowing` Read-only. Boolean indicating whether a chunk written
|
||||||
|
to the stream will be immediately emitted.
|
||||||
|
- `emittedEnd` Read-only. Boolean indicating whether the end-ish
|
||||||
|
events (ie, `end`, `prefinish`, `finish`) have been emitted.
|
||||||
|
Note that listening on any end-ish event will immediateyl
|
||||||
|
re-emit it if it has already been emitted.
|
||||||
|
- `writable` Whether the stream is writable. Default `true`. Set
|
||||||
|
to `false` when `end()`
|
||||||
|
- `readable` Whether the stream is readable. Default `true`.
|
||||||
|
- `pipes` An array of Pipe objects referencing streams that this
|
||||||
|
stream is piping into.
|
||||||
|
- `destroyed` A getter that indicates whether the stream was
|
||||||
|
destroyed.
|
||||||
|
- `paused` True if the stream has been explicitly paused,
|
||||||
|
otherwise false.
|
||||||
|
- `objectMode` Indicates whether the stream is in `objectMode`.
|
||||||
|
- `aborted` Readonly property set when the `AbortSignal`
|
||||||
|
dispatches an `abort` event.
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
- `data` Emitted when there's data to read. Argument is the data
|
||||||
|
to read. This is never emitted while not flowing. If a listener
|
||||||
|
is attached, that will resume the stream.
|
||||||
|
- `end` Emitted when there's no more data to read. This will be
|
||||||
|
emitted immediately for empty streams when `end()` is called.
|
||||||
|
If a listener is attached, and `end` was already emitted, then
|
||||||
|
it will be emitted again. All listeners are removed when `end`
|
||||||
|
is emitted.
|
||||||
|
- `prefinish` An end-ish event that follows the same logic as
|
||||||
|
`end` and is emitted in the same conditions where `end` is
|
||||||
|
emitted. Emitted after `'end'`.
|
||||||
|
- `finish` An end-ish event that follows the same logic as `end`
|
||||||
|
and is emitted in the same conditions where `end` is emitted.
|
||||||
|
Emitted after `'prefinish'`.
|
||||||
|
- `close` An indication that an underlying resource has been
|
||||||
|
released. Minipass does not emit this event, but will defer it
|
||||||
|
until after `end` has been emitted, since it throws off some
|
||||||
|
stream libraries otherwise.
|
||||||
|
- `drain` Emitted when the internal buffer empties, and it is
|
||||||
|
again suitable to `write()` into the stream.
|
||||||
|
- `readable` Emitted when data is buffered and ready to be read
|
||||||
|
by a consumer.
|
||||||
|
- `resume` Emitted when stream changes state from buffering to
|
||||||
|
flowing mode. (Ie, when `resume` is called, `pipe` is called,
|
||||||
|
or a `data` event listener is added.)
|
||||||
|
|
||||||
|
### Static Methods
|
||||||
|
|
||||||
|
- `Minipass.isStream(stream)` Returns `true` if the argument is a
|
||||||
|
stream, and false otherwise. To be considered a stream, the
|
||||||
|
object must be either an instance of Minipass, or an
|
||||||
|
EventEmitter that has either a `pipe()` method, or both
|
||||||
|
`write()` and `end()` methods. (Pretty much any stream in
|
||||||
|
node-land will return `true` for this.)
|
||||||
|
|
||||||
|
## EXAMPLES
|
||||||
|
|
||||||
|
Here are some examples of things you can do with Minipass
|
||||||
|
streams.
|
||||||
|
|
||||||
|
### simple "are you done yet" promise
|
||||||
|
|
||||||
|
```js
|
||||||
|
mp.promise().then(
|
||||||
|
() => {
|
||||||
|
// stream is finished
|
||||||
|
},
|
||||||
|
er => {
|
||||||
|
// stream emitted an error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### collecting
|
||||||
|
|
||||||
|
```js
|
||||||
|
mp.collect().then(all => {
|
||||||
|
// all is an array of all the data emitted
|
||||||
|
// encoding is supported in this case, so
|
||||||
|
// so the result will be a collection of strings if
|
||||||
|
// an encoding is specified, or buffers/objects if not.
|
||||||
|
//
|
||||||
|
// In an async function, you may do
|
||||||
|
// const data = await stream.collect()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### collecting into a single blob
|
||||||
|
|
||||||
|
This is a bit slower because it concatenates the data into one
|
||||||
|
chunk for you, but if you're going to do it yourself anyway, it's
|
||||||
|
convenient this way:
|
||||||
|
|
||||||
|
```js
|
||||||
|
mp.concat().then(onebigchunk => {
|
||||||
|
// onebigchunk is a string if the stream
|
||||||
|
// had an encoding set, or a buffer otherwise.
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### iteration
|
||||||
|
|
||||||
|
You can iterate over streams synchronously or asynchronously in
|
||||||
|
platforms that support it.
|
||||||
|
|
||||||
|
Synchronous iteration will end when the currently available data
|
||||||
|
is consumed, even if the `end` event has not been reached. In
|
||||||
|
string and buffer mode, the data is concatenated, so unless
|
||||||
|
multiple writes are occurring in the same tick as the `read()`,
|
||||||
|
sync iteration loops will generally only have a single iteration.
|
||||||
|
|
||||||
|
To consume chunks in this way exactly as they have been written,
|
||||||
|
with no flattening, create the stream with the `{ objectMode:
|
||||||
|
true }` option.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const mp = new Minipass({ objectMode: true })
|
||||||
|
mp.write('a')
|
||||||
|
mp.write('b')
|
||||||
|
for (let letter of mp) {
|
||||||
|
console.log(letter) // a, b
|
||||||
|
}
|
||||||
|
mp.write('c')
|
||||||
|
mp.write('d')
|
||||||
|
for (let letter of mp) {
|
||||||
|
console.log(letter) // c, d
|
||||||
|
}
|
||||||
|
mp.write('e')
|
||||||
|
mp.end()
|
||||||
|
for (let letter of mp) {
|
||||||
|
console.log(letter) // e
|
||||||
|
}
|
||||||
|
for (let letter of mp) {
|
||||||
|
console.log(letter) // nothing
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Asynchronous iteration will continue until the end event is reached,
|
||||||
|
consuming all of the data.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const mp = new Minipass({ encoding: 'utf8' })
|
||||||
|
|
||||||
|
// some source of some data
|
||||||
|
let i = 5
|
||||||
|
const inter = setInterval(() => {
|
||||||
|
if (i-- > 0) mp.write(Buffer.from('foo\n', 'utf8'))
|
||||||
|
else {
|
||||||
|
mp.end()
|
||||||
|
clearInterval(inter)
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
// consume the data with asynchronous iteration
|
||||||
|
async function consume() {
|
||||||
|
for await (let chunk of mp) {
|
||||||
|
console.log(chunk)
|
||||||
|
}
|
||||||
|
return 'ok'
|
||||||
|
}
|
||||||
|
|
||||||
|
consume().then(res => console.log(res))
|
||||||
|
// logs `foo\n` 5 times, and then `ok`
|
||||||
|
```
|
||||||
|
|
||||||
|
### subclass that `console.log()`s everything written into it
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Logger extends Minipass {
|
||||||
|
write(chunk, encoding, callback) {
|
||||||
|
console.log('WRITE', chunk, encoding)
|
||||||
|
return super.write(chunk, encoding, callback)
|
||||||
|
}
|
||||||
|
end(chunk, encoding, callback) {
|
||||||
|
console.log('END', chunk, encoding)
|
||||||
|
return super.end(chunk, encoding, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
someSource.pipe(new Logger()).pipe(someDest)
|
||||||
|
```
|
||||||
|
|
||||||
|
### same thing, but using an inline anonymous class
|
||||||
|
|
||||||
|
```js
|
||||||
|
// js classes are fun
|
||||||
|
someSource
|
||||||
|
.pipe(
|
||||||
|
new (class extends Minipass {
|
||||||
|
emit(ev, ...data) {
|
||||||
|
// let's also log events, because debugging some weird thing
|
||||||
|
console.log('EMIT', ev)
|
||||||
|
return super.emit(ev, ...data)
|
||||||
|
}
|
||||||
|
write(chunk, encoding, callback) {
|
||||||
|
console.log('WRITE', chunk, encoding)
|
||||||
|
return super.write(chunk, encoding, callback)
|
||||||
|
}
|
||||||
|
end(chunk, encoding, callback) {
|
||||||
|
console.log('END', chunk, encoding)
|
||||||
|
return super.end(chunk, encoding, callback)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
)
|
||||||
|
.pipe(someDest)
|
||||||
|
```
|
||||||
|
|
||||||
|
### subclass that defers 'end' for some reason
|
||||||
|
|
||||||
|
```js
|
||||||
|
class SlowEnd extends Minipass {
|
||||||
|
emit(ev, ...args) {
|
||||||
|
if (ev === 'end') {
|
||||||
|
console.log('going to end, hold on a sec')
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('ok, ready to end now')
|
||||||
|
super.emit('end', ...args)
|
||||||
|
}, 100)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return super.emit(ev, ...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### transform that creates newline-delimited JSON
|
||||||
|
|
||||||
|
```js
|
||||||
|
class NDJSONEncode extends Minipass {
|
||||||
|
write(obj, cb) {
|
||||||
|
try {
|
||||||
|
// JSON.stringify can throw, emit an error on that
|
||||||
|
return super.write(JSON.stringify(obj) + '\n', 'utf8', cb)
|
||||||
|
} catch (er) {
|
||||||
|
this.emit('error', er)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end(obj, cb) {
|
||||||
|
if (typeof obj === 'function') {
|
||||||
|
cb = obj
|
||||||
|
obj = undefined
|
||||||
|
}
|
||||||
|
if (obj !== undefined) {
|
||||||
|
this.write(obj)
|
||||||
|
}
|
||||||
|
return super.end(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### transform that parses newline-delimited JSON
|
||||||
|
|
||||||
|
```js
|
||||||
|
class NDJSONDecode extends Minipass {
|
||||||
|
constructor(options) {
|
||||||
|
// always be in object mode, as far as Minipass is concerned
|
||||||
|
super({ objectMode: true })
|
||||||
|
this._jsonBuffer = ''
|
||||||
|
}
|
||||||
|
write(chunk, encoding, cb) {
|
||||||
|
if (
|
||||||
|
typeof chunk === 'string' &&
|
||||||
|
typeof encoding === 'string' &&
|
||||||
|
encoding !== 'utf8'
|
||||||
|
) {
|
||||||
|
chunk = Buffer.from(chunk, encoding).toString()
|
||||||
|
} else if (Buffer.isBuffer(chunk)) {
|
||||||
|
chunk = chunk.toString()
|
||||||
|
}
|
||||||
|
if (typeof encoding === 'function') {
|
||||||
|
cb = encoding
|
||||||
|
}
|
||||||
|
const jsonData = (this._jsonBuffer + chunk).split('\n')
|
||||||
|
this._jsonBuffer = jsonData.pop()
|
||||||
|
for (let i = 0; i < jsonData.length; i++) {
|
||||||
|
try {
|
||||||
|
// JSON.parse can throw, emit an error on that
|
||||||
|
super.write(JSON.parse(jsonData[i]))
|
||||||
|
} catch (er) {
|
||||||
|
this.emit('error', er)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cb) cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
77
patcher/vendor/asar/node_modules/minipass/package.json
generated
vendored
Normal file
77
patcher/vendor/asar/node_modules/minipass/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
{
|
||||||
|
"name": "minipass",
|
||||||
|
"version": "7.1.3",
|
||||||
|
"description": "minimal implementation of a PassThrough stream",
|
||||||
|
"main": "./dist/commonjs/index.js",
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"module": "./dist/esm/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"tshy": {
|
||||||
|
"selfLink": false,
|
||||||
|
"compiler": "tsgo",
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"preversion": "npm test",
|
||||||
|
"postversion": "npm publish",
|
||||||
|
"prepublishOnly": "git push origin --follow-tags",
|
||||||
|
"prepare": "tshy",
|
||||||
|
"pretest": "npm run prepare",
|
||||||
|
"presnap": "npm run prepare",
|
||||||
|
"test": "tap",
|
||||||
|
"snap": "tap",
|
||||||
|
"format": "prettier --write . --loglevel warn",
|
||||||
|
"typedoc": "typedoc --tsconfig .tshy/esm.json ./src/*.ts"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"semi": false,
|
||||||
|
"printWidth": 75,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/end-of-stream": "^1.4.2",
|
||||||
|
"@types/node": "^25.2.3",
|
||||||
|
"end-of-stream": "^1.4.0",
|
||||||
|
"node-abort-controller": "^3.1.1",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"tap": "^21.6.1",
|
||||||
|
"through2": "^2.0.3",
|
||||||
|
"tshy": "^3.3.2",
|
||||||
|
"typedoc": "^0.28.17"
|
||||||
|
},
|
||||||
|
"repository": "https://github.com/isaacs/minipass",
|
||||||
|
"keywords": [
|
||||||
|
"passthrough",
|
||||||
|
"stream"
|
||||||
|
],
|
||||||
|
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
patcher/vendor/asar/node_modules/path-scurry/LICENSE.md
generated
vendored
Normal file
55
patcher/vendor/asar/node_modules/path-scurry/LICENSE.md
generated
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Blue Oak Model License
|
||||||
|
|
||||||
|
Version 1.0.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This license gives everyone as much permission to work with
|
||||||
|
this software as possible, while protecting contributors
|
||||||
|
from liability.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
In order to receive this license, you must agree to its
|
||||||
|
rules. The rules of this license are both obligations
|
||||||
|
under that agreement and conditions to your license.
|
||||||
|
You must not do anything with this software that triggers
|
||||||
|
a rule that you cannot or will not follow.
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
Each contributor licenses you to do everything with this
|
||||||
|
software that would otherwise infringe that contributor's
|
||||||
|
copyright in it.
|
||||||
|
|
||||||
|
## Notices
|
||||||
|
|
||||||
|
You must ensure that everyone who gets a copy of
|
||||||
|
any part of this software from you, with or without
|
||||||
|
changes, also gets the text of this license or a link to
|
||||||
|
<https://blueoakcouncil.org/license/1.0.0>.
|
||||||
|
|
||||||
|
## Excuse
|
||||||
|
|
||||||
|
If anyone notifies you in writing that you have not
|
||||||
|
complied with [Notices](#notices), you can keep your
|
||||||
|
license by taking all practical steps to comply within 30
|
||||||
|
days after the notice. If you do not do so, your license
|
||||||
|
ends immediately.
|
||||||
|
|
||||||
|
## Patent
|
||||||
|
|
||||||
|
Each contributor licenses you to do everything with this
|
||||||
|
software that would otherwise infringe any patent claims
|
||||||
|
they can license or become able to license.
|
||||||
|
|
||||||
|
## Reliability
|
||||||
|
|
||||||
|
No contributor can revoke this license.
|
||||||
|
|
||||||
|
## No Liability
|
||||||
|
|
||||||
|
***As far as the law allows, this software comes as is,
|
||||||
|
without any warranty or condition, and no contributor
|
||||||
|
will be liable to anyone for any damages related to this
|
||||||
|
software or this license, under any kind of legal claim.***
|
||||||
636
patcher/vendor/asar/node_modules/path-scurry/README.md
generated
vendored
Normal file
636
patcher/vendor/asar/node_modules/path-scurry/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,636 @@
|
||||||
|
# path-scurry
|
||||||
|
|
||||||
|
Extremely high performant utility for building tools that read
|
||||||
|
the file system, minimizing filesystem and path string munging
|
||||||
|
operations to the greatest degree possible.
|
||||||
|
|
||||||
|
## Ugh, yet another file traversal thing on npm?
|
||||||
|
|
||||||
|
Yes. None of the existing ones gave me exactly what I wanted.
|
||||||
|
|
||||||
|
## Well what is it you wanted?
|
||||||
|
|
||||||
|
While working on [glob](http://npm.im/glob), I found that I
|
||||||
|
needed a module to very efficiently manage the traversal over a
|
||||||
|
folder tree, such that:
|
||||||
|
|
||||||
|
1. No `readdir()` or `stat()` would ever be called on the same
|
||||||
|
file or directory more than one time.
|
||||||
|
2. No `readdir()` calls would be made if we can be reasonably
|
||||||
|
sure that the path is not a directory. (Ie, a previous
|
||||||
|
`readdir()` or `stat()` covered the path, and
|
||||||
|
`ent.isDirectory()` is false.)
|
||||||
|
3. `path.resolve()`, `dirname()`, `basename()`, and other
|
||||||
|
string-parsing/munging operations are be minimized. This means
|
||||||
|
it has to track "provisional" child nodes that may not exist
|
||||||
|
(and if we find that they _don't_ exist, store that
|
||||||
|
information as well, so we don't have to ever check again).
|
||||||
|
4. The API is not limited to use as a stream/iterator/etc. There
|
||||||
|
are many cases where an API like node's `fs` is preferrable.
|
||||||
|
5. It's more important to prevent excess syscalls than to be up
|
||||||
|
to date, but it should be smart enough to know what it
|
||||||
|
_doesn't_ know, and go get it seamlessly when requested.
|
||||||
|
6. Do not blow up the JS heap allocation if operating on a
|
||||||
|
directory with a huge number of entries.
|
||||||
|
7. Handle all the weird aspects of Windows paths, like UNC paths
|
||||||
|
and drive letters and wrongway slashes, so that the consumer
|
||||||
|
can return canonical platform-specific paths without having to
|
||||||
|
parse or join or do any error-prone string munging.
|
||||||
|
|
||||||
|
## PERFORMANCE
|
||||||
|
|
||||||
|
JavaScript people throw around the word "blazing" a lot. I hope
|
||||||
|
that this module doesn't blaze anyone. But it does go very fast,
|
||||||
|
in the cases it's optimized for, if used properly.
|
||||||
|
|
||||||
|
PathScurry provides ample opportunities to get extremely good
|
||||||
|
performance, as well as several options to trade performance for
|
||||||
|
convenience.
|
||||||
|
|
||||||
|
Benchmarks can be run by executing `npm run bench`.
|
||||||
|
|
||||||
|
As is always the case, doing more means going slower, doing less
|
||||||
|
means going faster, and there are trade offs between speed and
|
||||||
|
memory usage.
|
||||||
|
|
||||||
|
PathScurry makes heavy use of [LRUCache](http://npm.im/lru-cache)
|
||||||
|
to efficiently cache whatever it can, and `Path` objects remain
|
||||||
|
in the graph for the lifetime of the walker, so repeated calls
|
||||||
|
with a single PathScurry object will be extremely fast. However,
|
||||||
|
adding items to a cold cache means "doing more", so in those
|
||||||
|
cases, we pay a price. Nothing is free, but every effort has been
|
||||||
|
made to reduce costs wherever possible.
|
||||||
|
|
||||||
|
Also, note that a "cache as long as possible" approach means that
|
||||||
|
changes to the filesystem may not be reflected in the results of
|
||||||
|
repeated PathScurry operations.
|
||||||
|
|
||||||
|
For resolving string paths, `PathScurry` ranges from 5-50 times
|
||||||
|
faster than `path.resolve` on repeated resolutions, but around
|
||||||
|
100 to 1000 times _slower_ on the first resolution. If your
|
||||||
|
program is spending a lot of time resolving the _same_ paths
|
||||||
|
repeatedly (like, thousands or millions of times), then this can
|
||||||
|
be beneficial. But both implementations are pretty fast, and
|
||||||
|
speeding up an infrequent operation from 4µs to 400ns is not
|
||||||
|
going to move the needle on your app's performance.
|
||||||
|
|
||||||
|
For walking file system directory trees, a lot depends on how
|
||||||
|
often a given PathScurry object will be used, and also on the
|
||||||
|
walk method used.
|
||||||
|
|
||||||
|
With default settings on a folder tree of 100,000 items,
|
||||||
|
consisting of around a 10-to-1 ratio of normal files to
|
||||||
|
directories, PathScurry performs comparably to
|
||||||
|
[@nodelib/fs.walk](http://npm.im/@nodelib/fs.walk), which is the
|
||||||
|
fastest and most reliable file system walker I could find. As far
|
||||||
|
as I can tell, it's almost impossible to go much faster in a
|
||||||
|
Node.js program, just based on how fast you can push syscalls out
|
||||||
|
to the fs thread pool.
|
||||||
|
|
||||||
|
On my machine, that is about 1000-1200 completed walks per second
|
||||||
|
for async or stream walks, and around 500-600 walks per second
|
||||||
|
synchronously.
|
||||||
|
|
||||||
|
In the warm cache state, PathScurry's performance increases
|
||||||
|
around 4x for async `for await` iteration, 10-15x faster for
|
||||||
|
streams and synchronous `for of` iteration, and anywhere from 30x
|
||||||
|
to 80x faster for the rest.
|
||||||
|
|
||||||
|
```
|
||||||
|
# walk 100,000 fs entries, 10/1 file/dir ratio
|
||||||
|
# operations / ms
|
||||||
|
New PathScurry object | Reuse PathScurry object
|
||||||
|
stream: 1112.589 | 13974.917
|
||||||
|
sync stream: 492.718 | 15028.343
|
||||||
|
async walk: 1095.648 | 32706.395
|
||||||
|
sync walk: 527.632 | 46129.772
|
||||||
|
async iter: 1288.821 | 5045.510
|
||||||
|
sync iter: 498.496 | 17920.746
|
||||||
|
```
|
||||||
|
|
||||||
|
A hand-rolled walk calling `entry.readdir()` and recursing
|
||||||
|
through the entries can benefit even more from caching, with
|
||||||
|
greater flexibility and without the overhead of streams or
|
||||||
|
generators.
|
||||||
|
|
||||||
|
The cold cache state is still limited by the costs of file system
|
||||||
|
operations, but with a warm cache, the only bottleneck is CPU
|
||||||
|
speed and VM optimizations. Of course, in that case, some care
|
||||||
|
must be taken to ensure that you don't lose performance as a
|
||||||
|
result of silly mistakes, like calling `readdir()` on entries
|
||||||
|
that you know are not directories.
|
||||||
|
|
||||||
|
```
|
||||||
|
# manual recursive iteration functions
|
||||||
|
cold cache | warm cache
|
||||||
|
async: 1164.901 | 17923.320
|
||||||
|
cb: 1101.127 | 40999.344
|
||||||
|
zalgo: 1082.240 | 66689.936
|
||||||
|
sync: 526.935 | 87097.591
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, the speed improves by around 10-20x in the async
|
||||||
|
case, 40x in the case of using `entry.readdirCB` with protections
|
||||||
|
against synchronous callbacks, and 50-100x with callback
|
||||||
|
deferrals disabled, and _several hundred times faster_ for
|
||||||
|
synchronous iteration.
|
||||||
|
|
||||||
|
If you can think of a case that is not covered in these
|
||||||
|
benchmarks, or an implementation that performs significantly
|
||||||
|
better than PathScurry, please [let me
|
||||||
|
know](https://github.com/isaacs/path-scurry/issues).
|
||||||
|
|
||||||
|
## USAGE
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// hybrid module, load with either method
|
||||||
|
import { PathScurry, Path } from 'path-scurry'
|
||||||
|
// or:
|
||||||
|
const { PathScurry, Path } = require('path-scurry')
|
||||||
|
|
||||||
|
// very simple example, say we want to find and
|
||||||
|
// delete all the .DS_Store files in a given path
|
||||||
|
// note that the API is very similar to just a
|
||||||
|
// naive walk with fs.readdir()
|
||||||
|
import { unlink } from 'fs/promises'
|
||||||
|
|
||||||
|
// easy way, iterate over the directory and do the thing
|
||||||
|
const pw = new PathScurry(process.cwd())
|
||||||
|
for await (const entry of pw) {
|
||||||
|
if (entry.isFile() && entry.name === '.DS_Store') {
|
||||||
|
unlink(entry.fullpath())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// here it is as a manual recursive method
|
||||||
|
const walk = async (entry: Path) => {
|
||||||
|
const promises: Promise<any> = []
|
||||||
|
// readdir doesn't throw on non-directories, it just doesn't
|
||||||
|
// return any entries, to save stack trace costs.
|
||||||
|
// Items are returned in arbitrary unsorted order
|
||||||
|
for (const child of await pw.readdir(entry)) {
|
||||||
|
// each child is a Path object
|
||||||
|
if (child.name === '.DS_Store' && child.isFile()) {
|
||||||
|
// could also do pw.resolve(entry, child.name),
|
||||||
|
// just like fs.readdir walking, but .fullpath is
|
||||||
|
// a *slightly* more efficient shorthand.
|
||||||
|
promises.push(unlink(child.fullpath()))
|
||||||
|
} else if (child.isDirectory()) {
|
||||||
|
promises.push(walk(child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(pw.cwd).then(() => {
|
||||||
|
console.log('all .DS_Store files removed')
|
||||||
|
})
|
||||||
|
|
||||||
|
const pw2 = new PathScurry('/a/b/c') // pw2.cwd is the Path for /a/b/c
|
||||||
|
const relativeDir = pw2.cwd.resolve('../x') // Path entry for '/a/b/x'
|
||||||
|
const relative2 = pw2.cwd.resolve('/a/b/d/../x') // same path, same entry
|
||||||
|
assert.equal(relativeDir, relative2)
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
[Full TypeDoc API](https://isaacs.github.io/path-scurry)
|
||||||
|
|
||||||
|
There are platform-specific classes exported, but for the most
|
||||||
|
part, the default `PathScurry` and `Path` exports are what you
|
||||||
|
most likely need, unless you are testing behavior for other
|
||||||
|
platforms.
|
||||||
|
|
||||||
|
Intended public API is documented here, but the full
|
||||||
|
documentation does include internal types, which should not be
|
||||||
|
accessed directly.
|
||||||
|
|
||||||
|
### Interface `PathScurryOpts`
|
||||||
|
|
||||||
|
The type of the `options` argument passed to the `PathScurry`
|
||||||
|
constructor.
|
||||||
|
|
||||||
|
- `nocase`: Boolean indicating that file names should be compared
|
||||||
|
case-insensitively. Defaults to `true` on darwin and win32
|
||||||
|
implementations, `false` elsewhere.
|
||||||
|
|
||||||
|
**Warning** Performing case-insensitive matching on a
|
||||||
|
case-sensitive filesystem will result in occasionally very
|
||||||
|
bizarre behavior. Performing case-sensitive matching on a
|
||||||
|
case-insensitive filesystem may negatively impact performance.
|
||||||
|
|
||||||
|
- `childrenCacheSize`: Number of child entries to cache, in order
|
||||||
|
to speed up `resolve()` and `readdir()` calls. Defaults to
|
||||||
|
`16 * 1024` (ie, `16384`).
|
||||||
|
|
||||||
|
Setting it to a higher value will run the risk of JS heap
|
||||||
|
allocation errors on large directory trees. Setting it to `256`
|
||||||
|
or smaller will significantly reduce the construction time and
|
||||||
|
data consumption overhead, but with the downside of operations
|
||||||
|
being slower on large directory trees. Setting it to `0` will
|
||||||
|
mean that effectively no operations are cached, and this module
|
||||||
|
will be roughly the same speed as `fs` for file system
|
||||||
|
operations, and _much_ slower than `path.resolve()` for
|
||||||
|
repeated path resolution.
|
||||||
|
|
||||||
|
- `fs` An object that will be used to override the default `fs`
|
||||||
|
methods. Any methods that are not overridden will use Node's
|
||||||
|
built-in implementations.
|
||||||
|
|
||||||
|
- lstatSync
|
||||||
|
- readdir (callback `withFileTypes` Dirent variant, used for
|
||||||
|
readdirCB and most walks)
|
||||||
|
- readdirSync
|
||||||
|
- readlinkSync
|
||||||
|
- realpathSync
|
||||||
|
- promises: Object containing the following async methods:
|
||||||
|
- lstat
|
||||||
|
- readdir (Dirent variant only)
|
||||||
|
- readlink
|
||||||
|
- realpath
|
||||||
|
|
||||||
|
### Interface `WalkOptions`
|
||||||
|
|
||||||
|
The options object that may be passed to all walk methods.
|
||||||
|
|
||||||
|
- `withFileTypes`: Boolean, default true. Indicates that `Path`
|
||||||
|
objects should be returned. Set to `false` to get string paths
|
||||||
|
instead.
|
||||||
|
- `follow`: Boolean, default false. Attempt to read directory
|
||||||
|
entries from symbolic links. Otherwise, only actual directories
|
||||||
|
are traversed. Regardless of this setting, a given target path
|
||||||
|
will only ever be walked once, meaning that a symbolic link to
|
||||||
|
a previously traversed directory will never be followed.
|
||||||
|
|
||||||
|
Setting this imposes a slight performance penalty, because
|
||||||
|
`readlink` must be called on all symbolic links encountered, in
|
||||||
|
order to avoid infinite cycles.
|
||||||
|
|
||||||
|
- `filter`: Function `(entry: Path) => boolean`. If provided,
|
||||||
|
will prevent the inclusion of any entry for which it returns a
|
||||||
|
falsey value. This will not prevent directories from being
|
||||||
|
traversed if they do not pass the filter, though it will
|
||||||
|
prevent the directories themselves from being included in the
|
||||||
|
results. By default, if no filter is provided, then all entries
|
||||||
|
are included in the results.
|
||||||
|
- `walkFilter`: Function `(entry: Path) => boolean`. If provided,
|
||||||
|
will prevent the traversal of any directory (or in the case of
|
||||||
|
`follow:true` symbolic links to directories) for which the
|
||||||
|
function returns false. This will not prevent the directories
|
||||||
|
themselves from being included in the result set. Use `filter`
|
||||||
|
for that.
|
||||||
|
|
||||||
|
Note that TypeScript return types will only be inferred properly
|
||||||
|
from static analysis if the `withFileTypes` option is omitted, or
|
||||||
|
a constant `true` or `false` value.
|
||||||
|
|
||||||
|
### Class `PathScurry`
|
||||||
|
|
||||||
|
The main interface. Defaults to an appropriate class based on the
|
||||||
|
current platform.
|
||||||
|
|
||||||
|
Use `PathScurryWin32`, `PathScurryDarwin`, or `PathScurryPosix`
|
||||||
|
if implementation-specific behavior is desired.
|
||||||
|
|
||||||
|
All walk methods may be called with a `WalkOptions` argument to
|
||||||
|
walk over the object's current working directory with the
|
||||||
|
supplied options.
|
||||||
|
|
||||||
|
#### `async pw.walk(entry?: string | Path | WalkOptions, opts?: WalkOptions)`
|
||||||
|
|
||||||
|
Walk the directory tree according to the options provided,
|
||||||
|
resolving to an array of all entries found.
|
||||||
|
|
||||||
|
#### `pw.walkSync(entry?: string | Path | WalkOptions, opts?: WalkOptions)`
|
||||||
|
|
||||||
|
Walk the directory tree according to the options provided,
|
||||||
|
returning an array of all entries found.
|
||||||
|
|
||||||
|
#### `pw.iterate(entry?: string | Path | WalkOptions, opts?: WalkOptions)`
|
||||||
|
|
||||||
|
Iterate over the directory asynchronously, for use with `for
|
||||||
|
await of`. This is also the default async iterator method.
|
||||||
|
|
||||||
|
#### `pw.iterateSync(entry?: string | Path | WalkOptions, opts?: WalkOptions)`
|
||||||
|
|
||||||
|
Iterate over the directory synchronously, for use with `for of`.
|
||||||
|
This is also the default sync iterator method.
|
||||||
|
|
||||||
|
#### `pw.stream(entry?: string | Path | WalkOptions, opts?: WalkOptions)`
|
||||||
|
|
||||||
|
Return a [Minipass](http://npm.im/minipass) stream that emits
|
||||||
|
each entry or path string in the walk. Results are made available
|
||||||
|
asynchronously.
|
||||||
|
|
||||||
|
#### `pw.streamSync(entry?: string | Path | WalkOptions, opts?: WalkOptions)`
|
||||||
|
|
||||||
|
Return a [Minipass](http://npm.im/minipass) stream that emits
|
||||||
|
each entry or path string in the walk. Results are made available
|
||||||
|
synchronously, meaning that the walk will complete in a single
|
||||||
|
tick if the stream is fully consumed.
|
||||||
|
|
||||||
|
#### `pw.cwd`
|
||||||
|
|
||||||
|
Path object representing the current working directory for the
|
||||||
|
PathScurry.
|
||||||
|
|
||||||
|
#### `pw.chdir(path: string)`
|
||||||
|
|
||||||
|
Set the new effective current working directory for the scurry
|
||||||
|
object, so that `path.relative()` and `path.relativePosix()`
|
||||||
|
return values relative to the new cwd path.
|
||||||
|
|
||||||
|
#### `pw.depth(path?: Path | string): number`
|
||||||
|
|
||||||
|
Return the depth of the specified path (or the PathScurry cwd)
|
||||||
|
within the directory tree.
|
||||||
|
|
||||||
|
Root entries have a depth of `0`.
|
||||||
|
|
||||||
|
#### `pw.resolve(...paths: string[])`
|
||||||
|
|
||||||
|
Caching `path.resolve()`.
|
||||||
|
|
||||||
|
Significantly faster than `path.resolve()` if called repeatedly
|
||||||
|
with the same paths. Significantly slower otherwise, as it builds
|
||||||
|
out the cached Path entries.
|
||||||
|
|
||||||
|
To get a `Path` object resolved from the `PathScurry`, use
|
||||||
|
`pw.cwd.resolve(path)`. Note that `Path.resolve` only takes a
|
||||||
|
single string argument, not multiple.
|
||||||
|
|
||||||
|
#### `pw.resolvePosix(...paths: string[])`
|
||||||
|
|
||||||
|
Caching `path.resolve()`, but always using posix style paths.
|
||||||
|
|
||||||
|
This is identical to `pw.resolve(...paths)` on posix systems (ie,
|
||||||
|
everywhere except Windows).
|
||||||
|
|
||||||
|
On Windows, it returns the full absolute UNC path using `/`
|
||||||
|
separators. Ie, instead of `'C:\\foo\\bar`, it would return
|
||||||
|
`//?/C:/foo/bar`.
|
||||||
|
|
||||||
|
#### `pw.relative(path: string | Path): string`
|
||||||
|
|
||||||
|
Return the relative path from the PathWalker cwd to the supplied
|
||||||
|
path string or entry.
|
||||||
|
|
||||||
|
If the nearest common ancestor is the root, then an absolute path
|
||||||
|
is returned.
|
||||||
|
|
||||||
|
#### `pw.relativePosix(path: string | Path): string`
|
||||||
|
|
||||||
|
Return the relative path from the PathWalker cwd to the supplied
|
||||||
|
path string or entry, using `/` path separators.
|
||||||
|
|
||||||
|
If the nearest common ancestor is the root, then an absolute path
|
||||||
|
is returned.
|
||||||
|
|
||||||
|
On posix platforms (ie, all platforms except Windows), this is
|
||||||
|
identical to `pw.relative(path)`.
|
||||||
|
|
||||||
|
On Windows systems, it returns the resulting string as a
|
||||||
|
`/`-delimited path. If an absolute path is returned (because the
|
||||||
|
target does not share a common ancestor with `pw.cwd`), then a
|
||||||
|
full absolute UNC path will be returned. Ie, instead of
|
||||||
|
`'C:\\foo\\bar`, it would return `//?/C:/foo/bar`.
|
||||||
|
|
||||||
|
#### `pw.basename(path: string | Path): string`
|
||||||
|
|
||||||
|
Return the basename of the provided string or Path.
|
||||||
|
|
||||||
|
#### `pw.dirname(path: string | Path): string`
|
||||||
|
|
||||||
|
Return the parent directory of the supplied string or Path.
|
||||||
|
|
||||||
|
#### `async pw.readdir(dir = pw.cwd, opts = { withFileTypes: true })`
|
||||||
|
|
||||||
|
Read the directory and resolve to an array of strings if
|
||||||
|
`withFileTypes` is explicitly set to `false` or Path objects
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
Can be called as `pw.readdir({ withFileTypes: boolean })` as
|
||||||
|
well.
|
||||||
|
|
||||||
|
Returns `[]` if no entries are found, or if any error occurs.
|
||||||
|
|
||||||
|
Note that TypeScript return types will only be inferred properly
|
||||||
|
from static analysis if the `withFileTypes` option is omitted, or
|
||||||
|
a constant `true` or `false` value.
|
||||||
|
|
||||||
|
#### `pw.readdirSync(dir = pw.cwd, opts = { withFileTypes: true })`
|
||||||
|
|
||||||
|
Synchronous `pw.readdir()`
|
||||||
|
|
||||||
|
#### `async pw.readlink(link = pw.cwd, opts = { withFileTypes: false })`
|
||||||
|
|
||||||
|
Call `fs.readlink` on the supplied string or Path object, and
|
||||||
|
return the result.
|
||||||
|
|
||||||
|
Can be called as `pw.readlink({ withFileTypes: boolean })` as
|
||||||
|
well.
|
||||||
|
|
||||||
|
Returns `undefined` if any error occurs (for example, if the
|
||||||
|
argument is not a symbolic link), or a `Path` object if
|
||||||
|
`withFileTypes` is explicitly set to `true`, or a string
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
Note that TypeScript return types will only be inferred properly
|
||||||
|
from static analysis if the `withFileTypes` option is omitted, or
|
||||||
|
a constant `true` or `false` value.
|
||||||
|
|
||||||
|
#### `pw.readlinkSync(link = pw.cwd, opts = { withFileTypes: false })`
|
||||||
|
|
||||||
|
Synchronous `pw.readlink()`
|
||||||
|
|
||||||
|
#### `async pw.lstat(entry = pw.cwd)`
|
||||||
|
|
||||||
|
Call `fs.lstat` on the supplied string or Path object, and fill
|
||||||
|
in as much information as possible, returning the updated `Path`
|
||||||
|
object.
|
||||||
|
|
||||||
|
Returns `undefined` if the entry does not exist, or if any error
|
||||||
|
is encountered.
|
||||||
|
|
||||||
|
Note that some `Stats` data (such as `ino`, `dev`, and `mode`)
|
||||||
|
will not be supplied. For those things, you'll need to call
|
||||||
|
`fs.lstat` yourself.
|
||||||
|
|
||||||
|
#### `pw.lstatSync(entry = pw.cwd)`
|
||||||
|
|
||||||
|
Synchronous `pw.lstat()`
|
||||||
|
|
||||||
|
#### `pw.realpath(entry = pw.cwd, opts = { withFileTypes: false })`
|
||||||
|
|
||||||
|
Call `fs.realpath` on the supplied string or Path object, and
|
||||||
|
return the realpath if available.
|
||||||
|
|
||||||
|
Returns `undefined` if any error occurs.
|
||||||
|
|
||||||
|
May be called as `pw.realpath({ withFileTypes: boolean })` to run
|
||||||
|
on `pw.cwd`.
|
||||||
|
|
||||||
|
#### `pw.realpathSync(entry = pw.cwd, opts = { withFileTypes: false })`
|
||||||
|
|
||||||
|
Synchronous `pw.realpath()`
|
||||||
|
|
||||||
|
### Class `Path` implements [fs.Dirent](https://nodejs.org/docs/latest/api/fs.html#class-fsdirent)
|
||||||
|
|
||||||
|
Object representing a given path on the filesystem, which may or
|
||||||
|
may not exist.
|
||||||
|
|
||||||
|
Note that the actual class in use will be either `PathWin32` or
|
||||||
|
`PathPosix`, depending on the implementation of `PathScurry` in
|
||||||
|
use. They differ in the separators used to split and join path
|
||||||
|
strings, and the handling of root paths.
|
||||||
|
|
||||||
|
In `PathPosix` implementations, paths are split and joined using
|
||||||
|
the `'/'` character, and `'/'` is the only root path ever in use.
|
||||||
|
|
||||||
|
In `PathWin32` implementations, paths are split using either
|
||||||
|
`'/'` or `'\\'` and joined using `'\\'`, and multiple roots may
|
||||||
|
be in use based on the drives and UNC paths encountered. UNC
|
||||||
|
paths such as `//?/C:/` that identify a drive letter, will be
|
||||||
|
treated as an alias for the same root entry as their associated
|
||||||
|
drive letter (in this case `'C:\\'`).
|
||||||
|
|
||||||
|
#### `path.name`
|
||||||
|
|
||||||
|
Name of this file system entry.
|
||||||
|
|
||||||
|
**Important**: _always_ test the path name against any test
|
||||||
|
string using the `isNamed` method, and not by directly comparing
|
||||||
|
this string. Otherwise, unicode path strings that the system sees
|
||||||
|
as identical will not be properly treated as the same path,
|
||||||
|
leading to incorrect behavior and possible security issues.
|
||||||
|
|
||||||
|
#### `path.isNamed(name: string): boolean`
|
||||||
|
|
||||||
|
Return true if the path is a match for the given path name. This
|
||||||
|
handles case sensitivity and unicode normalization.
|
||||||
|
|
||||||
|
Note: even on case-sensitive systems, it is **not** safe to test
|
||||||
|
the equality of the `.name` property to determine whether a given
|
||||||
|
pathname matches, due to unicode normalization mismatches.
|
||||||
|
|
||||||
|
Always use this method instead of testing the `path.name`
|
||||||
|
property directly.
|
||||||
|
|
||||||
|
#### `path.isCWD`
|
||||||
|
|
||||||
|
Set to true if this `Path` object is the current working
|
||||||
|
directory of the `PathScurry` collection that contains it.
|
||||||
|
|
||||||
|
#### `path.getType()`
|
||||||
|
|
||||||
|
Returns the type of the Path object, `'File'`, `'Directory'`,
|
||||||
|
etc.
|
||||||
|
|
||||||
|
#### `path.isType(t: type)`
|
||||||
|
|
||||||
|
Returns true if `is{t}()` returns true.
|
||||||
|
|
||||||
|
For example, `path.isType('Directory')` is equivalent to
|
||||||
|
`path.isDirectory()`.
|
||||||
|
|
||||||
|
#### `path.depth()`
|
||||||
|
|
||||||
|
Return the depth of the Path entry within the directory tree.
|
||||||
|
Root paths have a depth of `0`.
|
||||||
|
|
||||||
|
#### `path.fullpath()`
|
||||||
|
|
||||||
|
The fully resolved path to the entry.
|
||||||
|
|
||||||
|
#### `path.fullpathPosix()`
|
||||||
|
|
||||||
|
The fully resolved path to the entry, using `/` separators.
|
||||||
|
|
||||||
|
On posix systems, this is identical to `path.fullpath()`. On
|
||||||
|
windows, this will return a fully resolved absolute UNC path
|
||||||
|
using `/` separators. Eg, instead of `'C:\\foo\\bar'`, it will
|
||||||
|
return `'//?/C:/foo/bar'`.
|
||||||
|
|
||||||
|
#### `path.isFile()`, `path.isDirectory()`, etc.
|
||||||
|
|
||||||
|
Same as the identical `fs.Dirent.isX()` methods.
|
||||||
|
|
||||||
|
#### `path.isUnknown()`
|
||||||
|
|
||||||
|
Returns true if the path's type is unknown. Always returns true
|
||||||
|
when the path is known to not exist.
|
||||||
|
|
||||||
|
#### `path.resolve(p: string)`
|
||||||
|
|
||||||
|
Return a `Path` object associated with the provided path string
|
||||||
|
as resolved from the current Path object.
|
||||||
|
|
||||||
|
#### `path.relative(): string`
|
||||||
|
|
||||||
|
Return the relative path from the PathWalker cwd to the supplied
|
||||||
|
path string or entry.
|
||||||
|
|
||||||
|
If the nearest common ancestor is the root, then an absolute path
|
||||||
|
is returned.
|
||||||
|
|
||||||
|
#### `path.relativePosix(): string`
|
||||||
|
|
||||||
|
Return the relative path from the PathWalker cwd to the supplied
|
||||||
|
path string or entry, using `/` path separators.
|
||||||
|
|
||||||
|
If the nearest common ancestor is the root, then an absolute path
|
||||||
|
is returned.
|
||||||
|
|
||||||
|
On posix platforms (ie, all platforms except Windows), this is
|
||||||
|
identical to `pw.relative(path)`.
|
||||||
|
|
||||||
|
On Windows systems, it returns the resulting string as a
|
||||||
|
`/`-delimited path. If an absolute path is returned (because the
|
||||||
|
target does not share a common ancestor with `pw.cwd`), then a
|
||||||
|
full absolute UNC path will be returned. Ie, instead of
|
||||||
|
`'C:\\foo\\bar`, it would return `//?/C:/foo/bar`.
|
||||||
|
|
||||||
|
#### `async path.readdir()`
|
||||||
|
|
||||||
|
Return an array of `Path` objects found by reading the associated
|
||||||
|
path entry.
|
||||||
|
|
||||||
|
If path is not a directory, or if any error occurs, returns `[]`,
|
||||||
|
and marks all children as provisional and non-existent.
|
||||||
|
|
||||||
|
#### `path.readdirSync()`
|
||||||
|
|
||||||
|
Synchronous `path.readdir()`
|
||||||
|
|
||||||
|
#### `async path.readlink()`
|
||||||
|
|
||||||
|
Return the `Path` object referenced by the `path` as a symbolic
|
||||||
|
link.
|
||||||
|
|
||||||
|
If the `path` is not a symbolic link, or any error occurs,
|
||||||
|
returns `undefined`.
|
||||||
|
|
||||||
|
#### `path.readlinkSync()`
|
||||||
|
|
||||||
|
Synchronous `path.readlink()`
|
||||||
|
|
||||||
|
#### `async path.lstat()`
|
||||||
|
|
||||||
|
Call `lstat` on the path object, and fill it in with details
|
||||||
|
determined.
|
||||||
|
|
||||||
|
If path does not exist, or any other error occurs, returns
|
||||||
|
`undefined`, and marks the path as "unknown" type.
|
||||||
|
|
||||||
|
#### `path.lstatSync()`
|
||||||
|
|
||||||
|
Synchronous `path.lstat()`
|
||||||
|
|
||||||
|
#### `async path.realpath()`
|
||||||
|
|
||||||
|
Call `realpath` on the path, and return a Path object
|
||||||
|
corresponding to the result, or `undefined` if any error occurs.
|
||||||
|
|
||||||
|
#### `path.realpathSync()`
|
||||||
|
|
||||||
|
Synchornous `path.realpath()`
|
||||||
72
patcher/vendor/asar/node_modules/path-scurry/package.json
generated
vendored
Normal file
72
patcher/vendor/asar/node_modules/path-scurry/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
{
|
||||||
|
"name": "path-scurry",
|
||||||
|
"version": "2.0.2",
|
||||||
|
"description": "walk paths fast and efficiently",
|
||||||
|
"author": "Isaac Z. Schlueter <i@izs.me> (https://blog.izs.me)",
|
||||||
|
"main": "./dist/commonjs/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"preversion": "npm test",
|
||||||
|
"postversion": "npm publish",
|
||||||
|
"prepublishOnly": "git push origin --follow-tags",
|
||||||
|
"prepare": "tshy",
|
||||||
|
"pretest": "npm run prepare",
|
||||||
|
"presnap": "npm run prepare",
|
||||||
|
"test": "tap",
|
||||||
|
"snap": "tap",
|
||||||
|
"format": "prettier --write . --log-level warn",
|
||||||
|
"typedoc": "typedoc --tsconfig tsconfig-esm.json ./src/*.ts",
|
||||||
|
"bench": "bash ./scripts/bench.sh"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nodelib/fs.walk": "^3.0.1",
|
||||||
|
"@types/node": "^25.3.0",
|
||||||
|
"mkdirp": "^3.0.0",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"rimraf": "^6.1.3",
|
||||||
|
"tap": "^21.6.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tshy": "^3.3.2",
|
||||||
|
"typedoc": "^0.28.17"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/isaacs/path-scurry"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^11.0.0",
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"tshy": {
|
||||||
|
"selfLink": false,
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"types": "./dist/commonjs/index.d.ts",
|
||||||
|
"module": "./dist/esm/index.js"
|
||||||
|
}
|
||||||
9
patcher/vendor/asar/package.json
vendored
Normal file
9
patcher/vendor/asar/package.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "bettercf-asar-vendor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Vendored @electron/asar for offline, zero-delay CurseForge patching",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@electron/asar": "^4.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue