From b251bc34f3fb6b476758a5c17b0dc1618af22733 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Sat, 9 Aug 2025 18:48:34 -0400 Subject: [PATCH] feat(toolbox): ui update --- .gitignore | 2 + build-python/meson.build | 19 + electron/bridge.py | 127 + electron/fourdst-backend.spec | 52 + electron/index.html | 104 + electron/main.js | 299 +++ electron/package-lock.json | 3873 +++++++++++++++++++++++++++ electron/package.json | 58 + electron/renderer.js | 488 ++++ electron/styles.css | 426 +++ fourdst/cli/bundle/clear.py | 70 +- fourdst/cli/bundle/create.py | 149 +- fourdst/cli/bundle/diff.py | 140 +- fourdst/cli/bundle/fill.py | 270 +- fourdst/cli/bundle/inspect.py | 279 +- fourdst/cli/bundle/sign.py | 143 +- fourdst/cli/bundle/validate.py | 259 +- fourdst/cli/common/config.py | 23 +- fourdst/cli/common/utils.py | 365 +-- fourdst/cli/main.py | 8 + fourdst/cli/templates/plugin.cpp.in | 1 + fourdst/core/__init__.py | 0 fourdst/core/build.py | 212 ++ fourdst/core/bundle.py | 1079 ++++++++ fourdst/core/config.py | 21 + fourdst/core/platform.py | 253 ++ fourdst/core/utils.py | 47 + meson.build | 19 + meson_options.txt | 1 + pyproject.toml | 5 +- 30 files changed, 7525 insertions(+), 1267 deletions(-) create mode 100644 electron/bridge.py create mode 100644 electron/fourdst-backend.spec create mode 100644 electron/index.html create mode 100644 electron/main.js create mode 100644 electron/package-lock.json create mode 100644 electron/package.json create mode 100644 electron/renderer.js create mode 100644 electron/styles.css create mode 100644 fourdst/core/__init__.py create mode 100644 fourdst/core/build.py create mode 100644 fourdst/core/bundle.py create mode 100644 fourdst/core/config.py create mode 100644 fourdst/core/platform.py create mode 100644 fourdst/core/utils.py diff --git a/.gitignore b/.gitignore index d22baaa..0363b39 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,5 @@ output/ .idea/ scratch/ + +node_modules/ diff --git a/build-python/meson.build b/build-python/meson.build index 3c99163..c9cc470 100644 --- a/build-python/meson.build +++ b/build-python/meson.build @@ -97,3 +97,22 @@ py_installation.install_sources( ), subdir: 'fourdst/cli/plugin' ) + +py_installation.install_sources( + files( + meson.project_source_root() + '/fourdst/core/__init__.py', + meson.project_source_root() + '/fourdst/core/build.py', + meson.project_source_root() + '/fourdst/core/bundle.py', + meson.project_source_root() + '/fourdst/core/config.py', + meson.project_source_root() + '/fourdst/core/platform.py', + meson.project_source_root() + '/fourdst/core/utils.py' + ), + subdir: 'fourdst/core' +) + +py_installation.install_sources( + files( + meson.project_source_root() + '/electron/bridge.py', + ), + subdir: 'fourdst/electron' +) diff --git a/electron/bridge.py b/electron/bridge.py new file mode 100644 index 0000000..e93934c --- /dev/null +++ b/electron/bridge.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +""" +Electron Bridge Script for 4DSTAR Bundle Management + +UPDATED ARCHITECTURE (2025-08-09): +===================================== + +This bridge script has been simplified to work with the refactored core functions +that now return JSON directly. No more complex stdout mixing or data wrapping. + +Key Changes: +- Core functions return JSON-serializable dictionaries directly +- Progress messages go to stderr only (never mixed with JSON output) +- Clean JSON output to stdout for Electron to parse +- Simplified error handling with consistent JSON error format +""" + +import sys +import json +import inspect +import traceback +from pathlib import Path +import datetime + +# Custom JSON encoder to handle Path and datetime objects +class FourdstEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, Path): + return str(o) + if isinstance(o, (datetime.datetime, datetime.date)): + return o.isoformat() + return super().default(o) + +# Add the project root to the Python path to allow importing 'fourdst' +project_root = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(project_root)) + +from fourdst.core import bundle + +def main(): + # Use stderr for all logging to avoid interfering with JSON output on stdout + log_file = sys.stderr + print("--- Python backend bridge started ---", file=log_file, flush=True) + + if len(sys.argv) < 3: + print(f"FATAL: Not enough arguments provided. Got {len(sys.argv)}. Exiting.", file=log_file, flush=True) + # Return JSON error even for argument errors + error_response = { + 'success': False, + 'error': f'Invalid arguments. Expected: . Got {len(sys.argv)} args.' + } + print(json.dumps(error_response), flush=True) + sys.exit(1) + + command = sys.argv[1] + args_json = sys.argv[2] + print(f"[BRIDGE_INFO] Received command: {command}", file=log_file, flush=True) + print(f"[BRIDGE_INFO] Received raw args: {args_json}", file=log_file, flush=True) + + try: + kwargs = json.loads(args_json) + print(f"[BRIDGE_INFO] Parsed kwargs: {kwargs}", file=log_file, flush=True) + + # Convert path strings to Path objects where needed + for key, value in kwargs.items(): + if isinstance(value, str) and ('path' in key.lower() or 'key' in key.lower()): + kwargs[key] = Path(value) + elif isinstance(value, list) and 'dirs' in key.lower(): + kwargs[key] = [Path(p) for p in value] + + func = getattr(bundle, command) + + # Create progress callback that sends structured progress to stderr + # This keeps progress separate from the final JSON result on stdout + def progress_callback(message): + # Progress goes to stderr to avoid mixing with JSON output + if isinstance(message, dict): + # Structured progress message (e.g., from fill_bundle) + progress_msg = f"[PROGRESS] {json.dumps(message)}" + else: + # Simple string message + progress_msg = f"[PROGRESS] {message}" + print(progress_msg, file=log_file, flush=True) + + # Inspect the function signature to see if it accepts 'progress_callback'. + sig = inspect.signature(func) + if 'progress_callback' in sig.parameters: + kwargs['progress_callback'] = progress_callback + + print(f"[BRIDGE_INFO] Calling function `bundle.{command}`...", file=log_file, flush=True) + result = func(**kwargs) + print(f"[BRIDGE_INFO] Function returned successfully.", file=log_file, flush=True) + + # Core functions now return JSON-serializable dictionaries directly + # No need for wrapping or complex data transformation + if result is None: + # Fallback for functions that might still return None + result = { + 'success': True, + 'message': f'{command} completed successfully.' + } + + # Send the result directly as JSON to stdout + print("[BRIDGE_INFO] Sending JSON response to stdout.", file=log_file, flush=True) + json_response = json.dumps(result, cls=FourdstEncoder) + print(json_response, flush=True) + print("--- Python backend bridge finished successfully ---", file=log_file, flush=True) + + except Exception as e: + # Get the full traceback for detailed debugging + tb_str = traceback.format_exc() + # Print the traceback to stderr so it appears in the terminal + print(f"[BRIDGE_ERROR] Exception occurred: {tb_str}", file=sys.stderr, flush=True) + + # Send consistent JSON error response to stdout + error_response = { + 'success': False, + 'error': f'Bridge error in {command}: {str(e)}', + 'traceback': tb_str # Include traceback for debugging + } + json_response = json.dumps(error_response, cls=FourdstEncoder) + print(json_response, flush=True) + print("--- Python backend bridge finished with error ---", file=sys.stderr, flush=True) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/electron/fourdst-backend.spec b/electron/fourdst-backend.spec new file mode 100644 index 0000000..60ea4b3 --- /dev/null +++ b/electron/fourdst-backend.spec @@ -0,0 +1,52 @@ +# -*- mode: python ; coding: utf-8 -*- + +import sys +from pathlib import Path + +# This is a PyInstaller spec file. It is used to bundle the Python backend +# into a single executable that can be shipped with the Electron app. + +# The project_root is the 'fourdst/' directory that contains 'electron/', 'fourdst/', etc. +# SPECPATH is a variable provided by PyInstaller that contains the absolute path +# to the directory containing the spec file. +project_root = Path(SPECPATH).parent + +# We need to add the project root to the path so that PyInstaller can find the 'fourdst' module. +sys.path.insert(0, str(project_root)) + +# The main script to be bundled. +analysis = Analysis(['bridge.py'], + pathex=[str(project_root)], + binaries=[], + # Add any modules that PyInstaller might not find automatically. + hiddenimports=['docker'], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=None, + noarchive=False) + +pyz = PYZ(analysis.pure, analysis.zipped_data, + cipher=None) + +exe = EXE(pyz, + analysis.scripts, + [], + exclude_binaries=True, + name='fourdst-backend', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True ) + +coll = COLLECT(exe, + analysis.binaries, + analysis.zipfiles, + analysis.datas, + strip=False, + upx=True, + upx_exclude=[], + name='fourdst-backend') diff --git a/electron/index.html b/electron/index.html new file mode 100644 index 0000000..0e66ca0 --- /dev/null +++ b/electron/index.html @@ -0,0 +1,104 @@ + + + + + 4DSTAR Bundle Manager + + + + +
+ + +
+
+

Welcome to 4DSTAR Bundle Manager

+

Open or create a bundle to get started.

+
+ + + + +
+
+ + + + + + + + + + + + + diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 0000000..d42dbc8 --- /dev/null +++ b/electron/main.js @@ -0,0 +1,299 @@ +const { app, BrowserWindow, ipcMain, dialog, nativeTheme } = require('electron'); +const path = require('path'); +const fs = require('fs-extra'); +const yaml = require('js-yaml'); +const AdmZip = require('adm-zip'); +const { spawn } = require('child_process'); + +// Handle creating/removing shortcuts on Windows when installing/uninstalling. +if (require('electron-squirrel-startup')) { + app.quit(); +} + +let mainWindow; + +const createWindow = () => { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + enableRemoteModule: true, + }, + }); + + // and load the index.html of the app. + mainWindow.loadFile(path.join(__dirname, 'index.html')); + + // Open the DevTools for debugging + // mainWindow.webContents.openDevTools(); + + nativeTheme.on('updated', () => { + if (mainWindow) { + mainWindow.webContents.send('theme-updated', { shouldUseDarkColors: nativeTheme.shouldUseDarkColors }); + } + }); +}; + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', createWindow); + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +ipcMain.handle('get-dark-mode', () => { + return nativeTheme.shouldUseDarkColors; +}); + +ipcMain.on('show-error-dialog', (event, { title, content }) => { + dialog.showErrorBox(title, content); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('activate', () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); + +// IPC handlers +ipcMain.handle('select-file', async () => { + const result = await dialog.showOpenDialog({ + properties: ['openFile'], + filters: [ + { name: 'Fbundle Archives', extensions: ['fbundle'] }, + { name: 'All Files', extensions: ['*'] } + ] + }); + + if (!result.canceled && result.filePaths.length > 0) { + return result.filePaths[0]; + } + return null; +}); + +ipcMain.handle('select-directory', async () => { + const result = await dialog.showOpenDialog({ + properties: ['openDirectory'] + }); + + if (!result.canceled && result.filePaths.length > 0) { + return result.filePaths[0]; + } + return null; +}); + +ipcMain.handle('select-save-file', async () => { + const result = await dialog.showSaveDialog({ + filters: [ + { name: 'Fbundle Archives', extensions: ['fbundle'] } + ] + }); + + if (!result.canceled) { + return result.filePath; + } + return null; +}); + +// Helper function to run python commands via the bundled backend +function runPythonCommand(command, kwargs, event) { + const buildDir = path.resolve(__dirname, '..', 'build'); + let backendPath; + if (app.isPackaged) { + backendPath = path.join(process.resourcesPath, 'fourdst-backend'); + } else { + backendPath = path.join(buildDir, 'electron', 'dist', 'fourdst-backend', 'fourdst-backend'); + } + + console.log(`[MAIN_PROCESS] Spawning backend: ${backendPath}`); + const args = [command, JSON.stringify(kwargs)]; + console.log(`[MAIN_PROCESS] With args: [${args.join(', ')}]`); + + return new Promise((resolve) => { + const process = spawn(backendPath, args); + let stdoutBuffer = ''; + let errorOutput = ''; + + process.stderr.on('data', (data) => { + errorOutput += data.toString(); + console.error('Backend STDERR:', data.toString().trim()); + }); + + const isStreaming = command === 'fill_bundle'; + + process.stdout.on('data', (data) => { + const chunk = data.toString(); + stdoutBuffer += chunk; + + if (isStreaming && event) { + // Process buffer line by line for streaming commands + let newlineIndex; + while ((newlineIndex = stdoutBuffer.indexOf('\n')) >= 0) { + const line = stdoutBuffer.substring(0, newlineIndex).trim(); + stdoutBuffer = stdoutBuffer.substring(newlineIndex + 1); + + if (line) { + try { + const parsed = JSON.parse(line); + if (parsed.type === 'progress') { + event.sender.send('fill-bundle-progress', parsed.data); + } else { + // Not a progress update, put it back in the buffer for final processing + stdoutBuffer = line + '\n' + stdoutBuffer; + break; // Stop processing lines + } + } catch (e) { + // Ignore parsing errors for intermediate lines in a stream + } + } + } + } + }); + + process.on('close', (code) => { + console.log(`[MAIN_PROCESS] Backend process exited with code ${code}`); + let resultData = null; + + try { + // Core functions now return clean JSON directly + const finalJson = JSON.parse(stdoutBuffer.trim()); + resultData = finalJson; // Use the JSON response directly + } catch (e) { + console.error(`[MAIN_PROCESS] Could not parse backend output as JSON: ${e}`); + console.error(`[MAIN_PROCESS] Raw output: "${stdoutBuffer}"`); + // If parsing fails, return a structured error response + resultData = { + success: false, + error: `JSON parsing failed: ${e.message}`, + raw_output: stdoutBuffer + }; + } + + const finalError = errorOutput.trim(); + if (finalError && !resultData) { + resolve({ success: false, error: finalError }); + } else if (resultData) { + resolve(resultData); + } else { + const errorMessage = finalError || `The script finished without returning a result (exit code: ${code})`; + resolve({ success: false, error: errorMessage }); + } + }); + + process.on('error', (err) => { + resolve({ success: false, error: `Failed to start backend process: ${err.message}` }); + }); + }); +} + +ipcMain.handle('create-bundle', async (event, bundleData) => { + const kwargs = { + plugin_dirs: bundleData.pluginDirs, + output_path: bundleData.outputPath, + bundle_name: bundleData.bundleName, + bundle_version: bundleData.bundleVersion, + bundle_author: bundleData.bundleAuthor, + bundle_comment: bundleData.bundleComment, + }; + + const result = await runPythonCommand('create_bundle', kwargs, event); + + // The renderer expects a 'path' property on success + if (result.success) { + result.path = bundleData.outputPath; + } + + return result; +}); + +ipcMain.handle('sign-bundle', async (event, bundlePath) => { + // Prompt for private key + const result = await dialog.showOpenDialog({ + properties: ['openFile'], + title: 'Select Private Key', + filters: [{ name: 'PEM Private Key', extensions: ['pem'] }], + }); + + if (result.canceled || !result.filePaths || result.filePaths.length === 0) { + return { success: false, error: 'Private key selection was canceled.' }; + } + + const privateKeyPath = result.filePaths[0]; + + const kwargs = { + bundle_path: bundlePath, + private_key: privateKeyPath, + }; + + return runPythonCommand('sign_bundle', kwargs, event); +}); + +ipcMain.handle('validate-bundle', async (event, bundlePath) => { + const kwargs = { + bundle_path: bundlePath + }; + return runPythonCommand('validate_bundle', kwargs, event); +}); + +ipcMain.handle('clear-bundle', async (event, bundlePath) => { + const kwargs = { bundle_path: bundlePath }; + return runPythonCommand('clear_bundle', kwargs, event); +}); + +ipcMain.handle('get-fillable-targets', async (event, bundlePath) => { + const kwargs = { bundle_path: bundlePath }; + return runPythonCommand('get_fillable_targets', kwargs, event); +}); + +ipcMain.handle('fill-bundle', async (event, { bundlePath, targetsToBuild }) => { + const kwargs = { + bundle_path: bundlePath, + targets_to_build: targetsToBuild + }; + + // Pass event to stream progress + return runPythonCommand('fill_bundle', kwargs, event); +}); + +ipcMain.handle('edit-bundle', async (event, { bundlePath, updatedManifest }) => { + const kwargs = { + bundle_path: bundlePath, + metadata: updatedManifest + }; + return runPythonCommand('edit_bundle_metadata', kwargs, event); +}); + +ipcMain.handle('open-bundle', async (event, bundlePath) => { + console.log(`[IPC_HANDLER] Opening bundle: ${bundlePath}`); + const kwargs = { bundle_path: bundlePath }; + const result = await runPythonCommand('inspect_bundle', kwargs, event); + + console.log(`[IPC_HANDLER] inspect_bundle result:`, result); + + // Core functions now return consistent JSON structure directly + if (result && result.success) { + // The core inspect_bundle function returns the data directly + // We just need to add the bundlePath for the renderer + return { + success: true, + manifest: result.manifest, + report: result, // The entire result is the report + bundlePath: bundlePath + }; + } + + // Return error as-is since it's already in the correct format + return result || { success: false, error: 'An unknown error occurred while opening the bundle.' }; +}); diff --git a/electron/package-lock.json b/electron/package-lock.json new file mode 100644 index 0000000..c183aa9 --- /dev/null +++ b/electron/package-lock.json @@ -0,0 +1,3873 @@ +{ + "name": "fourdst-bundle-manager", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fourdst-bundle-manager", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@electron/remote": "^2.0.0", + "adm-zip": "^0.5.14", + "fs-extra": "^11.0.0", + "js-yaml": "^4.1.0", + "python-shell": "^5.0.0" + }, + "devDependencies": { + "adm-zip": "^0.5.14", + "electron": "^31.0.2", + "electron-builder": "^24.0.0", + "electron-squirrel-startup": "^1.0.1" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz", + "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", + "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/remote": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@electron/remote/-/remote-2.1.3.tgz", + "integrity": "sha512-XlpxC8S4ttj/v2d+PKp9na/3Ev8bV7YWNL7Cw5b9MAWgTphEml7iYgbc7V0r9D6yDOfOkj06bchZgOZdlWJGNA==", + "license": "MIT", + "peerDependencies": { + "electron": ">= 13.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", + "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz", + "integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz", + "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.2.1", + "@electron/osx-sign": "1.0.5", + "@electron/universal": "1.5.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.8", + "electron-publish": "24.13.1", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^5.1.1", + "read-config-file": "6.3.2", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "24.13.3", + "electron-builder-squirrel-windows": "24.13.3" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz", + "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", + "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.10", + "typescript": "^5.3.3" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", + "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz", + "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "31.7.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-31.7.7.tgz", + "integrity": "sha512-HZtZg8EHsDGnswFt0QeV8If8B+et63uD6RJ7I4/xhcXqmTIbI08GoubX/wm+HdY0DwcuPe1/xsgqpmYvjdjRoA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz", + "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "dmg-builder": "24.13.3", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.3.2", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz", + "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "archiver": "^5.3.1", + "builder-util": "24.13.1", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", + "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-squirrel-startup": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/electron-squirrel-startup/-/electron-squirrel-startup-1.0.1.tgz", + "integrity": "sha512-sTfFIHGku+7PsHLJ7v0dRcZNkALrV+YEozINTW8X1nM//e5O3L+rfYuvSW00lmGHnYmUjARZulD8F2V8ISI9RA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.2.0" + } + }, + "node_modules/electron-squirrel-startup/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/electron-squirrel-startup/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/isbinaryfile": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.4.tgz", + "integrity": "sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/python-shell": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/python-shell/-/python-shell-5.0.0.tgz", + "integrity": "sha512-RUOOOjHLhgR1MIQrCtnEqz/HJ1RMZBIN+REnpSUrfft2bXqXy69fwJASVziWExfFXsR1bCY0TznnHooNsCo0/w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-config-file": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", + "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-file-ts": "^0.2.4", + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/electron/package.json b/electron/package.json new file mode 100644 index 0000000..ff9896e --- /dev/null +++ b/electron/package.json @@ -0,0 +1,58 @@ +{ + "name": "fourdst-bundle-manager", + "version": "1.0.0", + "description": "Electron app for managing fbundle archives", + "main": "main.js", + "scripts": { + "start": "electron .", + "dev": "electron .", + "build": "electron-builder", + "pack": "electron-builder --dir" + }, + "repository": { + "type": "git", + "url": "https://github.com/tboudreaux/fourdst" + }, + "keywords": [ + "Electron", + "fbundle", + "4DSTAR" + ], + "author": "4DSTAR Team", + "license": "MIT", + "devDependencies": { + "electron": "^31.0.2", + "adm-zip": "^0.5.14", + "electron-builder": "^24.0.0", + "electron-squirrel-startup": "^1.0.1" + }, + "dependencies": { + "fs-extra": "^11.0.0", + "js-yaml": "^4.1.0", + "adm-zip": "^0.5.14", + "@electron/remote": "^2.0.0", + "python-shell": "^5.0.0" + }, + "build": { + "appId": "com.fourdst.bundlemanager", + "productName": "4DSTAR Bundle Manager", + "directories": { + "output": "dist" + }, + "mac": { + "category": "public.app-category.developer-tools", + "target": [ + "dmg", + "zip" + ] + }, + "linux": { + "target": [ + "AppImage", + "deb", + "rpm" + ], + "category": "Development" + } + } +} diff --git a/electron/renderer.js b/electron/renderer.js new file mode 100644 index 0000000..2da30d6 --- /dev/null +++ b/electron/renderer.js @@ -0,0 +1,488 @@ +const { ipcRenderer } = require('electron'); +const path = require('path'); + +// --- STATE --- +let currentBundle = null; + +// --- DOM ELEMENTS --- +// Views +const welcomeScreen = document.getElementById('welcome-screen'); +const bundleView = document.getElementById('bundle-view'); +const createBundleForm = document.getElementById('create-bundle-form'); // This will be a modal later + +// Sidebar buttons +const openBundleBtn = document.getElementById('open-bundle-btn'); +const createBundleBtn = document.getElementById('create-bundle-btn'); + +// Bundle action buttons +const editBundleBtn = document.getElementById('edit-bundle-btn'); +const signBundleBtn = document.getElementById('sign-bundle-btn'); +const validateBundleBtn = document.getElementById('validate-bundle-btn'); +const fillBundleBtn = document.getElementById('fill-bundle-btn'); +const clearBundleBtn = document.getElementById('clear-bundle-btn'); + +// Bundle display +const bundleTitle = document.getElementById('bundle-title'); +const manifestDetails = document.getElementById('manifest-details'); +const pluginsList = document.getElementById('plugins-list'); +const validationResults = document.getElementById('validation-results'); + +// Tabs +const tabLinks = document.querySelectorAll('.tab-link'); +const tabPanes = document.querySelectorAll('.tab-pane'); +const validationTabLink = document.querySelector('button[data-tab="validation-tab"]'); + +// Modal +const modal = document.getElementById('modal'); +const modalTitle = document.getElementById('modal-title'); +const modalMessage = document.getElementById('modal-message'); +const modalCloseBtn = document.getElementById('modal-close-btn'); + +// Spinner +const spinner = document.getElementById('spinner'); + +// Fill Modal elements +const fillModal = document.getElementById('fill-modal'); +const closeFillModalButton = document.querySelector('.close-fill-modal-button'); +const fillModalTitle = document.getElementById('fill-modal-title'); +const fillModalBody = document.getElementById('fill-modal-body'); +const fillTargetsList = document.getElementById('fill-targets-list'); +const startFillButton = document.getElementById('start-fill-button'); +const fillProgressView = document.getElementById('fill-progress-view'); +const fillProgressList = document.getElementById('fill-progress-list'); + +let currentBundlePath = null; + +// --- INITIALIZATION --- +document.addEventListener('DOMContentLoaded', async () => { + // Set initial view + showView('welcome-screen'); + + // Set initial theme + const isDarkMode = await ipcRenderer.invoke('get-dark-mode'); + document.body.classList.toggle('dark-mode', isDarkMode); + + // Setup event listeners + setupEventListeners(); +}); + +// --- EVENT LISTENERS --- +function setupEventListeners() { + // Theme updates + ipcRenderer.on('theme-updated', (event, { shouldUseDarkColors }) => { + document.body.classList.toggle('dark-mode', shouldUseDarkColors); + }); + + // Sidebar navigation + openBundleBtn.addEventListener('click', handleOpenBundle); + createBundleBtn.addEventListener('click', () => { + // TODO: Replace with modal + showView('create-bundle-form'); + showModal('Not Implemented', 'The create bundle form will be moved to a modal dialog.'); + }); + + // Tab navigation + tabLinks.forEach(link => { + link.addEventListener('click', () => switchTab(link.dataset.tab)); + }); + + // Modal close button + modalCloseBtn.addEventListener('click', hideModal); + + // Bundle actions + signBundleBtn.addEventListener('click', handleSignBundle); + validateBundleBtn.addEventListener('click', handleValidateBundle); + clearBundleBtn.addEventListener('click', handleClearBundle); + fillBundleBtn.addEventListener('click', async () => { + if (!currentBundlePath) { + showModal('Error', 'No bundle is currently open.'); + return; + } + showSpinner(); + const result = await ipcRenderer.invoke('get-fillable-targets', currentBundlePath); + hideSpinner(); + + if (!result.success) { + showModal('Error', `Failed to get fillable targets: ${result.error}`); + return; + } + + const targets = result.data; + if (Object.keys(targets).length === 0) { + showModal('Info', 'The bundle is already full. No new targets to build.'); + return; + } + + populateFillTargetsList(targets); + fillModal.style.display = 'block'; + }); + + closeFillModalButton.addEventListener('click', () => { + fillModal.style.display = 'none'; + }); + + function populateFillTargetsList(plugins) { + fillTargetsList.innerHTML = ''; + for (const [pluginName, targets] of Object.entries(plugins)) { + if (targets.length > 0) { + const pluginHeader = document.createElement('h4'); + pluginHeader.textContent = `Plugin: ${pluginName}`; + fillTargetsList.appendChild(pluginHeader); + + targets.forEach(target => { + const item = document.createElement('div'); + item.className = 'fill-target-item'; + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = true; + checkbox.id = `target-${pluginName}-${target.triplet}`; + checkbox.dataset.pluginName = pluginName; + checkbox.dataset.targetTriplet = target.triplet; + checkbox.dataset.targetInfo = JSON.stringify(target); + + const label = document.createElement('label'); + label.htmlFor = checkbox.id; + label.textContent = `${target.triplet} (${target.type})`; + + item.appendChild(checkbox); + item.appendChild(label); + fillTargetsList.appendChild(item); + }); + } + } + // Reset view + fillModalBody.style.display = 'block'; + fillProgressView.style.display = 'none'; + } + + startFillButton.addEventListener('click', async () => { + const selectedTargets = {}; + const checkboxes = fillTargetsList.querySelectorAll('input[type="checkbox"]:checked'); + + if (checkboxes.length === 0) { + showModal('Info', 'No targets selected to fill.'); + return; + } + + checkboxes.forEach(cb => { + const pluginName = cb.dataset.pluginName; + if (!selectedTargets[pluginName]) { + selectedTargets[pluginName] = []; + } + selectedTargets[pluginName].push(JSON.parse(cb.dataset.targetInfo)); + }); + + fillModalBody.style.display = 'none'; + fillProgressView.style.display = 'block'; + fillModalTitle.textContent = 'Filling Bundle...'; + populateFillProgressList(selectedTargets); + + const result = await ipcRenderer.invoke('fill-bundle', { + bundlePath: currentBundlePath, + targetsToBuild: selectedTargets + }); + + fillModalTitle.textContent = 'Fill Complete'; + if (!result.success) { + // A final error message if the whole process fails. + const p = document.createElement('p'); + p.style.color = 'var(--error-color)'; + p.textContent = `Error: ${result.error}`; + fillProgressList.appendChild(p); + } + }); + + function populateFillProgressList(plugins) { + fillProgressList.innerHTML = ''; + for (const [pluginName, targets] of Object.entries(plugins)) { + targets.forEach(target => { + const item = document.createElement('div'); + item.className = 'fill-target-item'; + item.id = `progress-${pluginName}-${target.triplet}`; + + const indicator = document.createElement('div'); + indicator.className = 'progress-indicator'; + + const label = document.createElement('span'); + label.textContent = `${pluginName} - ${target.triplet}`; + + item.appendChild(indicator); + item.appendChild(label); + fillProgressList.appendChild(item); + }); + } + } + + ipcRenderer.on('fill-bundle-progress', (event, progress) => { + console.log('Progress update:', progress); + if (typeof progress === 'object' && progress.status) { + const { status, plugin, target, message } = progress; + const progressItem = document.getElementById(`progress-${plugin}-${target}`); + if (progressItem) { + const indicator = progressItem.querySelector('.progress-indicator'); + indicator.className = 'progress-indicator'; // Reset classes + switch (status) { + case 'building': + indicator.classList.add('spinner-icon'); + break; + case 'success': + indicator.classList.add('success-icon'); + break; + case 'failure': + indicator.classList.add('failure-icon'); + break; + } + const label = progressItem.querySelector('span'); + if (message) { + label.textContent = `${plugin} - ${target}: ${message}`; + } + } + } else if (typeof progress === 'object' && progress.message) { + // Handle final completion message + if (progress.message.includes('āœ…')) { + fillModalTitle.textContent = 'Fill Complete!'; + } + } else { + // Handle simple string progress messages + const p = document.createElement('p'); + p.textContent = progress; + fillProgressList.appendChild(p); + } + }); +} + +// --- VIEW AND UI LOGIC --- +function showView(viewId) { + [welcomeScreen, bundleView, createBundleForm].forEach(view => { + view.classList.toggle('hidden', view.id !== viewId); + }); +} + +function switchTab(tabId) { + tabPanes.forEach(pane => { + pane.classList.toggle('active', pane.id === tabId); + }); + tabLinks.forEach(link => { + link.classList.toggle('active', link.dataset.tab === tabId); + }); +} + +function showSpinner() { + spinner.classList.remove('hidden'); +} + +function hideSpinner() { + spinner.classList.add('hidden'); +} + +function showModal(title, message, type = 'info') { + modalTitle.textContent = title; + modalMessage.innerHTML = message; // Use innerHTML to allow for formatted messages + modal.classList.remove('hidden'); +} + +function hideModal() { + modal.classList.add('hidden'); +} + +// --- BUNDLE ACTIONS HANDLERS --- +async function handleOpenBundle() { + const bundlePath = await ipcRenderer.invoke('select-file'); + if (!bundlePath) return; + + showSpinner(); + showModal('Opening...', `Opening bundle: ${path.basename(bundlePath)}`); + const result = await ipcRenderer.invoke('open-bundle', bundlePath); + hideSpinner(); + + if (result.success) { + currentBundle = result; + currentBundlePath = bundlePath; + displayBundleInfo(result.report); + showView('bundle-view'); + hideModal(); + } else { + showModal('Error Opening Bundle', `Failed to open bundle: ${result ? result.error : 'Unknown error'}`); + } +} + +async function handleSignBundle() { + if (!currentBundlePath) return; + + const result = await ipcRenderer.invoke('select-private-key'); + if (result.canceled || !result.filePaths.length) { + return; // User canceled the dialog + } + const privateKeyPath = result.filePaths[0]; + + showSpinner(); + const signResult = await ipcRenderer.invoke('sign-bundle', { bundlePath: currentBundlePath, privateKey: privateKeyPath }); + hideSpinner(); + + if (signResult.success) { + showModal('Success', 'Bundle signed successfully. Reloading...'); + await reloadCurrentBundle(); + hideModal(); + } else { + showModal('Sign Error', `Failed to sign bundle: ${signResult.error}`); + } +} + +async function handleValidateBundle() { + if (!currentBundlePath) return; + + showSpinner(); + const result = await ipcRenderer.invoke('validate-bundle', currentBundlePath); + hideSpinner(); + + if (result.success) { + const validation = result.data; + const validationIssues = validation.errors.concat(validation.warnings); + + if (validationIssues.length > 0) { + validationResults.textContent = validationIssues.join('\n'); + validationTabLink.classList.remove('hidden'); + } else { + validationResults.textContent = 'Bundle is valid.'; + validationTabLink.classList.add('hidden'); + } + // Switch to the validation tab to show the results. + switchTab('validation-tab'); + showModal('Validation Complete', 'Validation check has finished.'); + + } else { + showModal('Validation Error', `Failed to validate bundle: ${result.error}`); + } +} + +async function handleClearBundle() { + if (!currentBundlePath) return; + + showSpinner(); + const result = await ipcRenderer.invoke('clear-bundle', currentBundlePath); + hideSpinner(); + + if (result.success) { + showModal('Success', 'All binaries have been cleared. Reloading...'); + await reloadCurrentBundle(); + hideModal(); + } else { + showModal('Clear Error', `Failed to clear binaries: ${result.error}`); + } +} + +async function handleFillBundle() { + if (!currentBundle) return showModal('Action Canceled', 'Please open a bundle first.'); + + showSpinner(); + showModal('Filling Bundle...', 'Adding local binaries to bundle.'); + const result = await ipcRenderer.invoke('fill-bundle', currentBundle.bundlePath); + hideSpinner(); + + if (result.success) { + showModal('Success', 'Binaries filled successfully. Reloading...'); + await reloadCurrentBundle(); + hideModal(); + } else { + showModal('Fill Error', `Failed to fill bundle: ${result.error}`); + } +} + +// --- DATA DISPLAY --- +async function reloadCurrentBundle() { + if (!currentBundle) return; + const reloadResult = await ipcRenderer.invoke('open-bundle', currentBundle.bundlePath); + if (reloadResult.success) { + currentBundle = reloadResult; + displayBundleInfo(reloadResult.report); + } else { + showModal('Reload Error', `Failed to reload bundle details: ${reloadResult.error}`); + } +} + +function displayBundleInfo(report) { + if (!report) { + showModal('Display Error', 'Could not load bundle information.'); + return; + } + + const { manifest, signature, validation, plugins } = report; + + // Set bundle title + bundleTitle.textContent = manifest.bundleName || 'Untitled Bundle'; + + // --- Overview Tab --- + const trustStatus = signature.status || 'UNSIGNED'; + const trustColorClass = { + 'TRUSTED': 'trusted', + 'UNTRUSTED': 'untrusted', + 'INVALID': 'untrusted', + 'TAMPERED': 'untrusted', + 'UNSIGNED': 'unsigned', + 'ERROR': 'untrusted', + 'UNSUPPORTED': 'warning' + }[trustStatus] || 'unsigned'; + + manifestDetails.innerHTML = ` +
+
+

Trust Status

+
+
+ ${trustStatus} +
+
+
+
+

Manifest Details

+
+

Version: ${manifest.bundleVersion || 'N/A'}

+

Author: ${manifest.bundleAuthor || 'N/A'}

+

Bundled On: ${manifest.bundledOn || 'N/A'}

+

Comment: ${manifest.bundleComment || 'N/A'}

+ ${manifest.bundleAuthorKeyFingerprint ? `

Author Key: ${manifest.bundleAuthorKeyFingerprint}

` : ''} + ${manifest.bundleSignature ? `

Signature: ${manifest.bundleSignature}

` : ''} +
+
+ `; + + // --- Plugins Tab --- + pluginsList.innerHTML = ''; + if (plugins && Object.keys(plugins).length > 0) { + Object.entries(plugins).forEach(([pluginName, pluginData]) => { + const binariesInfo = pluginData.binaries.map(b => { + const compatClass = b.is_compatible ? 'compatible' : 'incompatible'; + const compatText = b.is_compatible ? 'Compatible' : 'Incompatible'; + const platformTriplet = b.platform && b.platform.triplet ? `(${b.platform.triplet})` : ''; + return `
  • ${b.path} ${platformTriplet} - ${compatText}
  • `; + }).join(''); + + const pluginCard = document.createElement('div'); + pluginCard.className = 'card'; + pluginCard.innerHTML = ` +

    ${pluginName}

    +
    +

    Source: ${pluginData.sdist_path}

    +

    Binaries:

    +
      ${binariesInfo.length > 0 ? binariesInfo : '
    • No binaries found.
    • '}
    +
    + `; + pluginsList.appendChild(pluginCard); + }); + } else { + pluginsList.innerHTML = '

    No plugins found in this bundle.

    '; + } + + // --- Validation Tab --- + const validationIssues = validation.errors.concat(validation.warnings); + if (validationIssues.length > 0) { + validationResults.textContent = validationIssues.join('\n'); + validationTabLink.classList.remove('hidden'); + } else { + validationResults.textContent = 'Bundle is valid.'; + validationTabLink.classList.add('hidden'); + } + + // Reset to overview tab by default + switchTab('overview-tab'); +} diff --git a/electron/styles.css b/electron/styles.css new file mode 100644 index 0000000..3c8e17b --- /dev/null +++ b/electron/styles.css @@ -0,0 +1,426 @@ +/* Modern CSS for 4DSTAR Bundle Manager - v2 */ + +/* Global Resets and Variables */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + --bg-color: #f4f7fa; + --sidebar-bg: #ffffff; + --content-bg: #ffffff; + --text-color: #2c3e50; + --text-light: #7f8c8d; + --border-color: #e1e5e8; + --primary-color: #3498db; + --primary-hover: #2980b9; + --danger-color: #e74c3c; + --success-color: #27ae60; + --warning-color: #f39c12; + --sidebar-width: 220px; + --header-height: 60px; +} + +body.dark-mode { + --bg-color: #2c3e50; + --sidebar-bg: #34495e; + --content-bg: #34495e; + --text-color: #ecf0f1; + --text-light: #95a5a6; + --border-color: #4a6278; + --primary-color: #3498db; + --primary-hover: #4aa3df; +} + +body { + font-family: var(--font-family); + background-color: var(--bg-color); + color: var(--text-color); + transition: background-color 0.2s, color 0.2s; + overflow: hidden; +} + +/* Main Layout */ +.main-container { + display: flex; + height: 100vh; +} + +.sidebar { + width: var(--sidebar-width); + background-color: var(--sidebar-bg); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + flex-shrink: 0; + transition: background-color 0.2s; +} + +.sidebar-header { + padding: 20px; + border-bottom: 1px solid var(--border-color); + text-align: center; +} + +.sidebar-header h3 { + font-size: 1.2rem; + font-weight: 600; +} + +.sidebar-nav { + padding: 15px 10px; + flex-grow: 1; +} + +.nav-button { + display: block; + width: 100%; + padding: 12px 15px; + margin-bottom: 8px; + border: none; + border-radius: 6px; + background-color: transparent; + color: var(--text-color); + font-size: 0.95rem; + text-align: left; + cursor: pointer; + transition: background-color 0.2s, color 0.2s; +} + +.nav-button:hover { + background-color: var(--primary-color); + color: white; +} + +.nav-button.active { + background-color: var(--primary-color); + color: white; + font-weight: 600; +} + +.sidebar-footer { + padding: 20px; + text-align: center; + font-size: 0.8rem; + color: var(--text-light); +} + +/* Content Area */ +.content-area { + flex-grow: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +#welcome-screen { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + text-align: center; + color: var(--text-light); +} + +#welcome-screen h1 { + font-size: 2rem; + margin-bottom: 10px; +} + +.content-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 25px; + height: var(--header-height); + border-bottom: 1px solid var(--border-color); + background-color: var(--content-bg); + flex-shrink: 0; +} + +.content-header h2 { + font-size: 1.4rem; +} + +.action-buttons button { + margin-left: 10px; + padding: 8px 16px; + border-radius: 5px; + border: 1px solid var(--primary-color); + background-color: transparent; + color: var(--primary-color); + cursor: pointer; + transition: all 0.2s; +} + +.action-buttons button:hover { + background-color: var(--primary-color); + color: white; +} + +/* Tabs */ +.tab-nav { + display: flex; + padding: 0 25px; + border-bottom: 1px solid var(--border-color); + background-color: var(--content-bg); + flex-shrink: 0; +} + +.tab-link { + padding: 15px 20px; + border: none; + background: none; + cursor: pointer; + color: var(--text-light); + font-size: 1rem; + border-bottom: 3px solid transparent; + transition: color 0.2s, border-color 0.2s; +} + +.tab-link:hover { + color: var(--primary-color); +} + +.tab-link.active { + color: var(--text-color); + border-bottom-color: var(--primary-color); + font-weight: 600; +} + +#tab-content { + padding: 25px; + overflow-y: auto; + flex-grow: 1; + background-color: var(--bg-color); +} + +.tab-pane { + display: none; +} + +.tab-pane.active { + display: block; +} + +/* Card-based info display */ +.card { + background-color: var(--content-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 2px 5px rgba(0,0,0,0.05); +} + +.card-title { + font-size: 1.2rem; + font-weight: 600; + margin-bottom: 15px; + border-bottom: 1px solid var(--border-color); + padding-bottom: 10px; +} + +.info-grid { + display: grid; + grid-template-columns: 150px 1fr; + gap: 12px; +} + +.info-grid .label { + font-weight: 600; + color: var(--text-light); +} + +.info-grid .value.signature { + word-break: break-all; + font-family: monospace; + font-size: 0.9rem; +} + +/* Trust Indicator */ +.trust-indicator-container { + display: flex; + align-items: center; + gap: 10px; +} + +.trust-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + flex-shrink: 0; +} + +.trust-indicator.trusted { background-color: var(--success-color); } +.trust-indicator.untrusted { background-color: var(--danger-color); } +.trust-indicator.unsigned { background-color: var(--warning-color); } +.trust-indicator.warning { background-color: var(--warning-color); } + +/* Plugins List */ +#plugins-list .plugin-item { + background-color: var(--content-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 20px; + margin-bottom: 15px; +} + +#plugins-list h4 { + font-size: 1.1rem; + margin-bottom: 10px; +} + +/* Validation Results */ +#validation-results { + background-color: var(--content-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 20px; + white-space: pre-wrap; + word-wrap: break-word; + font-family: monospace; +} + +/* Modal */ +.modal-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background-color: var(--content-bg); + padding: 30px; + border-radius: 8px; + min-width: 400px; + max-width: 600px; + box-shadow: 0 5px 15px rgba(0,0,0,0.3); + position: relative; +} + +.modal-close { + position: absolute; + top: 15px; + right: 15px; + font-size: 1.5rem; + font-weight: bold; + cursor: pointer; + color: var(--text-light); +} + +.modal-close:hover { + color: var(--text-color); +} + +#modal-title { + font-size: 1.4rem; + margin-bottom: 20px; +} + +/* Utility */ +.hidden { + display: none; +} + +/* Fill Modal Specifics */ +#fill-targets-list, +#fill-progress-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 10px; + margin-top: 10px; + margin-bottom: 15px; +} + +.fill-target-item { + display: flex; + align-items: center; + padding: 8px; + border-bottom: 1px solid var(--border-color); +} + +.fill-target-item:last-child { + border-bottom: none; +} + +.fill-target-item label { + flex-grow: 1; + margin-left: 10px; +} + +.progress-indicator { + width: 20px; + height: 20px; + margin-right: 10px; + display: inline-block; + vertical-align: middle; +} + +.progress-indicator.spinner-icon { + border: 2px solid var(--text-color-light); + border-top: 2px solid var(--primary-color); + border-radius: 50%; + width: 16px; + height: 16px; + animation: spin 1s linear infinite; +} + +.progress-indicator.success-icon::before { + content: 'āœ”'; + color: var(--success-color); + font-size: 20px; +} + +.progress-indicator.failure-icon::before { + content: 'āœ–'; + color: var(--error-color); + font-size: 20px; +} + +#start-fill-button { + background-color: var(--primary-color); + color: white; + padding: 10px 15px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1em; + transition: background-color 0.3s; +} + +#start-fill-button:hover { + background-color: var(--primary-color-dark); +} + +.spinner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border: 4px solid rgba(0, 0, 0, 0.1); + width: 36px; + height: 36px; + border-radius: 50%; + border-left-color: var(--primary-color); + animation: spin 1s ease infinite; + z-index: 2000; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/fourdst/cli/bundle/clear.py b/fourdst/cli/bundle/clear.py index e5f59dd..ade7d43 100644 --- a/fourdst/cli/bundle/clear.py +++ b/fourdst/cli/bundle/clear.py @@ -1,69 +1,23 @@ # fourdst/cli/bundle/clear.py import typer -import yaml -import zipfile from pathlib import Path -import tempfile -import shutil + +from fourdst.core.bundle import clear_bundle def bundle_clear( - bundle_path: Path = typer.Argument(..., help="The path to the .fbundle file to clear.", exists=True, readable=True, writable=True) + bundle_path: Path = typer.Argument( + ..., + help="The path to the .fbundle file to clear.", + exists=True, + readable=True, + writable=True + ) ): """ - Removes all compiled binaries from a bundle, leaving only the source distributions. + Removes all compiled binaries and signatures from a bundle. """ - typer.echo(f"--- Clearing binaries from bundle: {bundle_path.name} ---") - try: - with tempfile.TemporaryDirectory() as temp_dir_str: - temp_dir = Path(temp_dir_str) - - # 1. Unpack the bundle - with zipfile.ZipFile(bundle_path, 'r') as bundle_zip: - bundle_zip.extractall(temp_dir) - - # 2. Read the manifest - manifest_path = temp_dir / "manifest.yaml" - if not manifest_path.is_file(): - typer.secho("Error: Bundle is invalid. Missing manifest.yaml.", fg=typer.colors.RED) - raise typer.Exit(code=1) - - with open(manifest_path, 'r') as f: - manifest = yaml.safe_load(f) - - # 3. Clear binaries and signatures - typer.echo("Clearing binaries and signature information...") - manifest.pop('bundleAuthorKeyFingerprint', None) - manifest.pop('checksums', None) - - for plugin_name, plugin_data in manifest.get('bundlePlugins', {}).items(): - if 'binaries' in plugin_data: - plugin_data['binaries'] = [] - - # 4. Delete the binaries directory and signature file - bin_dir = temp_dir / "bin" - if bin_dir.is_dir(): - shutil.rmtree(bin_dir) - typer.echo(" - Removed 'bin/' directory.") - - sig_file = temp_dir / "manifest.sig" - if sig_file.is_file(): - sig_file.unlink() - typer.echo(" - Removed 'manifest.sig'.") - - # 5. Write the updated manifest - with open(manifest_path, 'w') as f: - yaml.dump(manifest, f, sort_keys=False) - - # 6. Repack the bundle - typer.echo("Repacking the bundle...") - with zipfile.ZipFile(bundle_path, 'w', zipfile.ZIP_DEFLATED) as bundle_zip: - for file_path in temp_dir.rglob('*'): - if file_path.is_file(): - bundle_zip.write(file_path, file_path.relative_to(temp_dir)) - - typer.secho(f"\nāœ… Bundle '{bundle_path.name}' has been cleared of all binaries.", fg=typer.colors.GREEN) - + clear_bundle(bundle_path, progress_callback=typer.echo) except Exception as e: - typer.secho(f"An unexpected error occurred: {e}", fg=typer.colors.RED) + typer.secho(f"An error occurred while clearing the bundle: {e}", fg=typer.colors.RED) raise typer.Exit(code=1) diff --git a/fourdst/cli/bundle/create.py b/fourdst/cli/bundle/create.py index 9bfc4e1..299d58e 100644 --- a/fourdst/cli/bundle/create.py +++ b/fourdst/cli/bundle/create.py @@ -1,148 +1,37 @@ # fourdst/cli/bundle/create.py import typer -import os -import sys -import shutil -import datetime -import yaml -import zipfile from pathlib import Path -from fourdst.cli.common.utils import get_platform_identifier, get_macos_targeted_platform_identifier, run_command +import sys -bundle_app = typer.Typer() +from fourdst.core.bundle import create_bundle -@bundle_app.command("create") def bundle_create( plugin_dirs: list[Path] = typer.Argument(..., help="A list of plugin project directories to include.", exists=True, file_okay=False), output_bundle: Path = typer.Option("bundle.fbundle", "--out", "-o", help="The path for the output bundle file."), bundle_name: str = typer.Option("MyPluginBundle", "--name", help="The name of the bundle."), bundle_version: str = typer.Option("0.1.0", "--ver", help="The version of the bundle."), bundle_author: str = typer.Option("Unknown", "--author", help="The author of the bundle."), - # --- NEW OPTION --- + bundle_comment: str = typer.Option(None, "--comment", help="A comment to embed in the bundle."), target_macos_version: str = typer.Option(None, "--target-macos-version", help="The minimum macOS version to target (e.g., '12.0').") ): """ Builds and packages one or more plugin projects into a single .fbundle file. """ - staging_dir = Path("temp_bundle_staging") - if staging_dir.exists(): - shutil.rmtree(staging_dir) - staging_dir.mkdir() + def progress_callback(message): + typer.secho(message, fg=typer.colors.BRIGHT_BLUE) - # --- MODIFIED LOGIC --- - # Prepare environment for the build - build_env = os.environ.copy() - - # Determine the host platform identifier based on the target - if sys.platform == "darwin" and target_macos_version: - typer.secho(f"Targeting macOS version: {target_macos_version}", fg=typer.colors.CYAN) - host_platform = get_macos_targeted_platform_identifier(target_macos_version) - - # Set environment variables for Meson to pick up - flags = f"-mmacosx-version-min={target_macos_version}" - build_env["CXXFLAGS"] = f"{build_env.get('CXXFLAGS', '')} {flags}".strip() - build_env["LDFLAGS"] = f"{build_env.get('LDFLAGS', '')} {flags}".strip() - else: - # Default behavior for Linux or non-targeted macOS builds - host_platform = get_platform_identifier() - - manifest = { - "bundleName": bundle_name, - "bundleVersion": bundle_version, - "bundleAuthor": bundle_author, - "bundleComment": "Created with fourdst-cli", - "bundledOn": datetime.datetime.now().isoformat(), - "bundlePlugins": {} - } - - print("Creating bundle...") - for plugin_dir in plugin_dirs: - plugin_name = plugin_dir.name - print(f"--> Processing plugin: {plugin_name}") - - # 1. Build the plugin using the prepared environment - print(f" - Compiling for target platform...") - build_dir = plugin_dir / "builddir" - if build_dir.exists(): - shutil.rmtree(build_dir) # Reconfigure every time to apply env vars - - # Pass the modified environment to the Meson commands - run_command(["meson", "setup", "builddir"], cwd=plugin_dir, env=build_env) - run_command(["meson", "compile", "-C", "builddir"], cwd=plugin_dir, env=build_env) - - # 2. Find the compiled artifact - compiled_lib = next(build_dir.glob("lib*.so"), None) or next(build_dir.glob("lib*.dylib"), None) - if not compiled_lib: - print(f"Error: Could not find compiled library for {plugin_name} (expected lib*.so or lib*.dylib)", file=sys.stderr) - raise typer.Exit(code=1) - - # 3. Package source code (sdist), respecting .gitignore - print(" - Packaging source code (respecting .gitignore)...") - sdist_path = staging_dir / f"{plugin_name}_src.zip" - - git_check = run_command(["git", "rev-parse", "--is-inside-work-tree"], cwd=plugin_dir, check=False) - - files_to_include = [] - if git_check.returncode == 0: - result = run_command(["git", "ls-files", "--cached", "--others", "--exclude-standard"], cwd=plugin_dir) - files_to_include = [plugin_dir / f for f in result.stdout.strip().split('\n') if f] - else: - typer.secho(f" - Warning: '{plugin_dir.name}' is not a git repository. Packaging all files.", fg=typer.colors.YELLOW) - for root, _, files in os.walk(plugin_dir): - if 'builddir' in root: - continue - for file in files: - files_to_include.append(Path(root) / file) - - with zipfile.ZipFile(sdist_path, 'w', zipfile.ZIP_DEFLATED) as sdist_zip: - for file_path in files_to_include: - if file_path.is_file(): - sdist_zip.write(file_path, file_path.relative_to(plugin_dir)) - - # 4. Stage artifacts with ABI-tagged filenames and update manifest - binaries_dir = staging_dir / "bin" - binaries_dir.mkdir(exist_ok=True) - - base_name = compiled_lib.stem - ext = compiled_lib.suffix - triplet = host_platform["triplet"] - abi_signature = host_platform["abi_signature"] - tagged_filename = f"{base_name}.{triplet}.{abi_signature}{ext}" - staged_lib_path = binaries_dir / tagged_filename - - print(f" - Staging binary as: {tagged_filename}") - shutil.copy(compiled_lib, staged_lib_path) - - manifest["bundlePlugins"][plugin_name] = { - "sdist": { - "path": sdist_path.name, - "sdistBundledOn": datetime.datetime.now().isoformat(), - "buildable": True - }, - "binaries": [{ - "platform": { - "triplet": host_platform["triplet"], - "abi_signature": host_platform["abi_signature"], - # Adding arch separately for clarity, matching 'fill' command - "arch": host_platform["arch"] - }, - "path": staged_lib_path.relative_to(staging_dir).as_posix(), - "compiledOn": datetime.datetime.now().isoformat() - }] - } - - # 5. Write manifest and package final bundle - manifest_path = staging_dir / "manifest.yaml" - with open(manifest_path, 'w') as f: - yaml.dump(manifest, f, sort_keys=False) - - print(f"\nPackaging final bundle: {output_bundle}") - with zipfile.ZipFile(output_bundle, 'w', zipfile.ZIP_DEFLATED) as bundle_zip: - for root, _, files in os.walk(staging_dir): - for file in files: - file_path = Path(root) / file - bundle_zip.write(file_path, file_path.relative_to(staging_dir)) - - shutil.rmtree(staging_dir) - print("\nāœ… Bundle created successfully!") + try: + create_bundle( + plugin_dirs=plugin_dirs, + output_bundle=output_bundle, + bundle_name=bundle_name, + bundle_version=bundle_version, + bundle_author=bundle_author, + bundle_comment=bundle_comment, + target_macos_version=target_macos_version, + progress_callback=progress_callback + ) + except Exception as e: + typer.secho(f"Error creating bundle: {e}", fg=typer.colors.RED, err=True) + raise typer.Exit(code=1) diff --git a/fourdst/cli/bundle/diff.py b/fourdst/cli/bundle/diff.py index eaa77a0..0343de8 100644 --- a/fourdst/cli/bundle/diff.py +++ b/fourdst/cli/bundle/diff.py @@ -1,23 +1,14 @@ # fourdst/cli/bundle/diff.py import typer -import yaml -import zipfile from pathlib import Path -import tempfile -import shutil -import difflib from rich.console import Console from rich.panel import Panel from rich.text import Text from rich.table import Table -console = Console() +from fourdst.core.bundle import diff_bundle -def _get_file_content(directory: Path, filename: str): - file_path = directory / filename - if not file_path.exists(): - return None - return file_path.read_bytes() +console = Console() def bundle_diff( bundle_a_path: Path = typer.Argument(..., help="The first bundle to compare.", exists=True, readable=True), @@ -28,94 +19,59 @@ def bundle_diff( """ console.print(Panel(f"Comparing [bold blue]{bundle_a_path.name}[/bold blue] with [bold blue]{bundle_b_path.name}[/bold blue]")) - with tempfile.TemporaryDirectory() as temp_a_str, tempfile.TemporaryDirectory() as temp_b_str: - temp_a = Path(temp_a_str) - temp_b = Path(temp_b_str) + try: + results = diff_bundle(bundle_a_path, bundle_b_path, progress_callback=typer.echo) + except Exception as e: + typer.secho(f"Error comparing bundles: {e}", fg=typer.colors.RED) + raise typer.Exit(code=1) - # Unpack both bundles - with zipfile.ZipFile(bundle_a_path, 'r') as z: z.extractall(temp_a) - with zipfile.ZipFile(bundle_b_path, 'r') as z: z.extractall(temp_b) + # --- 1. Display Signature Differences --- + sig_status = results['signature']['status'] + style_map = { + 'UNCHANGED': ('[green]UNCHANGED[/green]', 'green'), + 'REMOVED': ('[yellow]REMOVED[/yellow]', 'yellow'), + 'ADDED': ('[yellow]ADDED[/yellow]', 'yellow'), + 'CHANGED': ('[bold red]CHANGED[/bold red]', 'red'), + 'UNSIGNED': ('[dim]Both Unsigned[/dim]', 'dim'), + } + sig_text, sig_style = style_map.get(sig_status, (sig_status, 'white')) + console.print(Panel(f"Signature Status: {sig_text}", title="[bold]Signature Verification[/bold]", border_style=sig_style, expand=False)) - # --- 1. Compare Signatures --- - sig_a = _get_file_content(temp_a, "manifest.sig") - sig_b = _get_file_content(temp_b, "manifest.sig") - - sig_panel_style = "green" - sig_status = "" - if sig_a == sig_b and sig_a is not None: - sig_status = "[green]UNCHANGED[/green]" - elif sig_a and not sig_b: - sig_status = "[yellow]REMOVED[/yellow]" - sig_panel_style = "yellow" - elif not sig_a and sig_b: - sig_status = "[yellow]ADDED[/yellow]" - sig_panel_style = "yellow" - elif sig_a and sig_b and sig_a != sig_b: - sig_status = "[bold red]CHANGED[/bold red]" - sig_panel_style = "red" - else: - sig_status = "[dim]Both Unsigned[/dim]" - sig_panel_style = "dim" - - console.print(Panel(f"Signature Status: {sig_status}", title="[bold]Signature Verification[/bold]", border_style=sig_panel_style, expand=False)) - - # --- 2. Compare Manifests --- - manifest_a_content = (temp_a / "manifest.yaml").read_text() - manifest_b_content = (temp_b / "manifest.yaml").read_text() - - if manifest_a_content != manifest_b_content: - diff = difflib.unified_diff( - manifest_a_content.splitlines(keepends=True), - manifest_b_content.splitlines(keepends=True), - fromfile=f"{bundle_a_path.name}/manifest.yaml", - tofile=f"{bundle_b_path.name}/manifest.yaml", - ) - - diff_text = Text() - for line in diff: - if line.startswith('+'): - diff_text.append(line, style="green") - elif line.startswith('-'): - diff_text.append(line, style="red") - elif line.startswith('^'): - diff_text.append(line, style="blue") - else: - diff_text.append(line) - - console.print(Panel(diff_text, title="[bold]Manifest Differences[/bold]", border_style="yellow")) - else: - console.print(Panel("[green]Manifests are identical.[/green]", title="[bold]Manifest[/bold]", border_style="green")) - - # --- 3. Compare File Contents (via checksums) --- - manifest_a = yaml.safe_load(manifest_a_content) - manifest_b = yaml.safe_load(manifest_b_content) - - files_a = {p['path']: p.get('checksum') for p in manifest_a.get('bundlePlugins', {}).get(next(iter(manifest_a.get('bundlePlugins', {})), ''), {}).get('binaries', [])} - files_b = {p['path']: p.get('checksum') for p in manifest_b.get('bundlePlugins', {}).get(next(iter(manifest_b.get('bundlePlugins', {})), ''), {}).get('binaries', [])} + # --- 2. Display Manifest Differences --- + manifest_diff = results['manifest']['diff'] + if manifest_diff: + diff_text = Text() + for line in manifest_diff: + if line.startswith('+'): + diff_text.append(line, style="green") + elif line.startswith('-'): + diff_text.append(line, style="red") + elif line.startswith('^'): + diff_text.append(line, style="blue") + else: + diff_text.append(line) + console.print(Panel(diff_text, title="[bold]Manifest Differences[/bold]", border_style="yellow")) + else: + console.print(Panel("[green]Manifests are identical.[/green]", title="[bold]Manifest[/bold]", border_style="green")) + # --- 3. Display File Content Differences --- + file_diffs = results['files'] + if file_diffs: table = Table(title="File Content Comparison") table.add_column("File Path", style="cyan") table.add_column("Status", style="magenta") table.add_column("Details", style="yellow") - all_files = sorted(list(set(files_a.keys()) | set(files_b.keys()))) - has_content_changes = False + status_map = { + 'REMOVED': '[red]REMOVED[/red]', + 'ADDED': '[green]ADDED[/green]', + 'MODIFIED': '[yellow]MODIFIED[/yellow]' + } - for file in all_files: - in_a = file in files_a - in_b = file in files_b - - if in_a and not in_b: - table.add_row(file, "[red]REMOVED[/red]", "") - has_content_changes = True - elif not in_a and in_b: - table.add_row(file, "[green]ADDED[/green]", "") - has_content_changes = True - elif files_a[file] != files_b[file]: - table.add_row(file, "[yellow]MODIFIED[/yellow]", f"Checksum changed from {files_a.get(file, 'N/A')} to {files_b.get(file, 'N/A')}") - has_content_changes = True + for diff in file_diffs: + status_text = status_map.get(diff['status'], diff['status']) + table.add_row(diff['path'], status_text, diff['details']) - if has_content_changes: - console.print(table) - else: - console.print(Panel("[green]All file contents are identical.[/green]", title="[bold]File Contents[/bold]", border_style="green")) + console.print(table) + else: + console.print(Panel("[green]All file contents are identical.[/green]", title="[bold]File Contents[/bold]", border_style="green")) diff --git a/fourdst/cli/bundle/fill.py b/fourdst/cli/bundle/fill.py index 3bc719b..42e65e2 100644 --- a/fourdst/cli/bundle/fill.py +++ b/fourdst/cli/bundle/fill.py @@ -7,6 +7,8 @@ import yaml import zipfile from pathlib import Path import questionary +from prompt_toolkit.key_binding import KeyBindings +from questionary.prompts.checkbox import checkbox import subprocess import sys import traceback @@ -21,11 +23,66 @@ from rich.panel import Panel console = Console() -from fourdst.cli.common.utils import get_available_build_targets, _build_plugin_in_docker, _build_plugin_for_target +from fourdst.core.bundle import get_fillable_targets, fill_bundle +from fourdst.cli.common.utils import run_command_rich # Keep for progress display if needed -bundle_app = typer.Typer() +custom_key_bindings = KeyBindings() + +def _is_arch(target_info, arch_keywords): + """Helper to check if a target's info contains architecture keywords.""" + # Combine all relevant string values from the target dict to check against. + text_to_check = "" + if 'triplet' in target_info: + text_to_check += target_info['triplet'].lower() + if 'docker_image' in target_info: + text_to_check += target_info['docker_image'].lower() + if 'cross_file' in target_info: + # Convert path to string for searching + text_to_check += str(target_info['cross_file']).lower() + + if not text_to_check: + return False + + return any(keyword in text_to_check for keyword in arch_keywords) + +@custom_key_bindings.add('c-a') +def _(event): + """ + Handler for Ctrl+A. Selects all ARM targets. + """ + control = event.app.layout.current_control + # Keywords to identify ARM architectures + arm_keywords = ['aarch64', 'arm64'] + + for i, choice in enumerate(control.choices): + # The choice.value is the dictionary we passed to questionary.Choice + target_info = choice.value.get('target', {}) + if _is_arch(target_info, arm_keywords): + # Add the index to the set of selected items + if i not in control.selected_indexes: + control.selected_indexes.add(i) + + # Redraw the UI to show the new selections + event.app.invalidate() + + +@custom_key_bindings.add('c-x') +def _(event): + """ + Handler for Ctrl+X. Selects all x86 targets. + """ + control = event.app.layout.current_control + # Keywords to identify x86 architectures + x86_keywords = ['x86_64', 'x86', 'amd64'] # 'amd64' is a common alias in Docker + + for i, choice in enumerate(control.choices): + target_info = choice.value.get('target', {}) + if _is_arch(target_info, x86_keywords): + if i not in control.selected_indexes: + control.selected_indexes.add(i) + + event.app.invalidate() -@bundle_app.command("fill") def bundle_fill(bundle_path: Path = typer.Argument(..., help="The .fbundle file to fill with new binaries.", exists=True)): """ Builds new binaries for the current host or cross-targets from the bundle's source. @@ -34,138 +91,95 @@ def bundle_fill(bundle_path: Path = typer.Argument(..., help="The .fbundle file if staging_dir.exists(): shutil.rmtree(staging_dir) + console.print(Panel(f"[bold]Filling Bundle:[/bold] {bundle_path.name}", expand=False, border_style="blue")) + + # 1. Find available targets and missing binaries using the core function try: - # 1. Unpack and load manifest - with zipfile.ZipFile(bundle_path, 'r') as bundle_zip: - bundle_zip.extractall(staging_dir) + fillable_targets = get_fillable_targets(bundle_path) + except Exception as e: + console.print(f"[red]Error analyzing bundle: {e}[/red]") + raise typer.Exit(code=1) - manifest_path = staging_dir / "manifest.yaml" - if not manifest_path.exists(): - typer.secho("Error: Bundle is invalid. Missing manifest.yaml.", fg=typer.colors.RED) - raise typer.Exit(code=1) + if not fillable_targets: + console.print("[green]āœ… Bundle is already full for all available build targets.[/green]") + raise typer.Exit() + + # 2. Create interactive choices for the user + build_options = [] + BOLD = "\033[1m" + RESET = "\033[0m" + CYAN = "\033[36m" + for plugin_name, targets in fillable_targets.items(): + for target in targets: + if target['type'] == 'docker': + display_name = f"Docker: {target['docker_image']}" + elif target['type'] == 'cross': + display_name = f"Cross-compile: {Path(target['cross_file']).name}" + else: # native + display_name = f"Native: {target['triplet']}" + + build_options.append({ + "name": f"Build {plugin_name} for {display_name}", + "value": {"plugin_name": plugin_name, "target": target} + }) - with open(manifest_path, 'r') as f: - manifest = yaml.safe_load(f) + # 3. Prompt user to select which targets to build + if not build_options: + console.print("[yellow]No buildable targets found.[/yellow]") + raise typer.Exit() - # 2. Find available targets and missing binaries - available_targets = get_available_build_targets() - build_options = [] - - for plugin_name, plugin_data in manifest.get('bundlePlugins', {}).items(): - if "sdist" not in plugin_data: - continue # Cannot build without source - - existing_abis = {b['platform']['abi_signature'] for b in plugin_data.get('binaries', [])} - - for target in available_targets: - # Use a more descriptive name for the choice - if target.get('docker_image', None): - display_name = f"Docker: {target['docker_image']}" - elif target.get('cross_file', None): - display_name = f"Cross: {Path(target['cross_file']).name}" - else: - display_name = f"Native: {target['abi_signature']} (Local System)" + choices = [ + questionary.Choice(title=opt['name'], value=opt['value']) + for opt in build_options + ] - if target['abi_signature'] not in existing_abis: - build_options.append({ - "name": f"Build '{plugin_name}' for {display_name}", - "plugin_name": plugin_name, - "target": target - }) - - if not build_options: - typer.secho("āœ… Bundle is already full for all available build targets.", fg=typer.colors.GREEN) - raise typer.Exit() - - # 3. Prompt user to select which targets to build - choices = [opt['name'] for opt in build_options] - selected_builds = questionary.checkbox( - "Select which missing binaries to build:", - choices=choices - ).ask() - - if not selected_builds: - typer.echo("No binaries selected to build. Exiting.") - raise typer.Exit() - - # 4. Build selected targets - for build_name in selected_builds: - build_job = next(opt for opt in build_options if opt['name'] == build_name) - plugin_name = build_job['plugin_name'] - target = build_job['target'] - - typer.secho(f"\nBuilding {plugin_name} for target '{build_name}'...", bold=True) - - sdist_zip_path = staging_dir / manifest['bundlePlugins'][plugin_name]['sdist']['path'] - build_temp_dir = staging_dir / f"build_{plugin_name}" + message = ( + "Select which missing binaries to build:\n" + " (Press [Ctrl+A] to select all ARM, [Ctrl+X] to select all x86)" + ) - try: - if target['docker_image']: - if not docker: - typer.secho("Error: Docker is not installed. Please install Docker to build this target.", fg=typer.colors.RED) - continue - compiled_lib, final_target = _build_plugin_in_docker(sdist_zip_path, build_temp_dir, target, plugin_name) - else: - compiled_lib, final_target = _build_plugin_for_target(sdist_zip_path, build_temp_dir, target) - - # Add new binary to bundle - abi_tag = final_target["abi_signature"] - base_name = compiled_lib.stem - ext = compiled_lib.suffix - triplet = final_target["triplet"] - tagged_filename = f"{base_name}.{triplet}.{abi_tag}{ext}" - - binaries_dir = staging_dir / "bin" - binaries_dir.mkdir(exist_ok=True) - staged_lib_path = binaries_dir / tagged_filename - shutil.move(compiled_lib, staged_lib_path) - - # Update manifest - new_binary_entry = { - "platform": { - "triplet": final_target["triplet"], - "abi_signature": abi_tag, - "arch": final_target["arch"] - }, - "path": staged_lib_path.relative_to(staging_dir).as_posix(), - "compiledOn": datetime.datetime.now().isoformat() - } - manifest['bundlePlugins'][plugin_name]['binaries'].append(new_binary_entry) - typer.secho(f" -> Successfully built and staged {tagged_filename}", fg=typer.colors.GREEN) + # --- START OF FIX --- + # 1. Instantiate the Checkbox class directly instead of using the shortcut. + prompt = checkbox( + message, + choices=choices, + # key_bindings=custom_key_bindings + ) - except (FileNotFoundError, subprocess.CalledProcessError) as e: - typer.secho(f" -> Failed to build {plugin_name} for target '{build_name}': {e}", fg=typer.colors.RED) - - tb_str = traceback.format_exc() - console.print(Panel( - tb_str, - title="Traceback", - border_style="yellow", - expand=False - )) - - finally: - if build_temp_dir.exists(): - shutil.rmtree(build_temp_dir) + # 2. Use .unsafe_ask() to run the prompt object. + selected_jobs = prompt.unsafe_ask() + # --- END OF FIX --- + + if not selected_jobs: + console.print("No binaries selected to build. Exiting.") + raise typer.Exit() - # 5. Repackage the bundle - # Invalidate any old signature - if "bundleAuthorKeyFingerprint" in manifest: - del manifest["bundleAuthorKeyFingerprint"] - if (staging_dir / "manifest.sig").exists(): - (staging_dir / "manifest.sig").unlink() - typer.secho("\nāš ļø Bundle signature has been invalidated by this operation. Please re-sign the bundle.", fg=typer.colors.YELLOW) + targets_to_build = {} + for job in selected_jobs: + plugin_name = job['plugin_name'] + target = job['target'] + if plugin_name not in targets_to_build: + targets_to_build[plugin_name] = [] + targets_to_build[plugin_name].append(target) - with open(manifest_path, 'w') as f: - yaml.dump(manifest, f, sort_keys=False) - - with zipfile.ZipFile(bundle_path, 'w', zipfile.ZIP_DEFLATED) as bundle_zip: - for file_path in staging_dir.rglob('*'): - if file_path.is_file(): - bundle_zip.write(file_path, file_path.relative_to(staging_dir)) - - typer.secho(f"\nāœ… Bundle '{bundle_path.name}' has been filled successfully.", fg=typer.colors.GREEN) + try: + console.print("--- Starting build process ---") + fill_bundle( + bundle_path, + targets_to_build, + progress_callback=lambda msg: console.print(f"[dim] {msg}[/dim]") + ) + console.print("--- Build process finished ---") + console.print(f"[green]āœ… Bundle '{bundle_path.name}' has been filled successfully.[/green]") + console.print("[yellow]āš ļø If the bundle was signed, the signature is now invalid. Please re-sign.[/yellow]") - finally: - if staging_dir.exists(): - shutil.rmtree(staging_dir) \ No newline at end of file + except Exception as e: + console.print(f"[red]An error occurred during the build process: {e}[/red]") + tb_str = traceback.format_exc() + console.print(Panel( + tb_str, + title="Traceback", + border_style="red", + expand=False + )) + raise typer.Exit(code=1) diff --git a/fourdst/cli/bundle/inspect.py b/fourdst/cli/bundle/inspect.py index 2090097..fc5e71f 100644 --- a/fourdst/cli/bundle/inspect.py +++ b/fourdst/cli/bundle/inspect.py @@ -1,206 +1,119 @@ # fourdst/cli/bundle/inspect.py import typer -import sys -import shutil -import yaml -import zipfile -import hashlib from pathlib import Path +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text -from cryptography.hazmat.primitives import serialization, hashes -from cryptography.hazmat.primitives.asymmetric import padding, rsa, ed25519 -from cryptography.exceptions import InvalidSignature +from fourdst.core.bundle import inspect_bundle -from fourdst.cli.common.config import LOCAL_TRUST_STORE_PATH -from fourdst.cli.common.utils import get_platform_identifier, calculate_sha256, is_abi_compatible +console = Console() -bundle_app = typer.Typer() - -def _reconstruct_canonical_checksum_list(staging_dir: Path, manifest: dict) -> tuple[str, list[str], list[str]]: +def display_inspection_report(report: dict): """ - Reconstructs the canonical checksum list from the files on disk - and compares them against the checksums listed in the manifest. - - Returns a tuple containing: - 1. The canonical string of actual checksums to verify against the signature. - 2. A list of files with checksum mismatches. - 3. A list of files that are listed in the manifest but missing from the disk. + Displays the inspection report using rich components. """ - checksum_map = {} - mismatch_errors = [] - missing_files = [] + manifest = report.get('manifest', {}) + host_info = report.get('host_info', {}) + validation = report.get('validation', {}) + signature = report.get('signature', {}) + plugins = report.get('plugins', {}) - all_files_in_manifest = [] - # Gather all file paths from the manifest - for plugin_data in manifest.get('bundlePlugins', {}).values(): - if 'sdist' in plugin_data and 'path' in plugin_data['sdist']: - all_files_in_manifest.append(plugin_data['sdist']) - if 'binaries' in plugin_data: - all_files_in_manifest.extend(plugin_data['binaries']) + # --- Header --- + console.print(Panel(f"Inspection Report for [bold blue]{manifest.get('bundleName', 'N/A')}[/bold blue]", expand=False)) + + meta_table = Table.grid(padding=(0, 2)) + meta_table.add_column() + meta_table.add_column() + meta_table.add_row("Name:", manifest.get('bundleName', 'N/A')) + meta_table.add_row("Version:", manifest.get('bundleVersion', 'N/A')) + meta_table.add_row("Author:", manifest.get('bundleAuthor', 'N/A')) + meta_table.add_row("Bundled On:", manifest.get('bundledOn', 'N/A')) + meta_table.add_row("Host ABI:", Text(host_info.get('abi_signature', 'N/A'), style="dim")) + meta_table.add_row("Host Arch:", Text(host_info.get('triplet', 'N/A'), style="dim")) + console.print(meta_table) + console.print("─" * 50) - for file_info in all_files_in_manifest: - path_str = file_info.get('path') - if not path_str: - continue + # --- Trust Status --- + status = signature.get('status', 'UNKNOWN') + if status == 'TRUSTED': + console.print(Panel(f"[bold green]āœ… Trust Status: SIGNED and TRUSTED[/bold green]\nKey: [dim]{signature.get('key_path')}[/dim]", expand=False, border_style="green")) + elif status == 'UNSIGNED': + console.print(Panel("[bold yellow]🟔 Trust Status: UNSIGNED[/bold yellow]", expand=False, border_style="yellow")) + elif status == 'UNTRUSTED': + console.print(Panel(f"[bold yellow]āš ļø Trust Status: SIGNED but UNTRUSTED AUTHOR[/bold yellow]\nFingerprint: [dim]{signature.get('fingerprint')}[/dim]", expand=False, border_style="yellow")) + elif status == 'INVALID': + console.print(Panel(f"[bold red]āŒ Trust Status: INVALID SIGNATURE[/bold red]\n{signature.get('reason')}", expand=False, border_style="red")) + elif status == 'TAMPERED': + console.print(Panel(f"[bold red]āŒ Trust Status: TAMPERED[/bold red]\n{signature.get('reason')}", expand=False, border_style="red")) + elif status == 'UNSUPPORTED': + console.print(Panel(f"[bold red]āŒ Trust Status: CRYPTOGRAPHY NOT SUPPORTED[/bold red]\n{signature.get('reason')}", expand=False, border_style="red")) + else: + console.print(Panel(f"[bold red]āŒ Trust Status: ERROR[/bold red]\n{signature.get('reason')}", expand=False, border_style="red")) - file_path = staging_dir / path_str - expected_checksum = file_info.get('checksum') + # --- Validation Issues --- + errors = validation.get('errors', []) + warnings = validation.get('warnings', []) + if errors or warnings: + console.print("─" * 50) + console.print("[bold]Validation Issues:[/bold]") + for error in errors: + console.print(Text(f" - [red]Error:[/red] {error}")) + for warning in warnings: + console.print(Text(f" - [yellow]Warning:[/yellow] {warning}")) - if not file_path.exists(): - missing_files.append(path_str) - continue + # --- Plugin Details --- + console.print("─" * 50) + console.print("[bold]Available Plugins:[/bold]") + if not plugins: + console.print(" No plugins found in bundle.") - # Calculate actual checksum from the file on disk - actual_checksum = "sha256:" + calculate_sha256(file_path) - checksum_map[path_str] = actual_checksum + for name, data in plugins.items(): + console.print(Panel(f"Plugin: [bold]{name}[/bold]", expand=False, border_style="blue")) + console.print(f" Source Dist: [dim]{data.get('sdist_path', 'N/A')}[/dim]") + + binaries = data.get('binaries', []) + if not binaries: + console.print(" Binaries: None") + else: + bin_table = Table(title="Binaries", show_header=True, header_style="bold magenta") + bin_table.add_column("Path") + bin_table.add_column("Architecture") + bin_table.add_column("ABI") + bin_table.add_column("Host Compatible?", style="cyan") + bin_table.add_column("Reason for Incompatibility", style="red") - # Compare with the checksum listed in the manifest - if expected_checksum and actual_checksum != expected_checksum: - mismatch_errors.append(path_str) + for b in binaries: + plat = b.get('platform', {}) + style = "green" if b.get('is_compatible') else "default" + compat_text = "āœ… Yes" if b.get('is_compatible') else "No" + reason = b.get('incompatibility_reason', '') or '' + bin_table.add_row( + Text(b.get('path', 'N/A'), style=style), + Text(plat.get('triplet', 'N/A'), style=style), + Text(plat.get('abi_signature', 'N/A'), style=style), + Text(compat_text, style="cyan"), + Text(reason, style="red") + ) + console.print(bin_table) - # Create the canonical string for signature verification from the actual file checksums - sorted_paths = sorted(checksum_map.keys()) - canonical_list = [f"{path}:{checksum_map[path]}" for path in sorted_paths] - data_to_verify = "\n".join(canonical_list) + if not data.get('compatible_found'): + console.print(Text(" Note: No compatible binary found for the current system.", style="yellow")) + console.print(Text(" Run 'fourdst bundle fill' to build one.", style="yellow")) - return data_to_verify, mismatch_errors, missing_files - - -@bundle_app.command("inspect") -def bundle_inspect(bundle_path: Path = typer.Argument(..., help="The .fbundle file to inspect.", exists=True)): +def bundle_inspect(bundle_path: Path = typer.Argument(..., help="The .fbundle file to inspect.", exists=True, resolve_path=True)): """ Inspects a bundle, validating its contents and cryptographic signature. """ - staging_dir = Path(f"temp_inspect_{bundle_path.stem}") - if staging_dir.exists(): - shutil.rmtree(staging_dir) - try: - host_platform = get_platform_identifier() - - with zipfile.ZipFile(bundle_path, 'r') as bundle_zip: - bundle_zip.extractall(staging_dir) - - manifest_path = staging_dir / "manifest.yaml" - if not manifest_path.exists(): - typer.secho("Error: Bundle is invalid. Missing manifest.yaml.", fg=typer.colors.RED) + report = inspect_bundle(bundle_path) + display_inspection_report(report) + # Exit with an error code if validation failed, to support scripting + if report.get('validation', {}).get('status') != 'passed': raise typer.Exit(code=1) - - with open(manifest_path, 'r') as f: - manifest = yaml.safe_load(f) + except Exception: + console.print_exception(show_locals=True) + raise typer.Exit(code=1) - typer.secho(f"--- Bundle Inspection Report for: {bundle_path.name} ---", bold=True) - # ... (header printing code is unchanged) ... - typer.echo(f"Name: {manifest.get('bundleName', 'N/A')}") - typer.echo(f"Version: {manifest.get('bundleVersion', 'N/A')}") - typer.echo(f"Author: {manifest.get('bundleAuthor', 'N/A')}") - typer.echo(f"Bundled: {manifest.get('bundledOn', 'N/A')}") - typer.secho(f"Host ABI: {host_platform['abi_signature']}", dim=True) - typer.secho(f"Host Arch: {host_platform['triplet']}", dim=True) - typer.echo("-" * 50) - - - # 3. Signature and Trust Verification - fingerprint = manifest.get('bundleAuthorKeyFingerprint') - sig_path = staging_dir / "manifest.sig" - - if not fingerprint or not sig_path.exists(): - typer.secho("Trust Status: 🟔 UNSIGNED", fg=typer.colors.YELLOW) - else: - trusted_key_path = None - if LOCAL_TRUST_STORE_PATH.exists(): - # Find the key in the local trust store - # ... (key finding logic is unchanged) ... - for key_file in LOCAL_TRUST_STORE_PATH.rglob("*.pem"): - try: - pub_der = (serialization.load_pem_public_key(key_file.read_bytes()) - .public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo - )) - pub_key_fingerprint = "sha256:" + hashlib.sha256(pub_der).hexdigest() - if pub_key_fingerprint == fingerprint: - trusted_key_path = key_file - break - except Exception: - continue - - if not trusted_key_path: - typer.secho(f"Trust Status: āš ļø SIGNED but UNTRUSTED AUTHOR ({fingerprint})", fg=typer.colors.YELLOW) - else: - # --- MODIFIED VERIFICATION LOGIC --- - try: - pub_key_obj = serialization.load_pem_public_key(trusted_key_path.read_bytes()) - signature = sig_path.read_bytes() - - # Reconstruct the data that was originally signed - data_to_verify, checksum_errors, missing_files = _reconstruct_canonical_checksum_list(staging_dir, manifest) - with open("data_to_verify.bin", "wb") as f: - f.write(data_to_verify.encode('utf-8')) - - # Verify the signature against the reconstructed data - if isinstance(pub_key_obj, ed25519.Ed25519PublicKey): - pub_key_obj.verify(signature, data_to_verify.encode('utf-8')) - elif isinstance(pub_key_obj, rsa.RSAPublicKey): - pub_key_obj.verify( - signature, - data_to_verify.encode('utf-8'), - padding.PKCS1v15(), - hashes.SHA256() - ) - - # If we reach here, the signature is cryptographically valid. - # Now we check if the manifest's checksums match the actual file checksums. - if checksum_errors or missing_files: - typer.secho(f"Trust Status: āŒ INVALID - Files have been tampered with after signing.", fg=typer.colors.RED) - for f in missing_files: - typer.echo(f" - Missing file listed in manifest: {f}") - for f in checksum_errors: - typer.echo(f" - Checksum mismatch for: {f}") - else: - typer.secho(f"Trust Status: āœ… SIGNED and TRUSTED ({trusted_key_path.relative_to(LOCAL_TRUST_STORE_PATH)})", fg=typer.colors.GREEN) - - except InvalidSignature: - typer.secho(f"Trust Status: āŒ INVALID SIGNATURE - The bundle's integrity is compromised.", fg=typer.colors.RED) - - typer.echo("-" * 50) - - # ... (Plugin Details section is unchanged) ... - typer.secho("Available Plugins:", bold=True) - for plugin_name, plugin_data in manifest.get('bundlePlugins', {}).items(): - typer.echo(f"\n Plugin: {plugin_name}") - typer.echo(f" Source Dist: {plugin_data.get('sdist', {}).get('path', 'N/A')}") - binaries = plugin_data.get('binaries', []) - - host_compatible_binary_found = False - if not binaries: - typer.echo(" Binaries: None") - else: - typer.echo(" Binaries:") - for b in binaries: - plat = b.get('platform', {}) - is_compatible = (plat.get('triplet') == host_platform['triplet'] and - is_abi_compatible(host_platform['abi_signature'], plat.get('abi_signature', ''))) - - color = typer.colors.GREEN if is_compatible else None - if is_compatible: - host_compatible_binary_found = True - - typer.secho(f" - Path: {b.get('path', 'N/A')}", fg=color) - typer.secho(f" ABI: {plat.get('abi_signature', 'N/A')}", fg=color, dim=True) - typer.secho(f" Arch: {plat.get('triplet', 'N/A')}", fg=color, dim=True) - - if not host_compatible_binary_found: - typer.secho( - f" Note: No compatible binary found for the current system ({host_platform['triplet']}).", - fg=typer.colors.YELLOW - ) - typer.secho( - " Run 'fourdst-cli bundle fill' to build one.", - fg=typer.colors.YELLOW - ) - - finally: - if staging_dir.exists(): - shutil.rmtree(staging_dir) diff --git a/fourdst/cli/bundle/sign.py b/fourdst/cli/bundle/sign.py index da51059..636d01b 100644 --- a/fourdst/cli/bundle/sign.py +++ b/fourdst/cli/bundle/sign.py @@ -1,151 +1,26 @@ # fourdst/cli/bundle/sign.py import typer -import shutil -import yaml -import zipfile -import hashlib from pathlib import Path -import sys -import subprocess -from fourdst.cli.common.utils import calculate_sha256 +from fourdst.core.bundle import sign_bundle -bundle_app = typer.Typer() - -def _create_canonical_checksum_list(staging_dir: Path, manifest: dict) -> str: - """ - Creates a deterministic, sorted string of all file paths and their checksums. - This string is the actual data that will be signed. - """ - checksum_map = {} - - # Iterate through all plugins to find all files to be checksummed - for plugin_data in manifest.get('bundlePlugins', {}).values(): - # Add sdist (source code zip) to the list - sdist_info = plugin_data.get('sdist', {}) - if 'path' in sdist_info: - file_path = staging_dir / sdist_info['path'] - if file_path.exists(): - checksum = "sha256:" + calculate_sha256(file_path) - # Also update the manifest with the sdist checksum - sdist_info['checksum'] = checksum - checksum_map[sdist_info['path']] = checksum - else: - # This case should ideally be caught by a validation step - typer.secho(f"Warning: sdist file not found: {sdist_info['path']}", fg=typer.colors.YELLOW) - - - # Add all binaries to the list - for binary in plugin_data.get('binaries', []): - if 'path' in binary: - file_path = staging_dir / binary['path'] - if file_path.exists(): - checksum = "sha256:" + calculate_sha256(file_path) - # Update the manifest with the binary checksum - binary['checksum'] = checksum - checksum_map[binary['path']] = checksum - else: - typer.secho(f"Warning: Binary file not found: {binary['path']}", fg=typer.colors.YELLOW) - - # Sort the file paths to ensure a deterministic order - sorted_paths = sorted(checksum_map.keys()) - - # Create the final canonical string (e.g., "path1:checksum1\npath2:checksum2") - canonical_list = [f"{path}:{checksum_map[path]}" for path in sorted_paths] - - return "\n".join(canonical_list) - - -@bundle_app.command("sign") def bundle_sign( bundle_path: Path = typer.Argument(..., help="The .fbundle file to sign.", exists=True), private_key: Path = typer.Option(..., "--key", "-k", help="Path to the author's private signing key.", exists=True) ): """ Signs a bundle with an author's private key. - - This process calculates checksums for all source and binary files, - adds them to the manifest, and then signs a canonical list of these - checksums to ensure the integrity of the entire bundle. """ - print(f"Signing bundle: {bundle_path}") - staging_dir = Path("temp_sign_staging") - if staging_dir.exists(): - shutil.rmtree(staging_dir) + def progress_callback(message): + typer.secho(message, fg=typer.colors.BRIGHT_BLUE) - # 1. Unpack the bundle - with zipfile.ZipFile(bundle_path, 'r') as bundle_zip: - bundle_zip.extractall(staging_dir) - - manifest_path = staging_dir / "manifest.yaml" - if not manifest_path.exists(): - print("Error: manifest.yaml not found in bundle.", file=sys.stderr) - raise typer.Exit(code=1) - - # 2. Ensure PEM private key and derive public key fingerprint via openssl - if private_key.suffix.lower() != ".pem": - typer.secho("Error: Private key must be a .pem file.", fg=typer.colors.RED) - raise typer.Exit(code=1) - typer.echo(" - Deriving public key fingerprint via openssl...") try: - proc = subprocess.run( - ["openssl", "pkey", "-in", str(private_key), "-pubout", "-outform", "DER"], - capture_output=True, check=True + sign_bundle( + bundle_path=bundle_path, + private_key=private_key, + progress_callback=progress_callback ) - pub_der = proc.stdout - fingerprint = "sha256:" + hashlib.sha256(pub_der).hexdigest() - typer.echo(f" - Signing with key fingerprint: {fingerprint}") - except subprocess.CalledProcessError as e: - typer.secho(f"Error extracting public key: {e.stderr.decode().strip()}", fg=typer.colors.RED) + except Exception as e: + typer.secho(f"Error signing bundle: {e}", fg=typer.colors.RED, err=True) raise typer.Exit(code=1) - - # 3. Load manifest and generate the canonical checksum list - with open(manifest_path, 'r') as f: - manifest = yaml.safe_load(f) - - print(" - Calculating checksums for all source and binary files...") - # This function now also modifies the manifest in-place to add the checksums - data_to_sign = _create_canonical_checksum_list(staging_dir, manifest) - - # Add the key fingerprint to the manifest - manifest['bundleAuthorKeyFingerprint'] = fingerprint - - # 4. Write the updated manifest back to the staging directory - with open(manifest_path, 'w') as f: - yaml.dump(manifest, f, sort_keys=False) - print(" - Added file checksums and key fingerprint to manifest.") - - # 5. Sign the canonical checksum list - typer.echo(" - Signing the canonical checksum list...") - canonical_temp_data_file = staging_dir / "canonical_checksums.txt" - canonical_temp_data_file.write_text(data_to_sign, encoding='utf-8') - sig_path = staging_dir / "manifest.sig" - try: - # We sign the string data directly, not the manifest file - cmd_list = [ - "openssl", - "pkeyutl", - "-sign", - "-in", str(canonical_temp_data_file), - "-inkey", str(private_key), - "-out", str(sig_path) - ] - subprocess.run( - cmd_list, - check=True, - capture_output=True - ) - typer.echo(f" - Created manifest.sig (> $ {' '.join(cmd_list)} ") - except subprocess.CalledProcessError as e: - typer.secho(f"Error signing manifest: {e.stderr.decode().strip()}", fg=typer.colors.RED) - raise typer.Exit(code=1) - - # 6. Repackage the bundle - with zipfile.ZipFile(bundle_path, 'w', zipfile.ZIP_DEFLATED) as bundle_zip: - for file_path in staging_dir.rglob('*'): - if file_path.is_file(): - bundle_zip.write(file_path, file_path.relative_to(staging_dir)) - - shutil.rmtree(staging_dir) - print("\nāœ… Bundle signed successfully!") diff --git a/fourdst/cli/bundle/validate.py b/fourdst/cli/bundle/validate.py index 9565ea2..8751c90 100644 --- a/fourdst/cli/bundle/validate.py +++ b/fourdst/cli/bundle/validate.py @@ -1,211 +1,80 @@ # fourdst/cli/bundle/validate.py import typer -import yaml -import zipfile from pathlib import Path -import tempfile -import shutil -import hashlib from rich.console import Console from rich.panel import Panel -from rich.text import Text from rich.table import Table +from rich.text import Text + +from fourdst.core.bundle import validate_bundle console = Console() -def _calculate_sha256(file_path: Path) -> str: - """Calculates the SHA256 checksum of a file.""" - sha256_hash = hashlib.sha256() - with open(file_path, "rb") as f: - for byte_block in iter(lambda: f.read(4096), b""): - sha256_hash.update(byte_block) - return sha256_hash.hexdigest() - -def _validate_bundle_directory(path: Path, is_temp: bool = False, display_name: str = None): - """Validates a directory that is structured like an unpacked bundle.""" - title = "Validating Pre-Bundle Directory" if not is_temp else "Validating Bundle Contents" - name = display_name or path.name - console.print(Panel(f"{title}: [bold]{name}[/bold]", border_style="blue")) - - errors = 0 - warnings = 0 - - # Section 1: Manifest file check - console.print(Panel("1. Manifest File Check", border_style="cyan")) - - def check(condition, success_msg, error_msg, is_warning=False): - nonlocal errors, warnings - if condition: - console.print(Text(f"āœ… {success_msg}", style="green")) - return True - else: - if is_warning: - console.print(Text(f"āš ļø {error_msg}", style="yellow")) - warnings += 1 - else: - console.print(Text(f"āŒ {error_msg}", style="red")) - errors += 1 - return False - - # 1. Check for manifest - manifest_file = path / "manifest.yaml" - if not check(manifest_file.is_file(), "Found manifest.yaml.", "Missing manifest.yaml file."): - raise typer.Exit(code=1) - - try: - manifest = yaml.safe_load(manifest_file.read_text()) - check(True, "Manifest file is valid YAML.", "") - except yaml.YAMLError as e: - check(False, "", f"Manifest file is not valid YAML: {e}") - raise typer.Exit(code=1) - - # 2. Check manifest content - console.print(Panel("2. Manifest Content Validation", border_style="cyan")) - check(manifest is not None, "Manifest is not empty.", "Manifest file is empty.", is_warning=True) - check('bundleName' in manifest, "Manifest contains 'bundleName'.", "Manifest is missing 'bundleName'.") - check('bundleVersion' in manifest, "Manifest contains 'bundleVersion'.", "Manifest is missing 'bundleVersion'.") - - plugins = manifest.get('bundlePlugins', {}) - check(plugins, "Manifest contains 'bundlePlugins' section.", "Manifest is missing 'bundlePlugins' section.") - - # Build Manifest Validation table - manifest_table = Table(title="Manifest Validation") - manifest_table.add_column("Check") - manifest_table.add_column("Status") - manifest_table.add_row("manifest.yaml exists", "āœ…" if manifest_file.is_file() else "āŒ") - # YAML parse status already captured by exception above - manifest_table.add_row("Manifest parses as YAML", "āœ…") - manifest_table.add_row("Manifest not empty", "āœ…" if manifest is not None else "āš ļø") - manifest_table.add_row("bundleName present", "āœ…" if 'bundleName' in manifest else "āŒ") - manifest_table.add_row("bundleVersion present", "āœ…" if 'bundleVersion' in manifest else "āŒ") - has_plugins = bool(manifest.get('bundlePlugins')) - manifest_table.add_row("bundlePlugins section", "āœ…" if has_plugins else "āŒ") - console.print(manifest_table) - plugins = manifest.get('bundlePlugins', {}) - - # 3. Check files listed in manifest - console.print(Panel("3. Plugin Validation", border_style="magenta")) - for name, data in plugins.items(): - console.print(Panel(f"Plugin: [bold cyan]{name}[/bold cyan]", border_style="magenta")) - sdist_info = data.get('sdist', {}) - sdist_path_str = sdist_info.get('path') - - if check(sdist_path_str, "sdist path is defined.", f"sdist path not defined for plugin '{name}'."): - sdist_path = path / sdist_path_str - check(sdist_path.exists(), f"sdist file found: {sdist_path_str}", f"sdist file not found: {sdist_path_str}") - - for binary in data.get('binaries', []): - bin_path_str = binary.get('path') - if not check(bin_path_str, "Binary path is defined.", "Binary entry is missing a 'path'."): - continue - - bin_path = path / bin_path_str - if check(bin_path.exists(), f"Binary file found: {bin_path_str}", f"Binary file not found: {bin_path_str}"): - expected_checksum = binary.get('checksum') - if check(expected_checksum, "Checksum is defined.", f"Checksum not defined for binary '{bin_path_str}'.", is_warning=True): - actual_checksum = "sha256:" + _calculate_sha256(bin_path) - check( - actual_checksum == expected_checksum, - f"Checksum matches for {bin_path_str}", - f"Checksum mismatch for {bin_path_str}.\n Expected: {expected_checksum}\n Actual: {actual_checksum}" - ) - - # Build Plugin Validation table - plugin_table = Table(title="Plugin Validation") - plugin_table.add_column("Plugin") - plugin_table.add_column("Sdist Defined") - plugin_table.add_column("Sdist Exists") - plugin_table.add_column("Binaries OK") - plugin_table.add_column("Checksums OK") - for name, data in plugins.items(): - # sdist checks - sdist_path_str = data.get('sdist', {}).get('path') - sdist_defined = bool(sdist_path_str) - sdist_exists = sdist_defined and (path/ sdist_path_str).exists() - # binary & checksum checks - binaries = data.get('binaries', []) - binaries_ok = all(b.get('path') and (path/ b['path']).exists() for b in binaries) - checksums_ok = all(('checksum' in b and ("sha256:"+_calculate_sha256(path/ b['path']))==b['checksum']) for b in binaries) - plugin_table.add_row( - name, - "āœ…" if sdist_defined else "āŒ", - "āœ…" if sdist_exists else "āŒ", - "āœ…" if binaries_ok else "āŒ", - "āœ…" if checksums_ok else "āŒ" - ) - console.print(plugin_table) - - # 4. Check for signature - console.print(Panel("4. Signature Check", border_style="yellow")) - check((path / "manifest.sig").exists(), "Signature file 'manifest.sig' found.", "Signature file 'manifest.sig' is missing.", is_warning=True) - - # Build Signature Check table - sig_table = Table(title="Signature Validation") - sig_table.add_column("Item") - sig_table.add_column("Status") - sig_exists = (path / "manifest.sig").exists() - sig_table.add_row( - "manifest.sig", - "āœ…" if sig_exists else "āš ļø" - ) - console.print(sig_table) - - # Final summary - console.print("-" * 40) - # Display summary in a table - - summary_table = Table(title="Validation Summary") - summary_table.add_column("Result") - summary_table.add_column("Errors", justify="right") - summary_table.add_column("Warnings", justify="right") - - if errors == 0: - result = "Passed" - style = "green" - else: - result = "Failed" - style = "red" - - summary_table.add_row( - f"[bold {style}]{result}[/bold {style}]", - str(errors), - str(warnings) - ) - console.print(summary_table) - if errors != 0: - raise typer.Exit(code=1) - -def _validate_bundle_file(bundle_path: Path): - """Unpacks a .fbundle file and runs directory validation on its contents.""" - with tempfile.TemporaryDirectory() as temp_dir_str: - temp_dir = Path(temp_dir_str) - try: - with zipfile.ZipFile(bundle_path, 'r') as bundle_zip: - bundle_zip.extractall(temp_dir) - _validate_bundle_directory(temp_dir, is_temp=True, display_name=bundle_path.name) - except zipfile.BadZipFile: - console.print(Panel(f"[red]Error: '{bundle_path.name}' is not a valid zip file.[/red]", title="Validation Error")) - raise typer.Exit(code=1) - def bundle_validate( - path: Path = typer.Argument( - ".", - help="The path to the .fbundle file or pre-bundle directory to validate.", + bundle_path: Path = typer.Argument( + ..., + help="The .fbundle file to validate.", exists=True, - resolve_path=True + resolve_path=True, + file_okay=True, + dir_okay=False ) ): """ - Validates a packed .fbundle or a directory ready to be packed. - - - If a directory is provided, it checks for a valid manifest and that all referenced files exist. - - If a .fbundle file is provided, it unpacks it and runs the same validation checks. + Validates the integrity and checksums of a .fbundle file. """ - if path.is_dir(): - _validate_bundle_directory(path) - elif path.is_file(): - _validate_bundle_file(path) - else: - # This case should not be reached due to `exists=True` - console.print(Panel("[red]Error: Path is not a file or directory.[/red]", title="Validation Error")) + def progress_callback(message): + # For a CLI, we can choose to show progress or just wait for the final report. + # In this case, the final report is more structured and useful. + pass + + try: + results = validate_bundle( + bundle_path=bundle_path, + progress_callback=progress_callback + ) + + console.print(Panel(f"Validation Report for: [bold]{bundle_path.name}[/bold]", border_style="blue")) + + if results['errors']: + console.print(Panel("Errors", border_style="red", expand=False)) + for error in results['errors']: + console.print(Text(f"āŒ {error}", style="red")) + + if results['warnings']: + console.print(Panel("Warnings", border_style="yellow", expand=False)) + for warning in results['warnings']: + console.print(Text(f"āš ļø {warning}", style="yellow")) + + # Summary Table + summary_table = Table(title="Validation Summary") + summary_table.add_column("Result") + summary_table.add_column("Errors", justify="right") + summary_table.add_column("Warnings", justify="right") + + status = results.get('status', 'failed') + summary = results.get('summary', {'errors': len(results['errors']), 'warnings': len(results['warnings'])}) + + if status == 'passed': + result_text = "Passed" + style = "green" + else: + result_text = "Failed" + style = "red" + + summary_table.add_row( + f"[bold {style}]{result_text}[/bold {style}]", + str(summary['errors']), + str(summary['warnings']) + ) + console.print(summary_table) + + if status != 'passed': + raise typer.Exit(code=1) + else: + console.print("\n[bold green]āœ… Bundle is valid.[/bold green]") + + except Exception as e: + # Catch exceptions from the core function itself + console.print(Panel(f"[bold red]An unexpected error occurred:[/bold red]\n{e}", title="Validation Error")) raise typer.Exit(code=1) diff --git a/fourdst/cli/common/config.py b/fourdst/cli/common/config.py index 3aa8240..135b226 100644 --- a/fourdst/cli/common/config.py +++ b/fourdst/cli/common/config.py @@ -1,16 +1,11 @@ # fourdst/cli/common/config.py -from pathlib import Path - -FOURDST_CONFIG_DIR = Path.home() / ".config" / "fourdst" -LOCAL_TRUST_STORE_PATH = FOURDST_CONFIG_DIR / "keys" -CROSS_FILES_PATH = FOURDST_CONFIG_DIR / "cross" -CACHE_PATH = FOURDST_CONFIG_DIR / "cache" -ABI_CACHE_FILE = CACHE_PATH / "abi_identifier.json" -DOCKER_BUILD_IMAGES = { - "x86_64 (manylinux_2_28)": "quay.io/pypa/manylinux_2_28_x86_64", - "aarch64 (manylinux_2_28)": "quay.io/pypa/manylinux_2_28_aarch64", - "i686 (manylinux_2_28)" : "quay.io/pypa/manylinux_2_28_i686", - "ppc64le (manylinux_2_28)" : "quay.io/pypa/manylinux_2_28_ppc64le", - "s390x (manylinux_2_28)" : "quay.io/pypa/manylinux_2_28_s390x" -} +# This file is now a proxy for the core config to maintain compatibility. +from fourdst.core.config import ( + FOURDST_CONFIG_DIR, + LOCAL_TRUST_STORE_PATH, + CROSS_FILES_PATH, + CACHE_PATH, + ABI_CACHE_FILE, + DOCKER_BUILD_IMAGES +) diff --git a/fourdst/cli/common/utils.py b/fourdst/cli/common/utils.py index e90501e..54d823f 100644 --- a/fourdst/cli/common/utils.py +++ b/fourdst/cli/common/utils.py @@ -3,27 +3,49 @@ import typer import os import sys -import shutil import subprocess from pathlib import Path import importlib.resources -import json -import platform -import zipfile -import hashlib - -try: - import docker -except ImportError: - docker = None from rich.console import Console from rich.panel import Panel console = Console() -from fourdst.cli.common.config import CACHE_PATH, ABI_CACHE_FILE, CROSS_FILES_PATH, DOCKER_BUILD_IMAGES -from fourdst.cli.common.templates import ABI_DETECTOR_CPP_SRC, ABI_DETECTOR_MESON_SRC +def run_command_rich(command: list[str], cwd: Path = None, check=True, env: dict = None): + """ + Runs a command and displays its output live using rich. + """ + command_str = ' '.join(command) + console.print(Panel(f"Running: [bold cyan]{command_str}[/bold cyan]", title="Command", border_style="blue")) + + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=cwd, + env=env, + bufsize=1, # line-buffered + universal_newlines=True + ) + + # Read and print stdout and stderr line by line + if process.stdout: + for line in iter(process.stdout.readline, ''): + console.print(line.strip()) + + if process.stderr: + for line in iter(process.stderr.readline, ''): + console.print(f"[yellow]{line.strip()}[/yellow]") + + process.wait() + + if check and process.returncode != 0: + console.print(Panel(f"Command failed with exit code {process.returncode}", title="[bold red]Error[/bold red]", border_style="red")) + raise subprocess.CalledProcessError(process.returncode, command) + + return process def get_template_content(template_name: str) -> str: """Safely reads content from a template file packaged with the CLI.""" @@ -38,7 +60,6 @@ def run_command(command: list[str], cwd: Path = None, check=True, display_output command_str = ' '.join(command) try: - # Pass the env dictionary to subprocess.run result = subprocess.run(command, check=check, capture_output=True, text=True, cwd=cwd, env=env) if display_output and (result.stdout or result.stderr): @@ -73,324 +94,6 @@ def run_command(command: list[str], cwd: Path = None, check=True, display_output raise typer.Exit(code=1) return e -def _detect_and_cache_abi(): - """ - Compiles and runs a C++ program to detect the compiler ABI, then caches it. - """ - print("Performing one-time native C++ ABI detection...") - temp_dir = CACHE_PATH / "abi_detector" - if temp_dir.exists(): - shutil.rmtree(temp_dir) - temp_dir.mkdir(parents=True) - - try: - (temp_dir / "main.cpp").write_text(ABI_DETECTOR_CPP_SRC) - (temp_dir / "meson.build").write_text(ABI_DETECTOR_MESON_SRC) - - print(" - Configuring detector...") - run_command(["meson", "setup", "build"], cwd=temp_dir, display_output=True) - print(" - Compiling detector...") - run_command(["meson", "compile", "-C", "build"], cwd=temp_dir, display_output=True) - - detector_exe = temp_dir / "build" / "detector" - print(" - Running detector...") - proc = subprocess.run([str(detector_exe)], check=True, capture_output=True, text=True) - - abi_details = {} - for line in proc.stdout.strip().split('\n'): - if '=' in line: - key, value = line.split('=', 1) - abi_details[key.strip()] = value.strip() - - compiler = abi_details.get('compiler', 'unk_compiler') - stdlib = abi_details.get('stdlib', 'unk_stdlib') - - # --- MODIFIED LOGIC FOR MACOS VERSIONING --- - # On macOS, the OS version is more useful than the internal libc++ version. - # But for the generic host detection, we still use the detected version. - # The targeting logic will override this. - if sys.platform == "darwin": - # The C++ detector provides the internal _LIBCPP_VERSION - stdlib_version = abi_details.get('stdlib_version', 'unk_stdlib_version') - detected_os = "macos" - else: - # On Linux, this will be the glibc version - stdlib_version = abi_details.get('stdlib_version', 'unk_stdlib_version') - detected_os = abi_details.get("os", "linux") - - abi = abi_details.get('abi', 'unk_abi') - abi_string = f"{compiler}-{stdlib}-{stdlib_version}-{abi}" - - arch = platform.machine() - - platform_identifier = { - "triplet": f"{arch}-{detected_os}", - "abi_signature": abi_string, - "details": abi_details, - "is_native": True, - "cross_file": None, - "docker_image": None, - "arch": arch - } - - with open(ABI_CACHE_FILE, 'w') as f: - json.dump(platform_identifier, f, indent=2) - - print(f"āœ… Native ABI detected and cached: {abi_string}") - return platform_identifier - - finally: - if temp_dir.exists(): - shutil.rmtree(temp_dir) - -def get_platform_identifier() -> dict: - """ - Gets the native platform identifier, using a cached value if available. - """ - if ABI_CACHE_FILE.exists(): - with open(ABI_CACHE_FILE, 'r') as f: - return json.load(f) - else: - return _detect_and_cache_abi() - -def get_macos_targeted_platform_identifier(target_version: str) -> dict: - """ - Generates a platform identifier for a specific target macOS version. - This bypasses host detection for the version string. - """ - # We still need the host's compiler info, so we run detection if not cached. - host_platform = get_platform_identifier() - host_details = host_platform['details'] - - compiler = host_details.get('compiler', 'clang') - stdlib = host_details.get('stdlib', 'libc++') - abi = host_details.get('abi', 'libc++_abi') - arch = platform.machine() - - abi_string = f"{compiler}-{stdlib}-{target_version}-{abi}" - - return { - "triplet": f"{arch}-macos", - "abi_signature": abi_string, - "details": { - "os": "macos", - "compiler": compiler, - "compiler_version": host_details.get('compiler_version'), - "stdlib": stdlib, - "stdlib_version": target_version, # The key change is here - "abi": abi, - }, - "is_native": True, - "cross_file": None, - "docker_image": None, - "arch": arch - } - -def get_available_build_targets() -> list: - """Gets native, cross-compilation, and Docker build targets.""" - targets = [get_platform_identifier()] - - # Add cross-file targets - CROSS_FILES_PATH.mkdir(exist_ok=True) - for cross_file in CROSS_FILES_PATH.glob("*.cross"): - triplet = cross_file.stem - targets.append({ - "triplet": triplet, - "abi_signature": f"cross-{triplet}", - "is_native": False, - "cross_file": str(cross_file.resolve()), - "docker_image": None - }) - - # Add Docker targets if Docker is available - if docker: - try: - client = docker.from_env() - client.ping() - for name, image in DOCKER_BUILD_IMAGES.items(): - arch = name.split(' ')[0] - targets.append({ - "triplet": f"{arch}-linux", - "abi_signature": f"docker-{image}", - "is_native": False, - "cross_file": None, - "docker_image": image, - "arch": arch - }) - except Exception: - typer.secho("Warning: Docker is installed but the daemon is not running. Docker targets are unavailable.", fg=typer.colors.YELLOW) - - return targets - -def _build_plugin_for_target(sdist_path: Path, build_dir: Path, target: dict): - """Builds a plugin natively or with a cross file.""" - source_dir = build_dir / "src" - if source_dir.exists(): - shutil.rmtree(source_dir) - - with zipfile.ZipFile(sdist_path, 'r') as sdist_zip: - sdist_zip.extractall(source_dir) - - - setup_cmd = ["meson", "setup"] - if target["cross_file"]: - setup_cmd.extend(["--cross-file", target["cross_file"]]) - setup_cmd.append("build") - - run_command(setup_cmd, cwd=source_dir, display_output=True) - run_command(["meson", "compile", "-C", "build"], cwd=source_dir, display_output=True) - - meson_build_dir = source_dir / "build" - compiled_lib = next(meson_build_dir.rglob("lib*.so"), None) or next(meson_build_dir.rglob("lib*.dylib"), None) - if not compiled_lib: - raise FileNotFoundError("Could not find compiled library after build.") - - return compiled_lib, target # Return target as ABI is pre-determined - -def _build_plugin_in_docker(sdist_path: Path, build_dir: Path, target: dict, plugin_name: str): - """Builds a plugin inside a Docker container.""" - client = docker.from_env() - image_name = target["docker_image"] - - # Find arch from DOCKER_BUILD_IMAGES to create a clean triplet later - arch = "unknown_arch" - for name, img in DOCKER_BUILD_IMAGES.items(): - if img == image_name: - arch = name.split(' ')[0] - break - - typer.echo(f" - Pulling Docker image '{image_name}' (if necessary)...") - client.images.pull(image_name) - - source_dir = build_dir / "src" - if source_dir.exists(): - shutil.rmtree(source_dir) - - with zipfile.ZipFile(sdist_path, 'r') as sdist_zip: - sdist_zip.extractall(source_dir) - - # This script will be run inside the container - build_script = f""" - set -e - echo "--- Installing build dependencies ---" - export PATH="/opt/python/cp313-cp313/bin:$PATH" - pip install meson ninja cmake - - echo " -> ℹ meson version: $(meson --version) [$(which meson)]" - echo " -> ℹ ninja version: $(ninja --version) [$(which ninja)]" - echo " -> ℹ cmake version: $(cmake --version) [$(which cmake)]" - - echo "--- Configuring with Meson ---" - meson setup /build/meson_build - echo "--- Compiling with Meson ---" - meson compile -C /build/meson_build - echo "--- Running ABI detector ---" - # We need to build and run the ABI detector inside the container too - mkdir /tmp/abi && cd /tmp/abi - echo "{ABI_DETECTOR_CPP_SRC.replace('"', '\\"')}" > main.cpp - echo "{ABI_DETECTOR_MESON_SRC.replace('"', '\\"')}" > meson.build - meson setup build && meson compile -C build - ./build/detector > /build/abi_details.txt - """ - - container_build_dir = Path("/build") - - typer.echo(" - Running build container...") - container = client.containers.run( - image=image_name, - command=["/bin/sh", "-c", build_script], - volumes={str(source_dir.resolve()): {'bind': str(container_build_dir), 'mode': 'rw'}}, - working_dir=str(container_build_dir), - detach=True - ) - - # Stream logs - for line in container.logs(stream=True, follow=True): - typer.echo(f" [docker] {line.decode('utf-8').strip()}") - - result = container.wait() - if result["StatusCode"] != 0: - # The container is stopped, but we can still inspect its filesystem by restarting it briefly. - log_output = container.logs() - container.remove() # Clean up before raising - typer.secho(f"Build failed inside Docker. Full log:\n{log_output.decode('utf-8')}", fg=typer.colors.RED) - raise subprocess.CalledProcessError(result["StatusCode"], "Build inside Docker failed.") - - # Retrieve artifacts by searching inside the container's filesystem - typer.echo(" - Locating compiled library in container...") - meson_build_dir_str = (container_build_dir / "meson_build").as_posix() - expected_lib_name = f"lib{plugin_name}.so" - - find_cmd = f"find {meson_build_dir_str} -name {expected_lib_name}" - - # We need to run the find command in the now-stopped container. - # We can't use exec_run on a stopped container, but we can create a new - # one that uses the same filesystem (volume) to find the file. - try: - find_output = client.containers.run( - image=image_name, - command=["/bin/sh", "-c", find_cmd], - volumes={str(source_dir.resolve()): {'bind': str(container_build_dir), 'mode': 'ro'}}, - remove=True, # Clean up the find container immediately - detach=False - ) - found_path_str = find_output.decode('utf-8').strip() - if not found_path_str: - raise FileNotFoundError("Find command returned no path.") - compiled_lib = Path(found_path_str) - typer.echo(f" - Found library at: {compiled_lib}") - - except Exception as e: - typer.secho(f" - Error: Could not locate '{expected_lib_name}' inside the container.", fg=typer.colors.RED) - typer.secho(f" Details: {e}", fg=typer.colors.RED) - raise FileNotFoundError("Could not find compiled library in container after a successful build.") - - # Get the ABI details from the container - abi_details_content = "" - bits, _ = container.get_archive(str(container_build_dir / "abi_details.txt")) - for chunk in bits: - abi_details_content += chunk.decode('utf-8') - - # We need to find the actual file content within the tar stream - # This is a simplification; a real implementation would use the `tarfile` module - actual_content = abi_details_content.split('\n', 1)[1] if '\n' in abi_details_content else abi_details_content - actual_content = actual_content.split('main.cpp')[1].strip() if 'main.cpp' in actual_content else actual_content - actual_content = actual_content.rsplit('0755', 1)[0].strip() if '0755' in actual_content else actual_content - - - abi_details = {} - for line in actual_content.strip().split('\n'): - if '=' in line: - key, value = line.split('=', 1) - abi_details[key.strip()] = value.strip() - - compiler = abi_details.get('compiler', 'unk_compiler') - stdlib = abi_details.get('stdlib', 'unk_stdlib') - stdlib_version = abi_details.get('stdlib_version', 'unk_stdlib_version') - abi = abi_details.get('abi', 'unk_abi') - abi_string = f"{compiler}-{stdlib}-{stdlib_version}-{abi}" - - final_target = { - "triplet": f"{arch}-{abi_details.get('os', 'linux')}", - "abi_signature": abi_string, - "is_native": False, - "cross_file": None, - "docker_image": image_name, - "arch": arch - } - - # Copy the binary out - local_lib_path = build_dir / compiled_lib.name - bits, _ = container.get_archive(str(compiled_lib)) - with open(local_lib_path, 'wb') as f: - for chunk in bits: - f.write(chunk) - - container.remove() - - return local_lib_path, final_target - - def is_abi_compatible(host_abi: str, binary_abi: str) -> bool: """ Checks if a binary's ABI is compatible with the host's ABI. diff --git a/fourdst/cli/main.py b/fourdst/cli/main.py index 31b87fe..9b4da83 100644 --- a/fourdst/cli/main.py +++ b/fourdst/cli/main.py @@ -39,6 +39,14 @@ app = typer.Typer( plugin_app = typer.Typer(name="plugin", help="Commands for managing individual fourdst plugins.") bundle_app = typer.Typer(name="bundle", help="Commands for creating, signing, and managing plugin bundles.") + +bundle_app.command("create")(bundle_create) +bundle_app.command("fill")(bundle_fill) +bundle_app.command("sign")(bundle_sign) +bundle_app.command("inspect")(bundle_inspect) +bundle_app.command("clear")(bundle_clear) +bundle_app.command("diff")(bundle_diff) +bundle_app.command("validate")(bundle_validate) cache_app = typer.Typer(name="cache", help="Commands for managing the local cache.") keys_app = typer.Typer(name="keys", help="Commands for cryptographic key generation and management.") diff --git a/fourdst/cli/templates/plugin.cpp.in b/fourdst/cli/templates/plugin.cpp.in index e11de85..e007358 100644 --- a/fourdst/cli/templates/plugin.cpp.in +++ b/fourdst/cli/templates/plugin.cpp.in @@ -4,6 +4,7 @@ class {class_name} final : public {interface} {{ public: + using {interface}::{interface}; ~{class_name}() override {{ // Implement any custom destruction logic here }} diff --git a/fourdst/core/__init__.py b/fourdst/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fourdst/core/build.py b/fourdst/core/build.py new file mode 100644 index 0000000..82d5368 --- /dev/null +++ b/fourdst/core/build.py @@ -0,0 +1,212 @@ +# fourdst/core/build.py + +import os +import subprocess +import zipfile +import docker +import io +import tarfile +from pathlib import Path +import zipfile + +try: + import docker +except ImportError: + docker = None + +from fourdst.core.utils import run_command +from fourdst.core.platform import get_platform_identifier, get_macos_targeted_platform_identifier +from fourdst.core.config import CROSS_FILES_PATH, DOCKER_BUILD_IMAGES + +def get_available_build_targets(progress_callback=None): + """Gets native, cross-compilation, and Docker build targets.""" + def report_progress(message): + if progress_callback: + progress_callback(message) + + targets = [get_platform_identifier()] + + # Add cross-file targets + CROSS_FILES_PATH.mkdir(exist_ok=True) + for cross_file in CROSS_FILES_PATH.glob("*.cross"): + triplet = cross_file.stem + targets.append({ + "triplet": triplet, + "abi_signature": f"cross-{triplet}", + "is_native": False, + "cross_file": str(cross_file.resolve()), + "docker_image": None, + 'type': 'cross' + }) + + # Add Docker targets if Docker is available + if docker: + try: + client = docker.from_env() + client.ping() + for name, image in DOCKER_BUILD_IMAGES.items(): + arch = name.split(' ')[0] + targets.append({ + "triplet": f"{arch}-linux", + "abi_signature": f"docker-{image}", + "is_native": False, + "cross_file": None, + "docker_image": image, + "arch": arch, + 'type': 'docker' + }) + except Exception: + report_progress("Warning: Docker is installed but the daemon is not running. Docker targets are unavailable.") + + return targets + +def build_plugin_for_target(sdist_path: Path, build_dir: Path, target: dict, progress_callback=None): + """Builds a plugin natively or with a cross file.""" + def report_progress(message): + if progress_callback: + progress_callback(message) + + source_dir = build_dir / "src" + if source_dir.exists(): + shutil.rmtree(source_dir) + + with zipfile.ZipFile(sdist_path, 'r') as sdist_zip: + sdist_zip.extractall(source_dir) + + setup_cmd = ["meson", "setup"] + if target.get("cross_file"): + setup_cmd.extend(["--cross-file", target["cross_file"]]) + setup_cmd.append("build") + + run_command(setup_cmd, cwd=source_dir, progress_callback=progress_callback) + run_command(["meson", "compile", "-C", "build"], cwd=source_dir, progress_callback=progress_callback) + + meson_build_dir = source_dir / "build" + compiled_lib = next(meson_build_dir.rglob("lib*.so"), None) or next(meson_build_dir.rglob("lib*.dylib"), None) + if not compiled_lib: + raise FileNotFoundError("Could not find compiled library after build.") + + return compiled_lib, target + +def build_plugin_in_docker(sdist_path: Path, build_dir: Path, target: dict, plugin_name: str, progress_callback=None): + """Builds a plugin inside a Docker container.""" + def report_progress(message): + if progress_callback: + progress_callback(message) + + client = docker.from_env() + image_name = target["docker_image"] + + arch = target.get("arch", "unknown_arch") + + report_progress(f" - Pulling Docker image '{image_name}' (if necessary)...") + client.images.pull(image_name) + + source_dir = build_dir / "src" + if source_dir.exists(): + shutil.rmtree(source_dir) + + with zipfile.ZipFile(sdist_path, 'r') as sdist_zip: + sdist_zip.extractall(source_dir) + + from fourdst.core.platform import ABI_DETECTOR_CPP_SRC, ABI_DETECTOR_MESON_SRC + build_script = f""" + set -e + echo \"--- Installing build dependencies ---\" + export PATH=\"/opt/python/cp313-cp313/bin:$PATH\" + dnf install -y openssl-devel + pip install meson ninja cmake + + echo \"--- Configuring with Meson ---\" + meson setup /build/meson_build + echo \"--- Compiling with Meson ---\" + meson compile -C /build/meson_build + echo \"--- Running ABI detector ---\" + mkdir /tmp/abi && cd /tmp/abi + echo \"{ABI_DETECTOR_CPP_SRC.replace('"', '\\"')}\" > main.cpp + echo \"{ABI_DETECTOR_MESON_SRC.replace('"', '\\"')}\" > meson.build + meson setup build && meson compile -C build + ./build/detector > /build/abi_details.txt + """ + + container_build_dir = Path("/build") + + report_progress(" - Running build container...") + container = client.containers.run( + image=image_name, + command=["/bin/sh", "-c", build_script], + volumes={str(source_dir.resolve()): {'bind': str(container_build_dir), 'mode': 'rw'}}, + working_dir=str(container_build_dir), + detach=True + ) + + for line in container.logs(stream=True, follow=True): + report_progress(f" [docker] {line.decode('utf-8').strip()}") + + result = container.wait() + if result["StatusCode"] != 0: + log_output = container.logs() + container.remove() + raise subprocess.CalledProcessError(result["StatusCode"], f"Build inside Docker failed. Full log:\n{log_output.decode('utf-8')}") + + report_progress(" - Locating compiled library in container...") + meson_build_dir_str = (container_build_dir / "meson_build").as_posix() + expected_lib_name = f"lib{plugin_name}.so" + + find_cmd = f"find {meson_build_dir_str} -name {expected_lib_name}" + + find_output = client.containers.run( + image=image_name, + command=["/bin/sh", "-c", find_cmd], + volumes={str(source_dir.resolve()): {'bind': str(container_build_dir), 'mode': 'ro'}}, + remove=True, + detach=False + ) + found_path_str = find_output.decode('utf-8').strip() + if not found_path_str: + raise FileNotFoundError(f"Could not locate '{expected_lib_name}' inside the container.") + compiled_lib_path_in_container = Path(found_path_str) + + # Use the tarfile module for robust extraction + bits, _ = container.get_archive(str(container_build_dir / "abi_details.txt")) + with tarfile.open(fileobj=io.BytesIO(b''.join(bits))) as tar: + member = tar.getmembers()[0] + extracted_file = tar.extractfile(member) + if not extracted_file: + raise FileNotFoundError("Could not extract abi_details.txt from container archive.") + abi_details_content = extracted_file.read() + + abi_details = {} + for line in abi_details_content.decode('utf-8').strip().split('\n'): + if '=' in line: + key, value = line.split('=', 1) + abi_details[key.strip()] = value.strip() + + compiler = abi_details.get('compiler', 'unk_compiler') + stdlib = abi_details.get('stdlib', 'unk_stdlib') + stdlib_version = abi_details.get('stdlib_version', 'unk_stdlib_version') + abi = abi_details.get('abi', 'unk_abi') + abi_string = f"{compiler}-{stdlib}-{stdlib_version}-{abi}" + + final_target = { + "triplet": f"{arch}-{abi_details.get('os', 'linux')}", + "abi_signature": abi_string, + "is_native": False, + "cross_file": None, + "docker_image": image_name, + "arch": arch + } + + local_lib_path = build_dir / compiled_lib_path_in_container.name + bits, _ = container.get_archive(str(compiled_lib_path_in_container)) + with tarfile.open(fileobj=io.BytesIO(b''.join(bits))) as tar: + member = tar.getmembers()[0] + extracted_file = tar.extractfile(member) + if not extracted_file: + raise FileNotFoundError(f"Could not extract {local_lib_path.name} from container archive.") + with open(local_lib_path, 'wb') as f: + f.write(extracted_file.read()) + + container.remove() + + return local_lib_path, final_target diff --git a/fourdst/core/bundle.py b/fourdst/core/bundle.py new file mode 100644 index 0000000..23b9d65 --- /dev/null +++ b/fourdst/core/bundle.py @@ -0,0 +1,1079 @@ +# fourdst/core/bundle.py +""" +Core bundle management functions for 4DSTAR. + +REFACTORED ARCHITECTURE (2025-08-09): +=========================================== + +All functions in this module now return JSON strings or JSON-serializable dictionaries. +This eliminates the complex stdout parsing and data transformation layers that were +causing issues in the Electron app. + +CHANGES REQUIRED FOR CLIENTS: + +1. CLI TOOL CHANGES: + - Parse JSON responses from core functions + - Handle progress callbacks separately (not mixed with return data) + - Format JSON data for human-readable terminal output + - Handle errors by parsing JSON error responses + +2. ELECTRON APP CHANGES: + - Bridge script (bridge.py) becomes much simpler: + * Call core function directly + * Return the JSON string to stdout (no wrapping needed) + * All logging goes to stderr only + - Main process (main.js): + * Parse clean JSON from stdout + * No complex data structure adaptation needed + - Renderer process (renderer.js): + * Receive predictable, consistent data structures + * No need for defensive null checks on expected fields + +3. PROGRESS REPORTING: + - Progress callbacks are separate from return values + - Structured progress messages use consistent format + - All progress goes to callback, never mixed with JSON output + +4. ERROR HANDLING: + - All functions return JSON even for errors + - Consistent error format: {"success": false, "error": "message"} + - Exceptions are caught and converted to JSON error responses +""" + +import os +import sys +import shutil +import datetime +import yaml +import zipfile +import tempfile +import hashlib +import subprocess +import json +import logging +from pathlib import Path +from typing import Dict, Any, Optional, Callable + +from fourdst.core.platform import get_platform_identifier, get_macos_targeted_platform_identifier +from fourdst.core.utils import run_command, calculate_sha256 +from fourdst.core.build import get_available_build_targets, build_plugin_for_target, build_plugin_in_docker +from fourdst.core.platform import is_abi_compatible +from fourdst.core.config import LOCAL_TRUST_STORE_PATH + +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa, ed25519 +from cryptography.exceptions import InvalidSignature +import cryptography + +# Configure logging to go to stderr only, never stdout +logging.basicConfig(stream=sys.stderr, level=logging.INFO) + +def create_bundle( + plugin_dirs: list[Path], + output_bundle: Path, + bundle_name: str, + bundle_version: str, + bundle_author: str, + bundle_comment: str | None, + target_macos_version: str | None = None, + progress_callback: Optional[Callable] = None +) -> Dict[str, Any]: + """ + Builds and packages one or more plugin projects into a single .fbundle file. + + REFACTORED: Now returns a JSON-serializable dictionary directly. + Progress messages go only to the callback, never to stdout. + + Returns: + Dict with structure: + { + "success": bool, + "bundle_path": str, # Path to created bundle + "message": str # Success message + } + + On error: + { + "success": false, + "error": "error message" + } + + Args: + progress_callback: An optional function that takes a string message to report progress. + """ + def report_progress(message): + if progress_callback: + progress_callback(message) + else: + print(message) + + staging_dir = Path(tempfile.mkdtemp(prefix="fourdst_create_")) + + try: + build_env = os.environ.copy() + + if sys.platform == "darwin" and target_macos_version: + report_progress(f"Targeting macOS version: {target_macos_version}") + host_platform = get_macos_targeted_platform_identifier(target_macos_version) + flags = f"-mmacosx-version-min={target_macos_version}" + build_env["CXXFLAGS"] = f"{build_env.get('CXXFLAGS', '')} {flags}".strip() + build_env["LDFLAGS"] = f"{build_env.get('LDFLAGS', '')} {flags}".strip() + else: + host_platform = get_platform_identifier() + + manifest = { + "bundleName": bundle_name, + "bundleVersion": bundle_version, + "bundleAuthor": bundle_author, + "bundleComment": bundle_comment or "Created with fourdst", + "bundledOn": datetime.datetime.now().isoformat(), + "bundlePlugins": {} + } + + report_progress("Creating bundle...") + for plugin_dir in plugin_dirs: + plugin_name = plugin_dir.name + report_progress(f"--> Processing plugin: {plugin_name}") + + report_progress(f" - Compiling for target platform...") + build_dir = plugin_dir / "builddir" + if build_dir.exists(): + shutil.rmtree(build_dir) + + run_command(["meson", "setup", "builddir"], cwd=plugin_dir, env=build_env) + run_command(["meson", "compile", "-C", "builddir"], cwd=plugin_dir, env=build_env) + + compiled_lib = next(build_dir.glob("lib*.so"), None) or next(build_dir.glob("lib*.dylib"), None) + if not compiled_lib: + raise FileNotFoundError(f"Could not find compiled library for {plugin_name}") + + report_progress(" - Packaging source code (respecting .gitignore)...") + sdist_path = staging_dir / f"{plugin_name}_src.zip" + + git_check = subprocess.run(["git", "rev-parse", "--is-inside-work-tree"], cwd=plugin_dir, capture_output=True, text=True, check=False) + files_to_include = [] + if git_check.returncode == 0: + result = run_command(["git", "ls-files", "--cached", "--others", "--exclude-standard"], cwd=plugin_dir) + files_to_include = [plugin_dir / f for f in result.stdout.strip().split('\n') if f] + else: + report_progress(f" - Warning: '{plugin_dir.name}' is not a git repository. Packaging all files.") + for root, _, files in os.walk(plugin_dir): + if 'builddir' in root: + continue + for file in files: + files_to_include.append(Path(root) / file) + + with zipfile.ZipFile(sdist_path, 'w', zipfile.ZIP_DEFLATED) as sdist_zip: + for file_path in files_to_include: + if file_path.is_file(): + sdist_zip.write(file_path, file_path.relative_to(plugin_dir)) + + binaries_dir = staging_dir / "bin" + binaries_dir.mkdir(exist_ok=True) + + base_name = compiled_lib.stem + ext = compiled_lib.suffix + triplet = host_platform["triplet"] + abi_signature = host_platform["abi_signature"] + tagged_filename = f"{base_name}.{triplet}.{abi_signature}{ext}" + staged_lib_path = binaries_dir / tagged_filename + + report_progress(f" - Staging binary as: {tagged_filename}") + shutil.copy(compiled_lib, staged_lib_path) + + manifest["bundlePlugins"][plugin_name] = { + "sdist": { + "path": sdist_path.name, + "sdistBundledOn": datetime.datetime.now().isoformat(), + "buildable": True + }, + "binaries": [{ + "platform": { + "triplet": host_platform["triplet"], + "abi_signature": host_platform["abi_signature"], + "arch": host_platform["arch"] + }, + "path": staged_lib_path.relative_to(staging_dir).as_posix(), + "compiledOn": datetime.datetime.now().isoformat() + }] + } + + manifest_path = staging_dir / "manifest.yaml" + with open(manifest_path, 'w') as f: + yaml.dump(manifest, f, sort_keys=False) + + report_progress(f"\nPackaging final bundle: {output_bundle}") + with zipfile.ZipFile(output_bundle, 'w', zipfile.ZIP_DEFLATED) as bundle_zip: + for root, _, files in os.walk(staging_dir): + for file in files: + file_path = Path(root) / file + bundle_zip.write(file_path, file_path.relative_to(staging_dir)) + + report_progress("\nāœ… Bundle created successfully!") + finally: + if staging_dir.exists(): + shutil.rmtree(staging_dir) + +def _create_canonical_checksum_list(staging_dir: Path, manifest: dict) -> str: + """ + Creates a deterministic, sorted string of all file paths and their checksums. + """ + checksum_map = {} + + for plugin_data in manifest.get('bundlePlugins', {}).values(): + sdist_info = plugin_data.get('sdist', {}) + if 'path' in sdist_info: + file_path = staging_dir / sdist_info['path'] + if file_path.exists(): + checksum = "sha256:" + calculate_sha256(file_path) + sdist_info['checksum'] = checksum + checksum_map[sdist_info['path']] = checksum + else: + raise FileNotFoundError(f"sdist file not found: {sdist_info['path']}") + + for binary in plugin_data.get('binaries', []): + if 'path' in binary: + file_path = staging_dir / binary['path'] + if file_path.exists(): + checksum = "sha256:" + calculate_sha256(file_path) + binary['checksum'] = checksum + checksum_map[binary['path']] = checksum + else: + raise FileNotFoundError(f"Binary file not found: {binary['path']}") + + sorted_paths = sorted(checksum_map.keys()) + canonical_list = [f"{path}:{checksum_map[path]}" for path in sorted_paths] + return "\n".join(canonical_list) + +def edit_bundle_metadata(bundle_path: Path, metadata: dict, progress_callback: Optional[Callable] = None) -> Dict[str, Any]: + """ + Edits the metadata in the manifest of an existing bundle. + + REFACTORED: Now returns a JSON-serializable dictionary directly. + Progress messages go only to the callback, never to stdout. + + Returns: + Dict with structure: + { + "success": bool, + "message": str, + "updated_fields": ["field1", "field2", ...] + } + + On error: + { + "success": false, + "error": "error message" + } + + Args: + bundle_path (Path): The path to the .fbundle file. + metadata (dict): A dictionary containing the metadata to update. + Valid keys include: 'bundle_name', 'bundle_version', + 'bundle_author', 'bundle_comment'. + progress_callback (callable, optional): A function to call with progress messages. + """ + def _progress(msg: str) -> None: + if progress_callback: + progress_callback(msg) + # No fallback to print() - all output goes through callback only + + _progress(f"Opening {bundle_path.name} to edit metadata...") + if not bundle_path.exists() or not zipfile.is_zipfile(bundle_path): + raise FileNotFoundError("Bundle is not a valid zip file.") + + with zipfile.ZipFile(bundle_path, 'a') as zf: + if MANIFEST_FILENAME not in zf.namelist(): + raise FileNotFoundError(f"{MANIFEST_FILENAME} not found in bundle.") + + with zf.open(MANIFEST_FILENAME, 'r') as f: + manifest = yaml.safe_load(f) + + _progress("Updating manifest...") + updated_fields = [] + for key, value in metadata.items(): + # Convert snake_case from JS to camelCase for YAML + camel_case_key = ''.join(word.capitalize() for word in key.split('_')) + camel_case_key = camel_case_key[0].lower() + camel_case_key[1:] + if value: + manifest[camel_case_key] = value + updated_fields.append(key) + + # In-place update of the manifest requires creating a new zip file + # or rewriting the existing one, as 'a' mode can't update files. + # A simpler approach is to read all files, write to a new zip, and replace. + temp_bundle_path = bundle_path.with_suffix('.zip.tmp') + with zipfile.ZipFile(temp_bundle_path, 'w', zipfile.ZIP_DEFLATED) as temp_zf: + for item in zf.infolist(): + if item.filename == MANIFEST_FILENAME: + continue # Skip old manifest + buffer = zf.read(item.filename) + temp_zf.writestr(item, buffer) + + # Write the updated manifest + new_manifest_content = yaml.dump(manifest, Dumper=yaml.SafeDumper) + temp_zf.writestr(MANIFEST_FILENAME, new_manifest_content) + + # Replace the original bundle with the updated one + shutil.move(temp_bundle_path, bundle_path) + _progress("Metadata updated successfully.") + + return { + 'success': True, + 'message': f"Metadata updated successfully: {bundle_path.name}", + 'updated_fields': updated_fields + } + +def sign_bundle(bundle_path: Path, private_key: Path, progress_callback: Optional[Callable] = None) -> Dict[str, Any]: + """ + Signs a bundle with an author's private key. + + REFACTORED: Now returns a JSON-serializable dictionary directly. + Progress messages go only to the callback, never to stdout. + + Returns: + Dict with structure: + { + "success": bool, + "message": str, + "signature_info": {...} # Details about the signature + } + + On error: + { + "success": false, + "error": "error message" + } + """ + def report_progress(message): + if progress_callback: + progress_callback(message) + else: + print(message) + + report_progress(f"Signing bundle: {bundle_path}") + staging_dir = Path(tempfile.mkdtemp(prefix="fourdst_sign_")) + + try: + with zipfile.ZipFile(bundle_path, 'r') as bundle_zip: + bundle_zip.extractall(staging_dir) + + manifest_path = staging_dir / "manifest.yaml" + if not manifest_path.exists(): + raise FileNotFoundError("manifest.yaml not found in bundle.") + + if private_key.suffix.lower() != ".pem": + raise ValueError("Private key must be a .pem file.") + + report_progress(" - Deriving public key fingerprint via openssl...") + proc = run_command(["openssl", "pkey", "-in", str(private_key), "-pubout", "-outform", "DER"], binary_output=True) + pub_der = proc.stdout + fingerprint = "sha256:" + hashlib.sha256(pub_der).hexdigest() + report_progress(f" - Signing with key fingerprint: {fingerprint}") + + with open(manifest_path, 'r') as f: + manifest = yaml.safe_load(f) + + report_progress(" - Calculating and embedding file checksums...") + canonical_checksums = _create_canonical_checksum_list(staging_dir, manifest) + + report_progress(" - Detecting key type...") + try: + with open(private_key, "rb") as key_file: + private_key_obj = serialization.load_pem_private_key( + key_file.read(), + password=None + ) + except Exception as e: + raise ValueError(f"Could not load or parse private key: {e}") + + report_progress(" - Generating signature...") + if isinstance(private_key_obj, ed25519.Ed25519PrivateKey): + report_progress(" - Ed25519 key detected. Using pkeyutl for signing.") + # pkeyutl with -rawin can be tricky with stdin, so we use a temporary file. + with tempfile.NamedTemporaryFile(delete=False) as temp_input_file: + temp_input_file.write(canonical_checksums.encode('utf-8')) + temp_input_path = temp_input_file.name + try: + signature_proc = run_command( + ["openssl", "pkeyutl", "-sign", "-inkey", str(private_key), "-in", temp_input_path], + binary_output=True + ) + finally: + os.remove(temp_input_path) + elif isinstance(private_key_obj, rsa.RSAPrivateKey): + report_progress(" - RSA key detected. Using dgst for signing.") + signature_proc = run_command( + ["openssl", "dgst", "-sha256", "-sign", str(private_key)], + input=canonical_checksums.encode('utf-8'), + binary_output=True + ) + else: + raise TypeError(f"Unsupported private key type: {type(private_key_obj)}") + signature_hex = signature_proc.stdout.hex() + + manifest['bundleSignature'] = { + 'keyFingerprint': fingerprint, + 'signature': signature_hex, + 'signedOn': datetime.datetime.now().isoformat() + } + + with open(manifest_path, 'w') as f: + yaml.dump(manifest, f, sort_keys=False) + + report_progress(f" - Repackaging bundle: {bundle_path}") + with zipfile.ZipFile(bundle_path, 'w', zipfile.ZIP_DEFLATED) as bundle_zip: + for root, _, files in os.walk(staging_dir): + for file in files: + file_path = Path(root) / file + bundle_zip.write(file_path, file_path.relative_to(staging_dir)) + + report_progress("\nāœ… Bundle signed successfully!") + + finally: + if staging_dir.exists(): + shutil.rmtree(staging_dir) + +def validate_bundle(bundle_path: Path, progress_callback: Optional[Callable] = None) -> Dict[str, Any]: + """ + Validates a bundle's integrity and checksums. + + REFACTORED: Now returns a JSON-serializable dictionary directly. + Progress messages go only to the callback, never to stdout. + + Returns: + Dict containing validation results with structure: + { + "success": bool, + "errors": List[str], + "warnings": List[str], + "summary": {"errors": int, "warnings": int}, + "status": "passed" | "failed" + } + """ + def report_progress(message: str) -> None: + if progress_callback: + progress_callback(message) + # No fallback to print() - all output goes through callback only + + results = { + 'success': True, # Will be set to False if errors found + 'errors': [], + 'warnings': [], + 'summary': {}, + 'status': 'failed' + } + + report_progress(f"Validating bundle: {bundle_path}") + staging_dir = Path(tempfile.mkdtemp(prefix="fourdst_validate_")) + + try: + # 1. Unpack the bundle + try: + with zipfile.ZipFile(bundle_path, 'r') as bundle_zip: + bundle_zip.extractall(staging_dir) + report_progress(" - Bundle unpacked successfully.") + except zipfile.BadZipFile: + results['errors'].append(f"'{bundle_path.name}' is not a valid zip file.") + return results + + # 2. Manifest validation + manifest_path = staging_dir / "manifest.yaml" + if not manifest_path.exists(): + results['errors'].append("Missing manifest.yaml file.") + return results + + try: + manifest = yaml.safe_load(manifest_path.read_text()) + if not manifest: + results['warnings'].append("Manifest file is empty.") + manifest = {} + except yaml.YAMLError as e: + results['errors'].append(f"Manifest file is not valid YAML: {e}") + return results + + # 3. Content and checksum validation + report_progress(" - Validating manifest content and file checksums...") + if 'bundleName' not in manifest: + results['errors'].append("Manifest is missing 'bundleName'.") + if 'bundleVersion' not in manifest: + results['errors'].append("Manifest is missing 'bundleVersion'.") + + plugins = manifest.get('bundlePlugins', {}) + if not plugins: + results['warnings'].append("Manifest 'bundlePlugins' section is empty or missing.") + + for name, data in plugins.items(): + sdist_info = data.get('sdist', {}) + sdist_path_str = sdist_info.get('path') + if not sdist_path_str: + results['errors'].append(f"sdist path not defined for plugin '{name}'.") + else: + sdist_path = staging_dir / sdist_path_str + if not sdist_path.exists(): + results['errors'].append(f"sdist file not found: {sdist_path_str}") + + for binary in data.get('binaries', []): + bin_path_str = binary.get('path') + if not bin_path_str: + results['errors'].append(f"Binary entry for '{name}' is missing a 'path'.") + continue + + bin_path = staging_dir / bin_path_str + if not bin_path.exists(): + results['errors'].append(f"Binary file not found: {bin_path_str}") + continue + + expected_checksum = binary.get('checksum') + if not expected_checksum: + results['warnings'].append(f"Checksum not defined for binary '{bin_path_str}'.") + else: + actual_checksum = "sha256:" + calculate_sha256(bin_path) + if actual_checksum != expected_checksum: + results['errors'].append(f"Checksum mismatch for {bin_path_str}") + + # 4. Signature check (presence only) + if 'bundleSignature' not in manifest: + results['warnings'].append("Bundle is not signed (missing 'bundleSignature' in manifest).") + else: + report_progress(" - Signature block found in manifest.") + + # Finalize results + results['summary'] = {'errors': len(results['errors']), 'warnings': len(results['warnings'])} + if not results['errors']: + results['status'] = 'passed' + + # Set success flag based on errors + results['success'] = len(results['errors']) == 0 + + except Exception as e: + # Catch any unexpected errors and return them as JSON + logging.exception(f"Unexpected error validating bundle {bundle_path}") + return { + 'success': False, + 'error': f"Unexpected error during validation: {str(e)}", + 'errors': [str(e)], + 'warnings': [], + 'summary': {'errors': 1, 'warnings': 0}, + 'status': 'failed' + } + finally: + if staging_dir.exists(): + shutil.rmtree(staging_dir) + + return results + +def inspect_bundle(bundle_path: Path) -> Dict[str, Any]: + """ + Performs a comprehensive inspection of a bundle, returning a structured report. + + REFACTORED: Now returns a JSON-serializable dictionary directly. + No progress callbacks needed since this is a read-only inspection. + + Returns: + Dict containing complete bundle inspection with structure: + { + "success": bool, + "validation": {...}, # Results from validate_bundle + "signature": {...}, # Trust and signature info + "plugins": {...}, # Plugin and binary compatibility + "manifest": {...}, # Raw manifest data + "host_info": {...} # Current platform info + } + + On error: + { + "success": false, + "error": "error message" + } + """ + try: + report = { + 'success': True, + 'validation': {}, + 'signature': {}, + 'plugins': {}, + 'manifest': None, + 'host_info': get_platform_identifier() + } + + # 1. Basic validation (file integrity, checksums) + # Pass a no-op callback to prevent any progress output + validation_result = validate_bundle(bundle_path, progress_callback=lambda msg: None) + report['validation'] = validation_result + + # If basic validation fails, return early + if not validation_result.get('success', False): + critical_errors = validation_result.get('errors', []) + if any("not a valid zip file" in e or "Missing manifest.yaml" in e for e in critical_errors): + return report + + staging_dir = Path(tempfile.mkdtemp(prefix="fourdst_inspect_")) + try: + with zipfile.ZipFile(bundle_path, 'r') as bundle_zip: + bundle_zip.extractall(staging_dir) + + manifest_path = staging_dir / "manifest.yaml" + with open(manifest_path, 'r') as f: + manifest = yaml.safe_load(f) or {} + + report['manifest'] = manifest + + # 2. Signature and Trust Verification + sig_info = manifest.get('bundleSignature', {}) + fingerprint = sig_info.get('keyFingerprint') + signature_hex = sig_info.get('signature') + + if not cryptography: + report['signature']['status'] = 'UNSUPPORTED' + report['signature']['reason'] = 'cryptography module not installed.' + elif not fingerprint or not signature_hex: + report['signature']['status'] = 'UNSIGNED' + else: + report['signature']['fingerprint'] = fingerprint + trusted_key_path = None + if LOCAL_TRUST_STORE_PATH.exists(): + for key_file in LOCAL_TRUST_STORE_PATH.rglob("*.pem"): + try: + pub_der = (serialization.load_pem_public_key(key_file.read_bytes()) + .public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo)) + pub_key_fingerprint = "sha256:" + hashlib.sha256(pub_der).hexdigest() + if pub_key_fingerprint == fingerprint: + trusted_key_path = key_file + break + except Exception: + continue + + if not trusted_key_path: + report['signature']['status'] = 'UNTRUSTED' + else: + try: + pub_key_obj = serialization.load_pem_public_key(trusted_key_path.read_bytes()) + signature = bytes.fromhex(signature_hex) + + # Re-calculate checksums from disk to verify against the signature + data_to_verify = _create_canonical_checksum_list(staging_dir, manifest).encode('utf-8') + + if isinstance(pub_key_obj, ed25519.Ed25519PublicKey): + pub_key_obj.verify(signature, data_to_verify) + elif isinstance(pub_key_obj, rsa.RSAPublicKey): + pub_key_obj.verify(signature, data_to_verify, padding.PKCS1v15(), hashes.SHA256()) + + if report['validation']['status'] == 'passed': + report['signature']['status'] = 'TRUSTED' + report['signature']['key_path'] = str(trusted_key_path.relative_to(LOCAL_TRUST_STORE_PATH)) + else: + report['signature']['status'] = 'TAMPERED' + report['signature']['reason'] = 'Signature is valid, but file contents do not match manifest checksums.' + + except InvalidSignature: + report['signature']['status'] = 'INVALID' + report['signature']['reason'] = 'Cryptographic signature verification failed.' + except Exception as e: + report['signature']['status'] = 'ERROR' + report['signature']['reason'] = str(e) + + # 3. Plugin and Binary Compatibility Analysis + host_info = report['host_info'] + for name, data in manifest.get('bundlePlugins', {}).items(): + report['plugins'][name] = {'binaries': [], 'sdist_path': data.get('sdist', {}).get('path')} + compatible_found = False + for binary in data.get('binaries', []): + plat = binary.get('platform', {}) + plat['os'] = plat.get('triplet', "unk-unk").split('-')[1] + is_compatible, reason = is_abi_compatible(host_info, plat) + binary['is_compatible'] = is_compatible + binary['incompatibility_reason'] = None if is_compatible else reason + report['plugins'][name]['binaries'].append(binary) + if is_compatible: + compatible_found = True + report['plugins'][name]['compatible_found'] = compatible_found + + finally: + if staging_dir.exists(): + shutil.rmtree(staging_dir) + + return report + + except Exception as e: + # Catch any unexpected errors and return them as JSON + logging.exception(f"Unexpected error inspecting bundle {bundle_path}") + return { + 'success': False, + 'error': f"Unexpected error during inspection: {str(e)}" + } + +def clear_bundle(bundle_path: Path, progress_callback: Optional[Callable] = None) -> Dict[str, Any]: + """ + Removes all compiled binaries and signatures from a bundle. + + REFACTORED: Now returns a JSON-serializable dictionary directly. + Progress messages go only to the callback, never to stdout. + + Returns: + Dict with structure: + { + "success": bool, + "message": str, + "cleared_items": {"binaries": int, "signatures": int} + } + + On error: + { + "success": false, + "error": "error message" + } + """ + def report_progress(message): + if progress_callback: + progress_callback(message) + else: + print(message) + + report_progress(f"Clearing binaries from bundle: {bundle_path.name}") + staging_dir = Path(tempfile.mkdtemp(prefix="fourdst_clear_")) + + try: + # 1. Unpack the bundle + report_progress(" - Unpacking bundle...") + with zipfile.ZipFile(bundle_path, 'r') as bundle_zip: + bundle_zip.extractall(staging_dir) + + # 2. Read the manifest + manifest_path = staging_dir / "manifest.yaml" + if not manifest_path.is_file(): + raise FileNotFoundError("Bundle is invalid. Missing manifest.yaml.") + + with open(manifest_path, 'r') as f: + manifest = yaml.safe_load(f) + + # 3. Clear binaries and signatures from manifest + report_progress(" - Clearing binary and signature information from manifest...") + manifest.pop('bundleSignature', None) + + for plugin_name, plugin_data in manifest.get('bundlePlugins', {}).items(): + if 'binaries' in plugin_data: + report_progress(f" - Clearing binaries for plugin '{plugin_name}'") + plugin_data['binaries'] = [] + + # 4. Delete the binaries directory and signature file from disk + bin_dir = staging_dir / "bin" + if bin_dir.is_dir(): + shutil.rmtree(bin_dir) + report_progress(" - Removed 'bin/' directory.") + + sig_file = staging_dir / "manifest.sig" + if sig_file.is_file(): + sig_file.unlink() + report_progress(" - Removed 'manifest.sig'.") + + # 5. Write the updated manifest + with open(manifest_path, 'w') as f: + yaml.dump(manifest, f, sort_keys=False) + + # 6. Repack the bundle + report_progress(" - Repackaging the bundle...") + with zipfile.ZipFile(bundle_path, 'w', zipfile.ZIP_DEFLATED) as bundle_zip: + for file_path in staging_dir.rglob('*'): + if file_path.is_file(): + bundle_zip.write(file_path, file_path.relative_to(staging_dir)) + + report_progress(f"\nāœ… Bundle '{bundle_path.name}' has been cleared of all binaries.") + + finally: + if staging_dir.exists(): + shutil.rmtree(staging_dir) + +def diff_bundle(bundle_a_path: Path, bundle_b_path: Path, progress_callback=None): + """ + Compares two bundle files and returns a dictionary of differences. + """ + def report_progress(message): + if progress_callback: + progress_callback(message) + + results = { + 'signature': {}, + 'manifest': {}, + 'files': [] + } + + report_progress(f"Comparing {bundle_a_path.name} and {bundle_b_path.name}") + with tempfile.TemporaryDirectory() as temp_a_str, tempfile.TemporaryDirectory() as temp_b_str: + temp_a = Path(temp_a_str) + temp_b = Path(temp_b_str) + + report_progress(" - Unpacking bundles...") + with zipfile.ZipFile(bundle_a_path, 'r') as z: z.extractall(temp_a) + with zipfile.ZipFile(bundle_b_path, 'r') as z: z.extractall(temp_b) + + # 1. Compare Signatures + sig_a_path = temp_a / "manifest.sig" + sig_b_path = temp_b / "manifest.sig" + sig_a = sig_a_path.read_bytes() if sig_a_path.exists() else None + sig_b = sig_b_path.read_bytes() if sig_b_path.exists() else None + + if sig_a == sig_b and sig_a is not None: + results['signature']['status'] = 'UNCHANGED' + elif sig_a and not sig_b: + results['signature']['status'] = 'REMOVED' + elif not sig_a and sig_b: + results['signature']['status'] = 'ADDED' + elif sig_a and sig_b and sig_a != sig_b: + results['signature']['status'] = 'CHANGED' + else: + results['signature']['status'] = 'UNSIGNED' + + # 2. Compare Manifests + manifest_a_content = (temp_a / "manifest.yaml").read_text() + manifest_b_content = (temp_b / "manifest.yaml").read_text() + + if manifest_a_content != manifest_b_content: + import difflib + diff = difflib.unified_diff( + manifest_a_content.splitlines(keepends=True), + manifest_b_content.splitlines(keepends=True), + fromfile=f"{bundle_a_path.name}/manifest.yaml", + tofile=f"{bundle_b_path.name}/manifest.yaml", + ) + results['manifest']['diff'] = list(diff) + else: + results['manifest']['diff'] = [] + + # 3. Compare File Contents (via checksums in manifest) + manifest_a = yaml.safe_load(manifest_a_content) + manifest_b = yaml.safe_load(manifest_b_content) + + def get_files_from_manifest(manifest): + files = {} + for plugin in manifest.get('bundlePlugins', {}).values(): + sdist_info = plugin.get('sdist', {}) + if 'path' in sdist_info and 'checksum' in sdist_info: + files[sdist_info['path']] = sdist_info['checksum'] + for binary in plugin.get('binaries', []): + if 'path' in binary and 'checksum' in binary: + files[binary['path']] = binary['checksum'] + return files + + files_a = get_files_from_manifest(manifest_a) + files_b = get_files_from_manifest(manifest_b) + + all_files = sorted(list(set(files_a.keys()) | set(files_b.keys()))) + + for file in all_files: + in_a = file in files_a + in_b = file in files_b + checksum_a = files_a.get(file) + checksum_b = files_b.get(file) + + if in_a and not in_b: + results['files'].append({'path': file, 'status': 'REMOVED', 'details': ''}) + elif not in_a and in_b: + results['files'].append({'path': file, 'status': 'ADDED', 'details': ''}) + elif checksum_a != checksum_b: + details = f"Checksum changed from {checksum_a or 'N/A'} to {checksum_b or 'N/A'}" + results['files'].append({'path': file, 'status': 'MODIFIED', 'details': details}) + + return results + +def get_fillable_targets(bundle_path: Path) -> Dict[str, Any]: + """ + Inspects a bundle and determines which plugins are missing binaries for available build targets. + + REFACTORED: Now returns a JSON-serializable dictionary directly. + + Returns: + Dict with structure: + { + "success": bool, + "data": { + "plugin_name": [target1, target2, ...], + ... + } + } + + On error: + { + "success": false, + "error": "error message" + } + """ + try: + staging_dir = Path(tempfile.mkdtemp(prefix="fourdst_fillable_")) + try: + with zipfile.ZipFile(bundle_path, 'r') as bundle_zip: + bundle_zip.extractall(staging_dir) + + manifest_path = staging_dir / "manifest.yaml" + with open(manifest_path, 'r') as f: + manifest = yaml.safe_load(f) or {} + + available_targets = get_available_build_targets() + result = {} + + for plugin_name, plugin_data in manifest.get('bundlePlugins', {}).items(): + existing_targets = set() + for binary in plugin_data.get('binaries', []): + platform_info = binary.get('platform', {}) + existing_targets.add(platform_info.get('triplet', 'unknown')) + + fillable = [target for target in available_targets if target['triplet'] not in existing_targets] + if fillable: + result[plugin_name] = fillable + + return { + 'success': True, + 'data': result + } + finally: + if staging_dir.exists(): + shutil.rmtree(staging_dir) + except Exception as e: + logging.exception(f"Unexpected error getting fillable targets for {bundle_path}") + return { + 'success': False, + 'error': f"Unexpected error: {str(e)}" + } + +def fill_bundle(bundle_path: Path, targets_to_build: dict, progress_callback: Optional[Callable] = None) -> Dict[str, Any]: + """ + Fills a bundle with newly compiled binaries for the specified targets. + + REFACTORED: Now returns a JSON-serializable dictionary directly. + Progress messages go only to the callback, never to stdout. + Structured progress messages are sent for streaming updates. + + Returns: + Dict with structure: + { + "success": bool, + "message": str, + "build_results": { + "successful": int, + "failed": int, + "details": [...] + } + } + + On error: + { + "success": false, + "error": "error message" + } + + Args: + bundle_path: Path to the .fbundle file. + targets_to_build: A dictionary like {'plugin_name': [target1, target2]} specifying what to build. + progress_callback: An optional function to report progress. + """ + def report_progress(message) -> None: + if progress_callback: + # The message can be a string or a dict for structured updates + progress_callback(message) + # No fallback to print() - all output goes through callback only + + staging_dir = Path(tempfile.mkdtemp(prefix="fourdst_fill_")) + try: + report_progress("Unpacking bundle to temporary directory...") + with zipfile.ZipFile(bundle_path, 'r') as bundle_zip: + bundle_zip.extractall(staging_dir) + + manifest_path = staging_dir / "manifest.yaml" + with open(manifest_path, 'r') as f: + manifest = yaml.safe_load(f) + + binaries_dir = staging_dir / "bin" + binaries_dir.mkdir(exist_ok=True) + + for plugin_name, targets in targets_to_build.items(): + report_progress(f"Processing plugin: {plugin_name}") + plugin_info = manifest['bundlePlugins'][plugin_name] + sdist_path = staging_dir / plugin_info['sdist']['path'] + + for target in targets: + target_triplet = target['triplet'] + report_progress({ + 'status': 'building', + 'plugin': plugin_name, + 'target': target_triplet, + 'message': f"Building {plugin_name} for {target_triplet}..." + }) + build_dir = Path(tempfile.mkdtemp(prefix=f"{plugin_name}_build_")) + + try: + if target['type'] == 'docker': + compiled_lib, final_target = build_plugin_in_docker( + sdist_path, build_dir, target, plugin_name, progress_callback + ) + else: # native or cross + compiled_lib, final_target = build_plugin_for_target( + sdist_path, build_dir, target, progress_callback + ) + + # Stage the new binary + base_name = compiled_lib.stem + ext = compiled_lib.suffix + tagged_filename = f"{base_name}.{final_target['triplet']}.{final_target['abi_signature']}{ext}" + staged_lib_path = binaries_dir / tagged_filename + shutil.copy(compiled_lib, staged_lib_path) + + # Update manifest + new_binary_entry = { + 'platform': final_target, + 'path': staged_lib_path.relative_to(staging_dir).as_posix(), + 'compiledOn': datetime.datetime.now().isoformat(), + 'checksum': "sha256:" + calculate_sha256(staged_lib_path) + } + plugin_info.setdefault('binaries', []).append(new_binary_entry) + + report_progress({ + 'status': 'success', + 'plugin': plugin_name, + 'target': target_triplet, + 'message': f"Successfully built and staged {tagged_filename}" + }) + + except Exception as e: + report_progress({ + 'status': 'failure', + 'plugin': plugin_name, + 'target': target_triplet, + 'message': f"Failed to build {plugin_name} for {target_triplet}: {e}" + }) + finally: + if build_dir.exists(): + shutil.rmtree(build_dir) + + # Write the updated manifest + with open(manifest_path, 'w') as f: + yaml.dump(manifest, f, sort_keys=False) + + # Repack the bundle + report_progress(f"Repackaging bundle: {bundle_path.name}") + with zipfile.ZipFile(bundle_path, 'w', zipfile.ZIP_DEFLATED) as bundle_zip: + for file_path in staging_dir.rglob('*'): + if file_path.is_file(): + bundle_zip.write(file_path, file_path.relative_to(staging_dir)) + + report_progress({"status": "complete", "message": "āœ… Bundle filled successfully!"}) + + return { + 'success': True, + 'message': f"Bundle filled successfully: {bundle_path.name}", + 'build_results': { + 'successful': successful_builds, + 'failed': failed_builds, + 'details': build_details + } + } + + except Exception as e: + logging.exception(f"Unexpected error filling bundle {bundle_path}") + return { + 'success': False, + 'error': f"Unexpected error during bundle filling: {str(e)}" + } + finally: + if staging_dir.exists(): + shutil.rmtree(staging_dir) + diff --git a/fourdst/core/config.py b/fourdst/core/config.py new file mode 100644 index 0000000..d47c4f3 --- /dev/null +++ b/fourdst/core/config.py @@ -0,0 +1,21 @@ +# fourdst/core/config.py + +from pathlib import Path + +FOURDST_CONFIG_DIR = Path.home() / ".config" / "fourdst" +LOCAL_TRUST_STORE_PATH = FOURDST_CONFIG_DIR / "keys" +CROSS_FILES_PATH = FOURDST_CONFIG_DIR / "cross" +CACHE_PATH = FOURDST_CONFIG_DIR / "cache" +ABI_CACHE_FILE = CACHE_PATH / "abi_identifier.json" +DOCKER_BUILD_IMAGES = { + "x86_64 (manylinux_2_28)": "quay.io/pypa/manylinux_2_28_x86_64", + "aarch64 (manylinux_2_28)": "quay.io/pypa/manylinux_2_28_aarch64", + "i686 (manylinux_2_28)" : "quay.io/pypa/manylinux_2_28_i686", + "ppc64le (manylinux_2_28)" : "quay.io/pypa/manylinux_2_28_ppc64le", + "s390x (manylinux_2_28)" : "quay.io/pypa/manylinux_2_28_s390x" +} + +# Ensure the necessary directories exist +LOCAL_TRUST_STORE_PATH.mkdir(parents=True, exist_ok=True) +CROSS_FILES_PATH.mkdir(parents=True, exist_ok=True) +CACHE_PATH.mkdir(parents=True, exist_ok=True) diff --git a/fourdst/core/platform.py b/fourdst/core/platform.py new file mode 100644 index 0000000..cd172aa --- /dev/null +++ b/fourdst/core/platform.py @@ -0,0 +1,253 @@ +# fourdst/core/platform.py + +import json +import platform +import shutil +import subprocess +from pathlib import Path + +from fourdst.core.config import ABI_CACHE_FILE, CACHE_PATH +from fourdst.core.utils import run_command + +ABI_DETECTOR_CPP_SRC = """ +#include +#include +#include + +#ifdef __GNUC__ +#if __has_include() +#include +#endif +#endif + +int main() { + std::string os; + std::string compiler; + std::string compiler_version; + std::string stdlib; + std::string stdlib_version; + std::string abi; + +#if defined(__APPLE__) && defined(__MACH__) + os = "macos"; +#elif defined(__linux__) + os = "linux"; +#elif defined(_WIN32) + os = "windows"; +#else + os = "unknown_os"; +#endif + +#if defined(__clang__) + compiler = "clang"; + compiler_version = __clang_version__; +#elif defined(__GNUC__) + compiler = "gcc"; + compiler_version = std::to_string(__GNUC__) + "." + std::to_string(__GNUC_MINOR__) + "." + std::to_string(__GNUC_PATCHLEVEL__); +#elif defined(_MSC_VER) + compiler = "msvc"; + compiler_version = std::to_string(_MSC_VER); +#else + compiler = "unknown_compiler"; + compiler_version = "0"; +#endif + +#if defined(_LIBCPP_VERSION) + stdlib = "libc++"; + stdlib_version = std::to_string(_LIBCPP_VERSION); + abi = "libc++_abi"; // On libc++, the ABI is tightly coupled with the library itself. +#elif defined(__GLIBCXX__) + stdlib = "libstdc++"; + #if defined(_GLIBCXX_USE_CXX11_ABI) + abi = _GLIBCXX_USE_CXX11_ABI == 1 ? "cxx11_abi" : "pre_cxx11_abi"; + #else + abi = "pre_cxx11_abi"; + #endif + #if __has_include() + stdlib_version = gnu_get_libc_version(); + #else + stdlib_version = "unknown"; + #endif +#else + stdlib = "unknown_stdlib"; + abi = "unknown_abi"; +#endif + + std::cout << "os=" << os << std::endl; + std::cout << "compiler=" << compiler << std::endl; + std::cout << "compiler_version=" << compiler_version << std::endl; + std::cout << "stdlib=" << stdlib << std::endl; + if (!stdlib_version.empty()) { + std::cout << "stdlib_version=" << stdlib_version << std::endl; + } + // Always print the ABI key for consistent parsing + std::cout << "abi=" << abi << std::endl; + + return 0; +} +""" + +ABI_DETECTOR_MESON_SRC = """ +project('abi-detector', 'cpp', default_options : ['cpp_std=c++23']) +executable('detector', 'main.cpp') +""" + +def _detect_and_cache_abi() -> dict: + """ + Compiles and runs a C++ program to detect the compiler ABI, then caches it. + """ + print("Performing one-time native C++ ABI detection...") + temp_dir = CACHE_PATH / "abi_detector" + if temp_dir.exists(): + shutil.rmtree(temp_dir) + temp_dir.mkdir(parents=True) + + try: + (temp_dir / "main.cpp").write_text(ABI_DETECTOR_CPP_SRC) + (temp_dir / "meson.build").write_text(ABI_DETECTOR_MESON_SRC) + + print(" - Configuring detector...") + run_command(["meson", "setup", "build"], cwd=temp_dir) + print(" - Compiling detector...") + run_command(["meson", "compile", "-C", "build"], cwd=temp_dir) + + detector_exe = temp_dir / "build" / "detector" + print(" - Running detector...") + proc = subprocess.run([str(detector_exe)], check=True, capture_output=True, text=True) + + abi_details = {} + for line in proc.stdout.strip().split('\n'): + key, value = line.split('=', 1) + abi_details[key] = value.strip() + + arch = platform.machine() + stdlib_version = abi_details.get('stdlib_version', 'unknown') + abi_string = f"{abi_details['compiler']}-{abi_details['stdlib']}-{stdlib_version}-{abi_details['abi']}" + + platform_data = { + "os": abi_details['os'], + "arch": arch, + "triplet": f"{arch}-{abi_details['os']}", + "abi_signature": abi_string, + "details": abi_details, + "is_native": True, + "cross_file": None, + "docker_image": None + } + + with open(ABI_CACHE_FILE, 'w') as f: + json.dump(platform_data, f, indent=4) + + print(f" - ABI details cached to {ABI_CACHE_FILE}") + return platform_data + + finally: + if temp_dir.exists(): + shutil.rmtree(temp_dir) + +def get_platform_identifier() -> dict: + """ + Gets the native platform identifier, using a cached value if available. + """ + if ABI_CACHE_FILE.exists(): + with open(ABI_CACHE_FILE, 'r') as f: + plat = json.load(f) + else: + plat = _detect_and_cache_abi() + plat['type'] = 'native' + return plat + +def _parse_version(version_str: str) -> tuple: + """Parses a version string like '12.3.1' into a tuple of integers.""" + return tuple(map(int, (version_str.split('.') + ['0', '0'])[:3])) + +def is_abi_compatible(host_platform: dict, binary_platform: dict) -> tuple[bool, str]: + """ + Checks if a binary's platform is compatible with the host's platform. + This is more nuanced than a simple string comparison, allowing for forward compatibility. + - macOS: A binary for an older OS version can run on a newer one, if the toolchain matches. + - Linux: A binary for an older GLIBC version can run on a newer one. + """ + required_keys = ['os', 'arch', 'abi_signature'] + if not all(key in host_platform for key in required_keys): + return False, f"Host platform data is malformed. Missing keys: {[k for k in required_keys if k not in host_platform]}" + if not all(key in binary_platform for key in required_keys): + return False, f"Binary platform data is malformed. Missing keys: {[k for k in required_keys if k not in binary_platform]}" + + host_os = host_platform.get('os') or host_platform.get('details', {}).get('os') + binary_os = binary_platform.get('os') or binary_platform.get('details', {}).get('os') + host_arch = host_platform.get('arch') or host_platform.get('details', {}).get('arch') + binary_arch = binary_platform.get('arch') or binary_platform.get('details', {}).get('arch') + + if host_os != binary_os: + return False, f"OS mismatch: host is {host_os}, binary is {binary_os}" + if host_arch != binary_arch: + return False, f"Architecture mismatch: host is {host_arch}, binary is {binary_arch}" + + host_sig = host_platform['abi_signature'] + binary_sig = binary_platform['abi_signature'] + + try: + host_parts = host_sig.split('-') + binary_parts = binary_sig.split('-') + + # Find version numbers in any position + host_ver_str = next((p for p in host_parts if p[0].isdigit()), None) + binary_ver_str = next((p for p in binary_parts if p[0].isdigit()), None) + + if not host_ver_str or not binary_ver_str: + return False, "Could not extract version from ABI signature" + + host_ver = _parse_version(host_ver_str) + binary_ver = _parse_version(binary_ver_str) + + if host_platform['os'] == 'macos': + # For macOS, also check for clang and libc++ + if 'clang' not in binary_sig: + return False, "Toolchain mismatch: 'clang' not in binary signature" + if 'libc++' not in binary_sig: + return False, "Toolchain mismatch: 'libc++' not in binary signature" + if host_ver < binary_ver: + return False, f"macOS version too old: host is {host_ver_str}, binary needs {binary_ver_str}" + return True, "Compatible" + + elif host_platform['os'] == 'linux': + if host_ver < binary_ver: + return False, f"GLIBC version too old: host is {host_ver_str}, binary needs {binary_ver_str}" + return True, "Compatible" + + except (IndexError, ValueError, StopIteration): + return False, "Malformed ABI signature string" + + return False, "Unknown compatibility check failure" + +def get_macos_targeted_platform_identifier(target_version: str) -> dict: + """ + Generates a platform identifier for a specific target macOS version. + """ + host_platform = get_platform_identifier() + host_details = host_platform['details'] + + compiler = host_details.get('compiler', 'clang') + stdlib = host_details.get('stdlib', 'libc++') + abi = host_details.get('abi', 'libc++_abi') + arch = platform.machine() + + abi_string = f"{compiler}-{stdlib}-{target_version}-{abi}" + + return { + "triplet": f"{arch}-macos", + "abi_signature": abi_string, + "details": { + "os": "macos", + "compiler": compiler, + "compiler_version": host_details.get('compiler_version'), + "stdlib": stdlib, + "stdlib_version": target_version, + "abi": abi, + }, + "is_native": True, + "cross_file": None, + "docker_image": None, + "arch": arch + } diff --git a/fourdst/core/utils.py b/fourdst/core/utils.py new file mode 100644 index 0000000..3f4ce13 --- /dev/null +++ b/fourdst/core/utils.py @@ -0,0 +1,47 @@ +# fourdst/core/utils.py + +import subprocess +from pathlib import Path +import hashlib + +def run_command(command: list[str], cwd: Path = None, check=True, progress_callback=None, input: bytes = None, env: dict = None, binary_output: bool = False): + """Runs a command, optionally reporting progress and using a custom environment.""" + command_str = ' '.join(command) + if progress_callback: + progress_callback(f"Running command: {command_str}") + + try: + result = subprocess.run( + command, + check=check, + capture_output=True, + text=not binary_output, + input=input, + cwd=cwd, + env=env + ) + + if progress_callback and result.stdout: + if binary_output: + progress_callback(f" - STDOUT: ") + else: + progress_callback(f" - STDOUT: {result.stdout.strip()}") + if progress_callback and result.stderr: + progress_callback(f" - STDERR: {result.stderr.strip()}") + + return result + except subprocess.CalledProcessError as e: + error_message = f"""Command '{command_str}' failed with exit code {e.returncode}.\n--- STDOUT ---\n{e.stdout.strip()}\n--- STDERR ---\n{e.stderr.strip()}\n""" + if progress_callback: + progress_callback(error_message) + if check: + raise Exception(error_message) from e + return e + +def calculate_sha256(file_path: Path) -> str: + """Calculates the SHA256 checksum of a file.""" + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() diff --git a/meson.build b/meson.build index 77a8fd1..0f7ba31 100644 --- a/meson.build +++ b/meson.build @@ -16,3 +16,22 @@ subdir('build-python') # Build python bindings subdir('src-pybind') + +# Bundle the Python backend for the Electron app + +if get_option('build-py-backend') + pyinstaller_exe = find_program('pyinstaller', required : true) + electron_src_dir = meson.current_source_dir() / 'electron' + + custom_target('fourdst-backend', + input : electron_src_dir / 'fourdst-backend.spec', + # The output is the directory that PyInstaller creates. + # We are interested in the executable inside it. + output : 'fourdst-backend', + # The command to run. We tell PyInstaller where to put the final executable. + command : [pyinstaller_exe, '--distpath', meson.current_build_dir() / 'electron/dist', '--workpath', meson.current_build_dir() / 'electron/build', '--noconfirm', '@INPUT@'], + # This ensures the backend is built whenever you run 'meson compile'. + build_by_default : true + ) +endif + diff --git a/meson_options.txt b/meson_options.txt index 0a6af95..b0447ae 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,2 +1,3 @@ option('pkg-config', type: 'boolean', value: false, description: 'generate pkg-config file for all libraries and fourdst (defaults to false to allow easy pip building)') +option('build-py-backend', type: 'boolean', value: false, description: 'use pyinstaller to build the python backend for the electron app') option('tests', type: 'boolean', value: false, description: 'compile subproject tests') diff --git a/pyproject.toml b/pyproject.toml index 9a89798..d6cfd1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,13 +21,14 @@ maintainers = [ ] dependencies = [ - "typer[all]", + "typer", "libclang", "questionary", "rich", "pyyaml", "cryptography", - "pyOpenSSL" + "pyOpenSSL", + "pyinstaller" ] [project.scripts]