From 3ed33d3d090579d85052b57917deff02e12cd2bb Mon Sep 17 00:00:00 2001 From: timbogdanov Date: Sun, 16 Aug 2026 20:06:12 -0500 Subject: [PATCH 1/4] Desktop app (Electron) for macOS and Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The editor becomes a real application rather than a wrapped web page: native menus, .patternfront documents, Open/Save/Save As, double-click to open from Finder or Explorer, recent files, exports through native Save dialogs, and an unsaved-changes prompt. app/patternfront.html stays the single source of truth. The same file is the browser build and the desktop renderer — nothing is forked. Every native call sits behind a native() feature test, and a test boots the code with no window.pfNative at all and fails if anything reaches for it. Two seams made this small. Exports already funnelled through one download() function, so redirecting them to a Save dialog was a single change. Documents already had serialiseDoc(); restoreDoc() split into loadDocFromJSON() so opening a file and restoring the autosave go down the same tested path. Security posture, since an open-source Electron app gets read by strangers: context isolation on, node integration off, sandbox on, webview disabled, navigation and popups denied, and the renderer served over a registered app:// scheme rather than file:// — a real origin, so localStorage works and a CSP can actually be enforced. The preload exposes ten validated calls and nothing else, asserted by a gate. Saves are atomic. A rename over the target means an interrupted write leaves the previous version intact rather than a truncated file, which is tested by making the directory read-only mid-save. Menu accelerators are owned by the menu. Electron dispatches them before the page sees the keydown, so the renderer skips those combos on desktop — without that, every undo fires twice. Two new suites and a headless launch: 45 checks on the shell's configuration, 19 on document handling, and an Electron smoke test that asserts the editor actually comes up rather than merely that it packaged. Builds are unsigned. Signing is wired but inert; enabling it is setting secrets, not editing config. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SPLUTYBHytZJX2BB6kCL9T --- .github/workflows/ci.yml | 70 + .github/workflows/release.yml | 96 + CONTRIBUTING.md | 88 + README.md | 56 +- app/patternfront.html | 104 +- build/entitlements.mac.plist | 23 + build/icon.png | Bin 0 -> 7833 bytes docs/assets/desktop.png | Bin 0 -> 92566 bytes electron-builder.yml | 78 + electron/documents.js | 163 ++ electron/main.js | 366 ++++ electron/menu.js | 134 ++ electron/preload.js | 55 + electron/window-state.js | 71 + package-lock.json | 3595 +++++++++++++++++++++++++++++++++ package.json | 33 + tools/gen-icon.py | 99 + tools/verify-all.sh | 2 + tools/verify-behaviour.js | 61 + tools/verify-documents.js | 148 ++ tools/verify-electron.py | 142 ++ tools/verify-ui.py | 36 + 22 files changed, 5403 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 CONTRIBUTING.md create mode 100644 build/entitlements.mac.plist create mode 100644 build/icon.png create mode 100644 docs/assets/desktop.png create mode 100644 electron-builder.yml create mode 100644 electron/documents.js create mode 100644 electron/main.js create mode 100644 electron/menu.js create mode 100644 electron/preload.js create mode 100644 electron/window-state.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tools/gen-icon.py create mode 100755 tools/verify-documents.js create mode 100755 tools/verify-electron.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 194c33b..7cb688b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,3 +60,73 @@ jobs: sys.exit(1) print(f"{len(a)} stamps match") PY + + smoke: + name: Desktop app launches + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + # Electron needs a display and the usual Chromium shared libraries. + # xvfb-run gives it a virtual one; without this the app cannot start at + # all and "it built" would be the only thing CI ever proved. + - name: Install a virtual display + run: | + sudo apt-get update + sudo apt-get install -y xvfb libgtk-3-0 libnss3 libasound2t64 \ + libgbm1 libxss1 libxtst6 + + - name: Launch the app and assert the editor came up + run: xvfb-run --auto-servernum npm run smoke + + - name: Capture the window + run: xvfb-run --auto-servernum npx electron . --shot /tmp/patternfront.png + + - uses: actions/upload-artifact@v4 + with: + name: screenshot + path: /tmp/patternfront.png + + package: + name: Package (${{ matrix.os }}) + # Packaging is slow, so it only runs once the cheap checks are green. + needs: [verify, smoke] + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + # Unsigned on purpose — see README. CSC_IDENTITY_AUTO_DISCOVERY=false + # stops electron-builder picking up a stray keychain identity on macOS + # runners and producing builds that are signed with something arbitrary. + - name: Build installers + run: npm run dist + env: + CSC_IDENTITY_AUTO_DISCOVERY: false + GH_TOKEN: "" + + - uses: actions/upload-artifact@v4 + with: + name: installers-${{ matrix.os }} + path: | + dist/*.dmg + dist/*.zip + dist/*.exe + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..53e2fe9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,96 @@ +name: Release + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: "Tag to build and release (e.g. v0.1.0)" + required: true + +permissions: + contents: write + +jobs: + build: + name: Build (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + # Refuse to cut a release from code that does not pass its own checks. + - name: Verify before packaging + shell: bash + run: ./tools/verify-all.sh + + # macOS builds both arm64 and x64 slices; Windows builds x64 + arm64. + # Unsigned: see README, "Install". To turn signing on, add CSC_LINK, + # CSC_KEY_PASSWORD, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD and + # APPLE_TEAM_ID as repository secrets and set hardenedRuntime: true in + # electron-builder.yml. No other change is needed. + - name: Build installers + run: npm run dist + env: + CSC_IDENTITY_AUTO_DISCOVERY: false + + - uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.os }} + path: | + dist/*.dmg + dist/*.zip + dist/*.exe + dist/latest*.yml + if-no-files-found: error + + release: + name: Publish release + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Collect + run: | + mkdir -p out + find artifacts -type f \( -name '*.dmg' -o -name '*.zip' -o -name '*.exe' \) \ + -exec cp {} out/ \; + ls -la out + + - name: Publish + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.inputs.tag || github.ref_name }} + draft: true + generate_release_notes: true + files: out/* + body: | + ## Install + + **macOS** — download the `.dmg` for your chip (`arm64` for Apple + silicon, `x64` for Intel). These builds are **unsigned**, so the + first launch needs: right-click the app → **Open** → **Open**. + + **Windows** — run the `Setup` installer, or use the portable + `.exe`. SmartScreen will show "Windows protected your PC" → + **More info** → **Run anyway**. + + Signing certificates cost money this project does not spend yet. + If you would rather not trust an unsigned build, `npm run dist` + builds it yourself from source in one command. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..830e2d8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,88 @@ +# Contributing + +## Getting set up + +```sh +git clone https://github.com/timbogdanov/patternfront +cd patternfront +npm install +npm start +``` + +Node 20+ and Python 3.10+. The Python side is stdlib only — no pip install, no virtualenv. + +## Before you open a pull request + +```sh +npm run verify +``` + +This has to pass. CI runs the same script, plus a headless launch of the desktop app. + +## How the project is laid out + +`app/patternfront.html` is the whole editor — markup, styles and logic in one file with no build +step and no dependencies. That is deliberate: it opens in a browser by double-clicking, and the +desktop app loads the identical file. There is no second copy to keep in sync. + +Native behaviour is feature-detected behind `window.pfNative`. If you add something desktop-only, +guard it with `native()` and make sure the browser path still works — there is a test that boots +the code with no `pfNative` at all and fails if anything reaches for it. + +## Things that will fail CI + +**Generated files must match their generator.** Stamps and codec fixtures are produced by scripts; +editing the output by hand is caught. Change the source and re-run: + +```sh +python3 tools/gen-stamps.py --emit # then paste the STAMPS block +python3 tools/gen-codec-fixtures.py +``` + +**The design rules are enforced.** `tools/verify-ui.py` fails the build on border radius above +2px, box shadows, blur, decorative gradients, chrome lighter than `--raised`, and stylesheet +lengths in `px` instead of `rem`. These are not suggestions; the visual direction was reset +several times before they existed. See `docs/09-design-system.md`. + +**No OpenFront assets.** The repository ships none, for licensing reasons set out in +[NOTICE](NOTICE), and there is a gate that fails if any reappear. + +## Adding a stamp + +Stamps are ASCII art in `tools/gen-stamps.py` — edit them as pictures, not as hex: + +```python +art("nature", "acorn", """ +................ +......####...... +.....######..... +... +""") +``` + +Run `python3 tools/gen-stamps.py`. The generator rejects anything under 8% or over 75% ink +(invisible, or a blob) and renders every surviving shape to `docs/assets/stamps.png`. + +**Look at that contact sheet.** It is the step that decides what ships. A 16×16 one-bit icon that +you know is a snail often reads as a camera to everyone else. Several shapes have been cut from +this library for exactly that reason, after two or three attempts each. Cutting one is a normal +outcome, not a failure. + +## Tests + +`tools/verify-behaviour.js` runs the editor's **real** functions by lifting them out of the HTML +and executing them in a sandbox. Nothing in it is a reimplementation — if you change a function, +the test runs your changed version. Add cases there rather than writing a parallel copy of the +logic. + +## Style + +Match what is around you. The codebase is dense and commented where the reasoning is not obvious +from the code — why a modulo has to be floor-mod, why a track is `minmax(0,1fr)`. Comments that +restate the line above are noise; comments that record a trap are the valuable kind. + +## Reporting bugs + +Include your OS and app version, and say what you expected. The version is in the About panel — +`PatternFront → About` on macOS, `Help → About` on Windows. If it is a rendering problem, a +screenshot saves a great deal of time. diff --git a/README.md b/README.md index 6fd86ab..2c43264 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # PatternFront A 1-bit pattern editor for [OpenFront.io](https://openfront.io) territory patterns — the small -tiling bitmaps that mark out a player's territory. Runs in any browser, from a single file. +tiling bitmaps that mark out a player's territory. Desktop app for macOS and Windows, and the +same editor runs in a browser. -![Stamps](docs/assets/stamps.png) +![PatternFront](docs/assets/desktop.png) Patterns are two colours and at most 129×65 pixels, which sounds trivial and is not: they tile against absolute world coordinates, so a seam that looks fine in the editor can read as a defect @@ -21,36 +22,60 @@ exports the exact `patternData` string the game takes. - **Live tiled preview** — see the seams before the game does - **Export** — PNG, sprite sheet, animated GIF, and OpenFront `patternData` (or JSON for `cosmetics.json`) -## Run it +On the desktop it is a real application, not a wrapped web page: native menus, `.patternfront` +documents with Open/Save/Save As, double-click to open from Finder or Explorer, recent files, +exports through native Save dialogs, remembered window position, and an unsaved-changes prompt +that will not let you lose work. -```sh -open app/patternfront.html # macOS -xdg-open app/patternfront.html # Linux -``` +## Install + +Grab a build from [Releases](https://github.com/timbogdanov/patternfront/releases). + +**These builds are unsigned.** Paying Apple and Microsoft for certificates is not something this +project does yet, so both systems will warn you the first time: -That is the whole thing. One self-contained file — no dependencies, no bundler, no build step and -no network access. Double-click it and it works. +- **macOS** — right-click the app → **Open** → **Open**. Once, then never again. +- **Windows** — SmartScreen shows "Windows protected your PC" → **More info** → **Run anyway**. -A desktop build for macOS and Windows is [in progress](../../pulls). +If that is not acceptable to you, build it yourself — it takes one command, below. -Running the checks needs Node 20+ and Python 3.10+: +> **Known issue building on recent macOS.** `npm run dist` can fail at the DMG step with +> `hdiutil: convert failed - Resource temporarily unavailable`, from the `dmgbuild` copy that +> electron-builder vendors. The `.zip` targets are unaffected, and the app inside them is +> identical — `npm run dist -- --mac zip` gets you a working build. Unzip and drag to +> Applications. + +## Run from source + +Needs Node 20+ and Python 3.10+ (Python is only for the generators and the verification suite). ```sh -./tools/verify-all.sh +npm install +npm start # run the desktop app +npm run verify # run every check +npm run dist # build installers for the current platform ``` +There is no build step for the editor itself. `app/patternfront.html` is one self-contained file +with no dependencies, no bundler and no network access — open it directly in a browser and it +works. The desktop build loads that same file; nothing is forked. + ## How it is put together ``` app/patternfront.html the entire editor — markup, styles, logic, stamps +electron/ main process, preload bridge, menus, document handling tools/ generators and the verification suite docs/ design documents tests/fixtures/ the codec corpus ``` +The renderer feature-detects `window.pfNative`. Without it — in a browser — every native path is +skipped and the file behaves exactly as it always did. That is asserted by tests, not assumed. + ## Verification -`./tools/verify-all.sh` runs nine suites. They exist because this project kept getting things subtly +`npm run verify` runs nine suites. They exist because this project kept getting things subtly wrong in ways that only mechanical checking caught — a seam metric that flagged correct patterns, a modulo that goes negative in JavaScript but not Python, an unguarded `localStorage` read that killed the app before first paint in any sandboxed frame. @@ -63,7 +88,8 @@ killed the app before first paint in any sandboxed frame. | map-scale sampling | `sampleAt` matches an independent oracle, including at negative world coordinates | | UI design rules | no shadows, no gradients, no radius above 2px, no light chrome — the design system, enforced | | stamp library | every stamp is a valid pattern, uniquely named, and legible at 1× | -| editor behaviour | ~70 assertions running the editor's real functions in a sandbox | +| editor behaviour | ~80 assertions running the editor's real functions in a sandbox | +| desktop smoke test | Electron actually launches and the editor comes up, headless | | docs vs OpenFront | *optional* — cross-checks the format docs against a local game checkout | The last one needs an OpenFront clone and **skips loudly** without one. It never silently passes. @@ -77,7 +103,7 @@ this repo's corpus hits exactly. ## Status, honestly -The editor is real and works. The AI features described in +The editor and the desktop app are real and work. The AI features described in [`docs/03`](docs/03-ai-integration.md), [`04`](docs/04-image-to-pattern.md) and [`07`](docs/07-cost-and-abuse.md) — text-to-pattern, AI region editing, hosted credits — are **designed and not built**. The image importer in the app today is ordinary quantisation and diff --git a/app/patternfront.html b/app/patternfront.html index b886be4..b5e7618 100644 --- a/app/patternfront.html +++ b/app/patternfront.html @@ -689,6 +689,10 @@ // iframe and in some privacy modes. An unguarded read at module scope takes the // whole editor down before it paints: stylesheet applies, title shows, nothing // renders. Every access goes through here so that can't happen again. +// True only inside the Electron shell. Everything native is behind this, so +// the same file is the browser build and the desktop renderer. +const native=()=>!!(window.pfNative&&window.pfNative.isDesktop); + const store={ get(k){try{return localStorage.getItem(k);}catch{return null;}}, set(k,v){try{localStorage.setItem(k,String(v));return true;}catch{return false;}}, @@ -1109,7 +1113,7 @@ zoom=Math.max(1,Math.min(48,z||1)); $('zoom').value=zoom;$('zoomO').textContent=zoom; layout();} -function commit(){paint();overlay();drawThumbs();syncStatus();saveSoon();} +function commit(){paint();overlay();drawThumbs();syncStatus();saveSoon();markDirty();} function fullSync(){alloc();layout();drawLayers();drawFrames();drawPalette(); drawPairs();drawStamps();syncColour();syncStatus();syncMode();syncHist();setTool(tool);} @@ -1613,7 +1617,16 @@ const im=new ImageData(doc.w,doc.h);im.data.set(frameRGBA(fi)); t.getContext('2d').putImageData(im,0,0); g.imageSmoothingEnabled=false;g.drawImage(t,0,0,c.width,c.height);return c;} -function download(b,n){const u=URL.createObjectURL(b),a=document.createElement('a'); +// The one place a file leaves the editor. In a browser that means an anchor +// with a download attribute; on the desktop it means a real Save dialog. Every +// export goes through here, so the desktop build needed no other change. +function download(b,n){ + if(native()){ + b.arrayBuffer().then(buf=>window.pfNative.saveExport(n,new Uint8Array(buf))) + .then(p=>{ if(p) toast('Saved '+p.split(/[\\/]/).pop()); }) + .catch(err=>toast('Could not save: '+err.message)); + return;} + const u=URL.createObjectURL(b),a=document.createElement('a'); a.href=u;a.download=n;document.body.appendChild(a);a.click();a.remove(); setTimeout(()=>URL.revokeObjectURL(u),5000);} $('expBtn').onclick=()=>{openOv('ovExp');refreshOF();}; @@ -2006,6 +2019,9 @@ const tag=(e.target.tagName||'').toLowerCase(); if(tag==='input'||tag==='textarea'||tag==='select')return; const meta=e.metaKey||e.ctrlKey; + // On the desktop the native menu owns these accelerators and fires first; + // handling them here as well would run every one of them twice. + if(meta&&native()&&MENU_KEYS.has(e.key.toLowerCase())) return; if(meta&&e.key.toLowerCase()==='z'){e.preventDefault();e.shiftKey?redo():undo();return;} if(meta&&e.key.toLowerCase()==='y'){e.preventDefault();redo();return;} if(meta&&e.key.toLowerCase()==='a'){e.preventDefault();sel={x:0,y:0,w:doc.w,h:doc.h}; @@ -2109,5 +2125,89 @@ new MutationObserver(()=>{paint();overlay();}) .observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']}); +/* ═══════ desktop ═══════ + Everything below is inert in a browser. The Electron shell installs + window.pfNative via a contextBridge preload; without it native() is false, + markDirty() is a no-op and no command bridge is published. */ + +let docPath=null, dirty=false; +function markDirty(){ + if(!native()||dirty) return; + dirty=true; window.pfNative.setDirty(true);} +function markClean(){ + dirty=false; + if(native()) window.pfNative.setDirty(false);} +function showPath(p){ + docPath=p||null; + if(native()) window.pfNative.setDocumentPath(docPath);} + +async function openDocument(){ + const r=await window.pfNative.openDocument(); + if(!r) return; // cancelled + if(!loadDocFromJSON(r.data)){ + toast('That file is not a PatternFront document');return;} + past.length=0;future.length=0; + aLayer=0;aFrame=0;fg=2;bg=1;sel=null;disarmStamp(); + $('cw').value=doc.w;$('ch').value=doc.h; + fullSync();fitCanvas();markClean();showPath(r.path); + toast('Opened '+r.path.split(/[\\/]/).pop());} + +async function saveDocument(saveAs){ + const p=await window.pfNative.saveDocument(saveAs?null:docPath,serialiseDoc()); + if(!p) return; // cancelled + markClean();showPath(p); + toast('Saved '+p.split(/[\\/]/).pop());} + +// The menu drives the same functions the buttons do — one implementation each, +// so a menu item cannot drift from its on-screen twin. +const COMMANDS={ + 'file.new': ()=>{startFresh();markClean();showPath(null);}, + 'file.open': ()=>openDocument(), + 'file.save': ()=>saveDocument(false), + 'file.saveAs': ()=>saveDocument(true), + 'file.openPath': p=>window.pfNative.readDocument(p).then(t=>{ + if(!t||!loadDocFromJSON(t)){toast('Could not open that file');return;} + past.length=0;future.length=0;aLayer=0;aFrame=0; + fg=2;bg=1;sel=null;disarmStamp(); + $('cw').value=doc.w;$('ch').value=doc.h; + fullSync();fitCanvas();markClean();showPath(p);}), + 'export.png': ()=>$('exPNG').click(), + 'export.sheet': ()=>$('exSheet').click(), + 'export.gif': ()=>$('exGIF').click(), + 'export.pattern':()=>{openOv('ovExp');refreshOF();}, + 'edit.undo': ()=>undo(), + 'edit.redo': ()=>redo(), + 'edit.clear': ()=>clearArt(false), + 'edit.clearAll': ()=>clearArt(true), + 'edit.selectAll':()=>{sel={x:0,y:0,w:doc.w,h:doc.h};overlay();syncStatus();}, + 'edit.deselect': ()=>{sel=null;overlay();syncStatus();}, + 'view.zoomIn': ()=>{zoom=Math.min(48,zoom+1);$('zoom').value=zoom; + $('zoomO').textContent=zoom;layout();}, + 'view.zoomOut': ()=>{zoom=Math.max(1,zoom-1);$('zoom').value=zoom; + $('zoomO').textContent=zoom;layout();}, + 'view.fit': ()=>fitCanvas(), + 'view.grid': ()=>{$('grid').click();}, + 'view.zen': ()=>toggleZen(), + 'view.scale': v=>applyUiScale(+v), + 'help.shortcuts':()=>toggleHelp(), +}; + +// Keys the native menu owns. Electron fires the accelerator BEFORE the page +// sees the keydown, so leaving these enabled here would undo twice per press. +const MENU_KEYS=new Set(['z','y','a','d','n','o','s']); + +if(native()){ + window.pfNative.onCommand((id,arg)=>{ + const fn=COMMANDS[id]; + if(fn) fn(arg); else console.warn('unknown command',id);}); + window.pfNative.setDirty(false); + // Asked before the window closes, so unsaved work cannot vanish silently. + window.__pfState=()=>({dirty,path:docPath,json:serialiseDoc()}); + document.querySelectorAll('a[href^="http"]').forEach(a=>{ + a.addEventListener('click',e=>{ + e.preventDefault();window.pfNative.openExternal(a.href);});}); +} +// Exposed for the smoke test and for anything driving the editor from outside. +window.__pf={commands:COMMANDS,isNative:native}; })(); diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist new file mode 100644 index 0000000..02f28fa --- /dev/null +++ b/build/entitlements.mac.plist @@ -0,0 +1,23 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.files.user-selected.read-write + + + diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..74551bbca3183fff7031c2ffa1a77a5fed3e4a39 GIT binary patch literal 7833 zcmds6&x;&I6#lxlhaD2v%%ZTUiz!(of(P+rPNpLpk`)O-#2f^7{s0d-E+}qoyhIRk z5yXpl5Ih;jn}Qy1RNa8G?b{nXan$z4yNNqpBvK zUb%d6>BO@q04!}>T7Me|`87me@#FsX?O%ZBha2nX-`V=@-mkANK|gU}_syN1;Jq7X ze#G(z_g?(g-2GzZyHmi|H||qZQw#y0KA(sW1Zcp=@Uh@pzD$?Y&qT7UBJ2s93X3Jx`YM4&UAnnr~MQk&yYBsF!a3X{YpO|D`%i!efx z<)#)!)-$LByuPC%uJwX#w~nv|BQ?wLYFgJ&AB)&Ox3?Xd1w4tp?a@SyUp|XJU=9}# zUcoht100-suqh)8cyN0Ej*P@IK_A=I2ypr9cld07!o}Oe%&Z%0u>Xl5X5&rAxaw^HiG39?sSJ9Ka zl3b8`bi+-KD_J{-b!3uVI}Pfo)qur>T4cx47w|Mi2f!8`&QDHv*@^pn@Ky3`@<^Mr1xiL zdq=cqu6EilvIe-fO`R?u{B@_DIp3`tI?{0*%$WC7QrIo0`_3^^wsT!@8)r`}Z|%N0 zNl2zp6IGXF0to#ob&P*zwU@}kSv{DW7Pe;!M>G@$!aU2ID<#T$PiG%T(?;1Xn zqh85D-aDN47>(Q4dBk*HeGG@?P8iseubk@bn+&&Ivzjk`n(_^YTB|ldW-^cG;GsHi z4OGN=AWSbC_6;gR+i|`b#!$Y?Wy5j1aJ?&3 zdU>sp4`r1+Nj>I(kj;CWkZm)j(Vv*1|;?f6yJ&Zt1AKV#xEmi3B5~^TwTls1~m@f4pIps-UxtUmNDx{wDtdFmJ z;xJ8{$C$tYKHiyScx7Yp>RVpzFCp1R39686$KRIPVUlDQU)))e+pCPDT9Zw(STla@ z@1uYX!M#a_1y!E>X`aoQeyv?K!d9<&^UX8i+QW;C~t2+AP< literal 0 HcmV?d00001 diff --git a/docs/assets/desktop.png b/docs/assets/desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..8bef1ac0de1828fe9dec80f55c1056482ffe730d GIT binary patch literal 92566 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

MAX_DOC_BYTES) { + throw new Error(`${(stat.size / 1048576).toFixed(1)} MB is too large for a pattern`); + } + const text = await fs.readFile(file, 'utf8'); + // Parse here so a corrupt file fails in the main process with a real dialog + // rather than silently returning nothing to the renderer. + const doc = JSON.parse(text); + if (!doc || doc.v !== 1 || !Array.isArray(doc.frames) || !doc.frames.length) { + throw new Error('not a PatternFront document'); + } + return text; +} + +async function openDocument(win, recent) { + const { canceled, filePaths } = await dialog.showOpenDialog(win, { + title: 'Open pattern', + filters: DOC_FILTERS, + properties: ['openFile'], + }); + if (canceled || !filePaths.length) return null; + return openPath(win, filePaths[0], recent); +} + +async function openPath(win, file, recent) { + try { + const data = await readDocument(file); + recent.add(file); + return { path: file, data }; + } catch (err) { + await dialog.showMessageBox(win, { + type: 'error', + message: 'Could not open that file', + detail: `${path.basename(file)} — ${err.message}`, + buttons: ['OK'], + }); + return null; + } +} + +async function saveDocument(win, file, json, recent) { + let target = file; + if (!target) { + const { canceled, filePath } = await dialog.showSaveDialog(win, { + title: 'Save pattern', + defaultPath: path.join(app.getPath('documents'), `untitled.${EXT}`), + filters: DOC_FILTERS, + }); + if (canceled || !filePath) return null; + target = filePath.endsWith(`.${EXT}`) ? filePath : `${filePath}.${EXT}`; + } + try { + await writeAtomic(target, json); + recent.add(target); + return target; + } catch (err) { + await dialog.showMessageBox(win, { + type: 'error', + message: 'Could not save', + detail: `${target} — ${err.message}`, + buttons: ['OK'], + }); + return null; + } +} + +/** + * Write to a sibling temp file and rename over the target. + * + * rename(2) is atomic within a filesystem, so an interrupted save leaves the + * previous version intact instead of a half-written file. Losing artwork to a + * crash mid-write is exactly the kind of thing people never forgive. + */ +async function writeAtomic(target, contents) { + const tmp = `${target}.${process.pid}.tmp`; + await fs.writeFile(tmp, contents, 'utf8'); + try { + await fs.rename(tmp, target); + } catch (err) { + await fs.rm(tmp, { force: true }); + throw err; + } +} + +async function saveExport(win, name, bytes) { + const ext = path.extname(name).toLowerCase(); + const { canceled, filePath } = await dialog.showSaveDialog(win, { + title: 'Export', + defaultPath: path.join(app.getPath('downloads'), name), + filters: EXPORT_FILTERS[ext] || [{ name: 'All files', extensions: ['*'] }], + }); + if (canceled || !filePath) return null; + try { + await fs.writeFile(filePath, Buffer.from(bytes)); + return filePath; + } catch (err) { + await dialog.showMessageBox(win, { + type: 'error', + message: 'Could not export', + detail: `${filePath} — ${err.message}`, + buttons: ['OK'], + }); + return null; + } +} + +/** Recent documents, mirrored into the OS list and kept for the File menu. */ +function makeRecent(onChange) { + const LIMIT = 10; + let list = []; + return { + get list() { return list.slice(); }, + add(file) { + list = [file, ...list.filter(f => f !== file)].slice(0, LIMIT); + app.addRecentDocument(file); + onChange(); + }, + clear() { + list = []; + app.clearRecentDocuments(); + onChange(); + }, + }; +} + +module.exports = { + EXT, MAX_DOC_BYTES, + readDocument, openDocument, openPath, saveDocument, saveExport, + writeAtomic, makeRecent, +}; diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 0000000..7a75806 --- /dev/null +++ b/electron/main.js @@ -0,0 +1,366 @@ +'use strict'; +/** + * PatternFront — Electron main process. + * + * The renderer is app/patternfront.html, unchanged from the browser build. It + * feature-detects `window.pfNative`; without it the same file is a plain web + * page. Nothing here is a fork of the editor. + * + * Serving over a registered `app://` scheme rather than `file://` buys a real + * origin — so localStorage (the autosave) works reliably, and a Content + * Security Policy can actually be enforced on the document. + */ + +const fs = require('fs/promises'); +const path = require('path'); +const { app, BrowserWindow, dialog, ipcMain, protocol, net, shell } = require('electron'); + +const docs = require('./documents'); +const menu = require('./menu'); +const windowState = require('./window-state'); + +const ROOT = path.join(__dirname, '..'); +const APP_DIR = path.join(ROOT, 'app'); +const ENTRY = 'app://patternfront/patternfront.html'; +const SMOKE = process.argv.includes('--smoke'); +// `electron . --shot out.png` renders the window offscreen and writes a PNG. +// Used for README images and for eyeballing a CI build without a display. +const SHOT = (() => { + const i = process.argv.indexOf('--shot'); + return i >= 0 && process.argv[i + 1] ? path.resolve(process.argv[i + 1]) : null; +})(); + +let win = null; +let recent = null; +let docPath = null; +let dirty = false; +/** Set once the user has confirmed discarding changes, so `close` proceeds. */ +let forceClose = false; +/** Paths queued by the OS before the window existed (double-click to open). */ +let pendingOpen = null; + +// The custom scheme must be privileged before `ready`, or it gets no origin +// and localStorage throws — the exact failure that once made this app render +// as a blank page in a sandboxed frame. +protocol.registerSchemesAsPrivileged([{ + scheme: 'app', + privileges: { standard: true, secure: true, supportFetchAPI: true }, +}]); + +/* ── serving the renderer ────────────────────────────────────────────── */ + +const MIME = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.png': 'image/png', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', +}; + +// Only files under app/ are reachable, and only after resolving the path — so +// `app://patternfront/../../etc/passwd` cannot escape the directory. +async function serve(request) { + const url = new URL(request.url); + const rel = decodeURIComponent(url.pathname).replace(/^\/+/, ''); + const file = path.resolve(APP_DIR, rel); + if (file !== APP_DIR && !file.startsWith(APP_DIR + path.sep)) { + return new Response('forbidden', { status: 403 }); + } + try { + const body = await fs.readFile(file); + return new Response(body, { + headers: { + 'content-type': MIME[path.extname(file).toLowerCase()] || 'application/octet-stream', + // The editor is entirely self-contained: no network, no CDN, no eval. + 'content-security-policy': + "default-src 'none'; img-src 'self' data: blob:; " + + "style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; " + + "font-src 'self'; connect-src 'self' blob: data:; form-action 'none'; " + + "base-uri 'none'; frame-ancestors 'none'", + }, + }); + } catch { + return new Response('not found', { status: 404 }); + } +} + +/* ── window ──────────────────────────────────────────────────────────── */ + +function createWindow() { + const state = windowState.restore(); + + win = new BrowserWindow({ + ...state.bounds, + minWidth: state.minWidth, + minHeight: state.minHeight, + show: false, + backgroundColor: '#202125', // --well, so there is no white flash + title: 'PatternFront', + // A normal title bar, deliberately. `hiddenInset` would drop the macOS + // traffic lights straight on top of the app's own 24px top bar, and the + // title is doing real work here — it carries the document name and the + // unsaved-changes dot that setTitle() maintains. + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + spellcheck: false, + webviewTag: false, + }, + }); + + if (state.maximised) win.maximize(); + windowState.track(win); + + win.once('ready-to-show', () => { if (!SMOKE && !SHOT) win.show(); }); + win.loadURL(ENTRY); + + // This app never navigates and never opens a second window. Anything that + // tries is either a bug or something hostile; send links to the browser. + win.webContents.on('will-navigate', (e, url) => { + if (url !== ENTRY) { e.preventDefault(); openExternal(url); } + }); + win.webContents.setWindowOpenHandler(({ url }) => { + openExternal(url); + return { action: 'deny' }; + }); + win.webContents.on('will-attach-webview', e => e.preventDefault()); + + win.on('close', onClose); + win.on('closed', () => { win = null; }); + + return win; +} + +function openExternal(url) { + // Never hand the shell anything but a web URL — file:// or a custom scheme + // here would be a way to launch arbitrary things. + try { + const u = new URL(url); + if (u.protocol === 'http:' || u.protocol === 'https:') shell.openExternal(url); + } catch { /* not a URL; ignore */ } +} + +/** Unsaved work must not disappear because someone hit the red button. */ +function onClose(e) { + if (forceClose || !dirty) return; + e.preventDefault(); + const name = docPath ? path.basename(docPath) : 'this pattern'; + dialog.showMessageBox(win, { + type: 'warning', + message: `Save changes to ${name}?`, + detail: 'Your changes will be lost if you don’t save them.', + buttons: ['Save', "Don't Save", 'Cancel'], + defaultId: 0, + cancelId: 2, + }).then(async ({ response }) => { + if (response === 2) return; + if (response === 1) { forceClose = true; win.close(); return; } + // Ask the renderer for the current document, then save before closing. + const json = await win.webContents.executeJavaScript('window.__pfState().json'); + const saved = await docs.saveDocument(win, docPath, json, recent); + if (saved) { forceClose = true; win.close(); } + }); +} + +function setTitle() { + if (!win) return; + const name = docPath ? path.basename(docPath) : 'Untitled'; + win.setTitle(`${dirty ? '• ' : ''}${name} — PatternFront`); + win.setDocumentEdited?.(dirty); + if (docPath) win.setRepresentedFilename?.(docPath); +} + +const send = (id, arg) => win && win.webContents.send('command', id, arg); + +function refreshMenu() { + menu.install({ + send, + recent, + openRecent: async file => { + const r = await docs.openPath(win, file, recent); + if (r) send('file.openPath', r.path); + }, + quit: () => { app.quit(); }, + }); +} + +/* ── opening files from the OS ───────────────────────────────────────── */ + +function queueOpen(file) { + if (!file) return; + if (win && !win.webContents.isLoading()) send('file.openPath', file); + else pendingOpen = file; +} + +/** Windows and Linux pass the file as an argv entry. */ +function fileFromArgv(argv) { + return argv.slice(1).find(a => a.toLowerCase().endsWith(`.${docs.EXT}`)) || null; +} + +// macOS delivers this before `ready`, so it is registered at module scope. +app.on('open-file', (e, file) => { e.preventDefault(); queueOpen(file); }); + +if (!app.requestSingleInstanceLock()) { + app.quit(); +} else { + app.on('second-instance', (_e, argv) => { + if (win) { if (win.isMinimized()) win.restore(); win.focus(); } + queueOpen(fileFromArgv(argv)); + }); +} + +/* ── IPC ─────────────────────────────────────────────────────────────── */ + +function wireIpc() { + ipcMain.handle('doc:open', async () => { + const r = await docs.openDocument(win, recent); + if (r) refreshMenu(); + return r; + }); + + ipcMain.handle('doc:read', async (_e, file) => { + try { return await docs.readDocument(file); } + catch { return null; } + }); + + ipcMain.handle('doc:save', async (_e, file, json) => { + const p = await docs.saveDocument(win, file, json, recent); + if (p) refreshMenu(); + return p; + }); + + ipcMain.handle('export:save', (_e, name, bytes) => + docs.saveExport(win, path.basename(String(name)), bytes)); + + ipcMain.on('doc:dirty', (_e, v) => { dirty = !!v; setTitle(); }); + ipcMain.on('doc:path', (_e, p) => { docPath = p || null; setTitle(); }); + ipcMain.on('shell:external', (_e, url) => openExternal(url)); +} + +/* ── smoke test ──────────────────────────────────────────────────────── */ + +/** + * `electron . --smoke` — prove the app actually runs, not merely that it built. + * Loads the real renderer offscreen, asserts the editor came up and the native + * bridge is wired, then exits 0 or 1. This is what CI runs under xvfb. + */ +async function smoke() { + const checks = []; + const t0 = Date.now(); + + // Collected in the main process rather than from a renderer global: a script + // that dies before it can install a handler would otherwise report clean. + const consoleErrors = []; + win.webContents.on('console-message', (e) => { + if (e.level === 'error') consoleErrors.push(`${e.message} (${e.sourceId}:${e.lineNumber})`); + }); + win.webContents.on('preload-error', (_e, file, err) => + consoleErrors.push(`preload ${path.basename(file)}: ${err.message}`)); + win.webContents.on('render-process-gone', (_e, d) => + consoleErrors.push(`renderer gone: ${d.reason}`)); + + try { + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('renderer never finished loading')), 30000); + win.webContents.once('did-finish-load', () => { clearTimeout(timer); resolve(); }); + win.webContents.once('did-fail-load', (_e, code, desc) => { + clearTimeout(timer); reject(new Error(`load failed ${code}: ${desc}`)); + }); + }); + + const report = await win.webContents.executeJavaScript(`(() => { + const q = s => document.querySelector(s); + const cv = q('#cv'); + return { + title: document.title, + native: !!(window.pfNative && window.pfNative.isDesktop), + bridge: !!(window.__pf && window.__pf.commands), + commands: window.__pf ? Object.keys(window.__pf.commands).length : 0, + app: !!q('.app'), + canvasPainted: !!(cv && cv.width > 0 && cv.height > 0), + rail: document.querySelectorAll('#rail button').length, + stamps: document.querySelectorAll('#stamps button').length, + elements: document.querySelectorAll('*').length, + }; + })()`); + + const expect = (name, ok, detail) => checks.push({ name, ok, detail }); + expect('title is PatternFront', report.title === 'PatternFront', report.title); + expect('native bridge installed', report.native === true); + expect('command table exposed', report.bridge && report.commands >= 15, + `${report.commands} commands`); + expect('editor root rendered', report.app === true); + expect('canvas has real dimensions', report.canvasPainted === true); + expect('tool rail built', report.rail > 5, `${report.rail} tools`); + expect('stamps built', report.stamps >= 50, `${report.stamps} stamps`); + expect('document is populated', report.elements > 300, `${report.elements} elements`); + expect('no console errors', consoleErrors.length === 0, + consoleErrors.slice(0, 3).join(' | ')); + } catch (err) { + checks.push({ name: 'renderer loaded', ok: false, detail: err.message }); + } + + for (const c of checks) { + console.log(` [${c.ok ? 'PASS' : 'FAIL'}] ${c.name}${c.detail ? ' ' + c.detail : ''}`); + } + const failed = checks.filter(c => !c.ok).length; + console.log(failed + ? `\n*** ${failed} FAILURE(S) ***` + : `\nSMOKE TEST PASSED (${Date.now() - t0} ms)`); + app.exit(failed ? 1 : 0); +} + +/** Render the window offscreen and write it to a PNG. */ +async function shoot() { + try { + await new Promise((resolve, reject) => { + const t = setTimeout(() => reject(new Error('load timed out')), 30000); + win.webContents.once('did-finish-load', () => { clearTimeout(t); resolve(); }); + }); + // Let the first paint, the fit-to-window rAF and the restore toast settle. + await new Promise(r => setTimeout(r, 1200)); + const image = await win.webContents.capturePage(); + await fs.writeFile(SHOT, image.toPNG()); + const { width, height } = image.getSize(); + console.log(`wrote ${SHOT} (${width}x${height})`); + app.exit(0); + } catch (err) { + console.error('screenshot failed:', err.message); + app.exit(1); + } +} + +/* ── lifecycle ───────────────────────────────────────────────────────── */ + +app.whenReady().then(() => { + protocol.handle('app', serve); + app.setAboutPanelOptions?.({ + applicationName: 'PatternFront', + applicationVersion: app.getVersion(), + website: menu.REPO, + }); + + recent = docs.makeRecent(() => refreshMenu()); + wireIpc(); + createWindow(); + refreshMenu(); + + win.webContents.once('did-finish-load', () => { + const queued = pendingOpen || fileFromArgv(process.argv); + if (queued) { pendingOpen = null; send('file.openPath', queued); } + }); + + if (SMOKE) smoke(); + else if (SHOT) shoot(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { createWindow(); refreshMenu(); } + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit(); +}); diff --git a/electron/menu.js b/electron/menu.js new file mode 100644 index 0000000..ad975d8 --- /dev/null +++ b/electron/menu.js @@ -0,0 +1,134 @@ +'use strict'; +/** + * The native menu. + * + * Every item sends a command id to the renderer, which looks it up in the + * COMMANDS table in app/patternfront.html. Menu items and on-screen buttons + * therefore call the same function — a menu entry cannot drift away from the + * behaviour of its twin in the sidebar. + * + * Accelerators listed here are owned by the menu. The renderer's own keydown + * handler skips them when running on the desktop (MENU_KEYS), because Electron + * fires the accelerator before the page sees the key and every one of them + * would otherwise run twice. + */ + +const { Menu, app, shell } = require('electron'); + +const REPO = 'https://github.com/timbogdanov/patternfront'; +const isMac = process.platform === 'darwin'; + +function build({ send, recent, openRecent, quit }) { + const cmd = (label, id, accelerator, extra = {}) => ({ + label, accelerator, click: () => send(id), ...extra, + }); + + const recentItems = recent.list.length + ? [ + ...recent.list.map(file => ({ + label: file.replace(app.getPath('home'), '~'), + click: () => openRecent(file), + })), + { type: 'separator' }, + { label: 'Clear Menu', click: () => recent.clear() }, + ] + : [{ label: 'No Recent Patterns', enabled: false }]; + + const template = [ + ...(isMac ? [{ + label: app.name, + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, + { type: 'separator' }, + { label: 'Quit PatternFront', accelerator: 'Cmd+Q', click: quit }, + ], + }] : []), + + { + label: '&File', + submenu: [ + cmd('New Pattern', 'file.new', 'CmdOrCtrl+N'), + cmd('Open…', 'file.open', 'CmdOrCtrl+O'), + { label: 'Open Recent', submenu: recentItems }, + { type: 'separator' }, + cmd('Save', 'file.save', 'CmdOrCtrl+S'), + cmd('Save As…', 'file.saveAs', 'CmdOrCtrl+Shift+S'), + { type: 'separator' }, + { + label: 'Export', + submenu: [ + cmd('PNG — Current Frame…', 'export.png', 'CmdOrCtrl+E'), + cmd('PNG — Sprite Sheet…', 'export.sheet'), + cmd('Animated GIF…', 'export.gif'), + { type: 'separator' }, + cmd('OpenFront patternData…', 'export.pattern', 'CmdOrCtrl+Shift+E'), + ], + }, + { type: 'separator' }, + isMac ? { role: 'close' } : { label: 'Quit', accelerator: 'Alt+F4', click: quit }, + ], + }, + + { + label: '&Edit', + submenu: [ + cmd('Undo', 'edit.undo', 'CmdOrCtrl+Z'), + cmd('Redo', 'edit.redo', isMac ? 'Cmd+Shift+Z' : 'Ctrl+Y'), + { type: 'separator' }, + cmd('Select All', 'edit.selectAll', 'CmdOrCtrl+A'), + cmd('Deselect', 'edit.deselect', 'CmdOrCtrl+D'), + { type: 'separator' }, + cmd('Clear', 'edit.clear', 'Delete'), + cmd('Clear Every Layer', 'edit.clearAll', 'Shift+Delete'), + ], + }, + + { + label: '&View', + submenu: [ + cmd('Zoom In', 'view.zoomIn', 'CmdOrCtrl+Plus'), + cmd('Zoom Out', 'view.zoomOut', 'CmdOrCtrl+-'), + cmd('Fit to Window', 'view.fit', 'CmdOrCtrl+Shift+0'), + { type: 'separator' }, + { + label: 'Interface Scale', + submenu: [1, 1.5, 2, 2.5, 3].map(v => ({ + label: `${v}×`, + click: () => send('view.scale', v), + })), + }, + cmd('Toggle Grid', 'view.grid'), + cmd('Hide Panels', 'view.zen', 'Tab'), + { type: 'separator' }, + { role: 'reload' }, + { role: 'toggleDevTools' }, + { role: 'togglefullscreen' }, + ], + }, + + { label: '&Window', role: 'windowMenu' }, + + { + label: '&Help', + submenu: [ + cmd('Keyboard Shortcuts', 'help.shortcuts'), + { type: 'separator' }, + { label: 'Documentation', click: () => shell.openExternal(`${REPO}#readme`) }, + { label: 'Report an Issue', click: () => shell.openExternal(`${REPO}/issues/new`) }, + ...(isMac ? [] : [{ type: 'separator' }, { role: 'about' }]), + ], + }, + ]; + + return Menu.buildFromTemplate(template); +} + +function install(opts) { + Menu.setApplicationMenu(build(opts)); +} + +module.exports = { build, install, REPO }; diff --git a/electron/preload.js b/electron/preload.js new file mode 100644 index 0000000..57f4dfe --- /dev/null +++ b/electron/preload.js @@ -0,0 +1,55 @@ +'use strict'; +/** + * The only bridge between the renderer and Node. + * + * The renderer is sandboxed with context isolation on and node integration off, + * so this is the entire native surface it can reach. Everything here is a fixed + * channel with validated arguments — the renderer cannot name an arbitrary IPC + * channel, read an arbitrary path, or hand the main process a function. + * + * Keep this list short. Every entry is something a compromised renderer could + * call, so each one has to be worth it. + */ + +const { contextBridge, ipcRenderer } = require('electron'); + +const str = (v, name) => { + if (typeof v !== 'string') throw new TypeError(`${name} must be a string`); + return v; +}; + +contextBridge.exposeInMainWorld('pfNative', { + isDesktop: true, + platform: process.platform, + + /** → { path, data } for the chosen file, or null if cancelled. */ + openDocument: () => ipcRenderer.invoke('doc:open'), + + /** Read a specific path — only ever a path the main process handed us. */ + readDocument: (file) => ipcRenderer.invoke('doc:read', str(file, 'file')), + + /** path=null means Save As. → the path written, or null if cancelled. */ + saveDocument: (file, json) => + ipcRenderer.invoke('doc:save', file === null ? null : str(file, 'file'), + str(json, 'json')), + + /** Save exported bytes. → the path written, or null if cancelled. */ + saveExport: (name, bytes) => { + if (!(bytes instanceof Uint8Array)) throw new TypeError('bytes must be a Uint8Array'); + // Copy into a plain array buffer so the structured clone is a byte payload + // and not a view onto renderer memory. + return ipcRenderer.invoke('export:save', str(name, 'name'), new Uint8Array(bytes)); + }, + + setDirty: (v) => ipcRenderer.send('doc:dirty', !!v), + setDocumentPath: (p) => ipcRenderer.send('doc:path', p === null ? null : str(p, 'path')), + + /** Menu → renderer. The callback receives (commandId, arg). */ + onCommand: (fn) => { + if (typeof fn !== 'function') throw new TypeError('onCommand needs a function'); + ipcRenderer.on('command', (_e, id, arg) => fn(String(id), arg)); + }, + + /** Open a URL in the user's browser. http(s) only — enforced in main too. */ + openExternal: (url) => ipcRenderer.send('shell:external', str(url, 'url')), +}); diff --git a/electron/window-state.js b/electron/window-state.js new file mode 100644 index 0000000..28eeb2b --- /dev/null +++ b/electron/window-state.js @@ -0,0 +1,71 @@ +'use strict'; +/** + * Remember where the window was. + * + * Small enough not to warrant a dependency. Bounds are clamped to a display + * that actually exists, so a window saved on an external monitor does not open + * off-screen once that monitor is unplugged. + */ + +const fs = require('fs'); +const path = require('path'); +const { app, screen } = require('electron'); + +const FILE = () => path.join(app.getPath('userData'), 'window-state.json'); +const DEFAULTS = { width: 1440, height: 900, maximised: false }; +const MIN_WIDTH = 900; +const MIN_HEIGHT = 600; + +function read() { + try { + const s = JSON.parse(fs.readFileSync(FILE(), 'utf8')); + if (typeof s.width !== 'number' || typeof s.height !== 'number') return { ...DEFAULTS }; + return s; + } catch { + return { ...DEFAULTS }; + } +} + +/** Bounds that are guaranteed to land on a connected display. */ +function restore() { + const s = read(); + const bounds = { + width: Math.max(MIN_WIDTH, Math.round(s.width)), + height: Math.max(MIN_HEIGHT, Math.round(s.height)), + }; + + if (typeof s.x === 'number' && typeof s.y === 'number') { + const visible = screen.getAllDisplays().some(d => { + const a = d.workArea; + // Require a decent overlap, not just a touching corner. + return s.x < a.x + a.width - 80 && s.x + bounds.width > a.x + 80 + && s.y < a.y + a.height - 40 && s.y + bounds.height > a.y; + }); + if (visible) { bounds.x = Math.round(s.x); bounds.y = Math.round(s.y); } + } + return { bounds, maximised: !!s.maximised, minWidth: MIN_WIDTH, minHeight: MIN_HEIGHT }; +} + +/** Persist on move/resize/maximise. Debounced — resizing fires continuously. */ +function track(win) { + let timer = null; + const save = () => { + clearTimeout(timer); + timer = setTimeout(() => { + if (win.isDestroyed()) return; + // getNormalBounds is the un-maximised geometry, which is what we want to + // restore to when the user un-maximises later. + const b = win.getNormalBounds(); + const state = { ...b, maximised: win.isMaximized() }; + try { + fs.mkdirSync(path.dirname(FILE()), { recursive: true }); + fs.writeFileSync(FILE(), JSON.stringify(state, null, 1)); + } catch { /* a window position is not worth surfacing an error for */ } + }, 400); + }; + + for (const ev of ['resize', 'move', 'maximize', 'unmaximize']) win.on(ev, save); + win.on('close', () => clearTimeout(timer)); +} + +module.exports = { restore, track }; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a337709 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3595 @@ +{ + "name": "patternfront", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "patternfront", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "electron": "^43.4.0", + "electron-builder": "^26.15.3" + } + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", + "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, + "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/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/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "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.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/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/get": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz", + "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "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.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "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/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/rebuild": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", + "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/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/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "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": ">= 12.13.0" + } + }, + "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/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.0.tgz", + "integrity": "sha512-AKvPMOM7LfK0uFe1m7o7+veOa8xQGPqsqOrKi3QKgCElzwjGp39mbhr2g7mt/v/mXQHiIvJmDj5cJS173x8Q9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "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==", + "dev": true, + "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.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "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.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "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==", + "dev": true, + "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": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.14", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.14.tgz", + "integrity": "sha512-T4EDRUBVZYRldYApjEJiU0e1stYWaRAX7CuSnKzrpwdZKo53zGV8/pqfzV6FfwNl9YThD2OumQYvqtvjvgG7aQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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-lib": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.3", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.3", + "electron-builder-squirrel-windows": "26.15.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "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": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.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==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/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/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.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "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": "26.15.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", + "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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/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/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "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/cross-spawn/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/cross-spawn/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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/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/dir-compare/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "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.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", + "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "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": "43.4.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.4.0.tgz", + "integrity": "sha512-3qxGF0CeQbiox5oWV1JlbWGQ1VerbmDhTFqW4sJ8h7uqTHniFYPObXJcDna0DMh32et0fFyKzz0YY8lJv3t5jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", + "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.3", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "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": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", + "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/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==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "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==", + "dev": 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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "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-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/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/filelist/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "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/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==", + "dev": true, + "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": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "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/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/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "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==", + "dev": true, + "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/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "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==", + "dev": 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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "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==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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-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" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "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==", + "dev": true, + "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.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "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==", + "dev": true, + "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/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.33.0.tgz", + "integrity": "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "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/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.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" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "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/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "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", + "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/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/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "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==", + "dev": true, + "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.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" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "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/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==", + "dev": true, + "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.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", + "dependencies": { + "safe-buffer": "~5.1.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/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==", + "dev": true, + "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": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "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==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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" + }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "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.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..742e096 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "patternfront", + "productName": "PatternFront", + "version": "0.1.0", + "description": "A 1-bit pattern editor for OpenFront territory patterns — desktop app for macOS and Windows.", + "keywords": ["pixel-art", "openfront", "pattern-editor", "1-bit", "electron"], + "homepage": "https://github.com/timbogdanov/patternfront#readme", + "bugs": "https://github.com/timbogdanov/patternfront/issues", + "repository": { + "type": "git", + "url": "https://github.com/timbogdanov/patternfront.git" + }, + "license": "MIT", + "author": "timbogdanov", + "main": "electron/main.js", + "private": true, + "scripts": { + "start": "electron .", + "smoke": "electron . --smoke", + "verify": "./tools/verify-all.sh", + "pack": "electron-builder --dir", + "dist": "electron-builder", + "dist:mac": "electron-builder --mac", + "dist:win": "electron-builder --win", + "icon": "python3 tools/gen-icon.py", + "stamps": "python3 tools/gen-stamps.py", + "fixtures": "python3 tools/gen-codec-fixtures.py" + }, + "devDependencies": { + "electron": "^43.4.0", + "electron-builder": "^26.15.3" + } +} diff --git a/tools/gen-icon.py b/tools/gen-icon.py new file mode 100644 index 0000000..08b76a1 --- /dev/null +++ b/tools/gen-icon.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +Draw the application icon. + +The icon is the editor's own subject matter: a 1-bit tiling pattern, rendered +big. It is generated rather than hand-painted so it stays consistent with the +artwork the app makes, and so there is no binary blob in the repo that nobody +can regenerate. + +electron-builder derives .icns and .ico from this single 1024x1024 PNG, so this +is the only image the build needs. + + python3 tools/gen-icon.py +""" + +from __future__ import annotations + +import os +import struct +import sys +import zlib + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +OUT = os.path.join(ROOT, "build", "icon.png") + +SIZE = 1024 +GRID = 16 # the icon is a 16x16 pattern, blown up +CELL = SIZE // GRID + +# Aseprite's palette, which is the app's own chrome. +INK = (0xE6, 0xE6, 0xE8) +PAPER = (0x20, 0x21, 0x25) +ACCENT = (0x29, 0x62, 0xFF) + +# A "P" that also reads as a tiling motif: the counter and the stem give it +# structure at 16px, and it still resolves at 32px in a taskbar. +ART = [ + "................", + "..############..", + "..############..", + "..####....####..", + "..####....####..", + "..####....####..", + "..####....####..", + "..############..", + "..############..", + "..####..........", + "..####..........", + "..####..........", + "..####..........", + "..####..........", + "..####..........", + "................", +] + + +def png(path: str, width: int, height: int, pixel) -> None: + rows = bytearray() + for y in range(height): + rows.append(0) # filter: none + for x in range(width): + rows += bytes(pixel(x, y)) + + def chunk(tag: bytes, data: bytes) -> bytes: + return (struct.pack(">I", len(data)) + tag + data + + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)) + + blob = (b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)) + + chunk(b"IDAT", zlib.compress(bytes(rows), 9)) + + chunk(b"IEND", b"")) + os.makedirs(os.path.dirname(path), exist_ok=True) + open(path, "wb").write(blob) + + +def main() -> int: + if len(ART) != GRID or any(len(r) != GRID for r in ART): + print(f"ART must be {GRID}x{GRID}") + return 1 + + # A thin accent rule along the bottom, the way the editor marks the active + # thing. Keeps the icon from being two flat greys. + rule_top = SIZE - CELL // 2 + + def pixel(x: int, y: int): + if y >= rule_top: + return ACCENT + return INK if ART[y // CELL][x // CELL] == "#" else PAPER + + png(OUT, SIZE, SIZE, pixel) + print(f"wrote {OUT} ({SIZE}x{SIZE}, {os.path.getsize(OUT):,} bytes)") + for r in ART: + print(" " + r.replace("#", "██").replace(".", " ")) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/verify-all.sh b/tools/verify-all.sh index e2e4b52..cc0b939 100755 --- a/tools/verify-all.sh +++ b/tools/verify-all.sh @@ -42,6 +42,8 @@ run "map-scale sampling" python3 tools/preview_prototype.py run "UI design rules" python3 tools/verify-ui.py run "stamp library" python3 tools/verify-stamps.py run "editor behaviour" node tools/verify-behaviour.js +run "desktop documents" node tools/verify-documents.js +run "electron shell" python3 tools/verify-electron.py # Optional: only runs with a local OpenFront checkout. run "docs match OpenFront source" python3 tools/verify-docs.py "$OPENFRONT" diff --git a/tools/verify-behaviour.js b/tools/verify-behaviour.js index fe363a8..d21ad17 100755 --- a/tools/verify-behaviour.js +++ b/tools/verify-behaviour.js @@ -630,6 +630,66 @@ function clearTests() { vm.runInContext('snaps', snapCtx) === 0); } +/* ── 9. the browser build must not notice the desktop code ─────────────── */ + +async function browserBuildTests() { + console.log('\n=== browser build is untouched ==='); + + // A browser has no window.pfNative. If any of the desktop code reached for it + // unguarded, this throws — which is the whole point of native() existing. + const ctx = sandbox({ window: {}, console }); + let threw = null; + try { + run(ctx, [ + grabConst('native'), + 'globalThis._isNative = native();', + grabFunction('markDirty'), grabFunction('markClean'), + 'var dirty=false;', + 'markDirty(); markClean(); markDirty();', + ].join('\n')); + } catch (e) { threw = e.message; } + + chk('native() is false with no pfNative', threw === null && ctx._isNative === false, + threw || `native()=${ctx._isNative}`); + chk('dirty tracking is inert in a browser', + threw === null && vm.runInContext('dirty', ctx) === false); + + // ...and true when the shell installs it. + const desk = sandbox({ window: { pfNative: { isDesktop: true } } }); + run(desk, [grabConst('native'), 'globalThis._n = native();'].join('\n')); + chk('native() is true inside the shell', desk._n === true); + + // download() must not build an anchor on the desktop, and must not call + // pfNative in a browser. Run the real function both ways. + const dl = grabFunction('download'); + const seen = { anchor: 0, saved: null }; + const web = sandbox({ + window: {}, + URL: { createObjectURL: () => 'blob:x', revokeObjectURL() {} }, + document: { + createElement: () => { seen.anchor++; return { click() {}, remove() {}, style: {} }; }, + body: { appendChild() {} }, + }, + setTimeout, + }); + run(web, [grabConst('native'), dl, + "download({arrayBuffer:()=>Promise.resolve(new ArrayBuffer(4))},'a.png');"].join('\n')); + chk('a browser still gets an anchor download', seen.anchor === 1); + + const app = sandbox({ + window: { pfNative: { isDesktop: true, + saveExport: (n) => { seen.saved = n; return Promise.resolve('/tmp/' + n); } } }, + toast: () => {}, + document: { createElement: () => { seen.anchor++; return {}; } }, + }); + run(app, [grabConst('native'), dl, + "download({arrayBuffer:()=>Promise.resolve(new ArrayBuffer(4))},'b.png');"].join('\n')); + // download() reads the blob asynchronously, so let the microtasks drain. + await new Promise(r => setTimeout(r, 10)); + chk('the desktop gets a save dialog, not an anchor', + seen.saved === 'b.png' && seen.anchor === 1, `anchors=${seen.anchor}`); +} + /* ── run ───────────────────────────────────────────────────────────────── */ oversize.then(async () => { @@ -639,6 +699,7 @@ oversize.then(async () => { fitTests(); grabTests(); clearTests(); + await browserBuildTests(); console.log(); if (failures) { console.log(`*** ${failures} FAILURE(S) ***`); diff --git a/tools/verify-documents.js b/tools/verify-documents.js new file mode 100755 index 0000000..4b94bd1 --- /dev/null +++ b/tools/verify-documents.js @@ -0,0 +1,148 @@ +#!/usr/bin/env node +/* + * Exercise the desktop document layer without launching Electron. + * + * electron/documents.js does the file handling: validating what is opened, + * writing atomically so a crash mid-save cannot destroy the previous version, + * and refusing implausible input. Those are the paths where a bug loses + * somebody's artwork, so they get tested directly rather than only through the + * smoke test. + * + * `electron` is stubbed — the module only uses app.getPath and dialog, neither + * of which is involved in the parts worth testing here. + * + * node tools/verify-documents.js + */ + +'use strict'; +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const Module = require('module'); + +// Stub `electron` before requiring the module under test. +const stub = { + app: { + getPath: (n) => os.tmpdir(), + addRecentDocument() {}, + clearRecentDocuments() {}, + }, + dialog: { + showOpenDialog: async () => ({ canceled: true, filePaths: [] }), + showSaveDialog: async () => ({ canceled: true, filePath: null }), + showMessageBox: async () => ({ response: 0 }), + }, +}; +const origResolve = Module._resolveFilename; +Module._resolveFilename = function (request, ...rest) { + if (request === 'electron') return 'electron-stub'; + return origResolve.call(this, request, ...rest); +}; +require.cache['electron-stub'] = { id: 'electron-stub', filename: 'electron-stub', + loaded: true, exports: stub }; + +const docs = require('../electron/documents.js'); + +let failures = 0; +const chk = (name, ok, detail) => { + console.log(` [${ok ? 'PASS' : 'FAIL'}] ${name}${detail ? ' ' + detail : ''}`); + if (!ok) failures++; +}; + +const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pf-doc-')); +const p = (n) => path.join(tmp, n); + +// A minimal but valid document, in the shape serialiseDoc() emits. +const VALID = JSON.stringify({ + v: 1, w: 4, h: 3, + palette: [[0, 0, 0, 0], [255, 255, 255, 255], [0, 0, 0, 255]], + layers: [['Layer 1', 1, 0, 1]], + frames: [[Buffer.from([1, 1, 2, 2, 1, 1, 2, 2, 1, 1, 2, 2]).toString('base64')]], +}); + +(async () => { + console.log('=== reading documents ==='); + + fs.writeFileSync(p('good.patternfront'), VALID); + const back = await docs.readDocument(p('good.patternfront')); + chk('a valid document reads back byte-identically', back === VALID); + + const rejects = [ + ['not JSON at all', 'nope.patternfront', 'this is not json'], + ['valid JSON that is not a document', 'wrong.patternfront', '{"hello":"world"}'], + ['a future format version', 'v9.patternfront', '{"v":9,"frames":[[]]}'], + ['a document with no frames', 'empty.patternfront', '{"v":1,"frames":[]}'], + ['JSON null', 'null.patternfront', 'null'], + ]; + for (const [label, file, body] of rejects) { + fs.writeFileSync(p(file), body); + let threw = null; + try { await docs.readDocument(p(file)); } catch (e) { threw = e.message; } + chk(`rejects ${label}`, threw !== null, threw || 'accepted it'); + } + + let threw = null; + try { await docs.readDocument(p('does-not-exist')); } catch (e) { threw = e.code || e.message; } + chk('a missing file raises rather than returning junk', threw !== null, String(threw)); + + threw = null; + try { await docs.readDocument(tmp); } catch (e) { threw = e.message; } + chk('a directory is not a document', threw !== null, threw); + + // Oversized input must be refused before it is read into memory. + const big = p('huge.patternfront'); + fs.writeFileSync(big, Buffer.alloc(docs.MAX_DOC_BYTES + 1024, 0x20)); + threw = null; + try { await docs.readDocument(big); } catch (e) { threw = e.message; } + chk('an oversized file is refused', threw !== null && /too large/.test(threw), threw); + + console.log('\n=== atomic writes ==='); + + const target = p('atomic.patternfront'); + await docs.writeAtomic(target, VALID); + chk('writes the file', fs.readFileSync(target, 'utf8') === VALID); + + await docs.writeAtomic(target, VALID.replace('"w":4', '"w":8')); + chk('overwrites in place', /"w":8/.test(fs.readFileSync(target, 'utf8'))); + chk('leaves no temp file behind', + fs.readdirSync(tmp).filter(f => f.endsWith('.tmp')).length === 0, + fs.readdirSync(tmp).filter(f => f.endsWith('.tmp')).join(',')); + + // If the rename cannot happen, the previous contents must survive intact — + // this is the whole reason for writing to a temp file first. + const readonlyDir = p('ro'); + fs.mkdirSync(readonlyDir); + const guarded = path.join(readonlyDir, 'keep.patternfront'); + fs.writeFileSync(guarded, VALID); + fs.chmodSync(readonlyDir, 0o500); + threw = null; + try { await docs.writeAtomic(guarded, 'REPLACED'); } catch (e) { threw = e.code; } + chk('a failed write raises', threw !== null, String(threw)); + chk('and the previous version is untouched', + fs.readFileSync(guarded, 'utf8') === VALID); + fs.chmodSync(readonlyDir, 0o700); + + console.log('\n=== recent documents ==='); + let changes = 0; + const recent = docs.makeRecent(() => changes++); + recent.add('/a.patternfront'); + recent.add('/b.patternfront'); + recent.add('/a.patternfront'); + chk('most recent first, no duplicates', + JSON.stringify(recent.list) === JSON.stringify(['/a.patternfront', '/b.patternfront']), + JSON.stringify(recent.list)); + chk('every change notifies the menu', changes === 3, String(changes)); + for (let i = 0; i < 20; i++) recent.add(`/f${i}.patternfront`); + chk('the list is capped', recent.list.length === 10, String(recent.list.length)); + recent.clear(); + chk('clearing empties it', recent.list.length === 0); + + fs.rmSync(tmp, { recursive: true, force: true }); + + console.log(); + if (failures) { + console.log(`*** ${failures} FAILURE(S) ***`); + process.exit(1); + } + console.log('ALL DOCUMENT CHECKS PASSED'); +})(); diff --git a/tools/verify-electron.py b/tools/verify-electron.py new file mode 100755 index 0000000..9651f6d --- /dev/null +++ b/tools/verify-electron.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +""" +Check the Electron shell's security posture and packaging config. + +An open-source Electron app gets read by strangers, and the default-insecure +knobs (nodeIntegration, a disabled sandbox, an unrestricted window.open) are +exactly what people look for first. These are cheap greps, but they are what +stops a convenient shortcut during a late-night debugging session shipping. + +Run: python3 tools/verify-electron.py +""" + +from __future__ import annotations + +import os +import re +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +E = os.path.join(ROOT, "electron") + +fails: list[str] = [] + + +def chk(name: str, ok: bool, detail: str = "") -> None: + print(f" [{'PASS' if ok else 'FAIL'}] {name}" + (f" {detail}" if detail else "")) + if not ok: + fails.append(name) + + +def read(name: str) -> str: + p = os.path.join(E, name) + return open(p).read() if os.path.exists(p) else "" + + +def main() -> int: + main_js = read("main.js") + preload = read("preload.js") + menu = read("menu.js") + documents = read("documents.js") + if not main_js: + print("electron/main.js not found") + return 2 + + print("=== renderer sandbox ===") + chk("context isolation on", "contextIsolation: true" in main_js) + chk("node integration off", "nodeIntegration: false" in main_js) + chk("sandbox on", "sandbox: true" in main_js) + chk("webview tag disabled", + "webviewTag: false" in main_js and "will-attach-webview" in main_js) + chk("a preload is set", "preload: path.join(__dirname, 'preload.js')" in main_js) + # `true` for any of these would undo the rest of the list. + for bad in ("nodeIntegration: true", "contextIsolation: false", + "sandbox: false", "webSecurity: false", "allowRunningInsecureContent"): + chk(f"never sets {bad}", bad not in main_js) + + print("\n=== navigation ===") + chk("navigation away from the app is blocked", "will-navigate" in main_js) + chk("popups are denied", "setWindowOpenHandler" in main_js + and "action: 'deny'" in main_js) + chk("only http(s) reaches the shell", + "u.protocol === 'http:' || u.protocol === 'https:'" in main_js) + + print("\n=== serving the renderer ===") + chk("served over a registered scheme, not file://", + "registerSchemesAsPrivileged" in main_js and "protocol.handle('app'" in main_js) + chk("the scheme is standard and secure, so localStorage works", + "standard: true" in main_js and "secure: true" in main_js) + chk("a CSP is sent with the document", "content-security-policy" in main_js) + for directive in ("default-src 'none'", "frame-ancestors 'none'", + "base-uri 'none'", "form-action 'none'"): + chk(f"CSP has {directive!r}", directive in main_js) + chk("path traversal out of app/ is refused", + "path.resolve(APP_DIR, rel)" in main_js + and "file.startsWith(APP_DIR + path.sep)" in main_js) + + print("\n=== the preload surface ===") + exposed = set(re.findall(r"^\s{2}(\w+):", preload, re.M)) + declared = {"isDesktop", "platform", "openDocument", "readDocument", + "saveDocument", "saveExport", "setDirty", "setDocumentPath", + "onCommand", "openExternal"} + chk("exposes exactly the declared surface", exposed == declared, + f"extra={sorted(exposed - declared)} missing={sorted(declared - exposed)}") + chk("uses contextBridge, not a raw window assignment", + "contextBridge.exposeInMainWorld" in preload) + chk("does not hand the renderer ipcRenderer itself", + "exposeInMainWorld('ipcRenderer'" not in preload + and "ipcRenderer," not in preload.replace("const { contextBridge, ipcRenderer }", "")) + chk("arguments are type-checked before crossing", "const str = (v, name)" in preload) + chk("no channel is chosen by the renderer", + not re.search(r"ipcRenderer\.(invoke|send)\(\s*(channel|name|id)\b", preload)) + + print("\n=== documents ===") + chk("saves are atomic", "async function writeAtomic" in documents + and "fs.rename(tmp, target)" in documents) + chk("input size is bounded", "MAX_DOC_BYTES" in documents) + chk("a document is validated before it is trusted", + "doc.v !== 1" in documents and "Array.isArray(doc.frames)" in documents) + chk("unsaved work prompts before the window closes", + "function onClose" in main_js and "Save changes to" in main_js + and "e.preventDefault()" in main_js) + + print("\n=== menu ===") + chk("menu items dispatch command ids, not inline logic", + "click: () => send(id)" in menu) + for accel in ("CmdOrCtrl+N", "CmdOrCtrl+O", "CmdOrCtrl+S", "CmdOrCtrl+Shift+S", + "CmdOrCtrl+Z", "CmdOrCtrl+A", "CmdOrCtrl+D"): + chk(f"{accel} is bound", accel in menu) + chk("file associations are registered", + "fileAssociations" in read_root("electron-builder.yml") + and "ext: patternfront" in read_root("electron-builder.yml")) + + print("\n=== packaging ===") + yml = read_root("electron-builder.yml") + chk("mac builds both architectures", "arch: [arm64, x64]" in yml) + chk("windows gets an installer and a portable build", + "target: nsis" in yml and "target: portable" in yml) + chk("only the app ships, not the repo", + "- electron/**/*" in yml and "- app/**/*" in yml and "tools/" not in yml) + chk("signing is wired but inert", "hardenedRuntime: false" in yml + and "entitlements: build/entitlements.mac.plist" in yml + and os.path.exists(os.path.join(ROOT, "build", "entitlements.mac.plist"))) + chk("an icon exists for the build", + os.path.exists(os.path.join(ROOT, "build", "icon.png"))) + + print() + if fails: + print(f"*** {len(fails)} FAILURE(S) ***") + for f in fails: + print(f" - {f}") + return 1 + print("ALL ELECTRON CHECKS PASSED") + return 0 + + +def read_root(name: str) -> str: + p = os.path.join(ROOT, name) + return open(p).read() if os.path.exists(p) else "" + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/verify-ui.py b/tools/verify-ui.py index 1a575ef..18d2ee8 100644 --- a/tools/verify-ui.py +++ b/tools/verify-ui.py @@ -290,6 +290,42 @@ def main() -> int: chk("the preview box scrolls when the canvas outgrows it", re.search(r"\.pvbox\{[^}]*overflow:auto", css) is not None) + print("\n=== desktop seam ===") + # The same file is the browser build and the Electron renderer. Every native + # call sits behind native(), so a browser is never asked for pfNative. + chk("native() is the single feature test", + "const native=()=>!!(window.pfNative&&window.pfNative.isDesktop);" in js) + # Whether the browser build ever touches pfNative is a runtime property, so + # it is asserted in verify-behaviour.js by booting with no pfNative at all. + # Here we only count the surface, to keep it small and reviewable. + calls = sorted({c.split(".")[-1] for c in re.findall(r"window\.pfNative\.\w+", js)}) + allowed = {"isDesktop", "saveExport", "openDocument", "saveDocument", + "readDocument", "onCommand", "setDirty", "setDocumentPath", + "openExternal"} + chk("renderer uses only the declared native surface", + set(calls) <= allowed, f"{calls}") + chk("exports route through the one download() seam", + "function download(b,n){" in js and "if(native()){" in js + and "window.pfNative.saveExport" in js) + chk("open and autosave share a load path", + "function loadDocFromJSON(raw)" in js + and "function restoreDoc(){ return loadDocFromJSON(store.get(SAVE_KEY)); }" in js) + chk("a command table backs the native menu", + "const COMMANDS={" in js and "window.__pf={commands:COMMANDS" in js) + for cmd in ("file.open", "file.save", "file.saveAs", "edit.undo", + "edit.clear", "view.fit", "help.shortcuts"): + chk(f"command {cmd!r} exists", f"'{cmd}':" in js) + # Electron dispatches the accelerator before the page sees the keydown, so + # handling it in both places runs it twice. + chk("menu-owned accelerators are not handled twice", + "const MENU_KEYS=new Set(" in js + and "if(meta&&native()&&MENU_KEYS.has(e.key.toLowerCase())) return;" in js) + chk("dirty state is tracked for the close prompt", + "function markDirty()" in js and "function markClean()" in js + and "syncStatus();saveSoon();markDirty();" in js) + chk("external links leave the app window", + "openExternal" in js and 'a[href^="http"]' in js) + print("\n=== no third-party content ===") # OpenFront's cosmetics.json is CC BY-SA 4.0 and includes characters they # cannot sublicense onward, so none of it is redistributed here. These are From d4af54f82962a7d18eea2bd57f36bb39e52c2b05 Mon Sep 17 00:00:00 2001 From: timbogdanov Date: Sun, 16 Aug 2026 20:09:02 -0500 Subject: [PATCH 2/4] CI: give Chromium its sandbox on the Linux runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The smoke test aborted with "The SUID sandbox helper binary was found, but is not configured correctly" — chrome-sandbox has to be owned by root with the setuid bit, and npm cannot install it that way. Fixed by setting the ownership rather than passing --no-sandbox. The whole point of the smoke test is to exercise the configuration users actually get, and this app deliberately runs with sandbox: true. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SPLUTYBHytZJX2BB6kCL9T --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cb688b..84182c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,15 @@ jobs: sudo apt-get install -y xvfb libgtk-3-0 libnss3 libasound2t64 \ libgbm1 libxss1 libxtst6 + # Chromium's SUID sandbox helper has to be owned by root with the setuid + # bit; npm cannot install it that way. The alternative is --no-sandbox, + # which would mean the smoke test exercises a different configuration + # from the one users actually run — so fix the permissions instead. + - name: Let Chromium use its sandbox + run: | + sudo chown root:root node_modules/electron/dist/chrome-sandbox + sudo chmod 4755 node_modules/electron/dist/chrome-sandbox + - name: Launch the app and assert the editor came up run: xvfb-run --auto-servernum npm run smoke From caac7ea5c16e0617f6266149051ba2b70c582546 Mon Sep 17 00:00:00 2001 From: timbogdanov Date: Sun, 16 Aug 2026 20:11:32 -0500 Subject: [PATCH 3/4] CI: fetch the Electron binary before fixing its sandbox permissions node_modules/electron/dist is populated lazily on first use, not by npm ci, so the chown ran against a path that did not exist yet. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SPLUTYBHytZJX2BB6kCL9T --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84182c6..1dc0476 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,8 +87,12 @@ jobs: # bit; npm cannot install it that way. The alternative is --no-sandbox, # which would mean the smoke test exercises a different configuration # from the one users actually run — so fix the permissions instead. + # + # The binary is fetched lazily rather than during `npm ci`, so it has to + # be pulled down before there is anything to chown. - name: Let Chromium use its sandbox run: | + npx electron --version sudo chown root:root node_modules/electron/dist/chrome-sandbox sudo chmod 4755 node_modules/electron/dist/chrome-sandbox From 76bdb6f5d196d13c1bdf8bb49daa81c279d7cf5b Mon Sep 17 00:00:00 2001 From: timbogdanov Date: Sun, 16 Aug 2026 20:18:42 -0500 Subject: [PATCH 4/4] CI: download Electron with its own installer, not by launching it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npx electron --version` starts the binary, which aborts on the same SUID sandbox error the step exists to fix — so the chown never ran. Calling install.js fetches dist/ without launching anything. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SPLUTYBHytZJX2BB6kCL9T --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1dc0476..1c8be77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,13 +88,15 @@ jobs: # which would mean the smoke test exercises a different configuration # from the one users actually run — so fix the permissions instead. # - # The binary is fetched lazily rather than during `npm ci`, so it has to - # be pulled down before there is anything to chown. + # The binary is fetched lazily rather than during `npm ci`, so run the + # package's own downloader first. Not `npx electron --version` — that + # launches the binary, which trips the very sandbox error being fixed. - name: Let Chromium use its sandbox run: | - npx electron --version + node node_modules/electron/install.js sudo chown root:root node_modules/electron/dist/chrome-sandbox sudo chmod 4755 node_modules/electron/dist/chrome-sandbox + ls -l node_modules/electron/dist/chrome-sandbox - name: Launch the app and assert the editor came up run: xvfb-run --auto-servernum npm run smoke