Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a7d47cf14 | ||
|
|
2cbc333477 | ||
|
|
4a300ed5fa | ||
|
|
cd8f3b7671 | ||
|
|
0693668b08 | ||
|
|
26872c7396 | ||
|
|
c6f1c8b203 |
No files matched your search
@@ -1,12 +1,13 @@
|
||||
name: Build
|
||||
|
||||
# Every push and pull request: check the code, then build installers for each
|
||||
# Every push to main and every pull request: check the code, then build installers for each
|
||||
# platform and attach them to the run as artifacts. The release workflow calls
|
||||
# this same file with a version to stamp into the app.
|
||||
on:
|
||||
# Pushes to main, plus pull requests. Branch pushes aren't built separately, so a
|
||||
# pull request's commits run once (as pull_request), not twice.
|
||||
push:
|
||||
branches: ["**"]
|
||||
tags-ignore: ["**"]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -18,6 +19,10 @@ on:
|
||||
description: Commit to build.
|
||||
type: string
|
||||
default: ""
|
||||
sign_mac:
|
||||
description: Sign and notarize the macOS build (needs the release-signing environment).
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.workflow }}-${{ inputs.ref || github.ref }}
|
||||
@@ -56,9 +61,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: macOS
|
||||
os: macos-latest
|
||||
args: --mac --arm64 --x64
|
||||
- name: Windows
|
||||
os: windows-latest
|
||||
args: --win --x64
|
||||
@@ -98,7 +100,7 @@ jobs:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
run: |
|
||||
pnpm build
|
||||
pnpm exec electron-builder ${{ matrix.args }} --publish never
|
||||
pnpm exec electron-builder --config electron-builder.config.cjs ${{ matrix.args }} --publish never
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frameloader-${{ matrix.os }}
|
||||
@@ -109,3 +111,75 @@ jobs:
|
||||
apps/desktop/release/*.AppImage
|
||||
apps/desktop/release/*.deb
|
||||
if-no-files-found: error
|
||||
|
||||
# macOS is its own job so release builds can run inside the protected
|
||||
# release-signing environment (approval required, v* tags only), which holds
|
||||
# the Developer ID certificate and the notarization key. Pushes and pull
|
||||
# requests never touch it and produce ad-hoc signed builds.
|
||||
package-mac:
|
||||
name: Package (macOS${{ inputs.sign_mac && ', signed' || '' }})
|
||||
needs: check
|
||||
runs-on: macos-latest
|
||||
environment: ${{ inputs.sign_mac && 'release-signing' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Stamp version
|
||||
if: inputs.version != ''
|
||||
run: node scripts/version.mjs set "${{ inputs.version }}"
|
||||
# Signing secrets live only in this step's environment, never in GITHUB_ENV, so no
|
||||
# later step or action (artifact upload, post steps) can read them. They're exported
|
||||
# only for signed builds: electron-builder treats an empty CSC_LINK as a file path.
|
||||
- name: Build installers
|
||||
working-directory: apps/desktop
|
||||
env:
|
||||
SIGN_MAC: ${{ inputs.sign_mac }}
|
||||
SIGN_CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
SIGN_CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
SIGN_APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
SIGN_APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
SIGN_APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
run: |
|
||||
if [ "$SIGN_MAC" = "true" ]; then
|
||||
for name in SIGN_CSC_LINK SIGN_CSC_KEY_PASSWORD SIGN_APPLE_API_KEY SIGN_APPLE_API_KEY_ID SIGN_APPLE_API_ISSUER; do
|
||||
if [ -z "${!name}" ]; then
|
||||
echo "::error::release-signing secret ${name#SIGN_} is missing. See README → Signing."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
key_file="$RUNNER_TEMP/notary.p8"
|
||||
printf '%s' "$SIGN_APPLE_API_KEY" > "$key_file"
|
||||
trap 'rm -f "$key_file"' EXIT
|
||||
export CSC_LINK="$SIGN_CSC_LINK" CSC_KEY_PASSWORD="$SIGN_CSC_KEY_PASSWORD"
|
||||
export APPLE_API_KEY="$key_file" APPLE_API_KEY_ID="$SIGN_APPLE_API_KEY_ID" APPLE_API_ISSUER="$SIGN_APPLE_API_ISSUER"
|
||||
else
|
||||
# Unsigned (ad-hoc) build: don't let electron-builder look for a certificate.
|
||||
# Not set for signed builds: with it, electron-builder imports CSC_LINK but then
|
||||
# won't look the identity up, and silently skips signing.
|
||||
export CSC_IDENTITY_AUTO_DISCOVERY=false
|
||||
fi
|
||||
unset SIGN_CSC_LINK SIGN_CSC_KEY_PASSWORD SIGN_APPLE_API_KEY SIGN_APPLE_API_KEY_ID SIGN_APPLE_API_ISSUER
|
||||
pnpm build
|
||||
pnpm exec electron-builder --config electron-builder.config.cjs --mac --arm64 --x64 --publish never
|
||||
- name: Check signature
|
||||
if: inputs.sign_mac
|
||||
working-directory: apps/desktop
|
||||
run: |
|
||||
app="release/mac-arm64/Frameloader.app"
|
||||
codesign --verify --deep --strict --verbose=2 "$app"
|
||||
spctl --assess --type execute --verbose=2 "$app"
|
||||
xcrun stapler validate "$app"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frameloader-macos
|
||||
path: |
|
||||
apps/desktop/release/*.dmg
|
||||
apps/desktop/release/*.zip
|
||||
if-no-files-found: error
|
||||
@@ -37,9 +37,11 @@ jobs:
|
||||
name: Build
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/build.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
ref: refs/tags/${{ github.event.release.tag_name }}
|
||||
sign_mac: true
|
||||
|
||||
attach:
|
||||
name: Attach installers to the release
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<p align="center">
|
||||
<img src="apps/desktop/build/icon.png" alt="Frameloader logo" width="160" />
|
||||
</p>
|
||||
|
||||
# Frameloader
|
||||
|
||||
[www.frameloader.com](https://www.frameloader.com)
|
||||
@@ -64,7 +68,7 @@ Frameloader has no analytics, telemetry, crash reporting or accounts. It connect
|
||||
|
||||
## Builds and releases
|
||||
|
||||
Every push and pull request runs `.github/workflows/build.yml`: typecheck, tests, the website build, then installers for macOS (arm64 and x64), Windows x64, and Linux x64 and arm64. The installers are attached to the run as artifacts.
|
||||
Every push to `main` and every pull request runs `.github/workflows/build.yml`: typecheck, tests, the website build, then installers for macOS (arm64 and x64), Windows x64, and Linux x64 and arm64. The installers are attached to the run as artifacts.
|
||||
|
||||
To ship a release:
|
||||
|
||||
@@ -74,7 +78,7 @@ To ship a release:
|
||||
|
||||
Nothing is pushed to `main`, so this works with branch protection. The version in `apps/desktop/package.json` is only what local and CI builds report; releases take their version from the tag.
|
||||
|
||||
Builds are not code-signed yet. On macOS the app is ad-hoc signed, so the first launch needs right-click → Open (or `xattr -dr com.apple.quarantine /Applications/Frameloader.app`). Windows SmartScreen will warn until the installer is signed.
|
||||
**Signing.** Release builds of the Mac app are signed with a Developer ID certificate and notarized, so they open normally. The certificate and the App Store Connect notarization key live only in the `release-signing` GitHub environment as `CSC_LINK`, `CSC_KEY_PASSWORD`, `APPLE_API_KEY`, `APPLE_API_KEY_ID` and `APPLE_API_ISSUER`. That environment only accepts `v*` tags and needs approval, so the release run pauses until you approve it. Push and pull-request builds are ad-hoc signed; to open one on macOS 15 or later, use System Settings → Privacy & Security → Open Anyway, or run `xattr -dr com.apple.quarantine /Applications/Frameloader.app`. Windows builds are unsigned, so SmartScreen warns before the installer runs.
|
||||
|
||||
Installed apps notice new releases on their own: they check GitHub's releases API and show a banner with a link once a release has its installers attached. Nothing is downloaded or installed automatically, and Settings → Updates turns the check off.
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- V8's JIT needs these under the hardened runtime. -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 821 KiB |
@@ -0,0 +1,85 @@
|
||||
// electron-builder config. Pass with: electron-builder --config electron-builder.config.cjs
|
||||
//
|
||||
// macOS signing depends on the environment:
|
||||
// - Release builds (the release-signing GitHub environment) set CSC_LINK, so the app is
|
||||
// signed with the Developer ID certificate, uses the hardened runtime, and is notarized
|
||||
// with the App Store Connect key (APPLE_API_KEY / APPLE_API_KEY_ID / APPLE_API_ISSUER).
|
||||
// - Everything else (local builds, pushes, pull requests) is ad-hoc signed.
|
||||
const signed = Boolean(process.env.CSC_LINK);
|
||||
|
||||
const base = {
|
||||
"appId": "dev.traveler.frameloader",
|
||||
"productName": "Frameloader",
|
||||
"directories": {
|
||||
"output": "release",
|
||||
"buildResources": "build"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"package.json",
|
||||
"!dist/test/**",
|
||||
"!dist/**/*.map"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "vendor/devkit-utils",
|
||||
"to": "devkit-utils",
|
||||
"filter": [
|
||||
"**/*",
|
||||
"!**/__pycache__/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.utilities",
|
||||
"target": [
|
||||
"dmg",
|
||||
"zip"
|
||||
],
|
||||
"artifactName": "Frameloader-mac-${arch}.${ext}",
|
||||
"extendInfo": {
|
||||
"NSLocalNetworkUsageDescription": "Frameloader connects to your Steam Frame on your local network to install and launch apps.",
|
||||
"NSBonjourServices": [
|
||||
"_steamos-devkit._tcp",
|
||||
"_ssh._tcp"
|
||||
]
|
||||
}
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis",
|
||||
"zip"
|
||||
],
|
||||
"artifactName": "Frameloader-win-${arch}.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage",
|
||||
"deb"
|
||||
],
|
||||
"category": "Utility",
|
||||
"artifactName": "Frameloader-linux-${arch}.${ext}",
|
||||
"executableName": "frameloader"
|
||||
},
|
||||
"electronVersion": "44.5.1"
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
mac: {
|
||||
...base.mac,
|
||||
...(signed
|
||||
? {
|
||||
hardenedRuntime: true,
|
||||
entitlements: "build/entitlements.mac.plist",
|
||||
entitlementsInherit: "build/entitlements.mac.plist",
|
||||
notarize: true,
|
||||
}
|
||||
: { identity: "-", hardenedRuntime: false }),
|
||||
},
|
||||
};
|
||||
@@ -4,7 +4,10 @@
|
||||
"version": "0.0.1",
|
||||
"description": "Drop an APK, Windows app, or Linux build onto your Steam Frame and it shows up in your Steam library.",
|
||||
"license": "MIT",
|
||||
"author": "Zack <zack@traveler.dev>",
|
||||
"author": {
|
||||
"name": "Traveler Dev Ltd",
|
||||
"email": "hi@traveler.dev"
|
||||
},
|
||||
"private": true,
|
||||
"main": "dist/main.cjs",
|
||||
"type": "module",
|
||||
@@ -14,88 +17,31 @@
|
||||
"start": "pnpm build && electron .",
|
||||
"typecheck": "tsc -p tsconfig.main.json --noEmit && tsc -p tsconfig.renderer.json --noEmit",
|
||||
"test": "pnpm build && node --test dist/test/*.test.cjs",
|
||||
"dist": "pnpm build && electron-builder --publish never",
|
||||
"dist:mac": "pnpm build && electron-builder --mac --arm64 --publish never",
|
||||
"dist:win": "pnpm build && electron-builder --win --x64 --publish never",
|
||||
"dist:linux": "pnpm build && electron-builder --linux --x64 --arm64 --publish never",
|
||||
"dist": "pnpm build && electron-builder --config electron-builder.config.cjs --publish never",
|
||||
"dist:mac": "pnpm build && electron-builder --config electron-builder.config.cjs --mac --arm64 --publish never",
|
||||
"dist:win": "pnpm build && electron-builder --config electron-builder.config.cjs --win --x64 --publish never",
|
||||
"dist:linux": "pnpm build && electron-builder --config electron-builder.config.cjs --linux --x64 --arm64 --publish never",
|
||||
"screenshots": "pnpm build && electron scripts/screenshot.cjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"frameloader-tokens": "workspace:*",
|
||||
"@types/multicast-dns": "^7.2.4",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/ssh2": "^1.15.6",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"app-info-parser": "^1.1.6",
|
||||
"electron": "44.5.1",
|
||||
"electron-builder": "^26.15.3",
|
||||
"electron-builder": "^26.17.0",
|
||||
"esbuild": "^0.28.2",
|
||||
"frameloader-tokens": "workspace:*",
|
||||
"multicast-dns": "^7.2.5",
|
||||
"ssh2": "^1.17.0",
|
||||
"typescript": "^7.0.2",
|
||||
"yauzl": "^3.2.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "dev.traveler.frameloader",
|
||||
"productName": "Frameloader",
|
||||
"directories": {
|
||||
"output": "release",
|
||||
"buildResources": "build"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"package.json",
|
||||
"!dist/test/**",
|
||||
"!dist/**/*.map"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "vendor/devkit-utils",
|
||||
"to": "devkit-utils",
|
||||
"filter": [
|
||||
"**/*",
|
||||
"!**/__pycache__/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.utilities",
|
||||
"target": [
|
||||
"dmg",
|
||||
"zip"
|
||||
],
|
||||
"artifactName": "Frameloader-mac-${arch}.${ext}",
|
||||
"identity": "-",
|
||||
"hardenedRuntime": false
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis",
|
||||
"zip"
|
||||
],
|
||||
"artifactName": "Frameloader-win-${arch}.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage",
|
||||
"deb"
|
||||
],
|
||||
"category": "Utility",
|
||||
"artifactName": "Frameloader-linux-${arch}.${ext}",
|
||||
"executableName": "frameloader"
|
||||
},
|
||||
"electronVersion": "44.5.1"
|
||||
},
|
||||
"homepage": "https://www.frameloader.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/travelerdev/frameloader.git",
|
||||
"directory": "apps/desktop"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ const configs = [
|
||||
function copyStatic() {
|
||||
mkdirSync("dist", { recursive: true });
|
||||
cpSync("src/renderer/index.html", "dist/index.html");
|
||||
cpSync("src/renderer/logo.png", "dist/logo.png");
|
||||
// Shared tokens first, then the app's own styles.
|
||||
writeFileSync("dist/styles.css", readFileSync(TOKENS, "utf8") + "\n" + readFileSync("src/renderer/styles.css", "utf8"));
|
||||
}
|
||||
@@ -64,7 +65,7 @@ if (watch) {
|
||||
// Keep static files fresh too.
|
||||
const { watch: fsWatch } = await import("node:fs");
|
||||
fsWatch("src/renderer", { recursive: true }, (_e, f) => {
|
||||
if (f && (f.endsWith(".html") || f.endsWith(".css"))) copyStatic();
|
||||
if (f && (f.endsWith(".html") || f.endsWith(".css") || f.endsWith(".png"))) copyStatic();
|
||||
});
|
||||
fsWatch(TOKENS, () => copyStatic());
|
||||
console.log("watching…");
|
||||
|
||||
@@ -42,12 +42,33 @@ function open(): Mdns | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
// Addresses seen for headset hostnames, across scans, for display only. Responders won't
|
||||
// repeat a record they multicast within the last second, so a scan can see a name without
|
||||
// its address. Only names a scan asked about are stored, and connecting never uses this:
|
||||
// it always resolves live, and known headsets are pinned by SSH host key anyway.
|
||||
const seenAddresses = new Map<string, { address: string; at: number }>();
|
||||
const SEEN_TTL_MS = 10 * 60 * 1000;
|
||||
|
||||
function remember(host: string, address: string): void {
|
||||
if (isIP(address) === 4) seenAddresses.set(host, { address, at: Date.now() });
|
||||
}
|
||||
|
||||
function recalled(host: string): string | undefined {
|
||||
const hit = seenAddresses.get(host);
|
||||
return hit && Date.now() - hit.at < SEEN_TTL_MS ? hit.address : undefined;
|
||||
}
|
||||
|
||||
function records(r: { answers?: Record_[]; additionals?: Record_[] }): Record_[] {
|
||||
return [...(r.answers ?? []), ...(r.additionals ?? [])];
|
||||
}
|
||||
|
||||
/** "<label>.local" when a service instance name is also a valid hostname (SteamOS uses the hostname). */
|
||||
function hostGuess(instanceName: string): string | undefined {
|
||||
return /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i.test(instanceName) ? `${instanceName.toLowerCase()}.local` : undefined;
|
||||
}
|
||||
|
||||
/** Browse for headsets advertising the devkit service. Resolves after timeoutMs with whatever answered. */
|
||||
export function browse(timeoutMs = 2500): Promise<DiscoveredFrame[]> {
|
||||
export function browse(timeoutMs = 3000): Promise<DiscoveredFrame[]> {
|
||||
return new Promise((resolve) => {
|
||||
const m = open();
|
||||
if (!m) return resolve([]);
|
||||
@@ -60,12 +81,23 @@ export function browse(timeoutMs = 2500): Promise<DiscoveredFrame[]> {
|
||||
const instances = new Map<string, Inst>();
|
||||
const addresses = new Map<string, Set<string>>();
|
||||
const asked = new Set<string>();
|
||||
const askA = (host: string) => {
|
||||
if (addresses.has(host) || asked.has(host)) return;
|
||||
asked.add(host);
|
||||
m.query({ questions: [{ name: host, type: "A" }] });
|
||||
};
|
||||
const inst = (full: string): Inst => {
|
||||
const key = norm(full);
|
||||
let i = instances.get(key);
|
||||
if (!i) {
|
||||
i = { name: key.replace("." + DEVKIT_SERVICE, ""), txt: {} };
|
||||
instances.set(key, i);
|
||||
// SteamOS's responder answers the PTR browse with just the PTR (plus an A record),
|
||||
// not the SRV/TXT it points to, so ask for them. Also ask for <name>.local, which
|
||||
// is what SteamOS uses as the target, in case the SRV answer never comes.
|
||||
m.query({ questions: [{ name: key, type: "SRV" }, { name: key, type: "TXT" }] });
|
||||
const guess = hostGuess(i.name);
|
||||
if (guess) askA(guess);
|
||||
}
|
||||
return i;
|
||||
};
|
||||
@@ -78,28 +110,41 @@ export function browse(timeoutMs = 2500): Promise<DiscoveredFrame[]> {
|
||||
const i = inst(name);
|
||||
i.host = norm(d.target);
|
||||
i.port = d.port;
|
||||
if (!addresses.has(i.host) && !asked.has(i.host)) {
|
||||
asked.add(i.host);
|
||||
m.query({ questions: [{ name: i.host, type: "A" }] });
|
||||
}
|
||||
askA(i.host);
|
||||
} else if (rec.type === "TXT" && name.endsWith("." + DEVKIT_SERVICE)) Object.assign(inst(name).txt, parseTxt(rec.data));
|
||||
else if (rec.type === "A") {
|
||||
if (!addresses.has(name)) addresses.set(name, new Set());
|
||||
addresses.get(name)!.add(String(rec.data));
|
||||
if (asked.has(name) || [...instances.values()].some((i) => (i.host ?? hostGuess(i.name)) === name)) remember(name, String(rec.data));
|
||||
}
|
||||
}
|
||||
});
|
||||
const ask = () => m.query({ questions: [{ name: DEVKIT_SERVICE, type: "PTR" }] });
|
||||
ask();
|
||||
const again = setTimeout(ask, Math.min(900, timeoutMs / 2));
|
||||
// Ask a few times: an idle headset's Wi-Fi drops or delays packets, and responders
|
||||
// skip records they multicast in the last second. Each round also re-asks for any
|
||||
// instance still missing its SRV or address.
|
||||
const round = () => {
|
||||
m.query({ questions: [{ name: DEVKIT_SERVICE, type: "PTR" }] });
|
||||
for (const [key, i] of instances) {
|
||||
if (!i.host) m.query({ questions: [{ name: key, type: "SRV" }, { name: key, type: "TXT" }] });
|
||||
const host = i.host ?? hostGuess(i.name);
|
||||
if (host && !addresses.get(host)?.size) m.query({ questions: [{ name: host, type: "A" }] });
|
||||
}
|
||||
};
|
||||
round();
|
||||
const rounds = [0.3, 0.6].map((f) => setTimeout(round, Math.round(timeoutMs * f)));
|
||||
setTimeout(() => {
|
||||
clearTimeout(again);
|
||||
rounds.forEach(clearTimeout);
|
||||
m.destroy();
|
||||
const out: DiscoveredFrame[] = [];
|
||||
for (const i of instances.values()) {
|
||||
if (!i.host) continue;
|
||||
const addrs = [...(addresses.get(i.host) ?? [])].filter((a) => isIP(a) === 4);
|
||||
out.push({ name: i.name, host: i.host, address: addrs[0] ?? "", port: i.port ?? 32000, login: i.txt.login, addresses: addrs });
|
||||
const host = i.host ?? hostGuess(i.name);
|
||||
if (!host) continue;
|
||||
const addrs = [...(addresses.get(host) ?? [])].filter((a) => isIP(a) === 4);
|
||||
if (!addrs.length) {
|
||||
const prior = recalled(host);
|
||||
if (prior) addrs.push(prior);
|
||||
}
|
||||
out.push({ name: i.name, host, address: addrs[0] ?? "", port: i.port ?? 32000, login: i.txt.login, addresses: addrs });
|
||||
}
|
||||
resolve(out.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
}, timeoutMs);
|
||||
@@ -107,7 +152,7 @@ export function browse(timeoutMs = 2500): Promise<DiscoveredFrame[]> {
|
||||
}
|
||||
|
||||
/** Resolve "<name>.local" (or a bare name) to an IPv4 address with our own mDNS query. */
|
||||
export function resolveMdns(host: string, timeoutMs = 2000): Promise<string | undefined> {
|
||||
export function resolveMdns(host: string, timeoutMs = 3000): Promise<string | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
const m = open();
|
||||
if (!m) return resolve(undefined);
|
||||
@@ -116,7 +161,7 @@ export function resolveMdns(host: string, timeoutMs = 2000): Promise<string | un
|
||||
const finish = (addr?: string) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
clearTimeout(again);
|
||||
retries.forEach(clearTimeout);
|
||||
clearTimeout(timer);
|
||||
m.destroy();
|
||||
resolve(addr);
|
||||
@@ -127,8 +172,11 @@ export function resolveMdns(host: string, timeoutMs = 2000): Promise<string | un
|
||||
}
|
||||
});
|
||||
const ask = () => m.query({ questions: [{ name: want, type: "A" }] });
|
||||
// Responders drop repeats of a record they multicast within the last second,
|
||||
// and an idle headset's Wi-Fi adds latency, so ask a few times.
|
||||
ask();
|
||||
const again = setTimeout(ask, 700);
|
||||
const retries = [700, 1600].map((ms) => setTimeout(ask, ms));
|
||||
// No cached fallback here: this result is a connection target, so it must be a live answer.
|
||||
const timer = setTimeout(() => finish(undefined), timeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ function wireIpc(): void {
|
||||
),
|
||||
);
|
||||
|
||||
ipcMain.handle("connection:discover", () => browse(2500));
|
||||
ipcMain.handle("connection:discover", () => browse());
|
||||
|
||||
ipcMain.handle("payload:inspect", async (_e, paths: string[]): Promise<PayloadInfo> => {
|
||||
discardAll();
|
||||
|
||||
@@ -276,34 +276,34 @@ export function cleanError(e: unknown): string {
|
||||
return m.replace(/^Error invoking remote method '[^']+': (Error: )?/, "");
|
||||
}
|
||||
|
||||
let discoveryTimer: number | undefined;
|
||||
let discoveryStarted = false;
|
||||
let discoveryBusy = false;
|
||||
|
||||
async function discoverOnce(): Promise<void> {
|
||||
/** One scan of the local network. Results stay on screen until the user asks again. */
|
||||
export async function discoverOnce(): Promise<void> {
|
||||
if (discoveryBusy) return;
|
||||
discoveryBusy = true;
|
||||
store.set({ discovering: true });
|
||||
try {
|
||||
const discovered = await api.connection.discover();
|
||||
store.set({ discovered, discovering: false });
|
||||
store.set({ discovered, discovering: false, discoveryDone: true });
|
||||
} catch {
|
||||
store.set({ discovering: false });
|
||||
store.set({ discovering: false, discoveryDone: true });
|
||||
} finally {
|
||||
discoveryBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Keep browsing for headsets while the connect screen is up. */
|
||||
/** Scan once when the connect screen opens; further scans happen only when the user asks. */
|
||||
export function startDiscovery(): void {
|
||||
if (discoveryTimer !== undefined) return;
|
||||
if (discoveryStarted) return;
|
||||
discoveryStarted = true;
|
||||
void discoverOnce();
|
||||
discoveryTimer = window.setInterval(() => void discoverOnce(), 5000);
|
||||
}
|
||||
|
||||
/** Called when the connect screen closes, so the next visit scans again. */
|
||||
export function stopDiscovery(): void {
|
||||
if (discoveryTimer === undefined) return;
|
||||
clearInterval(discoveryTimer);
|
||||
discoveryTimer = undefined;
|
||||
discoveryStarted = false;
|
||||
}
|
||||
|
||||
export function useDiscovered(host: string): void {
|
||||
|
||||
@@ -52,7 +52,6 @@ const ICONS: Record<string, string> = {
|
||||
eraser: '<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/>',
|
||||
info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
|
||||
settings: '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
|
||||
drop: '<path d="M12 3v12"/><path d="m8 11 4 4 4-4"/><rect x="4" y="15" width="16" height="6" rx="2"/>',
|
||||
};
|
||||
|
||||
export function icon(name: string, cls = ""): HTMLElement {
|
||||
|
||||
|
After Width: | Height: | Size: 23 KiB |
@@ -77,7 +77,8 @@ export function installMock(): void {
|
||||
installRuntime: async () => ({ ok: true }),
|
||||
discover: async () => {
|
||||
await sleep(600);
|
||||
return scenario === "fresh" || scenario === "connected"
|
||||
// ?nodisc=1 pretends no headset answers.
|
||||
return !q.has("nodisc") && (scenario === "fresh" || scenario === "connected")
|
||||
? [{ name: "frame", host: "frame.local", address: "192.168.1.42", addresses: ["192.168.1.42"], port: 32000, login: "steamos" }]
|
||||
: [];
|
||||
},
|
||||
|
||||
@@ -68,6 +68,8 @@ export interface State {
|
||||
inspectError?: string;
|
||||
discovered: DiscoveredFrame[];
|
||||
discovering: boolean;
|
||||
/** At least one scan has finished since the connect screen opened. */
|
||||
discoveryDone: boolean;
|
||||
}
|
||||
|
||||
export const initialState: State = {
|
||||
@@ -89,6 +91,7 @@ export const initialState: State = {
|
||||
inspecting: false,
|
||||
discovered: [],
|
||||
discovering: false,
|
||||
discoveryDone: false,
|
||||
};
|
||||
|
||||
type Listener = (s: State) => void;
|
||||
|
||||
@@ -36,11 +36,9 @@ p { margin: 0; }
|
||||
}
|
||||
.topbar > * { -webkit-app-region: no-drag; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 15px; }
|
||||
.brand .logo { width: 24px; height: 24px; border-radius: 7px; background: var(--accent); color: #fff; display: grid; place-items: center; }
|
||||
.brand .logo svg { width: 15px; height: 15px; }
|
||||
.brand .logo { width: 24px; height: 24px; border-radius: 6px; display: block; }
|
||||
.brand-lg { font-size: 20px; gap: 12px; }
|
||||
.brand-lg .logo { width: 32px; height: 32px; border-radius: 9px; }
|
||||
.brand-lg .logo svg { width: 20px; height: 20px; }
|
||||
.brand-lg .logo { width: 32px; height: 32px; border-radius: 8px; }
|
||||
.main { flex: 1; overflow: auto; padding: 24px 20px 32px; }
|
||||
.container { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; gap: 20px; }
|
||||
.center { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100%; gap: 20px; }
|
||||
|
||||
@@ -2,7 +2,7 @@ import { h, icon } from "../dom";
|
||||
|
||||
const TERMS_URL = "https://www.frameloader.com/terms";
|
||||
import { store, type State } from "../state";
|
||||
import { cancelPair, connectSaved, connectWithPassword, openSettings, pair, useDiscovered } from "../actions";
|
||||
import { cancelPair, connectSaved, connectWithPassword, discoverOnce, openSettings, pair, useDiscovered } from "../actions";
|
||||
|
||||
export function connectView(s: State): HTMLElement {
|
||||
const f = s.connectForm;
|
||||
@@ -30,15 +30,25 @@ export function connectView(s: State): HTMLElement {
|
||||
h("div", { class: "help" }, "Changed the hostname in Steam Settings → System? Enter it here. Frameloader finds the address itself, so .local is optional."),
|
||||
);
|
||||
|
||||
const searching = s.discovering || !s.discoveryDone;
|
||||
const found = h(
|
||||
"div",
|
||||
{ class: "found" },
|
||||
h(
|
||||
"div",
|
||||
{ class: "row small muted", style: "gap: 8px" },
|
||||
s.discovering && !s.discovered.length ? h("span", { class: "spinner" }) : icon("headset"),
|
||||
s.discovered.length ? "Found on your network" : s.discovering ? "Looking for headsets on your network…" : "No headset found yet. Developer Mode must be on and the Frame awake.",
|
||||
),
|
||||
s.discovered.length
|
||||
? h(
|
||||
"div",
|
||||
{ class: "row spread small muted", style: "gap: 8px" },
|
||||
h("span", { class: "row", style: "gap: 8px" }, icon("headset"), "Found on your network"),
|
||||
h("button", { class: "link small", disabled: searching || pairing, onclick: () => void discoverOnce() }, searching ? "Searching…" : "Search again"),
|
||||
)
|
||||
: searching
|
||||
? h("div", { class: "row small muted", style: "gap: 8px" }, h("span", { class: "spinner" }), "Looking for headsets on your network…")
|
||||
: h(
|
||||
"div",
|
||||
{ class: "row spread small muted", style: "gap: 8px" },
|
||||
h("span", { class: "row", style: "gap: 8px" }, icon("headset"), "No headset found. Developer Mode must be on and the Frame awake."),
|
||||
h("button", { class: "btn sm", style: "white-space: nowrap; flex: none", disabled: pairing, onclick: () => void discoverOnce() }, icon("refresh"), "Click to try again"),
|
||||
),
|
||||
s.discovered.length
|
||||
? h(
|
||||
"div",
|
||||
@@ -56,6 +66,7 @@ export function connectView(s: State): HTMLElement {
|
||||
: null,
|
||||
);
|
||||
|
||||
|
||||
const pairBlock = h(
|
||||
"div",
|
||||
{ class: "stack" },
|
||||
@@ -144,7 +155,7 @@ export function connectView(s: State): HTMLElement {
|
||||
step(3, "Open Pair new host", "Settings → Developer → Pair new host, when you click Pair."),
|
||||
);
|
||||
|
||||
const brand = h("div", { class: "brand brand-lg" }, h("span", { class: "logo" }, icon("drop")), "Frameloader");
|
||||
const brand = h("div", { class: "brand brand-lg" }, h("img", { class: "logo", src: "logo.png", alt: "" }), "Frameloader");
|
||||
|
||||
const settingsBtn = h("button", { class: "btn sm quiet corner", "aria-label": "Settings", title: "Settings", onclick: () => openSettings() }, icon("settings"));
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export function topbar(s: State): HTMLElement {
|
||||
return h(
|
||||
"div",
|
||||
{ class: "topbar" },
|
||||
h("div", { class: "brand" }, h("span", { class: "logo" }, icon("drop")), "Frameloader"),
|
||||
h("div", { class: "brand" }, h("img", { class: "logo", src: "logo.png", alt: "" }), "Frameloader"),
|
||||
h(
|
||||
"div",
|
||||
{ class: "row", style: "align-items: flex-start; gap: 6px" },
|
||||
|
||||
@@ -3,24 +3,29 @@ import assert from "node:assert/strict";
|
||||
import mdns from "multicast-dns";
|
||||
import { browse, resolveHost, DEVKIT_SERVICE } from "../src/main/discovery";
|
||||
|
||||
// A fake Steam Frame answering on the local multicast group. Skips (not fails)
|
||||
// when multicast isn't available on this machine.
|
||||
// A fake Steam Frame answering on the local multicast group the way SteamOS does:
|
||||
// the PTR browse gets only the PTR and an A record; SRV and TXT are answered only
|
||||
// when asked for. Skips (not fails) when multicast isn't available on this machine.
|
||||
function fakeFrame(name: string, host: string, address: string) {
|
||||
const m = mdns();
|
||||
m.on("error", () => undefined);
|
||||
const instance = `${name}.${DEVKIT_SERVICE}`;
|
||||
m.on("query", (q) => {
|
||||
const wantsPtr = q.questions.some((x) => x.type === "PTR" && x.name.toLowerCase() === DEVKIT_SERVICE);
|
||||
const wantsA = q.questions.some((x) => x.type === "A" && x.name.toLowerCase() === host);
|
||||
if (!wantsPtr && !wantsA) return;
|
||||
const instance = `${name}.${DEVKIT_SERVICE}`;
|
||||
m.respond({
|
||||
answers: wantsPtr ? [{ name: DEVKIT_SERVICE, type: "PTR", ttl: 120, data: instance }] : [],
|
||||
additionals: [
|
||||
{ name: instance, type: "SRV", ttl: 120, data: { target: host, port: 32000, priority: 0, weight: 0 } },
|
||||
{ name: instance, type: "TXT", ttl: 120, data: [Buffer.from("login=steamos")] },
|
||||
{ name: host, type: "A", ttl: 120, data: address },
|
||||
],
|
||||
});
|
||||
const answers: { name: string; type: string; ttl: number; data: unknown }[] = [];
|
||||
for (const x of q.questions) {
|
||||
const qname = x.name.toLowerCase();
|
||||
if (x.type === "PTR" && qname === DEVKIT_SERVICE) {
|
||||
answers.push({ name: DEVKIT_SERVICE, type: "PTR", ttl: 120, data: instance });
|
||||
answers.push({ name: host, type: "A", ttl: 120, data: address });
|
||||
} else if (x.type === "SRV" && qname === instance) {
|
||||
answers.push({ name: instance, type: "SRV", ttl: 120, data: { target: host, port: 32000, priority: 0, weight: 0 } });
|
||||
} else if (x.type === "TXT" && qname === instance) {
|
||||
answers.push({ name: instance, type: "TXT", ttl: 120, data: [Buffer.from("login=steamos")] });
|
||||
} else if (x.type === "A" && qname === host) {
|
||||
answers.push({ name: host, type: "A", ttl: 120, data: address });
|
||||
}
|
||||
}
|
||||
if (answers.length) m.respond({ answers } as never);
|
||||
});
|
||||
return () => m.destroy();
|
||||
}
|
||||
|
||||
@@ -6,14 +6,15 @@
|
||||
<title>Page not found · Frameloader</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="stylesheet" href="./src/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="nav">
|
||||
<div class="wrap nav-inner">
|
||||
<a class="brand" href="/" aria-label="Frameloader home">
|
||||
<span class="logo"><svg viewBox="0 0 24 24"><path d="M12 3v12"/><path d="m8 11 4 4 4-4"/><rect x="4" y="15" width="16" height="6" rx="2"/></svg></span>
|
||||
<img class="logo" src="/logo.png" width="28" height="28" alt="" />
|
||||
Frameloader
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -13,14 +13,15 @@
|
||||
<meta property="og:image" content="https://www.frameloader.com/screenshots/home-light.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<link rel="canonical" href="https://www.frameloader.com/" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="stylesheet" href="./src/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="nav">
|
||||
<div class="wrap nav-inner">
|
||||
<a class="brand" href="#top" aria-label="Frameloader home">
|
||||
<span class="logo"><svg viewBox="0 0 24 24"><path d="M12 3v12"/><path d="m8 11 4 4 4-4"/><rect x="4" y="15" width="16" height="6" rx="2"/></svg></span>
|
||||
<img class="logo" src="/logo.png" width="28" height="28" alt="" />
|
||||
Frameloader
|
||||
</a>
|
||||
<nav class="nav-links" aria-label="Sections">
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
"private": true,
|
||||
"description": "The Frameloader website.",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Traveler Dev Ltd",
|
||||
"email": "hi@traveler.dev"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="9" fill="#2E6BE6"/><g fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M16 6v12"/><path d="m11.5 13.5 4.5 4.5 4.5-4.5"/><rect x="8" y="20" width="16" height="6" rx="2"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 325 B |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 134 KiB |
@@ -28,8 +28,7 @@ svg { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; s
|
||||
.nav-inner { display: flex; align-items: center; justify-content: space-between; height: 60px; gap: 16px; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 16px; color: var(--text); }
|
||||
.brand:hover { text-decoration: none; }
|
||||
.logo { width: 28px; height: 28px; border-radius: 8px; background: var(--accent); color: #fff; display: grid; place-items: center; }
|
||||
.logo svg { width: 17px; height: 17px; stroke-width: 2.2; }
|
||||
.logo { width: 28px; height: 28px; border-radius: 7px; display: block; }
|
||||
.nav-links { display: flex; align-items: center; gap: 20px; font-size: 15px; }
|
||||
.nav-links > a:not(.btn) { color: var(--text-2); }
|
||||
.nav-links > a:not(.btn):hover { color: var(--text); text-decoration: none; }
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
<meta name="description" content="Frameloader is free software provided as is, with no warranty. Use it at your own risk." />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<link rel="canonical" href="https://www.frameloader.com/terms" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="stylesheet" href="./src/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="nav">
|
||||
<div class="wrap nav-inner">
|
||||
<a class="brand" href="/" aria-label="Frameloader home">
|
||||
<span class="logo"><svg viewBox="0 0 24 24"><path d="M12 3v12"/><path d="m8 11 4 4 4-4"/><rect x="4" y="15" width="16" height="6" rx="2"/></svg></span>
|
||||
<img class="logo" src="/logo.png" width="28" height="28" alt="" />
|
||||
Frameloader
|
||||
</a>
|
||||
<nav class="nav-links" aria-label="Sections">
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"name": "frameloader-monorepo",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Traveler Dev Ltd",
|
||||
"email": "hi@traveler.dev"
|
||||
},
|
||||
"packageManager": "pnpm@11.24.0",
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
"private": true,
|
||||
"description": "Shared color, type and shape tokens for the Frameloader app and website.",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Traveler Dev Ltd",
|
||||
"email": "hi@traveler.dev"
|
||||
},
|
||||
"exports": {
|
||||
"./tokens.css": "./tokens.css"
|
||||
},
|
||||
|
||||
@@ -33,8 +33,8 @@ importers:
|
||||
specifier: 44.5.1
|
||||
version: 44.5.1(supports-color@10.2.2)
|
||||
electron-builder:
|
||||
specifier: ^26.15.3
|
||||
version: 26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@10.2.2)
|
||||
specifier: ^26.17.0
|
||||
version: 26.17.0(electron-builder-squirrel-windows@26.17.0)(supports-color@10.2.2)
|
||||
esbuild:
|
||||
specifier: ^0.28.2
|
||||
version: 0.28.2
|
||||
@@ -665,10 +665,6 @@ packages:
|
||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
'@noble/hashes@2.4.0':
|
||||
resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@oxc-project/types@0.152.0':
|
||||
resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==}
|
||||
|
||||
@@ -992,12 +988,12 @@ packages:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
app-builder-lib@26.15.3:
|
||||
resolution: {integrity: sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==}
|
||||
app-builder-lib@26.17.0:
|
||||
resolution: {integrity: sha512-wfL7EhujuODDW3uMSD8RPcr/MLzmT0dmpVfWV0wFUWXI0rIdaukzAJ7EFUSyHXOpxuBZqaIGlQt6VeQhtHhetw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
dmg-builder: 26.15.3
|
||||
electron-builder-squirrel-windows: 26.15.3
|
||||
dmg-builder: 26.17.0
|
||||
electron-builder-squirrel-windows: 26.17.0
|
||||
|
||||
app-info-parser@1.1.6:
|
||||
resolution: {integrity: sha512-ZAFCM0bN88cbpsMoRhL/JfdX3b+nb5iBEXcu30xABvbaqtw6tXfHujDnuKSpNmA3P0uwpkIxTV/Wun5HfEch8A==}
|
||||
@@ -1091,8 +1087,8 @@ packages:
|
||||
resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
builder-util@26.15.3:
|
||||
resolution: {integrity: sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==}
|
||||
builder-util@26.16.0:
|
||||
resolution: {integrity: sha512-RLyJhB7Si3YkzKR9ubQslWuXW3Vhs3CGe1i+SeixBZ0qTd1mk3XBmssvY22TlB6CS5blyko8Gu1JzpYk8UkYAg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
bytebuffer@5.0.1:
|
||||
@@ -1234,8 +1230,8 @@ packages:
|
||||
dir-compare@4.2.0:
|
||||
resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==}
|
||||
|
||||
dmg-builder@26.15.3:
|
||||
resolution: {integrity: sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==}
|
||||
dmg-builder@26.17.0:
|
||||
resolution: {integrity: sha512-EAw1f2iGkICmUvAVBOjm7v5mxJdB7/+OZSycHFhKBimiTkCXtvkCC6wBKzHo7ZQYi58C2MmwIleRbgRnROlK2Q==}
|
||||
|
||||
dns-packet@5.6.1:
|
||||
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
|
||||
@@ -1261,16 +1257,16 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
hasBin: true
|
||||
|
||||
electron-builder-squirrel-windows@26.15.3:
|
||||
resolution: {integrity: sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==}
|
||||
electron-builder-squirrel-windows@26.17.0:
|
||||
resolution: {integrity: sha512-uyh+D+B0m12qCRp9c/7dwBirHTh7JcvAzj5eTEbb4dXPPGAhYPx1nozzbu1fC6AdULfat6ZNqgWhipTFpb82lQ==}
|
||||
|
||||
electron-builder@26.15.3:
|
||||
resolution: {integrity: sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==}
|
||||
electron-builder@26.17.0:
|
||||
resolution: {integrity: sha512-iYHBRiagS9sDIbZx1ZD113f5rEGQvtpvTvHf70ovHKJ8mRvxwIkWX7JCwfmVQmVS4eX735p7TZmoxHnBPwF3vA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
|
||||
electron-publish@26.15.3:
|
||||
resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==}
|
||||
electron-publish@26.16.0:
|
||||
resolution: {integrity: sha512-Vt3KzQIiw9BImvNOYtndg9Mjki+tl4+1sQiC/+G5j8khWaENOJFWodiB+sUl6yyHwtd37avehskdtPw7f8y/+Q==}
|
||||
|
||||
electron-winstaller@5.4.0:
|
||||
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
|
||||
@@ -2283,7 +2279,7 @@ snapshots:
|
||||
|
||||
'@electron/get@5.1.0(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
env-paths: 3.0.0
|
||||
graceful-fs: 4.2.11
|
||||
progress: 2.0.3
|
||||
@@ -2644,8 +2640,6 @@ snapshots:
|
||||
|
||||
'@noble/hashes@1.8.0': {}
|
||||
|
||||
'@noble/hashes@2.4.0': {}
|
||||
|
||||
'@oxc-project/types@0.152.0': {}
|
||||
|
||||
'@peculiar/asn1-schema@2.10.0':
|
||||
@@ -2870,7 +2864,7 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
app-builder-lib@26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3)(supports-color@10.2.2):
|
||||
app-builder-lib@26.17.0(dmg-builder@26.17.0)(electron-builder-squirrel-windows@26.17.0)(supports-color@10.2.2):
|
||||
dependencies:
|
||||
'@electron/asar': 3.4.1
|
||||
'@electron/fuses': 1.8.0
|
||||
@@ -2880,23 +2874,23 @@ snapshots:
|
||||
'@electron/rebuild': 4.2.0(supports-color@10.2.2)
|
||||
'@electron/universal': 2.0.3(supports-color@10.2.2)
|
||||
'@malept/flatpak-bundler': 0.4.0(supports-color@10.2.2)
|
||||
'@noble/hashes': 2.4.0
|
||||
'@noble/hashes': 1.8.0
|
||||
'@peculiar/webcrypto': 1.7.1
|
||||
'@types/fs-extra': 9.0.13
|
||||
ajv: 8.20.0
|
||||
asn1js: 3.0.10
|
||||
async-exit-hook: 2.0.1
|
||||
builder-util: 26.15.3(supports-color@10.2.2)
|
||||
builder-util: 26.16.0(supports-color@10.2.2)
|
||||
builder-util-runtime: 9.7.0(supports-color@10.2.2)
|
||||
chromium-pickle-js: 0.2.0
|
||||
ci-info: 4.3.1
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@10.2.2)
|
||||
dmg-builder: 26.17.0(electron-builder-squirrel-windows@26.17.0)(supports-color@10.2.2)
|
||||
dotenv: 16.6.1
|
||||
dotenv-expand: 11.0.7
|
||||
ejs: 3.1.10
|
||||
electron-builder-squirrel-windows: 26.15.3(dmg-builder@26.15.3)(supports-color@10.2.2)
|
||||
electron-publish: 26.15.3(supports-color@10.2.2)
|
||||
electron-builder-squirrel-windows: 26.17.0(dmg-builder@26.17.0)(supports-color@10.2.2)
|
||||
electron-publish: 26.16.0(supports-color@10.2.2)
|
||||
fs-extra: 10.1.0
|
||||
hosted-git-info: 4.1.0
|
||||
isbinaryfile: 5.0.7
|
||||
@@ -3006,7 +3000,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
builder-util@26.15.3(supports-color@10.2.2):
|
||||
builder-util@26.16.0(supports-color@10.2.2):
|
||||
dependencies:
|
||||
'@types/debug': 4.1.13
|
||||
builder-util-runtime: 9.7.0(supports-color@10.2.2)
|
||||
@@ -3128,12 +3122,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
supports-color: 10.2.2
|
||||
|
||||
debug@4.4.3(supports-color@7.2.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 7.2.0
|
||||
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
@@ -3166,10 +3154,10 @@ snapshots:
|
||||
minimatch: 3.1.5
|
||||
p-limit: 3.1.0
|
||||
|
||||
dmg-builder@26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@10.2.2):
|
||||
dmg-builder@26.17.0(electron-builder-squirrel-windows@26.17.0)(supports-color@10.2.2):
|
||||
dependencies:
|
||||
app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3)(supports-color@10.2.2)
|
||||
builder-util: 26.15.3(supports-color@10.2.2)
|
||||
app-builder-lib: 26.17.0(dmg-builder@26.17.0)(electron-builder-squirrel-windows@26.17.0)(supports-color@10.2.2)
|
||||
builder-util: 26.16.0(supports-color@10.2.2)
|
||||
fs-extra: 10.1.0
|
||||
js-yaml: 4.3.2
|
||||
transitivePeerDependencies:
|
||||
@@ -3200,23 +3188,23 @@ snapshots:
|
||||
dependencies:
|
||||
jake: 10.9.4
|
||||
|
||||
electron-builder-squirrel-windows@26.15.3(dmg-builder@26.15.3)(supports-color@10.2.2):
|
||||
electron-builder-squirrel-windows@26.17.0(dmg-builder@26.17.0)(supports-color@10.2.2):
|
||||
dependencies:
|
||||
app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3)(supports-color@10.2.2)
|
||||
builder-util: 26.15.3(supports-color@10.2.2)
|
||||
app-builder-lib: 26.17.0(dmg-builder@26.17.0)(electron-builder-squirrel-windows@26.17.0)(supports-color@10.2.2)
|
||||
builder-util: 26.16.0(supports-color@10.2.2)
|
||||
electron-winstaller: 5.4.0(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- dmg-builder
|
||||
- supports-color
|
||||
|
||||
electron-builder@26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@10.2.2):
|
||||
electron-builder@26.17.0(electron-builder-squirrel-windows@26.17.0)(supports-color@10.2.2):
|
||||
dependencies:
|
||||
app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3)(supports-color@10.2.2)
|
||||
builder-util: 26.15.3(supports-color@10.2.2)
|
||||
app-builder-lib: 26.17.0(dmg-builder@26.17.0)(electron-builder-squirrel-windows@26.17.0)(supports-color@10.2.2)
|
||||
builder-util: 26.16.0(supports-color@10.2.2)
|
||||
builder-util-runtime: 9.7.0(supports-color@10.2.2)
|
||||
chalk: 4.1.2
|
||||
ci-info: 4.4.0
|
||||
dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@10.2.2)
|
||||
dmg-builder: 26.17.0(electron-builder-squirrel-windows@26.17.0)(supports-color@10.2.2)
|
||||
fs-extra: 10.1.0
|
||||
lazy-val: 1.0.5
|
||||
simple-update-notifier: 2.0.0
|
||||
@@ -3225,11 +3213,11 @@ snapshots:
|
||||
- electron-builder-squirrel-windows
|
||||
- supports-color
|
||||
|
||||
electron-publish@26.15.3(supports-color@10.2.2):
|
||||
electron-publish@26.16.0(supports-color@10.2.2):
|
||||
dependencies:
|
||||
'@types/fs-extra': 9.0.13
|
||||
aws4: 1.13.2
|
||||
builder-util: 26.15.3(supports-color@10.2.2)
|
||||
builder-util: 26.16.0(supports-color@10.2.2)
|
||||
builder-util-runtime: 9.7.0(supports-color@10.2.2)
|
||||
chalk: 4.1.2
|
||||
form-data: 4.0.6
|
||||
|
||||