mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-08 05:18:49 +03:00
feat(desktop): add native host scaffold
This commit is contained in:
+6
-2
@@ -6,8 +6,12 @@
|
|||||||
.env
|
.env
|
||||||
.web
|
.web
|
||||||
.orion
|
.orion
|
||||||
nanobot-desktop/
|
|
||||||
desktop/
|
# Desktop app generated artifacts
|
||||||
|
desktop/build/
|
||||||
|
desktop/dist/
|
||||||
|
desktop/node_modules/
|
||||||
|
desktop/resources/nanobot-engine/
|
||||||
|
|
||||||
# Claude / AI assistant artifacts
|
# Claude / AI assistant artifacts
|
||||||
docs/superpowers/
|
docs/superpowers/
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# nanobot Desktop
|
||||||
|
|
||||||
|
Electron package for the Mac-first nanobot native app.
|
||||||
|
|
||||||
|
This folder is a host shell. It reuses the root WebUI build at
|
||||||
|
`nanobot/web/dist`; it does not copy or fork `webui/src`. Electron owns the
|
||||||
|
local engine lifecycle, exposes `window.nanobotHost` to the renderer, serves the
|
||||||
|
`nanobot-app://` app protocol, and proxies `/api/*` plus `/webui/bootstrap` to a
|
||||||
|
private Unix socket `nanobot desktop-gateway` process.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd desktop
|
||||||
|
bun run dev:webui
|
||||||
|
```
|
||||||
|
|
||||||
|
In another terminal:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd desktop
|
||||||
|
bun run dev:app
|
||||||
|
```
|
||||||
|
|
||||||
|
`dev:app` points Electron at the Vite dev server so WebUI changes hot reload.
|
||||||
|
For source checkouts, the app uses `python3` by default and injects the repo
|
||||||
|
root into `PYTHONPATH`. Packaged builds look for a bundled interpreter at
|
||||||
|
`Resources/nanobot-engine/bin/python3`.
|
||||||
|
|
||||||
|
## Engine Bundle
|
||||||
|
|
||||||
|
Release builds prepare `resources/nanobot-engine/` from a macOS
|
||||||
|
`python-build-standalone` archive before running `electron-builder`.
|
||||||
|
By default the script discovers the latest `astral-sh/python-build-standalone`
|
||||||
|
CPython 3.12 `install_only` asset for the requested architecture.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd desktop
|
||||||
|
bun run make:mac:arm64
|
||||||
|
bun run make:mac:x64
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful overrides:
|
||||||
|
|
||||||
|
- `NANOBOT_DESKTOP_ARCH=arm64|x64`
|
||||||
|
- `NANOBOT_DESKTOP_PYTHON_VERSION=3.12`
|
||||||
|
- `PYTHON_STANDALONE_RELEASE=20260510`
|
||||||
|
- `PYTHON_STANDALONE_TARBALL=/path/to/archive.tar.gz`
|
||||||
|
- `PYTHON_STANDALONE_URL=https://.../cpython-...tar.gz`
|
||||||
|
- `NANOBOT_WHEELHOUSE=/path/to/wheels` to install from a locked wheelhouse
|
||||||
|
|
||||||
|
The script installs the current checkout's `nanobot-ai[api]` into the bundled
|
||||||
|
runtime and writes `nanobot-engine.json` for diagnostics.
|
||||||
|
|
||||||
|
## Updating Builds
|
||||||
|
|
||||||
|
The native host does not copy the WebUI source or fork the Python agent code. A
|
||||||
|
release bundle is assembled from the current repository state:
|
||||||
|
|
||||||
|
1. Build the shared WebUI:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun run build --prefix webui
|
||||||
|
```
|
||||||
|
|
||||||
|
`electron-builder` packages the resulting `nanobot/web/dist` directory as
|
||||||
|
`Resources/nanobot-webui`.
|
||||||
|
|
||||||
|
2. Prepare the bundled Python engine:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd desktop
|
||||||
|
NANOBOT_DESKTOP_ARCH=arm64 bun run prepare-engine
|
||||||
|
```
|
||||||
|
|
||||||
|
The script installs the current checkout's `nanobot-ai[api]` package into
|
||||||
|
`resources/nanobot-engine/`, so agent, provider, tool, WebSocket, and config
|
||||||
|
changes flow into the next desktop build automatically.
|
||||||
|
|
||||||
|
3. Build the desktop app and DMG:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun run make:mac:arm64
|
||||||
|
bun run make:mac:x64
|
||||||
|
```
|
||||||
|
|
||||||
|
User data is not stored in the app bundle. Config, sessions, logs, workspace
|
||||||
|
state, and the default workspace remain under the platform app data directory,
|
||||||
|
so updating the app replaces code without overwriting local user state.
|
||||||
|
|
||||||
|
## Runtime Contract
|
||||||
|
|
||||||
|
- User data lives under the platform app data directory (`~/Library/Application Support/nanobot/` on macOS).
|
||||||
|
- Fresh installs start the private engine directly. The Python desktop gateway
|
||||||
|
creates the first `config.json` with defaults, then shared WebUI settings own
|
||||||
|
provider, model, and credential setup.
|
||||||
|
- The gateway listens on a per-user Unix socket in the app data directory and uses a transient secret.
|
||||||
|
- The gateway starts with only the WebSocket local channel enabled and does not serve the WebUI static bundle.
|
||||||
|
- The renderer loads assets through `nanobot-app://app/...`; browser users cannot open the native UI from a localhost port.
|
||||||
|
- WebSocket traffic uses the generic `nanobot-host://engine/...` URL, is bridged over Electron IPC, and still uses the short-lived token minted by `/webui/bootstrap`.
|
||||||
|
- Host IPC only accepts the trusted app origin, and socket bridging only accepts the `nanobot-host://engine/...` scheme.
|
||||||
|
- Native WebUI responses include a restrictive Content Security Policy.
|
||||||
|
- WebUI talks only to the generic `window.nanobotHost` contract. Product-specific native behavior stays in this folder.
|
||||||
|
|
||||||
|
Generated release artifacts, node modules, and bundled runtimes remain ignored
|
||||||
|
so the tracked desktop package stays source-only.
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
- [`docs/development.md`](docs/development.md)
|
||||||
|
- [`docs/host-contract.md`](docs/host-contract.md)
|
||||||
|
- [`docs/webui-sync.md`](docs/webui-sync.md)
|
||||||
@@ -0,0 +1,594 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "@nanobot/desktop",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"electron": "^42.3.0",
|
||||||
|
"electron-builder": "^26.8.1",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="],
|
||||||
|
|
||||||
|
"@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
|
||||||
|
|
||||||
|
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||||
|
|
||||||
|
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
|
||||||
|
|
||||||
|
"@electron/get": ["@electron/get@5.0.0", "", { "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" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA=="],
|
||||||
|
|
||||||
|
"@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="],
|
||||||
|
|
||||||
|
"@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "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" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="],
|
||||||
|
|
||||||
|
"@electron/rebuild": ["@electron/rebuild@4.0.4", "", { "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" } }, "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg=="],
|
||||||
|
|
||||||
|
"@electron/universal": ["@electron/universal@2.0.3", "", { "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" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="],
|
||||||
|
|
||||||
|
"@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "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" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="],
|
||||||
|
|
||||||
|
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||||
|
|
||||||
|
"@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="],
|
||||||
|
|
||||||
|
"@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="],
|
||||||
|
|
||||||
|
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
|
||||||
|
|
||||||
|
"@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
|
||||||
|
|
||||||
|
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
|
||||||
|
|
||||||
|
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||||
|
|
||||||
|
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
|
||||||
|
|
||||||
|
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
|
||||||
|
|
||||||
|
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
||||||
|
|
||||||
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@22.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="],
|
||||||
|
|
||||||
|
"@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="],
|
||||||
|
|
||||||
|
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
|
||||||
|
|
||||||
|
"@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="],
|
||||||
|
|
||||||
|
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
||||||
|
|
||||||
|
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
|
||||||
|
|
||||||
|
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
|
||||||
|
|
||||||
|
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||||
|
|
||||||
|
"ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
|
"app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="],
|
||||||
|
|
||||||
|
"app-builder-lib": ["app-builder-lib@26.8.1", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@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.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "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.8.1", "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.0.3", "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", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw=="],
|
||||||
|
|
||||||
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
|
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
|
||||||
|
|
||||||
|
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
|
||||||
|
|
||||||
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
|
"async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
|
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
|
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||||
|
|
||||||
|
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||||
|
|
||||||
|
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||||
|
|
||||||
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
|
"builder-util": ["builder-util@26.8.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.5.1", "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" } }, "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw=="],
|
||||||
|
|
||||||
|
"builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="],
|
||||||
|
|
||||||
|
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
|
||||||
|
|
||||||
|
"cacheable-request": ["cacheable-request@7.0.4", "", { "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" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||||
|
|
||||||
|
"chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="],
|
||||||
|
|
||||||
|
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
|
||||||
|
|
||||||
|
"cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
|
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
|
"commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
||||||
|
|
||||||
|
"compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="],
|
||||||
|
|
||||||
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
|
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
||||||
|
|
||||||
|
"crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="],
|
||||||
|
|
||||||
|
"cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||||
|
|
||||||
|
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
|
||||||
|
|
||||||
|
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||||
|
|
||||||
|
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
|
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
|
||||||
|
|
||||||
|
"dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="],
|
||||||
|
|
||||||
|
"dmg-builder": ["dmg-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg=="],
|
||||||
|
|
||||||
|
"dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": "bin/dmg-license.js" }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="],
|
||||||
|
|
||||||
|
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||||
|
|
||||||
|
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": "bin/cli.js" }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||||
|
|
||||||
|
"electron": ["electron@42.3.0", "", { "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-9ZiLdRXk+WDxW1OgIUz8J2rIQ5TYU9o629gCOjU48Q3dQiOmym7osWsH5Ubs/Jh4uuFLn6m6SBD2rmRXLAPz9g=="],
|
||||||
|
|
||||||
|
"electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "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" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="],
|
||||||
|
|
||||||
|
"electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA=="],
|
||||||
|
|
||||||
|
"electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="],
|
||||||
|
|
||||||
|
"electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||||
|
|
||||||
|
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||||
|
|
||||||
|
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
|
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
|
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
|
||||||
|
|
||||||
|
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": "cli.js" }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
||||||
|
|
||||||
|
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
|
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||||
|
|
||||||
|
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
||||||
|
|
||||||
|
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
|
||||||
|
|
||||||
|
"glob": ["glob@7.2.3", "", { "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" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
|
"global-agent": ["global-agent@3.0.0", "", { "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" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
|
||||||
|
|
||||||
|
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"got": ["got@11.8.6", "", { "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" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
|
|
||||||
|
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||||
|
|
||||||
|
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
||||||
|
|
||||||
|
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||||
|
|
||||||
|
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||||
|
|
||||||
|
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
|
||||||
|
|
||||||
|
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
|
"iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
|
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
||||||
|
|
||||||
|
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": "bin/cli.js" }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.7.0", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||||
|
|
||||||
|
"js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="],
|
||||||
|
|
||||||
|
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
|
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||||
|
|
||||||
|
"json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||||
|
|
||||||
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
|
"lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="],
|
||||||
|
|
||||||
|
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
||||||
|
|
||||||
|
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||||
|
|
||||||
|
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"mime": ["mime@2.6.0", "", { "bin": "cli.js" }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||||
|
|
||||||
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
|
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||||
|
|
||||||
|
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
||||||
|
|
||||||
|
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": "bin/cmd.js" }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"node-abi": ["node-abi@4.31.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw=="],
|
||||||
|
|
||||||
|
"node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="],
|
||||||
|
|
||||||
|
"node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="],
|
||||||
|
|
||||||
|
"node-gyp": ["node-gyp@12.3.0", "", { "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": "bin/node-gyp.js" }, "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg=="],
|
||||||
|
|
||||||
|
"nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": "bin/nopt.js" }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="],
|
||||||
|
|
||||||
|
"normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],
|
||||||
|
|
||||||
|
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||||
|
|
||||||
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
|
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
||||||
|
|
||||||
|
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||||
|
|
||||||
|
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="],
|
||||||
|
|
||||||
|
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||||
|
|
||||||
|
"plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
|
||||||
|
|
||||||
|
"postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": "dist/cli.js" }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="],
|
||||||
|
|
||||||
|
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
|
||||||
|
|
||||||
|
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
|
||||||
|
|
||||||
|
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
|
||||||
|
|
||||||
|
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
|
||||||
|
|
||||||
|
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
||||||
|
|
||||||
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
|
||||||
|
|
||||||
|
"read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": "cli.js" }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
|
"resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="],
|
||||||
|
|
||||||
|
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
|
||||||
|
|
||||||
|
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
||||||
|
|
||||||
|
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||||
|
|
||||||
|
"rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
|
||||||
|
|
||||||
|
"roarr": ["roarr@2.15.4", "", { "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" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="],
|
||||||
|
|
||||||
|
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
||||||
|
|
||||||
|
"semver": ["semver@7.8.1", "", { "bin": "bin/semver.js" }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
|
||||||
|
|
||||||
|
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
|
||||||
|
|
||||||
|
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||||
|
|
||||||
|
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
|
||||||
|
|
||||||
|
"slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
|
||||||
|
|
||||||
|
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
||||||
|
|
||||||
|
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
|
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||||
|
|
||||||
|
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||||
|
|
||||||
|
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
|
||||||
|
|
||||||
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
|
"tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="],
|
||||||
|
|
||||||
|
"temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="],
|
||||||
|
|
||||||
|
"temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="],
|
||||||
|
|
||||||
|
"tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||||
|
|
||||||
|
"tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="],
|
||||||
|
|
||||||
|
"tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="],
|
||||||
|
|
||||||
|
"truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="],
|
||||||
|
|
||||||
|
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici": ["undici@7.27.0", "", {}, "sha512-+t2Z/GwkZQDtu00813aP66ygViGtPHKhhoFZpQKpKrE+9jIgES+Zw+mFNaDWOVRKiuJjuqKHzD3B1sfGg8+ZOQ=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||||
|
|
||||||
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
|
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
|
||||||
|
|
||||||
|
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
||||||
|
|
||||||
|
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
|
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
|
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@17.7.2", "", { "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" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
|
||||||
|
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
||||||
|
|
||||||
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
|
"@electron/asar/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||||
|
|
||||||
|
"@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||||
|
|
||||||
|
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||||
|
|
||||||
|
"@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
|
||||||
|
|
||||||
|
"@electron/universal/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
|
||||||
|
|
||||||
|
"@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||||
|
|
||||||
|
"@electron/windows-sign/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
|
||||||
|
|
||||||
|
"@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||||
|
|
||||||
|
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "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" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
|
||||||
|
|
||||||
|
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
|
||||||
|
|
||||||
|
"app-builder-lib/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
|
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
|
||||||
|
|
||||||
|
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||||
|
|
||||||
|
"electron/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
|
||||||
|
|
||||||
|
"electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
|
||||||
|
|
||||||
|
"filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
|
||||||
|
|
||||||
|
"glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||||
|
|
||||||
|
"lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
|
"node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||||
|
|
||||||
|
"node-gyp/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="],
|
||||||
|
|
||||||
|
"node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
|
||||||
|
|
||||||
|
"postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
|
||||||
|
|
||||||
|
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": "bin/semver" }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||||
|
|
||||||
|
"@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
|
||||||
|
|
||||||
|
"@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
|
||||||
|
|
||||||
|
"app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||||
|
|
||||||
|
"app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
|
||||||
|
|
||||||
|
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
|
||||||
|
|
||||||
|
"electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||||
|
|
||||||
|
"electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||||
|
|
||||||
|
"electron/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
|
"filelist/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
|
||||||
|
|
||||||
|
"glob/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
|
||||||
|
|
||||||
|
"node-gyp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
|
||||||
|
|
||||||
|
"@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||||
|
|
||||||
|
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||||
|
|
||||||
|
"dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Desktop Development Guide
|
||||||
|
|
||||||
|
`desktop` is the native host for the shared nanobot WebUI. It is not a fork of
|
||||||
|
the WebUI, and it should not grow a second copy of product UI.
|
||||||
|
|
||||||
|
The healthy mental model is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
nanobot core -> agent runtime, gateway, providers, tools, memory
|
||||||
|
webui -> shared product UI and runtime-aware UI
|
||||||
|
desktop -> native host, engine lifecycle, secure host capabilities
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
Run the shared WebUI dev server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd desktop
|
||||||
|
bun run dev:webui
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the Electron host in another terminal:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd desktop
|
||||||
|
bun run dev:app
|
||||||
|
```
|
||||||
|
|
||||||
|
In development, Electron loads `http://127.0.0.1:5173`, so changes under
|
||||||
|
`webui/src` hot reload. Changes under `desktop/src` require restarting
|
||||||
|
`dev:app`.
|
||||||
|
|
||||||
|
For source checkouts, the host starts the engine with local `python3` and
|
||||||
|
injects the repository root into `PYTHONPATH`. This means Python changes under
|
||||||
|
`nanobot/` are picked up from the current checkout.
|
||||||
|
|
||||||
|
## Where Code Goes
|
||||||
|
|
||||||
|
Use this table before adding a desktop feature:
|
||||||
|
|
||||||
|
| Change | Location |
|
||||||
|
| --- | --- |
|
||||||
|
| Agent behavior, tools, providers, memory, config schema | `nanobot/` |
|
||||||
|
| Shared chat UI, settings UI, reusable product UI | `webui/` |
|
||||||
|
| Runtime-aware UI rows, such as native engine status or open logs buttons | `webui/` |
|
||||||
|
| The implementation behind native capabilities | `desktop/src/main.ts` |
|
||||||
|
| The trusted renderer bridge contract | `desktop/src/preload.cts` and `desktop/docs/host-contract.md` |
|
||||||
|
| Electron window, app protocol, native menus, lifecycle, packaging | `desktop/src` and `desktop/package.json` |
|
||||||
|
| WebSocket-over-Unix-socket bridge | `desktop/src/unixWebSocket.ts` |
|
||||||
|
| Bundled Python runtime preparation | `desktop/scripts/prepare-engine.mjs` |
|
||||||
|
|
||||||
|
For example, if desktop Settings needs an "Open logs" button, the button belongs
|
||||||
|
in the shared WebUI settings page because it is product UI. The actual filesystem
|
||||||
|
operation belongs in the desktop host and is exposed through `window.nanobotHost`.
|
||||||
|
|
||||||
|
## Host Contract
|
||||||
|
|
||||||
|
The shared WebUI talks to desktop through `window.nanobotHost`. WebUI code may
|
||||||
|
check for host capabilities, but it must not import Electron, Node.js modules,
|
||||||
|
or desktop source files.
|
||||||
|
|
||||||
|
Prefer capability-driven UI:
|
||||||
|
|
||||||
|
```text
|
||||||
|
if host can open logs -> show Open logs
|
||||||
|
if host can restart engine -> show Restart engine
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid platform-driven UI:
|
||||||
|
|
||||||
|
```text
|
||||||
|
if desktop -> run Electron-specific logic in WebUI
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps the WebUI usable in browsers and leaves room for future native hosts
|
||||||
|
without rewriting product screens.
|
||||||
|
|
||||||
|
## Adding A Desktop Feature
|
||||||
|
|
||||||
|
Before implementing, answer these questions:
|
||||||
|
|
||||||
|
1. Is this product UI or a native capability?
|
||||||
|
2. Can the WebUI express it through a generic capability instead of a desktop flag?
|
||||||
|
3. Does the host API validate trusted origins and accepted URL schemes?
|
||||||
|
4. Does browser WebUI still work when `window.nanobotHost` is missing?
|
||||||
|
5. Does the engine behavior belong in nanobot core instead of Electron?
|
||||||
|
6. Does packaged mode use app data for user state instead of app resources?
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
- Do not copy or fork `webui/src` into `desktop/`.
|
||||||
|
- Do not import Electron or Node.js modules from `webui/src`.
|
||||||
|
- Do not add provider-specific onboarding screens to `desktop/`.
|
||||||
|
- Do not duplicate WebUI settings or login flows in Electron-owned HTML.
|
||||||
|
- Do not make `desktop/src/main.ts` own agent behavior.
|
||||||
|
- Do not commit `desktop/node_modules`, `desktop/build`, `desktop/dist`, DMGs,
|
||||||
|
or `desktop/resources/nanobot-engine`.
|
||||||
|
|
||||||
|
## Release Shape
|
||||||
|
|
||||||
|
Release builds assemble three existing parts:
|
||||||
|
|
||||||
|
1. the shared WebUI build from `nanobot/web/dist`,
|
||||||
|
2. the Python engine prepared under `desktop/resources/nanobot-engine`,
|
||||||
|
3. the Electron host compiled from `desktop/src`.
|
||||||
|
|
||||||
|
User config, logs, sessions, workspace state, and the default workspace live in
|
||||||
|
the platform app data directory, not inside the app bundle.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Native Host Contract
|
||||||
|
|
||||||
|
`desktop` is a native host shell around the shared WebUI build. The
|
||||||
|
renderer must not import Electron directly. It receives a minimal bridge at
|
||||||
|
`window.nanobotHost`.
|
||||||
|
|
||||||
|
## Runtime API
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type HostRuntimeInfo = {
|
||||||
|
surface: "native";
|
||||||
|
app_version: string;
|
||||||
|
engine_status: "starting" | "ready" | "restarting" | "stopped" | "crashed";
|
||||||
|
data_dir: string;
|
||||||
|
logs_dir: string;
|
||||||
|
config_path: string;
|
||||||
|
workspace_path: string;
|
||||||
|
python: string;
|
||||||
|
engine_transport?: "unix_socket";
|
||||||
|
};
|
||||||
|
|
||||||
|
type HostSocketEvent =
|
||||||
|
| { id: string; type: "open" }
|
||||||
|
| { id: string; type: "message"; data: string }
|
||||||
|
| { id: string; type: "error"; message: string }
|
||||||
|
| { id: string; type: "close"; code?: number; reason?: string };
|
||||||
|
|
||||||
|
type NanobotHost = {
|
||||||
|
getRuntimeInfo(): Promise<HostRuntimeInfo>;
|
||||||
|
restartEngine(): Promise<void>;
|
||||||
|
pickFolder(): Promise<string | null>;
|
||||||
|
openLogs(): Promise<void>;
|
||||||
|
exportDiagnostics(): Promise<string>;
|
||||||
|
checkForUpdates(): Promise<{ supported: boolean; message?: string }>;
|
||||||
|
openSocket(url: string): Promise<string>;
|
||||||
|
sendSocket(id: string, data: string): Promise<void>;
|
||||||
|
closeSocket(id: string): Promise<void>;
|
||||||
|
onSocketEvent(listener: (event: HostSocketEvent) => void): () => void;
|
||||||
|
onRuntimeStatus(listener: (status: HostRuntimeInfo["engine_status"]) => void): () => void;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## First Run
|
||||||
|
|
||||||
|
The desktop host starts the private engine immediately. If the native data
|
||||||
|
directory has no `config.json`, `nanobot desktop-gateway` creates one with
|
||||||
|
defaults before serving the shared WebUI. Provider, model, credential, and login
|
||||||
|
setup stay in WebUI settings instead of Electron-owned HTML.
|
||||||
|
|
||||||
|
## Socket Bridge
|
||||||
|
|
||||||
|
The engine listens on a per-user Unix socket under the app data directory.
|
||||||
|
`/webui/bootstrap` returns `runtime_surface: "native"` and a WebSocket URL in
|
||||||
|
the `nanobot-host://engine/...` scheme. WebUI never opens that URL directly in
|
||||||
|
the browser runtime; it hands the URL to `window.nanobotHost.openSocket`.
|
||||||
|
|
||||||
|
The native host then performs the WebSocket handshake against the Unix socket
|
||||||
|
and forwards events over Electron IPC.
|
||||||
|
|
||||||
|
## Host Security Boundary
|
||||||
|
|
||||||
|
The host bridge is intentionally narrower than a general Electron preload:
|
||||||
|
|
||||||
|
- IPC calls are accepted only from renderer frames loaded from `nanobot-app://app/...`.
|
||||||
|
- `openSocket` accepts only `nanobot-host://engine/...` URLs.
|
||||||
|
- External navigation is denied in the app window; safe web links are opened by
|
||||||
|
the operating system.
|
||||||
|
- Native WebUI responses carry a restrictive Content Security Policy and
|
||||||
|
`X-Content-Type-Options: nosniff`.
|
||||||
|
- The renderer runs with `nodeIntegration: false`, `contextIsolation: true`,
|
||||||
|
`sandbox: true`, and `webSecurity: true`.
|
||||||
|
|
||||||
|
Security-sensitive tool behavior still belongs in nanobot core. The host
|
||||||
|
protects the native app boundary; the engine protects file, network, and tool
|
||||||
|
permissions.
|
||||||
|
|
||||||
|
## Data Directory
|
||||||
|
|
||||||
|
The host stores config, workspace, sessions, logs, and transient socket files
|
||||||
|
under the platform app data directory. On macOS this is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/Library/Application Support/nanobot/
|
||||||
|
```
|
||||||
|
|
||||||
|
The app bundle is replaceable. User data is not stored in the bundle.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# WebUI Sync Workflow
|
||||||
|
|
||||||
|
`desktop` consumes the shared WebUI build output. It must not copy,
|
||||||
|
fork, or vendor `webui/src`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Run the WebUI dev server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd desktop
|
||||||
|
bun run dev:webui
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the native host in another terminal:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd desktop
|
||||||
|
bun run dev:app
|
||||||
|
```
|
||||||
|
|
||||||
|
The host loads `http://127.0.0.1:5173` in development, so React changes hot
|
||||||
|
reload. Main/preload changes still require restarting `dev:app`.
|
||||||
|
|
||||||
|
## Release Build
|
||||||
|
|
||||||
|
1. Build the shared WebUI:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun run build --prefix webui
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Prepare the bundled Python engine:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd desktop
|
||||||
|
NANOBOT_DESKTOP_ARCH=arm64 bun run prepare-engine
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build the app:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun run make:mac:arm64
|
||||||
|
bun run make:mac:x64
|
||||||
|
```
|
||||||
|
|
||||||
|
`electron-builder` packages `nanobot/web/dist` as `Resources/nanobot-webui`.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- WebUI source remains product-neutral:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rg -n "desktop|Desktop|nanobotDesktop" webui/src
|
||||||
|
```
|
||||||
|
|
||||||
|
This command should print nothing.
|
||||||
|
|
||||||
|
- Native host behavior is implemented in `desktop/src`.
|
||||||
|
- Provider, model, credential, and login setup stay in shared WebUI settings.
|
||||||
|
Do not duplicate those flows in Electron-owned HTML.
|
||||||
|
- Shared UI behavior is implemented in `webui/src` through `window.nanobotHost`
|
||||||
|
and generic runtime capability checks.
|
||||||
|
- Do not copy React components from `webui/src` into this folder.
|
||||||
|
- Do not commit bundled runtimes, DMGs, or `node_modules`.
|
||||||
Generated
+3698
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "@nanobot/desktop",
|
||||||
|
"version": "0.2.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "build/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"build:webui": "cd ../webui && bun run build",
|
||||||
|
"prepare-engine": "node scripts/prepare-engine.mjs",
|
||||||
|
"print-engine-url": "node scripts/prepare-engine.mjs --print-runtime-url",
|
||||||
|
"dev": "bun run dev:app",
|
||||||
|
"dev:app": "NANOBOT_DESKTOP_WEB_DEV_URL=http://127.0.0.1:5173 bun run build && electron .",
|
||||||
|
"dev:webui": "cd ../webui && bun run dev",
|
||||||
|
"start": "electron .",
|
||||||
|
"make:mac:arm64": "bun run build:webui && NANOBOT_DESKTOP_ARCH=arm64 bun run prepare-engine && bun run build && electron-builder --mac dmg --arm64",
|
||||||
|
"make:mac:x64": "bun run build:webui && NANOBOT_DESKTOP_ARCH=x64 bun run prepare-engine && bun run build && electron-builder --mac dmg --x64",
|
||||||
|
"dist:mac:arm64": "electron-builder --mac dmg --arm64",
|
||||||
|
"dist:mac:x64": "electron-builder --mac dmg --x64"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"electron": "^42.3.0",
|
||||||
|
"electron-builder": "^26.8.1",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "ai.nanobot.desktop",
|
||||||
|
"productName": "nanobot",
|
||||||
|
"asar": true,
|
||||||
|
"files": [
|
||||||
|
"build/**/*",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "../nanobot/web/dist",
|
||||||
|
"to": "nanobot-webui"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/nanobot-engine",
|
||||||
|
"to": "nanobot-engine",
|
||||||
|
"filter": [
|
||||||
|
"**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mac": {
|
||||||
|
"category": "public.app-category.developer-tools",
|
||||||
|
"target": [
|
||||||
|
"dmg"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import {
|
||||||
|
cp,
|
||||||
|
lstat,
|
||||||
|
mkdir,
|
||||||
|
readdir,
|
||||||
|
readFile,
|
||||||
|
readlink,
|
||||||
|
rm,
|
||||||
|
stat,
|
||||||
|
symlink,
|
||||||
|
unlink,
|
||||||
|
writeFile,
|
||||||
|
} from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||||
|
const desktopRoot = path.resolve(__dirname, "..");
|
||||||
|
const engineDest = path.resolve(
|
||||||
|
process.env.NANOBOT_ENGINE_DEST ?? path.join(desktopRoot, "resources", "nanobot-engine"),
|
||||||
|
);
|
||||||
|
const pythonVersion = process.env.NANOBOT_DESKTOP_PYTHON_VERSION ?? "3.12";
|
||||||
|
const githubBase = "https://github.com/astral-sh/python-build-standalone";
|
||||||
|
|
||||||
|
function run(command, args, options = {}) {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`${command} ${args.join(" ")} failed`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function download(url, destination) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "nanobot/desktop-build",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`failed to download ${url}: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
await writeFile(destination, Buffer.from(await response.arrayBuffer()));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchText(url) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "nanobot/desktop-build",
|
||||||
|
"Accept": "text/html",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`failed to fetch ${url}: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
return await response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetTriple() {
|
||||||
|
const requested = process.env.NANOBOT_DESKTOP_ARCH ?? process.arch;
|
||||||
|
if (requested === "arm64" || requested === "aarch64") return "aarch64-apple-darwin";
|
||||||
|
if (requested === "x64" || requested === "x86_64") return "x86_64-apple-darwin";
|
||||||
|
throw new Error(`unsupported desktop engine arch: ${requested}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestReleaseTag(html) {
|
||||||
|
const match = html.match(/\/astral-sh\/python-build-standalone\/releases\/tag\/(\d{8})/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error("could not find latest python-build-standalone release tag");
|
||||||
|
}
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function defaultStandaloneUrl() {
|
||||||
|
const release =
|
||||||
|
process.env.PYTHON_STANDALONE_RELEASE
|
||||||
|
?? latestReleaseTag(await fetchText(`${githubBase}/releases`));
|
||||||
|
const triple = targetTriple();
|
||||||
|
const assetHtml = await fetchText(`${githubBase}/releases/expanded_assets/${release}`);
|
||||||
|
const escapedVersion = pythonVersion.replace(".", "\\.");
|
||||||
|
const assetPattern = new RegExp(
|
||||||
|
`cpython-${escapedVersion}\\.\\d+\\+${release}-${triple}-install_only\\.tar\\.gz`,
|
||||||
|
);
|
||||||
|
const asset = assetHtml.match(assetPattern)?.[0];
|
||||||
|
if (!asset) {
|
||||||
|
throw new Error(
|
||||||
|
`could not find a CPython ${pythonVersion} install_only asset for ${triple} in ${release}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return `${githubBase}/releases/download/${release}/${asset}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function walk(dir, matches = []) {
|
||||||
|
for (const entry of await readdir(dir)) {
|
||||||
|
const fullPath = path.join(dir, entry);
|
||||||
|
const info = await stat(fullPath);
|
||||||
|
if (info.isDirectory()) {
|
||||||
|
await walk(fullPath, matches);
|
||||||
|
} else if (entry === "python3" || entry === "python") {
|
||||||
|
matches.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findStandaloneRoot(extractDir) {
|
||||||
|
const candidates = await walk(extractDir);
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const normalized = candidate.split(path.sep).join("/");
|
||||||
|
if (normalized.endsWith("/install/bin/python3")) {
|
||||||
|
return path.dirname(path.dirname(candidate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const parent = path.dirname(candidate);
|
||||||
|
if (path.basename(parent) === "bin") {
|
||||||
|
return path.dirname(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("could not find python-build-standalone bin/python3 in extracted archive");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInside(parent, child) {
|
||||||
|
const relative = path.relative(parent, child);
|
||||||
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rewriteInternalSymlinks(root, sourceRoot) {
|
||||||
|
const entries = await readdir(root, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(root, entry.name);
|
||||||
|
const info = await lstat(fullPath);
|
||||||
|
if (info.isSymbolicLink()) {
|
||||||
|
const target = await readlink(fullPath);
|
||||||
|
if (!path.isAbsolute(target) || !isInside(sourceRoot, target)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const targetInBundle = path.join(engineDest, path.relative(sourceRoot, target));
|
||||||
|
const relativeTarget = path.relative(path.dirname(fullPath), targetInBundle);
|
||||||
|
await unlink(fullPath);
|
||||||
|
await symlink(relativeTarget, fullPath);
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
await rewriteInternalSymlinks(fullPath, sourceRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertNoExternalSymlinks(root) {
|
||||||
|
const entries = await readdir(root, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(root, entry.name);
|
||||||
|
const info = await lstat(fullPath);
|
||||||
|
if (info.isSymbolicLink()) {
|
||||||
|
const target = await readlink(fullPath);
|
||||||
|
if (path.isAbsolute(target) && !isInside(engineDest, target)) {
|
||||||
|
throw new Error(`external symlink left in engine bundle: ${fullPath} -> ${target}`);
|
||||||
|
}
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
await assertNoExternalSymlinks(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tarSupportsZstd() {
|
||||||
|
const result = spawnSync("tar", ["--help"], { encoding: "utf8" });
|
||||||
|
return `${result.stdout}\n${result.stderr}`.includes("zstd");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractArchive(archive, destination) {
|
||||||
|
await mkdir(destination, { recursive: true });
|
||||||
|
if (archive.endsWith(".tar.zst") && !(await tarSupportsZstd())) {
|
||||||
|
throw new Error("tar.zst archives require a tar build with zstd support");
|
||||||
|
}
|
||||||
|
run("tar", ["-xf", archive, "-C", destination]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveArchive() {
|
||||||
|
const localArchive = process.env.PYTHON_STANDALONE_TARBALL;
|
||||||
|
if (localArchive) {
|
||||||
|
return { archive: path.resolve(localArchive), cleanupArchive: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = process.env.PYTHON_STANDALONE_URL ?? await defaultStandaloneUrl();
|
||||||
|
const suffix = url.endsWith(".tar.gz") ? ".tar.gz" : path.extname(url);
|
||||||
|
const downloadPath = path.join(tmpdir(), `nanobot-python-${Date.now()}${suffix}`);
|
||||||
|
console.log(`Downloading Python runtime from ${url}`);
|
||||||
|
await download(url, downloadPath);
|
||||||
|
return { archive: downloadPath, cleanupArchive: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installNanobot(pythonPath) {
|
||||||
|
run(pythonPath, ["-m", "ensurepip", "--upgrade"]);
|
||||||
|
run(pythonPath, ["-m", "pip", "install", "--upgrade", "pip"]);
|
||||||
|
|
||||||
|
const installArgs = ["-m", "pip", "install", "--upgrade"];
|
||||||
|
const wheelhouse = process.env.NANOBOT_WHEELHOUSE;
|
||||||
|
if (wheelhouse) {
|
||||||
|
installArgs.push("--no-index", "--find-links", path.resolve(wheelhouse));
|
||||||
|
}
|
||||||
|
installArgs.push(`${repoRoot}[api]`);
|
||||||
|
run(pythonPath, installArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeManifest(pythonPath) {
|
||||||
|
const version = spawnSync(pythonPath, ["--version"], { encoding: "utf8" });
|
||||||
|
const pyproject = await readFile(path.join(repoRoot, "pyproject.toml"), "utf8");
|
||||||
|
const match = pyproject.match(/^version\s*=\s*"([^"]+)"/m);
|
||||||
|
await writeFile(
|
||||||
|
path.join(engineDest, "nanobot-engine.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
python: version.stdout.trim() || version.stderr.trim(),
|
||||||
|
nanobot_version: match?.[1] ?? "unknown",
|
||||||
|
prepared_at: new Date().toISOString(),
|
||||||
|
source: "python-build-standalone",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (process.argv.includes("--print-runtime-url")) {
|
||||||
|
console.log(await defaultStandaloneUrl());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { archive, cleanupArchive } = await resolveArchive();
|
||||||
|
const extractDir = path.join(tmpdir(), `nanobot-engine-${Date.now()}`);
|
||||||
|
try {
|
||||||
|
await rm(extractDir, { recursive: true, force: true });
|
||||||
|
await extractArchive(archive, extractDir);
|
||||||
|
|
||||||
|
const standaloneRoot = await findStandaloneRoot(extractDir);
|
||||||
|
await rm(engineDest, { recursive: true, force: true });
|
||||||
|
await mkdir(path.dirname(engineDest), { recursive: true });
|
||||||
|
await cp(standaloneRoot, engineDest, { recursive: true });
|
||||||
|
await rewriteInternalSymlinks(engineDest, standaloneRoot);
|
||||||
|
await assertNoExternalSymlinks(engineDest);
|
||||||
|
|
||||||
|
const pythonPath = path.join(engineDest, "bin", "python3");
|
||||||
|
await installNanobot(pythonPath);
|
||||||
|
await writeManifest(pythonPath);
|
||||||
|
await writeFile(path.join(engineDest, ".gitkeep"), "", "utf8");
|
||||||
|
console.log(`Prepared nanobot desktop engine at ${engineDest}`);
|
||||||
|
} finally {
|
||||||
|
await rm(extractDir, { recursive: true, force: true });
|
||||||
|
if (cleanupArchive) {
|
||||||
|
await rm(archive, { force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,701 @@
|
|||||||
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
import { createWriteStream, existsSync } from "node:fs";
|
||||||
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import http from "node:http";
|
||||||
|
import path from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
import {
|
||||||
|
app,
|
||||||
|
BrowserWindow,
|
||||||
|
dialog,
|
||||||
|
ipcMain,
|
||||||
|
net as electronNet,
|
||||||
|
protocol,
|
||||||
|
session,
|
||||||
|
shell,
|
||||||
|
} from "electron";
|
||||||
|
import type { IpcMainInvokeEvent, WebContents } from "electron";
|
||||||
|
|
||||||
|
import { UnixWebSocketClient } from "./unixWebSocket.js";
|
||||||
|
|
||||||
|
type EngineStatus = "starting" | "ready" | "restarting" | "stopped" | "crashed";
|
||||||
|
|
||||||
|
type HostRuntime = {
|
||||||
|
configPath: string;
|
||||||
|
gateway: ChildProcess;
|
||||||
|
logsDir: string;
|
||||||
|
python: string;
|
||||||
|
secret: string;
|
||||||
|
socketPath: string;
|
||||||
|
status: EngineStatus;
|
||||||
|
workspacePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let runtime: HostRuntime | null = null;
|
||||||
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
let crashRestartAttempts = 0;
|
||||||
|
const hostSockets = new Map<string, UnixWebSocketClient>();
|
||||||
|
const APP_PROTOCOL = "nanobot-app:";
|
||||||
|
const APP_HOST = "app";
|
||||||
|
const HOST_SOCKET_PROTOCOL = "nanobot-host:";
|
||||||
|
const HOST_SOCKET_HOST = "engine";
|
||||||
|
const SAFE_EXTERNAL_PROTOCOLS = new Set(["https:", "http:", "mailto:"]);
|
||||||
|
|
||||||
|
protocol.registerSchemesAsPrivileged([
|
||||||
|
{
|
||||||
|
scheme: "nanobot-app",
|
||||||
|
privileges: {
|
||||||
|
standard: true,
|
||||||
|
secure: true,
|
||||||
|
supportFetchAPI: true,
|
||||||
|
corsEnabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function repoRoot(): string {
|
||||||
|
return process.env.NANOBOT_DESKTOP_REPO_ROOT
|
||||||
|
? path.resolve(process.env.NANOBOT_DESKTOP_REPO_ROOT)
|
||||||
|
: path.resolve(app.getAppPath(), "..");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bundledResourcePath(name: string): string {
|
||||||
|
return app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, name)
|
||||||
|
: path.join(repoRoot(), "desktop", "resources", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function webDistPath(root: string): string {
|
||||||
|
if (process.env.NANOBOT_DESKTOP_WEB_DIST) {
|
||||||
|
return path.resolve(process.env.NANOBOT_DESKTOP_WEB_DIST);
|
||||||
|
}
|
||||||
|
const bundled = path.join(process.resourcesPath, "nanobot-webui");
|
||||||
|
if (app.isPackaged && existsSync(path.join(bundled, "index.html"))) {
|
||||||
|
return bundled;
|
||||||
|
}
|
||||||
|
return path.join(root, "nanobot", "web", "dist");
|
||||||
|
}
|
||||||
|
|
||||||
|
function webDevUrl(): string | null {
|
||||||
|
const value = process.env.NANOBOT_DESKTOP_WEB_DEV_URL?.trim();
|
||||||
|
return value ? value.replace(/\/+$/, "") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTrustedAppUrl(rawUrl: string): boolean {
|
||||||
|
try {
|
||||||
|
const url = new URL(rawUrl);
|
||||||
|
return url.protocol === APP_PROTOCOL && url.host === APP_HOST;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertTrustedIpc(event: IpcMainInvokeEvent): void {
|
||||||
|
const frameUrl = event.senderFrame?.url || event.sender.getURL();
|
||||||
|
if (!isTrustedAppUrl(frameUrl)) {
|
||||||
|
throw new Error("Blocked host API call from an untrusted renderer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHostSocketUrl(rawUrl: unknown): string {
|
||||||
|
if (typeof rawUrl !== "string") {
|
||||||
|
throw new Error("Host socket URL must be a string");
|
||||||
|
}
|
||||||
|
const url = new URL(rawUrl);
|
||||||
|
if (url.protocol !== HOST_SOCKET_PROTOCOL || url.host !== HOST_SOCKET_HOST) {
|
||||||
|
throw new Error("Host socket URL is not allowed");
|
||||||
|
}
|
||||||
|
if (url.username || url.password) {
|
||||||
|
throw new Error("Host socket URL credentials are not allowed");
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openExternalIfSafe(rawUrl: string): void {
|
||||||
|
try {
|
||||||
|
const url = new URL(rawUrl);
|
||||||
|
if (SAFE_EXTERNAL_PROTOCOLS.has(url.protocol)) {
|
||||||
|
void shell.openExternal(url.toString());
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed or unsupported external URLs.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function desktopContentSecurityPolicy(devUrl: string | null): string {
|
||||||
|
const connectSrc = ["'self'", "nanobot-host:"];
|
||||||
|
if (devUrl) {
|
||||||
|
const url = new URL(devUrl);
|
||||||
|
connectSrc.push(url.origin, url.origin.replace(/^http/, "ws"));
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
"default-src 'self'",
|
||||||
|
"base-uri 'self'",
|
||||||
|
"object-src 'none'",
|
||||||
|
"frame-ancestors 'none'",
|
||||||
|
"form-action 'none'",
|
||||||
|
"script-src 'self'",
|
||||||
|
"style-src 'self' 'unsafe-inline'",
|
||||||
|
"img-src 'self' data: blob: https: nanobot-app:",
|
||||||
|
"font-src 'self' data:",
|
||||||
|
"media-src 'self' data: blob:",
|
||||||
|
"worker-src 'self' blob:",
|
||||||
|
`connect-src ${connectSrc.join(" ")}`,
|
||||||
|
].join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function withSecurityHeaders(response: Response, devUrl: string | null): Response {
|
||||||
|
const headers = new Headers(response.headers);
|
||||||
|
headers.set("Content-Security-Policy", desktopContentSecurityPolicy(devUrl));
|
||||||
|
headers.set("X-Content-Type-Options", "nosniff");
|
||||||
|
if (devUrl) {
|
||||||
|
headers.set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
|
||||||
|
headers.set("Pragma", "no-cache");
|
||||||
|
headers.set("Expires", "0");
|
||||||
|
}
|
||||||
|
return new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHostIpc(
|
||||||
|
channel: string,
|
||||||
|
handler: (event: IpcMainInvokeEvent, ...args: unknown[]) => unknown | Promise<unknown>,
|
||||||
|
): void {
|
||||||
|
ipcMain.handle(channel, async (event, ...args) => {
|
||||||
|
assertTrustedIpc(event);
|
||||||
|
return await handler(event, ...args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function userDataPath(name: string): string {
|
||||||
|
return path.join(app.getPath("userData"), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function engineSocketPath(): string {
|
||||||
|
return userDataPath("engine.sock");
|
||||||
|
}
|
||||||
|
|
||||||
|
function pythonExecutable(): string {
|
||||||
|
if (process.env.NANOBOT_DESKTOP_PYTHON) {
|
||||||
|
return path.resolve(process.env.NANOBOT_DESKTOP_PYTHON);
|
||||||
|
}
|
||||||
|
const bundled = path.join(bundledResourcePath("nanobot-engine"), "bin", "python3");
|
||||||
|
if (existsSync(bundled)) return bundled;
|
||||||
|
return "python3";
|
||||||
|
}
|
||||||
|
|
||||||
|
function engineCwd(root: string): string {
|
||||||
|
return app.isPackaged ? app.getPath("userData") : root;
|
||||||
|
}
|
||||||
|
|
||||||
|
function engineEnv(root: string): NodeJS.ProcessEnv {
|
||||||
|
if (app.isPackaged) {
|
||||||
|
return { ...process.env };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...process.env,
|
||||||
|
PYTHONPATH: [root, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureAppDirs(): Promise<{
|
||||||
|
configPath: string;
|
||||||
|
logsDir: string;
|
||||||
|
workspacePath: string;
|
||||||
|
}> {
|
||||||
|
const dataDir = app.getPath("userData");
|
||||||
|
const logsDir = userDataPath("logs");
|
||||||
|
const workspacePath = userDataPath("workspace");
|
||||||
|
await Promise.all([
|
||||||
|
mkdir(dataDir, { recursive: true }),
|
||||||
|
mkdir(logsDir, { recursive: true }),
|
||||||
|
mkdir(workspacePath, { recursive: true }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
configPath: userDataPath("config.json"),
|
||||||
|
logsDir,
|
||||||
|
workspacePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendGatewayLogs(gateway: ChildProcess, logsDir: string): void {
|
||||||
|
const logPath = path.join(logsDir, "engine.log");
|
||||||
|
const stream = createWriteStream(logPath, { flags: "a" });
|
||||||
|
gateway.stdout?.on("data", (chunk) => {
|
||||||
|
stream.write(chunk);
|
||||||
|
process.stdout.write(`[nanobot] ${chunk}`);
|
||||||
|
});
|
||||||
|
gateway.stderr?.on("data", (chunk) => {
|
||||||
|
stream.write(chunk);
|
||||||
|
process.stderr.write(`[nanobot] ${chunk}`);
|
||||||
|
});
|
||||||
|
gateway.once("exit", (code, signal) => {
|
||||||
|
stream.write(`\n[nanobot] engine exited code=${code ?? ""} signal=${signal ?? ""}\n`);
|
||||||
|
stream.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyRuntimeStatus(status: EngineStatus): void {
|
||||||
|
if (runtime) runtime.status = status;
|
||||||
|
sendToRenderer(mainWindow?.webContents, "nanobot:runtime-status", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToRenderer(
|
||||||
|
sender: WebContents | null | undefined,
|
||||||
|
channel: string,
|
||||||
|
payload: unknown,
|
||||||
|
): void {
|
||||||
|
if (!sender || sender.isDestroyed()) return;
|
||||||
|
sender.send(channel, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeHostSockets(): void {
|
||||||
|
for (const [id, socket] of hostSockets) {
|
||||||
|
socket.close();
|
||||||
|
hostSockets.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGateway(
|
||||||
|
current: HostRuntime,
|
||||||
|
requestPath: string,
|
||||||
|
init: {
|
||||||
|
body?: ArrayBuffer;
|
||||||
|
headers?: Headers | Record<string, string>;
|
||||||
|
method: string;
|
||||||
|
},
|
||||||
|
): Promise<Response> {
|
||||||
|
const body = init.body ? Buffer.from(init.body) : undefined;
|
||||||
|
const headers: http.OutgoingHttpHeaders = {};
|
||||||
|
if (init.headers instanceof Headers) {
|
||||||
|
init.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
for (const [key, value] of Object.entries(init.headers ?? {})) {
|
||||||
|
headers[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body) headers["content-length"] = String(body.length);
|
||||||
|
|
||||||
|
return await new Promise<Response>((resolve, reject) => {
|
||||||
|
const req = http.request(
|
||||||
|
{
|
||||||
|
socketPath: current.socketPath,
|
||||||
|
path: requestPath,
|
||||||
|
method: init.method,
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
res.on("end", () => {
|
||||||
|
const responseHeaders = new Headers();
|
||||||
|
for (const [key, value] of Object.entries(res.headers)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) responseHeaders.append(key, item);
|
||||||
|
} else if (value !== undefined) {
|
||||||
|
responseHeaders.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(
|
||||||
|
new Response(Buffer.concat(chunks), {
|
||||||
|
status: res.statusCode ?? 500,
|
||||||
|
statusText: res.statusMessage,
|
||||||
|
headers: responseHeaders,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
req.on("error", reject);
|
||||||
|
if (body) req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startGateway(): Promise<HostRuntime> {
|
||||||
|
const root = repoRoot();
|
||||||
|
const dirs = await ensureAppDirs();
|
||||||
|
const socketPath = engineSocketPath();
|
||||||
|
const secret = randomBytes(32).toString("base64url");
|
||||||
|
const python = pythonExecutable();
|
||||||
|
const args = [
|
||||||
|
"-m",
|
||||||
|
"nanobot",
|
||||||
|
"desktop-gateway",
|
||||||
|
"--config",
|
||||||
|
dirs.configPath,
|
||||||
|
"--workspace",
|
||||||
|
dirs.workspacePath,
|
||||||
|
"--webui-socket",
|
||||||
|
socketPath,
|
||||||
|
"--token-issue-secret",
|
||||||
|
secret,
|
||||||
|
];
|
||||||
|
const gateway = spawn(python, args, {
|
||||||
|
cwd: engineCwd(root),
|
||||||
|
env: engineEnv(root),
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
appendGatewayLogs(gateway, dirs.logsDir);
|
||||||
|
gateway.once("exit", () => scheduleCrashRestart(gateway));
|
||||||
|
return {
|
||||||
|
configPath: dirs.configPath,
|
||||||
|
gateway,
|
||||||
|
logsDir: dirs.logsDir,
|
||||||
|
python,
|
||||||
|
secret,
|
||||||
|
socketPath,
|
||||||
|
status: "starting",
|
||||||
|
workspacePath: dirs.workspacePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootstrapFromGateway(current: HostRuntime): Promise<Record<string, unknown>> {
|
||||||
|
const response = await fetchGateway(current, "/webui/bootstrap", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-Nanobot-Auth": current.secret,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`engine bootstrap failed: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
return await response.json() as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForGateway(current: HostRuntime): Promise<void> {
|
||||||
|
let lastError: unknown;
|
||||||
|
for (let attempt = 0; attempt < 160; attempt += 1) {
|
||||||
|
if (current.gateway.exitCode !== null) {
|
||||||
|
throw new Error(`engine gateway exited with code ${current.gateway.exitCode}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await bootstrapFromGateway(current);
|
||||||
|
crashRestartAttempts = 0;
|
||||||
|
notifyRuntimeStatus("ready");
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError instanceof Error ? lastError : new Error("engine gateway did not become ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopGateway(current: HostRuntime | null): Promise<void> {
|
||||||
|
if (!current || current.gateway.exitCode !== null) return;
|
||||||
|
current.status = "stopped";
|
||||||
|
closeHostSockets();
|
||||||
|
current.gateway.kill("SIGTERM");
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (current.gateway.exitCode === null) current.gateway.kill("SIGKILL");
|
||||||
|
resolve();
|
||||||
|
}, 2500);
|
||||||
|
current.gateway.once("exit", () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startRuntime(): Promise<void> {
|
||||||
|
notifyRuntimeStatus("starting");
|
||||||
|
runtime = await startGateway();
|
||||||
|
await waitForGateway(runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restartRuntime(): Promise<void> {
|
||||||
|
const previous = runtime;
|
||||||
|
notifyRuntimeStatus("restarting");
|
||||||
|
await stopGateway(previous);
|
||||||
|
await startRuntime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleCrashRestart(gateway: ChildProcess): void {
|
||||||
|
if (runtime?.gateway !== gateway || runtime.status === "restarting" || runtime.status === "stopped") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifyRuntimeStatus("crashed");
|
||||||
|
if (crashRestartAttempts >= 3) return;
|
||||||
|
crashRestartAttempts += 1;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (runtime?.gateway !== gateway || runtime.status === "stopped") return;
|
||||||
|
void startRuntime().catch((error) => {
|
||||||
|
console.error("failed to restart nanobot engine", error);
|
||||||
|
notifyRuntimeStatus("crashed");
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proxyToGateway(request: Request): Promise<Response> {
|
||||||
|
if (!runtime) {
|
||||||
|
return new Response("Engine unavailable", { status: 503 });
|
||||||
|
}
|
||||||
|
const requestUrl = new URL(request.url);
|
||||||
|
const headers = new Headers(request.headers);
|
||||||
|
headers.delete("host");
|
||||||
|
if (requestUrl.pathname === "/webui/bootstrap") {
|
||||||
|
headers.set("X-Nanobot-Auth", runtime.secret);
|
||||||
|
}
|
||||||
|
const init: {
|
||||||
|
body?: ArrayBuffer;
|
||||||
|
headers: Headers;
|
||||||
|
method: string;
|
||||||
|
} = {
|
||||||
|
method: request.method,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||||
|
init.body = await request.arrayBuffer();
|
||||||
|
}
|
||||||
|
const response = await fetchGateway(
|
||||||
|
runtime,
|
||||||
|
`${requestUrl.pathname}${requestUrl.search}`,
|
||||||
|
init,
|
||||||
|
);
|
||||||
|
if (requestUrl.pathname !== "/webui/bootstrap" || !response.ok) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
const body = await response.json() as Record<string, unknown>;
|
||||||
|
const wsPath = typeof body.ws_path === "string" ? body.ws_path : "/";
|
||||||
|
const normalizedWsPath = wsPath.startsWith("/") ? wsPath : `/${wsPath}`;
|
||||||
|
return Response.json({
|
||||||
|
...body,
|
||||||
|
ws_url: `nanobot-host://engine${normalizedWsPath}`,
|
||||||
|
runtime_surface: "native",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveStaticAsset(webDist: string, requestUrl: string): string | null {
|
||||||
|
const url = new URL(requestUrl);
|
||||||
|
const rawPath = decodeURIComponent(url.pathname);
|
||||||
|
const relativePath = rawPath === "/" ? "index.html" : rawPath.replace(/^\/+/, "");
|
||||||
|
const resolved = path.resolve(webDist, relativePath);
|
||||||
|
if (resolved !== webDist && !resolved.startsWith(`${webDist}${path.sep}`)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (existsSync(resolved)) return resolved;
|
||||||
|
if (!path.extname(relativePath)) return path.join(webDist, "index.html");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerAppProtocol(webDist: string, devUrl: string | null): void {
|
||||||
|
protocol.handle("nanobot-app", async (request) => {
|
||||||
|
if (!isTrustedAppUrl(request.url)) {
|
||||||
|
return new Response("Forbidden", { status: 403 });
|
||||||
|
}
|
||||||
|
const requestUrl = new URL(request.url);
|
||||||
|
if (
|
||||||
|
requestUrl.pathname === "/webui/bootstrap"
|
||||||
|
|| requestUrl.pathname.startsWith("/api/")
|
||||||
|
) {
|
||||||
|
return proxyToGateway(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devUrl) {
|
||||||
|
const upstream = new URL(
|
||||||
|
`${requestUrl.pathname}${requestUrl.search}`,
|
||||||
|
devUrl,
|
||||||
|
);
|
||||||
|
const response = await electronNet.fetch(upstream.toString());
|
||||||
|
return withSecurityHeaders(response, devUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetPath = resolveStaticAsset(webDist, request.url);
|
||||||
|
if (!assetPath) {
|
||||||
|
return new Response("Not Found", { status: 404 });
|
||||||
|
}
|
||||||
|
const response = await electronNet.fetch(pathToFileURL(assetPath).toString());
|
||||||
|
return withSecurityHeaders(response, devUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindow(): BrowserWindow {
|
||||||
|
const preload = path.join(app.getAppPath(), "build", "preload.cjs");
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 1180,
|
||||||
|
height: 820,
|
||||||
|
minWidth: 920,
|
||||||
|
minHeight: 640,
|
||||||
|
title: "nanobot",
|
||||||
|
titleBarStyle: "hiddenInset",
|
||||||
|
trafficLightPosition: { x: 14, y: 16 },
|
||||||
|
backgroundColor: process.platform === "darwin" ? "#00000000" : "#ffffff",
|
||||||
|
transparent: process.platform === "darwin",
|
||||||
|
...(process.platform === "darwin"
|
||||||
|
? {
|
||||||
|
vibrancy: "sidebar" as const,
|
||||||
|
visualEffectState: "active" as const,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
preload,
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
sandbox: true,
|
||||||
|
webSecurity: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
win.once("ready-to-show", () => win.show());
|
||||||
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
openExternalIfSafe(url);
|
||||||
|
return { action: "deny" };
|
||||||
|
});
|
||||||
|
win.webContents.on("will-navigate", (event, url) => {
|
||||||
|
if (!isTrustedAppUrl(url)) {
|
||||||
|
event.preventDefault();
|
||||||
|
openExternalIfSafe(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
win.webContents.on("preload-error", (_event, preloadPath, error) => {
|
||||||
|
console.error(`Preload failed: ${preloadPath}`, error);
|
||||||
|
});
|
||||||
|
win.on("closed", () => {
|
||||||
|
if (mainWindow === win) mainWindow = null;
|
||||||
|
closeHostSockets();
|
||||||
|
});
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runtimeInfo() {
|
||||||
|
return {
|
||||||
|
surface: "native" as const,
|
||||||
|
app_version: app.getVersion(),
|
||||||
|
engine_status: runtime?.status ?? "stopped",
|
||||||
|
data_dir: app.getPath("userData"),
|
||||||
|
logs_dir: runtime?.logsDir ?? userDataPath("logs"),
|
||||||
|
config_path: runtime?.configPath ?? userDataPath("config.json"),
|
||||||
|
workspace_path: runtime?.workspacePath ?? userDataPath("workspace"),
|
||||||
|
python: runtime?.python ?? pythonExecutable(),
|
||||||
|
engine_transport: "unix_socket",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerIpcHandlers(): void {
|
||||||
|
handleHostIpc("nanobot:get-runtime-info", () => runtimeInfo());
|
||||||
|
handleHostIpc("nanobot:restart-engine", async () => {
|
||||||
|
await restartRuntime();
|
||||||
|
});
|
||||||
|
handleHostIpc("nanobot:pick-folder", async () => {
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
properties: ["openDirectory", "createDirectory"],
|
||||||
|
});
|
||||||
|
if (result.canceled || !result.filePaths[0]) return null;
|
||||||
|
return path.resolve(result.filePaths[0]);
|
||||||
|
});
|
||||||
|
handleHostIpc("nanobot:open-logs", async () => {
|
||||||
|
const logsDir = runtime?.logsDir ?? userDataPath("logs");
|
||||||
|
await mkdir(logsDir, { recursive: true });
|
||||||
|
const error = await shell.openPath(logsDir);
|
||||||
|
if (error) throw new Error(error);
|
||||||
|
});
|
||||||
|
handleHostIpc("nanobot:export-diagnostics", async () => {
|
||||||
|
const diagnosticsPath = path.join(
|
||||||
|
app.getPath("temp"),
|
||||||
|
`nanobot-diagnostics-${Date.now()}.json`,
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
diagnosticsPath,
|
||||||
|
JSON.stringify(runtimeInfo(), null, 2),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
shell.showItemInFolder(diagnosticsPath);
|
||||||
|
return diagnosticsPath;
|
||||||
|
});
|
||||||
|
handleHostIpc("nanobot:check-for-updates", () => ({
|
||||||
|
supported: false,
|
||||||
|
message: "Auto update is not configured for this build.",
|
||||||
|
}));
|
||||||
|
handleHostIpc("nanobot:ws-connect", (event, rawUrl) => {
|
||||||
|
if (!runtime) throw new Error("nanobot engine is not running");
|
||||||
|
const url = parseHostSocketUrl(rawUrl);
|
||||||
|
const id = randomBytes(12).toString("hex");
|
||||||
|
const client = new UnixWebSocketClient(runtime.socketPath, url, {
|
||||||
|
onOpen: () => sendToRenderer(event.sender, "nanobot:ws-event", { id, type: "open" }),
|
||||||
|
onMessage: (data) => sendToRenderer(event.sender, "nanobot:ws-event", { id, type: "message", data }),
|
||||||
|
onError: (message) => sendToRenderer(event.sender, "nanobot:ws-event", { id, type: "error", message }),
|
||||||
|
onClose: (code, reason) => {
|
||||||
|
hostSockets.delete(id);
|
||||||
|
sendToRenderer(event.sender, "nanobot:ws-event", { id, type: "close", code, reason });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
hostSockets.set(id, client);
|
||||||
|
client.connect();
|
||||||
|
event.sender.once("destroyed", () => {
|
||||||
|
client.close();
|
||||||
|
hostSockets.delete(id);
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
handleHostIpc("nanobot:ws-send", (_event, id, data) => {
|
||||||
|
if (typeof id !== "string" || typeof data !== "string") {
|
||||||
|
throw new Error("Invalid host socket send arguments");
|
||||||
|
}
|
||||||
|
const socket = hostSockets.get(id);
|
||||||
|
if (!socket) throw new Error("Host socket not found");
|
||||||
|
socket.send(data);
|
||||||
|
});
|
||||||
|
handleHostIpc("nanobot:ws-close", (_event, id) => {
|
||||||
|
if (typeof id !== "string") {
|
||||||
|
throw new Error("Invalid host socket close argument");
|
||||||
|
}
|
||||||
|
hostSockets.get(id)?.close();
|
||||||
|
hostSockets.delete(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAppWindow(win: BrowserWindow): Promise<void> {
|
||||||
|
if (!runtime || runtime.status === "stopped" || runtime.status === "crashed") {
|
||||||
|
await startRuntime();
|
||||||
|
}
|
||||||
|
await win.loadURL("nanobot-app://app/index.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
const root = repoRoot();
|
||||||
|
const webDist = webDistPath(root);
|
||||||
|
const devUrl = webDevUrl();
|
||||||
|
if (!devUrl && !existsSync(path.join(webDist, "index.html"))) {
|
||||||
|
throw new Error(`WebUI dist not found at ${webDist}. Run npm run build:webui first.`);
|
||||||
|
}
|
||||||
|
if (devUrl) {
|
||||||
|
await session.defaultSession.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerIpcHandlers();
|
||||||
|
registerAppProtocol(webDist, devUrl);
|
||||||
|
|
||||||
|
mainWindow = createWindow();
|
||||||
|
await loadAppWindow(mainWindow);
|
||||||
|
|
||||||
|
app.on("activate", () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
mainWindow = createWindow();
|
||||||
|
void loadAppWindow(mainWindow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
if (process.platform !== "darwin") app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
if (runtime) runtime.status = "stopped";
|
||||||
|
if (runtime?.gateway.exitCode === null) {
|
||||||
|
runtime.gateway.kill("SIGTERM");
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
|
|
||||||
|
type HostRuntimeInfo = {
|
||||||
|
surface: "native";
|
||||||
|
app_version: string;
|
||||||
|
engine_status: "starting" | "ready" | "restarting" | "stopped" | "crashed";
|
||||||
|
data_dir: string;
|
||||||
|
logs_dir: string;
|
||||||
|
config_path: string;
|
||||||
|
workspace_path: string;
|
||||||
|
python: string;
|
||||||
|
engine_transport?: "unix_socket";
|
||||||
|
};
|
||||||
|
|
||||||
|
type HostSocketEvent =
|
||||||
|
| { id: string; type: "open" }
|
||||||
|
| { data: string; id: string; type: "message" }
|
||||||
|
| { id: string; message: string; type: "error" }
|
||||||
|
| { code?: number; id: string; reason?: string; type: "close" };
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld("nanobotHost", {
|
||||||
|
getRuntimeInfo: (): Promise<HostRuntimeInfo> =>
|
||||||
|
ipcRenderer.invoke("nanobot:get-runtime-info"),
|
||||||
|
restartEngine: (): Promise<void> => ipcRenderer.invoke("nanobot:restart-engine"),
|
||||||
|
pickFolder: (): Promise<string | null> => ipcRenderer.invoke("nanobot:pick-folder"),
|
||||||
|
openLogs: (): Promise<void> => ipcRenderer.invoke("nanobot:open-logs"),
|
||||||
|
exportDiagnostics: (): Promise<string> =>
|
||||||
|
ipcRenderer.invoke("nanobot:export-diagnostics"),
|
||||||
|
checkForUpdates: (): Promise<{ supported: boolean; message?: string }> =>
|
||||||
|
ipcRenderer.invoke("nanobot:check-for-updates"),
|
||||||
|
openSocket: (url: string): Promise<string> =>
|
||||||
|
ipcRenderer.invoke("nanobot:ws-connect", url),
|
||||||
|
sendSocket: (id: string, data: string): Promise<void> =>
|
||||||
|
ipcRenderer.invoke("nanobot:ws-send", id, data),
|
||||||
|
closeSocket: (id: string): Promise<void> =>
|
||||||
|
ipcRenderer.invoke("nanobot:ws-close", id),
|
||||||
|
onSocketEvent: (
|
||||||
|
listener: (event: HostSocketEvent) => void,
|
||||||
|
): (() => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, payload: HostSocketEvent) => {
|
||||||
|
listener(payload);
|
||||||
|
};
|
||||||
|
ipcRenderer.on("nanobot:ws-event", handler);
|
||||||
|
return () => ipcRenderer.removeListener("nanobot:ws-event", handler);
|
||||||
|
},
|
||||||
|
onRuntimeStatus: (
|
||||||
|
listener: (status: HostRuntimeInfo["engine_status"]) => void,
|
||||||
|
): (() => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, status: HostRuntimeInfo["engine_status"]) => {
|
||||||
|
listener(status);
|
||||||
|
};
|
||||||
|
ipcRenderer.on("nanobot:runtime-status", handler);
|
||||||
|
return () => ipcRenderer.removeListener("nanobot:runtime-status", handler);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import net from "node:net";
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
|
||||||
|
type UnixWebSocketHandlers = {
|
||||||
|
onClose: (code?: number, reason?: string) => void;
|
||||||
|
onError: (message: string) => void;
|
||||||
|
onMessage: (data: string) => void;
|
||||||
|
onOpen: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OPCODE_CONTINUATION = 0x0;
|
||||||
|
const OPCODE_TEXT = 0x1;
|
||||||
|
const OPCODE_CLOSE = 0x8;
|
||||||
|
const OPCODE_PING = 0x9;
|
||||||
|
const OPCODE_PONG = 0xa;
|
||||||
|
|
||||||
|
export class UnixWebSocketClient {
|
||||||
|
private frameBuffer = Buffer.alloc(0);
|
||||||
|
private handshakeBuffer = Buffer.alloc(0);
|
||||||
|
private open = false;
|
||||||
|
private socket: net.Socket | null = null;
|
||||||
|
private fragmentedText: Buffer[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly socketPath: string,
|
||||||
|
private readonly url: string,
|
||||||
|
private readonly handlers: UnixWebSocketHandlers,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
connect(): void {
|
||||||
|
const socket = net.createConnection(this.socketPath);
|
||||||
|
this.socket = socket;
|
||||||
|
socket.once("connect", () => this.writeHandshake());
|
||||||
|
socket.on("data", (chunk) => this.handleData(chunk));
|
||||||
|
socket.on("error", (error) => this.fail(error.message));
|
||||||
|
socket.on("close", () => {
|
||||||
|
if (this.open) {
|
||||||
|
this.open = false;
|
||||||
|
this.handlers.onClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: string): void {
|
||||||
|
if (!this.open || !this.socket || this.socket.destroyed) {
|
||||||
|
throw new Error("host socket is not open");
|
||||||
|
}
|
||||||
|
this.socket.write(encodeFrame(OPCODE_TEXT, Buffer.from(data, "utf8")));
|
||||||
|
}
|
||||||
|
|
||||||
|
close(code = 1000, reason = ""): void {
|
||||||
|
const socket = this.socket;
|
||||||
|
if (!socket || socket.destroyed) return;
|
||||||
|
const reasonBuffer = Buffer.from(reason, "utf8");
|
||||||
|
const payload = Buffer.alloc(2 + reasonBuffer.length);
|
||||||
|
payload.writeUInt16BE(code, 0);
|
||||||
|
reasonBuffer.copy(payload, 2);
|
||||||
|
socket.write(encodeFrame(OPCODE_CLOSE, payload));
|
||||||
|
socket.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeHandshake(): void {
|
||||||
|
const socket = this.socket;
|
||||||
|
if (!socket) return;
|
||||||
|
const requestUrl = new URL(this.url);
|
||||||
|
const path = `${requestUrl.pathname || "/"}${requestUrl.search}`;
|
||||||
|
const key = randomBytes(16).toString("base64");
|
||||||
|
socket.write(
|
||||||
|
[
|
||||||
|
`GET ${path} HTTP/1.1`,
|
||||||
|
"Host: nanobot.host",
|
||||||
|
"Upgrade: websocket",
|
||||||
|
"Connection: Upgrade",
|
||||||
|
`Sec-WebSocket-Key: ${key}`,
|
||||||
|
"Sec-WebSocket-Version: 13",
|
||||||
|
"\r\n",
|
||||||
|
].join("\r\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleData(chunk: Buffer): void {
|
||||||
|
if (!this.open) {
|
||||||
|
this.handshakeBuffer = Buffer.concat([this.handshakeBuffer, chunk]);
|
||||||
|
const headerEnd = this.handshakeBuffer.indexOf("\r\n\r\n");
|
||||||
|
if (headerEnd === -1) return;
|
||||||
|
const header = this.handshakeBuffer.subarray(0, headerEnd).toString("utf8");
|
||||||
|
const remainder = this.handshakeBuffer.subarray(headerEnd + 4);
|
||||||
|
this.handshakeBuffer = Buffer.alloc(0);
|
||||||
|
if (!header.startsWith("HTTP/1.1 101")) {
|
||||||
|
this.fail(`host socket upgrade failed: ${header.split("\r\n")[0]}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.open = true;
|
||||||
|
this.handlers.onOpen();
|
||||||
|
if (remainder.length > 0) this.handleFrames(remainder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.handleFrames(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFrames(chunk: Buffer): void {
|
||||||
|
this.frameBuffer = Buffer.concat([this.frameBuffer, chunk]);
|
||||||
|
while (this.frameBuffer.length >= 2) {
|
||||||
|
const first = this.frameBuffer[0];
|
||||||
|
const second = this.frameBuffer[1];
|
||||||
|
const fin = (first & 0x80) !== 0;
|
||||||
|
const opcode = first & 0x0f;
|
||||||
|
const masked = (second & 0x80) !== 0;
|
||||||
|
let length = second & 0x7f;
|
||||||
|
let offset = 2;
|
||||||
|
|
||||||
|
if (length === 126) {
|
||||||
|
if (this.frameBuffer.length < offset + 2) return;
|
||||||
|
length = this.frameBuffer.readUInt16BE(offset);
|
||||||
|
offset += 2;
|
||||||
|
} else if (length === 127) {
|
||||||
|
if (this.frameBuffer.length < offset + 8) return;
|
||||||
|
const bigLength = this.frameBuffer.readBigUInt64BE(offset);
|
||||||
|
if (bigLength > BigInt(Number.MAX_SAFE_INTEGER)) {
|
||||||
|
this.fail("host socket frame is too large");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
length = Number(bigLength);
|
||||||
|
offset += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mask: Buffer | null = null;
|
||||||
|
if (masked) {
|
||||||
|
if (this.frameBuffer.length < offset + 4) return;
|
||||||
|
mask = this.frameBuffer.subarray(offset, offset + 4);
|
||||||
|
offset += 4;
|
||||||
|
}
|
||||||
|
if (this.frameBuffer.length < offset + length) return;
|
||||||
|
|
||||||
|
const rawPayload = Buffer.from(this.frameBuffer.subarray(offset, offset + length));
|
||||||
|
this.frameBuffer = this.frameBuffer.subarray(offset + length);
|
||||||
|
const payload = mask ? unmask(rawPayload, mask) : rawPayload;
|
||||||
|
|
||||||
|
if (opcode === OPCODE_TEXT || opcode === OPCODE_CONTINUATION) {
|
||||||
|
this.handleTextFrame(opcode, payload, fin);
|
||||||
|
} else if (opcode === OPCODE_PING) {
|
||||||
|
this.socket?.write(encodeFrame(OPCODE_PONG, payload));
|
||||||
|
} else if (opcode === OPCODE_CLOSE) {
|
||||||
|
const code = payload.length >= 2 ? payload.readUInt16BE(0) : undefined;
|
||||||
|
const reason = payload.length > 2 ? payload.subarray(2).toString("utf8") : undefined;
|
||||||
|
this.open = false;
|
||||||
|
this.socket?.end();
|
||||||
|
this.handlers.onClose(code, reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleTextFrame(opcode: number, payload: Buffer, fin: boolean): void {
|
||||||
|
if (opcode === OPCODE_TEXT && fin) {
|
||||||
|
this.handlers.onMessage(payload.toString("utf8"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (opcode === OPCODE_TEXT) {
|
||||||
|
this.fragmentedText = [payload];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.fragmentedText.length === 0) return;
|
||||||
|
this.fragmentedText.push(payload);
|
||||||
|
if (fin) {
|
||||||
|
const data = Buffer.concat(this.fragmentedText).toString("utf8");
|
||||||
|
this.fragmentedText = [];
|
||||||
|
this.handlers.onMessage(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fail(message: string): void {
|
||||||
|
this.handlers.onError(message);
|
||||||
|
this.socket?.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeFrame(opcode: number, payload: Buffer): Buffer {
|
||||||
|
const length = payload.length;
|
||||||
|
const headerLength = length < 126 ? 2 : length <= 0xffff ? 4 : 10;
|
||||||
|
const header = Buffer.alloc(headerLength + 4);
|
||||||
|
header[0] = 0x80 | opcode;
|
||||||
|
if (length < 126) {
|
||||||
|
header[1] = 0x80 | length;
|
||||||
|
} else if (length <= 0xffff) {
|
||||||
|
header[1] = 0x80 | 126;
|
||||||
|
header.writeUInt16BE(length, 2);
|
||||||
|
} else {
|
||||||
|
header[1] = 0x80 | 127;
|
||||||
|
header.writeBigUInt64BE(BigInt(length), 2);
|
||||||
|
}
|
||||||
|
const maskOffset = headerLength;
|
||||||
|
const mask = randomBytes(4);
|
||||||
|
mask.copy(header, maskOffset);
|
||||||
|
const masked = Buffer.alloc(payload.length);
|
||||||
|
for (let i = 0; i < payload.length; i += 1) {
|
||||||
|
masked[i] = payload[i] ^ mask[i % 4];
|
||||||
|
}
|
||||||
|
return Buffer.concat([header, masked]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmask(payload: Buffer, mask: Buffer): Buffer {
|
||||||
|
const out = Buffer.alloc(payload.length);
|
||||||
|
for (let i = 0; i < payload.length; i += 1) {
|
||||||
|
out[i] = payload[i] ^ mask[i % 4];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"types": ["node", "electron"],
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmitOnError": true,
|
||||||
|
"outDir": "build",
|
||||||
|
"rootDir": "src",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user