@stacksjs/desktop-build 0.75.241 → 0.75.242
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +20 -33
- package/dist/invites.js +1 -0
- package/dist/launcher.js +1 -2
- package/dist/probe/html.js +45 -0
- package/dist/probe/index.js +1 -7
- package/dist/probe/page.js +3 -4
- package/dist/probe/payload.js +1 -0
- package/dist/probe/record.js +1 -0
- package/dist/probe/records.js +1 -0
- package/dist/probe/render.js +4 -0
- package/dist/probe/runner.js +8 -108
- package/dist/probe/schema.js +3 -0
- package/dist/probe/stats.js +1 -0
- package/dist/support.js +1 -0
- package/dist/updater.js +1 -0
- package/package.json +9 -3
package/dist/index.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
var g=(e,t,r)=>()=>{if(e)try{t=e(e=0)}catch(n){r=[n]}if(r)throw r[0];return t};function U(e,t,r={}){if(!t.trim())throw Error("Invite token is required");let n=new URL("/invite",/^https?:\/\//.test(e)?e:`https://${e}`);if(n.searchParams.set("token",t),r.email)n.searchParams.set("email",r.email);if(r.team)n.searchParams.set("team",r.team);if(r.role)n.searchParams.set("role",r.role);if(r.expiresAt){let i=r.expiresAt instanceof Date?r.expiresAt:new Date(r.expiresAt);if(Number.isNaN(i.valueOf()))throw Error("Invite expiration must be a valid date");n.searchParams.set("expires",i.toISOString())}return n.toString()}async function ke(e,t){return Promise.all(e.map(async(r)=>{let n=typeof r==="string"?{email:r}:r;try{M(n.email);let i=await t.createToken(n),s=U(t.baseUrl,i,{email:n.email,team:n.team,role:n.role,expiresAt:t.expiresAt});return await t.send({recipient:n,url:s}),{recipient:n,url:s,delivered:!0}}catch(i){return{recipient:n,url:"",delivered:!1,error:i instanceof Error?i.message:String(i)}}}))}function M(e){if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))throw Error(`Invalid invite email: ${e}`)}import{createHash as j,createPublicKey as V,verify as _}from"crypto";import{mkdir as z,rename as K,rm as q,writeFile as G}from"fs/promises";import v from"process";import{dirname as H}from"path";async function xe(e){S(e.manifestUrl);let t=await(e.fetcher||fetch)(e.manifestUrl,{headers:{Accept:"application/json"}});if(!t.ok)throw Error(`Desktop update check failed with HTTP ${t.status}`);let r=D(await t.json());return x(r,e.trustedKeys),E(r,e.target),W(r.version,e.currentVersion)>0?r:null}async function Ee(e,t,r=fetch,n,i){if(D(e),!n)throw Error("Desktop update staging requires trusted signing keys");x(e,n),E(e,i),S(e.url);let s=await r(e.url);if(!s.ok)throw Error(`Desktop update download failed with HTTP ${s.status}`);let a=new Uint8Array(await s.arrayBuffer());if(a.byteLength!==e.size)throw Error(`Desktop update size mismatch: expected ${e.size}, received ${a.byteLength}`);if(j("sha256").update(a).digest("hex")!==e.sha256.toLowerCase())throw Error("Desktop update checksum mismatch");await z(H(t),{recursive:!0});let p=`${t}.download-${crypto.randomUUID()}`;try{await G(p,a,{mode:448}),await K(p,t)}catch(d){throw await q(p,{force:!0}),d}return{manifest:e,path:t,bytes:a.byteLength}}function W(e,t){let r=f(e),n=f(t);for(let i=0;i<3;i++){let s=r[i]-n[i];if(s!==0)return Math.sign(s)}return 0}function D(e){if(!e||typeof e!=="object")throw Error("Desktop update manifest must be an object");let t=e;if(f(t.version||""),typeof t.url!=="string")throw Error("Desktop update manifest requires a URL");if(typeof t.sha256!=="string"||!/^[a-f0-9]{64}$/i.test(t.sha256))throw Error("Desktop update manifest requires a SHA-256 checksum");if(!Number.isInteger(t.size)||t.size<0)throw Error("Desktop update manifest requires a non-negative integer size");if(typeof t.sourceRevision!=="string"||!/^[0-9a-f]{40}$/.test(t.sourceRevision))throw Error("Desktop update manifest requires a full source revision");if(typeof t.platform!=="string"||!["darwin","linux","win32"].includes(t.platform))throw Error("Desktop update manifest requires a supported platform identifier");if(typeof t.architecture!=="string"||!/^[a-z0-9_-]+$/.test(t.architecture))throw Error("Desktop update manifest requires an architecture");if(typeof t.signingKeyId!=="string"||!/^[a-zA-Z0-9._-]+$/.test(t.signingKeyId))throw Error("Desktop update manifest requires a signing key ID");if(typeof t.signature!=="string"||!/^[A-Za-z0-9_-]+$/.test(t.signature))throw Error("Desktop update manifest requires an Ed25519 signature");return t}function Y(e){return Buffer.from(JSON.stringify({version:e.version,url:e.url,sha256:e.sha256.toLowerCase(),size:e.size,sourceRevision:e.sourceRevision,platform:e.platform,architecture:e.architecture,signingKeyId:e.signingKeyId,publishedAt:e.publishedAt||null,notes:e.notes||null}),"utf8")}function x(e,t){let r=t[e.signingKeyId];if(!r)throw Error(`Desktop update signing key is not trusted: ${e.signingKeyId}`);let n="ed25519-public:";if(!r.startsWith(n))throw Error("Desktop update signing key has an invalid encoding");try{let i=V({key:Buffer.from(r.slice(n.length),"base64url"),format:"der",type:"spki"}),s=Buffer.from(e.signature,"base64url");if(i.asymmetricKeyType!=="ed25519"||s.length!==64||!_(null,Y(e),i,s))throw Error("invalid")}catch{throw Error("Desktop update manifest signature verification failed")}}function E(e,t={platform:v.platform,architecture:v.arch}){if(e.platform!==t.platform||e.architecture!==t.architecture)throw Error(`Desktop update is built for ${e.platform}-${e.architecture}, not this app's ${t.platform}-${t.architecture}`)}function f(e){let t=e.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/);if(!t)throw Error(`Invalid semantic version: ${e}`);return[Number(t[1]),Number(t[2]),Number(t[3])]}function S(e){let t=new URL(e),r=["localhost","127.0.0.1","::1"].includes(t.hostname);if(t.protocol!=="https:"&&!(t.protocol==="http:"&&r))throw Error("Desktop updates require HTTPS outside loopback development")}var I=()=>{};function J(e=process.platform,t=process.arch){return Z.find((r)=>r.platform===e&&r.architecture===t)}function Ie(e,t=process.platform,r=process.arch){let n=J(t,r);if(!n)throw Error(`Desktop target ${t}/${r} is unsupported`);if(e==="stable"&&(n.status!=="stable"||n.signing!=="enforced"||n.platform==="darwin"&&n.notarization!=="enforced"||!n.installLaunchEvidence||!n.updateRollbackEvidence))throw Error(`Desktop target ${t}/${r} cannot be released as stable; complete stacksjs/stacks#2059, #2062, and #2063 first`);return n}var Z;var C=g(()=>{Z=[{platform:"darwin",architecture:"arm64",status:"experimental",osVersions:["macOS 27.0 (local run, Craft v0.0.107 published archive)","macOS 15 runner (CI, Craft v0.0.48 from source)"],packageFormat:"DMG + PKG",signing:"pending",notarization:"pending",installLaunchEvidence:"https://github.com/stacksjs/stacks/blob/main/storage/framework/core/desktop/evidence/lifecycle-darwin-arm64.json",updateRollbackEvidence:"https://github.com/stacksjs/stacks/blob/main/storage/framework/core/desktop/evidence/lifecycle-darwin-arm64.json",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["The Stacks-built app bundle, DMG and PKG are unsigned while platform identities remain unprovisioned. The Craft runtime inside them is Developer ID-signed by Craft's own release workflow, which does not make the app signed.","Lifecycle evidence was re-run on 2026-10-03 against the published Craft v0.0.107 archive (craft-darwin-arm64.zip, sha256 98e1a7ae478a8900256add2884d05cc07aea2f3de7d550590d9458abd4fe7725) with `storage/framework/core/desktop/evidence/lifecycle.ts`: PKG and DMG install, a real Craft window launched from the installed bundle, update, a refused truncated update, rollback and uninstall all passed.","That run was local on macOS 27.0, not CI, and installed into the user domain (~/Applications) rather than /Applications. Gatekeeper was not exercised, since nothing carried a quarantine flag. The last CI lifecycle run for this target (https://github.com/stacksjs/stacks/actions/runs/29890804159, macOS 15 runner) used Craft v0.0.48 built from source."]},{platform:"darwin",architecture:"x64",status:"experimental",osVersions:["macOS 15 Intel runner"],packageFormat:"DMG + PKG",signing:"pending",notarization:"pending",installLaunchEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",updateRollbackEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["Lifecycle fixtures are unsigned while platform identities remain unprovisioned.","Lifecycle evidence was produced against Craft v0.0.48 built from source (bf75807). Craft now publishes a prebuilt craft-darwin-x64.zip with every release, and no Stacks lifecycle run has re-verified a prebuilt release yet."]},{platform:"linux",architecture:"x64",status:"experimental",osVersions:["Ubuntu 24.04 runner"],packageFormat:"DEB",signing:"pending",notarization:"not-applicable",installLaunchEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",updateRollbackEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["The lifecycle fixture is unsigned and no package repository support policy is published.","Lifecycle evidence was produced against Craft v0.0.48 built from source (bf75807). Craft now publishes a prebuilt craft-linux-x64.zip with every release, and no Stacks lifecycle run has re-verified a prebuilt release yet."]},{platform:"win32",architecture:"x64",status:"experimental",osVersions:["Windows Server 2025 runner"],packageFormat:"MSI + ZIP",signing:"pending",notarization:"not-applicable",installLaunchEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",updateRollbackEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["The lifecycle fixture is not Authenticode-signed.","Lifecycle evidence was produced against Craft v0.0.48 built from source (bf75807). Craft now publishes a prebuilt craft-windows-x64.zip with every release, and no Stacks lifecycle run has re-verified a prebuilt release yet."]}]});import{closeSync as P,existsSync as m,openSync as R,readSync as T,statSync as X}from"fs";import{dirname as Q,join as l}from"path";import u from"process";import{fileURLToPath as ee}from"url";import{craftBinaryNotFoundMessage as te,resolveCraftBinary as re}from"craft-native";function L(e=u.env.CRAFT_BIN){return re(e)}function Le(e=u.env.CRAFT_BIN,t=(r)=>Bun.which(r)){let r=L(e);if(r!=="craft")return r;let n=t(r);if(!n)throw Error(te(r));return n}function Ne(e=u.cwd()){return m(l(e,N))}function Fe(e){return["build","--compile",...e.minify===!1?[]:["--minify"],"--no-compile-autoload-bunfig","--no-compile-autoload-dotenv",e.entry,"--outfile",e.outfile]}function Oe(e=u.cwd()){let t=l(e,N);if(m(t))return t;let r=Q(ee(import.meta.url)),i=[l(r,"launcher.ts"),l(r,"launcher.js")].find((s)=>m(s));if(!i)throw Error("Desktop launcher entrypoint not found. Reinstall @stacksjs/desktop - the package must ship dist/launcher.js.");return i}function c(e){return e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function h(e,t=""){if(typeof e==="string")return`<string>${c(e)}</string>`;if(typeof e==="boolean")return e?"<true/>":"<false/>";if(typeof e==="number"){if(!Number.isFinite(e))throw Error(`Unsupported Info.plist number: ${e}`);return Number.isInteger(e)?`<integer>${e}</integer>`:`<real>${e}</real>`}if(Array.isArray(e)){if(e.length===0)return"<array/>";return`<array>
|
|
3
|
-
${e.map((n)=>`${t} ${h(n,`${t} `)}`).join(`
|
|
1
|
+
import{closeSync,existsSync,openSync,readSync,statSync}from"node:fs";import{dirname,join}from"node:path";import process from"node:process";import{fileURLToPath}from"node:url";import{craftBinaryNotFoundMessage,resolveCraftBinary as resolveCraftNativeBinary}from"craft-native";export*from"./invites";export*from"./updater";export*from"./support";export function resolveCraftBinary(explicit=process.env.CRAFT_BIN){return resolveCraftNativeBinary(explicit)}export function resolveCraftExecutable(explicit=process.env.CRAFT_BIN,findOnPath=(binary)=>Bun.which(binary)){const resolved=resolveCraftBinary(explicit);if(resolved!=="craft")return resolved;const executable=findOnPath(resolved);if(!executable)throw Error(craftBinaryNotFoundMessage(resolved));return executable}export const USERLAND_LAUNCHER="app/Desktop/launcher.ts";export function hasUserlandDesktopLauncher(projectRoot=process.cwd()){return existsSync(join(projectRoot,USERLAND_LAUNCHER))}export function desktopLauncherCompileArgs(options){return["build","--compile",...options.minify===!1?[]:["--minify"],"--no-compile-autoload-bunfig","--no-compile-autoload-dotenv",options.entry,"--outfile",options.outfile]}export function resolveDesktopLauncher(projectRoot=process.cwd()){const userland=join(projectRoot,USERLAND_LAUNCHER);if(existsSync(userland))return userland;const here=dirname(fileURLToPath(import.meta.url)),launcher=[join(here,"launcher.ts"),join(here,"launcher.js")].find((candidate)=>existsSync(candidate));if(!launcher)throw Error("Desktop launcher entrypoint not found. Reinstall @stacksjs/desktop - the package must ship dist/launcher.js.");return launcher}export const RESERVED_PLIST_KEYS=new Set(["CFBundleName","CFBundleDisplayName","CFBundleIdentifier","CFBundleVersion","CFBundleShortVersionString","CFBundleExecutable","CFBundlePackageType","CFBundleInfoDictionaryVersion","CFBundleIconFile"]);export function xmlEscape(value){return value.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}export function plistValue(value,indent=""){if(typeof value==="string")return`<string>${xmlEscape(value)}</string>`;if(typeof value==="boolean")return value?"<true/>":"<false/>";if(typeof value==="number"){if(!Number.isFinite(value))throw Error(`Unsupported Info.plist number: ${value}`);return Number.isInteger(value)?`<integer>${value}</integer>`:`<real>${value}</real>`}if(Array.isArray(value)){if(value.length===0)return"<array/>";return`<array>
|
|
2
|
+
${value.map((item)=>`${indent} ${plistValue(item,`${indent} `)}`).join(`
|
|
4
3
|
`)}
|
|
5
|
-
${
|
|
6
|
-
${
|
|
4
|
+
${indent}</array>`}if(value&&typeof value==="object"){const entries=Object.entries(value);if(entries.length===0)return"<dict/>";return`<dict>
|
|
5
|
+
${entries.map(([key,val])=>`${indent} <key>${xmlEscape(key)}</key>${plistValue(val,`${indent} `)}`).join(`
|
|
7
6
|
`)}
|
|
8
|
-
${
|
|
9
|
-
${
|
|
10
|
-
`)}`,ignored
|
|
7
|
+
${indent}</dict>`}throw Error(`Unsupported Info.plist value: ${JSON.stringify(value)??String(value)}`)}export function renderUserlandPlistEntries(declared){const ignored=Object.keys(declared).filter((key)=>RESERVED_PLIST_KEYS.has(key)),kept=Object.entries(declared).filter(([key])=>!RESERVED_PLIST_KEYS.has(key));if(kept.length===0)return{xml:"",ignored};return{xml:`
|
|
8
|
+
${kept.map(([key,value])=>` <key>${xmlEscape(key)}</key>${plistValue(value," ")}`).join(`
|
|
9
|
+
`)}`,ignored}}export function renderInfoPlist(options){const declared=options.declared??{},{xml:extra,ignored}=renderUserlandPlistEntries(declared),overridden=(key)=>Object.hasOwn(declared,key),loopbackTransportSecurity=` <!-- The window talks to something this app started on loopback, so an
|
|
11
10
|
exception for 127.0.0.1 is all it needs. NSAllowsArbitraryLoads would
|
|
12
11
|
additionally permit every unencrypted host on the internet. -->
|
|
13
12
|
<key>NSAppTransportSecurity</key>
|
|
@@ -18,35 +17,23 @@ ${r.map(([i,s])=>` <key>${c(i)}</key>${h(s," ")}`).join(`
|
|
|
18
17
|
<key>127.0.0.1</key>
|
|
19
18
|
<dict><key>NSExceptionAllowsInsecureHTTPLoads</key><true/></dict>
|
|
20
19
|
</dict>
|
|
21
|
-
</dict>`,
|
|
22
|
-
<key>NSAppTransportSecurity</key><dict><key>NSAllowsArbitraryLoads</key><true/></dict>`,
|
|
23
|
-
`,
|
|
24
|
-
`,
|
|
25
|
-
|
|
26
|
-
additionally permit every unencrypted host on the internet. -->
|
|
27
|
-
<key>NSAppTransportSecurity</key>
|
|
28
|
-
<dict>
|
|
29
|
-
<key>NSAllowsLocalNetworking</key><true/>
|
|
30
|
-
<key>NSExceptionDomains</key>
|
|
31
|
-
<dict>
|
|
32
|
-
<key>127.0.0.1</key>
|
|
33
|
-
<dict><key>NSExceptionAllowsInsecureHTTPLoads</key><true/></dict>
|
|
34
|
-
</dict>
|
|
35
|
-
</dict>`:` <!-- The window loads a remote URL, so the app has to be allowed to reach it. -->
|
|
36
|
-
<key>NSAppTransportSecurity</key><dict><key>NSAllowsArbitraryLoads</key><true/></dict>`,F=e.iconFile?`
|
|
37
|
-
<key>CFBundleIconFile</key><string>${c(e.iconFile)}</string>`:"",w=c(e.appName),b=c(e.version);return{xml:`<?xml version="1.0" encoding="UTF-8"?>
|
|
20
|
+
</dict>`,remoteTransportSecurity=` <!-- The window loads a remote URL, so the app has to be allowed to reach it. -->
|
|
21
|
+
<key>NSAppTransportSecurity</key><dict><key>NSAllowsArbitraryLoads</key><true/></dict>`,minimumSystem=overridden("LSMinimumSystemVersion")?"":` <key>LSMinimumSystemVersion</key><string>11.0</string>
|
|
22
|
+
`,highResolution=overridden("NSHighResolutionCapable")?"":` <key>NSHighResolutionCapable</key><true/>
|
|
23
|
+
`,transportSecurity=overridden("NSAppTransportSecurity")?"":options.ownsLauncher?loopbackTransportSecurity:remoteTransportSecurity,icon=options.iconFile?`
|
|
24
|
+
<key>CFBundleIconFile</key><string>${xmlEscape(options.iconFile)}</string>`:"",name=xmlEscape(options.appName),version=xmlEscape(options.version);return{xml:`<?xml version="1.0" encoding="UTF-8"?>
|
|
38
25
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
39
26
|
<plist version="1.0">
|
|
40
27
|
<dict>
|
|
41
|
-
<key>CFBundleName</key><string>${
|
|
42
|
-
<key>CFBundleDisplayName</key><string>${
|
|
43
|
-
<key>CFBundleIdentifier</key><string>${
|
|
44
|
-
<key>CFBundleVersion</key><string>${
|
|
45
|
-
<key>CFBundleShortVersionString</key><string>${
|
|
46
|
-
<key>CFBundleExecutable</key><string>${
|
|
28
|
+
<key>CFBundleName</key><string>${name}</string>
|
|
29
|
+
<key>CFBundleDisplayName</key><string>${name}</string>
|
|
30
|
+
<key>CFBundleIdentifier</key><string>${xmlEscape(options.bundleId)}</string>
|
|
31
|
+
<key>CFBundleVersion</key><string>${version}</string>
|
|
32
|
+
<key>CFBundleShortVersionString</key><string>${version}</string>
|
|
33
|
+
<key>CFBundleExecutable</key><string>${xmlEscape(options.executable)}</string>
|
|
47
34
|
<key>CFBundlePackageType</key><string>APPL</string>
|
|
48
35
|
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
|
49
|
-
${
|
|
36
|
+
${minimumSystem}${highResolution}${transportSecurity}${icon}${extra}
|
|
50
37
|
</dict>
|
|
51
38
|
</plist>
|
|
52
|
-
`,ignored
|
|
39
|
+
`,ignored}}export function resolveDevWindowUrl(port,options={}){if(!Number.isInteger(port)||port<1||port>65535)throw RangeError(`Invalid desktop development port: ${port}`);const configured=options.url||process.env.APP_URL;if(!configured)return"https://stacks.test";const withProtocol=/^https?:\/\//.test(configured)?configured:`https://${configured}`;return new URL(withProtocol).toString().replace(/\/$/,"")}export function craftDevCommand(port,options={}){const command=[resolveCraftBinary(options.craftBin),resolveDevWindowUrl(port,options),"--title",options.title||"Stacks","--width",String(options.width||1400),"--height",String(options.height||900)];if(options.hotReload!==!1)command.push("--hot-reload");if(options.devTools===!1)command.push("--no-devtools");if(options.darkMode)command.push("--dark");if(options.systemTray)command.push("--system-tray");if(options.hideDockIcon)command.push("--hide-dock-icon");if(options.menubarOnly)command.push("--menubar-only");return command}export function createUpdateManifestUrl(baseUrl,channel="stable"){if(!/^[a-z0-9-]+$/i.test(channel))throw Error("Update channel may only contain letters, numbers, and hyphens");return new URL(`/desktop/updates/${channel}.json`,/^https?:\/\//.test(baseUrl)?baseUrl:`https://${baseUrl}`).toString()}async function launchCraft(command){Bun.spawn(command,{stdin:"inherit",stdout:"inherit",stderr:"inherit"}).unref()}export async function openDevWindow(port,options={},launcher=launchCraft){await launcher(craftDevCommand(port,options));return!0}export const BUN_EXECUTABLE_MARKER="---- Bun!";const MARKER_SEARCH_BYTES=8388608,RUNTIME_SIZE_FLOOR=41943040,MACH_O_MAGIC=new Set([4277009102,3472551422,4277009103,3489328638,3405691582,3199925962]);export function isMachO(path){try{const fd=openSync(path,"r");try{const head=Buffer.alloc(4);if(readSync(fd,head,0,4,0)<4)return!1;return MACH_O_MAGIC.has(head.readUInt32BE(0))}finally{closeSync(fd)}}catch{return!1}}export function bundleDirectoryFor(path){return isMachO(path)?"MacOS":"Resources"}export function desktopAppVersion(options={}){const override=options.override?.trim();if(override)return override;return(typeof options.declared==="string"?options.declared.trim():"")||"0.0.0"}export function dmgVolumeName(options){const override=options.override?.trim();if(override)return override;const appName=options.appName.trim(),version=options.version?.trim();return version?`${appName} ${version}`:`${appName} Installer`}function supplied(value){if(typeof value!=="string")return;return value.trim()||void 0}export function resolveDmgPackaging(inputs={}){const flags=inputs.flags??{},env=inputs.env??{},appName=supplied(flags.appName)??supplied(env.DESKTOP_APP_NAME)??supplied(inputs.manifestTitle)??supplied(env.APP_NAME)??"Stacks",version=desktopAppVersion({override:supplied(flags.appVersion)??supplied(env.DESKTOP_APP_VERSION),declared:inputs.declaredVersion}),bundleId=supplied(flags.bundleId)??supplied(env.DESKTOP_BUNDLE_ID)??`sh.stacks.${appName.toLowerCase().replace(/[^a-z0-9]+/g,"-")}`;return{appName,bundleId,version,volumeName:dmgVolumeName({appName,version,override:supplied(flags.volumeName)??supplied(env.DESKTOP_VOLUME_NAME)}),signingIdentity:supplied(flags.signingIdentity)??supplied(env.DESKTOP_SIGNING_IDENTITY),notaryProfile:supplied(flags.notaryProfile)??supplied(env.DESKTOP_NOTARY_PROFILE)}}export function codesignArgs(options){return["codesign","--force",options.identity==="-"?"--timestamp=none":"--timestamp","--options","runtime",...options.entitlements?["--entitlements",options.entitlements]:[],"--sign",options.identity,options.target]}export function looksLikeBunExecutable(path,read=readTail){const file=read(path);if(!file||file.size<RUNTIME_SIZE_FLOOR)return!1;return file.tail.includes(BUN_EXECUTABLE_MARKER)}function readTail(path){try{const{size}=statSync(path);if(size<RUNTIME_SIZE_FLOOR)return{size,tail:""};const fd=openSync(path,"r");try{const length=Math.min(MARKER_SEARCH_BYTES,size),buffer=Buffer.alloc(length);readSync(fd,buffer,0,length,size-length);return{size,tail:buffer.toString("latin1")}}finally{closeSync(fd)}}catch{return null}}export function describeRuntimeDuplication(runtimes){if(runtimes.length<2)return null;const sorted=[...runtimes].sort((a,b)=>b.bytes-a.bytes),wasted=sorted.slice(1).length*RUNTIME_SIZE_FLOOR,names=sorted.map((r)=>`${r.name} (${(r.bytes/1024/1024).toFixed(1)} MB)`).join(", ");return`This bundle ships ${runtimes.length} Bun-compiled executables: ${names}. Each embeds a full copy of the Bun runtime, so at least ${(wasted/1024/1024).toFixed(0)} MB of the bundle is the same runtime repeated. `+"Compiling one binary that dispatches on a subcommand \u2014 `MyApp agent`, `MyApp worker` \u2014 keeps the helpers as separate processes and pays for the runtime once."}
|
package/dist/invites.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function createInviteLink(baseUrl,token,options={}){if(!token.trim())throw Error("Invite token is required");const url=new URL("/invite",/^https?:\/\//.test(baseUrl)?baseUrl:`https://${baseUrl}`);url.searchParams.set("token",token);if(options.email)url.searchParams.set("email",options.email);if(options.team)url.searchParams.set("team",options.team);if(options.role)url.searchParams.set("role",options.role);if(options.expiresAt){const date=options.expiresAt instanceof Date?options.expiresAt:new Date(options.expiresAt);if(Number.isNaN(date.valueOf()))throw Error("Invite expiration must be a valid date");url.searchParams.set("expires",date.toISOString())}return url.toString()}export async function sendDesktopInvites(recipients,options){return Promise.all(recipients.map(async(input)=>{const recipient=typeof input==="string"?{email:input}:input;try{assertEmail(recipient.email);const token=await options.createToken(recipient),url=createInviteLink(options.baseUrl,token,{email:recipient.email,team:recipient.team,role:recipient.role,expiresAt:options.expiresAt});await options.send({recipient,url});return{recipient,url,delivered:!0}}catch(error){return{recipient,url:"",delivered:!1,error:error instanceof Error?error.message:String(error)}}}))}function assertEmail(email){if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))throw Error(`Invalid invite email: ${email}`)}
|
package/dist/launcher.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import{existsSync as s,readFileSync as d}from"fs";import{dirname as c,join as e}from"path";var o=c(process.execPath),n=[e(o,"..","Resources","desktop.json"),e(o,"desktop.json")].find((a)=>s(a))??e(o,"desktop.json"),r=e(o,process.platform==="win32"?"craft-runtime.exe":"craft-runtime");if(!s(n))throw Error(`Desktop manifest not found: ${n}`);if(!s(r))throw Error(`Craft runtime not found: ${r}`);var t=JSON.parse(d(n,"utf8")),i=[r,t.url,"--title",t.title,"--width",String(t.width),"--height",String(t.height),"--no-devtools"];if(t.darkMode)i.push("--dark");if(t.systemTray)i.push("--system-tray");if(t.hideDockIcon)i.push("--hide-dock-icon");var h=Bun.spawn(i,{stdin:"inherit",stdout:"inherit",stderr:"inherit"});process.exit(await h.exited);
|
|
1
|
+
import{existsSync,readFileSync}from"node:fs";import{dirname,join}from"node:path";const bundleDir=dirname(process.execPath),manifestPath=[join(bundleDir,"..","Resources","desktop.json"),join(bundleDir,"desktop.json")].find((candidate)=>existsSync(candidate))??join(bundleDir,"desktop.json"),craftPath=join(bundleDir,process.platform==="win32"?"craft-runtime.exe":"craft-runtime");if(!existsSync(manifestPath))throw Error(`Desktop manifest not found: ${manifestPath}`);if(!existsSync(craftPath))throw Error(`Craft runtime not found: ${craftPath}`);const manifest=JSON.parse(readFileSync(manifestPath,"utf8")),command=[craftPath,manifest.url,"--title",manifest.title,"--width",String(manifest.width),"--height",String(manifest.height),"--no-devtools"];if(manifest.darkMode)command.push("--dark");if(manifest.systemTray)command.push("--system-tray");if(manifest.hideDockIcon)command.push("--hide-dock-icon");const child=Bun.spawn(command,{stdin:"inherit",stdout:"inherit",stderr:"inherit"});process.exit(await child.exited);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function escapeScript(source){return source.replaceAll("</script","<\\/script")}export function probePageHtml(script,config){return`<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Stacks interactive probe</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; --bg: #0a0d14; --panel: #141a26; --text: #e6e9f0; --muted: #9aa3b5; --accent: #5b8cf2; }
|
|
9
|
+
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
10
|
+
#scene { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
|
|
11
|
+
#hud { position: fixed; left: 16px; right: 16px; top: 16px; padding: 12px 16px; background: color-mix(in srgb, var(--panel) 88%, transparent); border-radius: 10px; }
|
|
12
|
+
#hud h1 { margin: 0 0 4px; font-size: 15px; font-weight: 600; }
|
|
13
|
+
#status { color: var(--muted); margin: 0; }
|
|
14
|
+
#interactive { position: fixed; left: 16px; right: 16px; bottom: 16px; padding: 16px; background: var(--panel); border-radius: 10px; }
|
|
15
|
+
#interactive[hidden] { display: none; }
|
|
16
|
+
#interactive p { margin: 0 0 12px; }
|
|
17
|
+
#interactive ol { margin: 0 0 12px; padding-left: 20px; color: var(--muted); }
|
|
18
|
+
button { font: inherit; padding: 8px 14px; margin-right: 8px; border-radius: 8px; border: 1px solid var(--accent); background: transparent; color: var(--text); cursor: pointer; }
|
|
19
|
+
button[disabled] { opacity: 0.4; cursor: default; }
|
|
20
|
+
#countdown { color: var(--muted); margin-top: 10px; }
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<canvas id="scene"></canvas>
|
|
25
|
+
<div id="hud">
|
|
26
|
+
<h1>Stacks interactive-content probe</h1>
|
|
27
|
+
<p id="status">Starting</p>
|
|
28
|
+
</div>
|
|
29
|
+
<div id="interactive" hidden>
|
|
30
|
+
<p>These probes need a real person. Synthetic events are ignored, so skipping them records them as needing interaction rather than inventing a number.</p>
|
|
31
|
+
<ol>
|
|
32
|
+
<li>Click <strong>Fullscreen + audio</strong>. It enters fullscreen briefly and resumes audio.</li>
|
|
33
|
+
<li>Click <strong>Pointer lock</strong>. It locks the pointer for half a second.</li>
|
|
34
|
+
<li>Press any key five times, and move the mouse over the window.</li>
|
|
35
|
+
</ol>
|
|
36
|
+
<button id="fullscreen-button" type="button">Fullscreen + audio</button>
|
|
37
|
+
<button id="lock-button" type="button">Pointer lock</button>
|
|
38
|
+
<button id="done-button" type="button">Done</button>
|
|
39
|
+
<div id="countdown"></div>
|
|
40
|
+
</div>
|
|
41
|
+
<script type="application/json" id="probe-config">${JSON.stringify(config).replaceAll("<","\\u003c")}</script>
|
|
42
|
+
<script type="module">${escapeScript(script)}</script>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
`}
|
package/dist/probe/index.js
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
2
|
-
var re={idleMs:3000,loadMs:6000,cpuLoadMsPerFrame:6,instances:40000,interactionWindowMs:20000,tryNativeFullscreen:!0};var k=1,M=["available","unavailable","measured","requires-interaction","denied","error"],f=[["webgl2","WebGL2"],["webgpu","WebGPU"],["framePacing","Frame pacing"],["inputLatency","Input latency"],["audioLatency","Audio latency"],["gamepad","Gamepad API"],["fullscreen","Fullscreen"],["pointerLock","Pointer lock"]];function l(t){return typeof t==="object"&&t!==null&&!Array.isArray(t)}function m(t){return typeof t==="string"&&t.trim().length>0}function T(t){let e=[];if(!l(t))return["record is not an object"];if(t.schema!==1)e.push(`schema must be 1, got ${JSON.stringify(t.schema)}`);if(!m(t.measuredAt)||Number.isNaN(Date.parse(t.measuredAt)))e.push("measuredAt must be an ISO 8601 timestamp");let n=t.host;if(!l(n))e.push("host is missing");else{if(n.kind!=="craft"&&n.kind!=="browser")e.push(`host.kind must be craft or browser, got ${JSON.stringify(n.kind)}`);for(let s of["name","version","userAgent"])if(!m(n[s]))e.push(`host.${s} is required`);if(n.kind==="craft"&&m(n.version)&&!/^\d+\.\d+\.\d+/.test(n.version))e.push(`host.version must be a Craft release version, got ${JSON.stringify(n.version)}`)}let r=t.os;if(!l(r))e.push("os is missing");else for(let s of["platform","arch","name","version"])if(!m(r[s]))e.push(`os.${s} is required`);let o=t.machine;if(!l(o))e.push("machine is missing");else{for(let u of["model","cpu"])if(!m(o[u]))e.push(`machine.${u} is required`);let s=o.display;if(!l(s))e.push("machine.display is missing");else{if(s.refreshHz!==null&&!(typeof s.refreshHz==="number"&&s.refreshHz>0))e.push("machine.display.refreshHz must be a positive number or null");if(!(typeof s.devicePixelRatio==="number"&&s.devicePixelRatio>0))e.push("machine.display.devicePixelRatio must be a positive number")}}let a=t.run;if(!l(a))e.push("run is missing");else{for(let s of["idleMs","loadMs","cpuLoadMsPerFrame","triangles","interactionWindowMs"])if(!(typeof a[s]==="number"&&a[s]>=0))e.push(`run.${s} must be a non-negative number`);if(typeof a.sawRealInput!=="boolean")e.push("run.sawRealInput must be a boolean")}let i=t.probes;if(!l(i))return e.push("probes is missing"),e;for(let[s]of f){let u=i[s];if(!l(u)){e.push(`probes.${s} is missing`);continue}if(!M.includes(u.status))e.push(`probes.${s}.status must be one of ${M.join(", ")}, got ${JSON.stringify(u.status)}`);if(!m(u.note))e.push(`probes.${s}.note is required: say what was observed, even on success`);if(u.data!==void 0&&!l(u.data))e.push(`probes.${s}.data must be an object when present`);if(u.status==="measured"&&!l(u.data))e.push(`probes.${s} is measured but carries no data`)}for(let s of Object.keys(i))if(!f.some(([u])=>u===s))e.push(`probes.${s} is not a known probe`);return e}function S(t,e="probe record"){let n=T(t);if(n.length>0)throw Error(`${e} is invalid:
|
|
3
|
-
- ${n.join(`
|
|
4
|
-
- `)}`);return t}var C=[24,30,48,50,60,72,75,90,100,120,144,165,240];function d(t,e){if(t.length===0)return null;if(!Number.isFinite(e)||e<0||e>100)throw RangeError(`percentile must be between 0 and 100, got ${e}`);let n=[...t].sort((u,c)=>u-c),r=e/100*(n.length-1),o=Math.floor(r),a=Math.ceil(r),i=n[o],s=n[a];return i+(s-i)*(r-o)}function R(t){let e=[];for(let n=1;n<t.length;n++)e.push(t[n]-t[n-1]);return e}function H(t,e=1,n=50){if(e<=0||!Number.isFinite(e))throw RangeError(`bucketMs must be positive, got ${e}`);if(!Number.isInteger(n)||n<1)throw RangeError(`buckets must be a positive integer, got ${n}`);let r=Array.from({length:n},()=>0),o=0;for(let a of t){let i=Math.floor(Math.max(0,a)/e);if(i>=n)o++;else r[i]++}return{bucketMs:e,counts:r,overflow:o}}function A(t,e=0.03){if(!(t>0))throw RangeError(`period must be positive, got ${t}`);let n=1000/t,r=n,o=Number.POSITIVE_INFINITY;for(let a of C){let i=Math.abs(n-a)/a;if(i<o)r=a,o=i}return o<=e?r:Math.round(n*100)/100}function q(t,e){if(!(e>0))throw RangeError(`period must be positive, got ${e}`);let n=0;for(let r of t)n+=Math.max(0,Math.round(r/e)-1);return n}function h(t,e){if(t.length<3)return null;if(!(e>0))throw RangeError(`targetHz must be positive, got ${e}`);let n=R(t),r=t[t.length-1]-t[0],o=1000/e,a=(i)=>Math.round(i*100)/100;return{frames:t.length,durationMs:a(r),fps:a(r>0?n.length*1000/r:0),p50Ms:a(d(n,50)),p95Ms:a(d(n,95)),p99Ms:a(d(n,99)),maxMs:a(Math.max(...n)),targetPeriodMs:a(o),targetHz:e,droppedFrames:q(n,o),longFrames:n.filter((i)=>i>o*1.5).length,histogram:H(n)}}function w(t){let e=t.filter((r)=>Number.isFinite(r)&&r>=0);if(e.length===0)return null;let n=(r)=>Math.round(r*100)/100;return{samples:e.length,p50Ms:n(d(e,50)),p95Ms:n(d(e,95)),maxMs:n(Math.max(...e))}}function ae(t){let e=t?.match(/@\s*([\d.]+)\s*Hz/i);if(!e)return null;let n=Number(e[1]);return Number.isFinite(n)&&n>0?n:null}function le(t){let e=[["Edge",/Edg\/([\d.]+)/],["Firefox",/Firefox\/([\d.]+)/],["Chrome",/Chrome\/([\d.]+)/],["Safari",/Version\/([\d.]+).*Safari\//],["WebKit",/AppleWebKit\/([\d.]+)/]];for(let[n,r]of e){let o=t.match(r);if(o)return{name:n,version:o[1]}}return{name:"unknown",version:"unknown"}}function F(t){return typeof t==="number"?`${Math.round(t*1e4)/10} ms`:"not reported"}function g(t){return t.detail?`${t.outcome} (${t.detail})`:t.outcome}function _(t){let e=t.webgl2;if(!e.ok)return{status:"unavailable",note:`No WebGL2 context: ${e.error??"unknown reason"}.`,data:{...e}};let n=e.unmaskedRenderer||e.renderer;return{status:"available",note:`WebGL2 context created (${e.version}); renderer "${n}", max texture ${e.maxTextureSize}, MSAA up to ${e.maxSamples}x.`,data:{...e}}}function z(t){let e=t.webgpu;if(!e.present)return{status:"unavailable",note:"navigator.gpu is undefined.",data:{...e}};if(!e.adapter||!e.device)return{status:"unavailable",note:`navigator.gpu exists but ${e.adapter?"requestDevice()":"requestAdapter()"} failed: ${e.error??"no reason given"}.`,data:{...e}};return{status:"available",note:`Adapter (${[...new Set([e.vendor,e.architecture].filter(Boolean))].join(" ")||"unnamed adapter"}) and device created; maxTextureDimension2D ${e.maxTextureDimension2D??"unknown"}.`,data:{...e}}}function j(t,e){let{frames:n,config:r}=t;if(n.idle.length<3||n.load.length<3)return{status:"error",note:`Too few animation frames to measure (${n.idle.length} idle, ${n.load.length} under load).`};let o=A(d(R(n.idle),50)),a=e.machine.display.refreshHz??o,i=h(n.idle,a),s=h(n.load,a),u=h(n.load,o),c={rafHz:o,displayHz:a,timerResolutionMs:t.timerResolutionMs,triangles:n.triangles,cpuLoadMsPerFrame:r.cpuLoadMsPerFrame,hiddenDuringSampling:n.hiddenDuringSampling,idle:i,load:s,loadAtRafRate:{droppedFrames:u.droppedFrames,longFrames:u.longFrames}};if(n.hiddenDuringSampling)return{status:"error",note:"The page was hidden while sampling, which throttles requestAnimationFrame; these numbers are not representative. Re-run with the window visible.",data:c};let P=o<a-1?`requestAnimationFrame ran at ${o} Hz on a ${a} Hz display, so a game cannot reach the panel's rate. `:`requestAnimationFrame ran at the display's ${a} Hz. `,b=n.triangles>0?`${n.triangles.toLocaleString("en-US")} triangles`:"a 2D canvas fallback";return{status:"measured",note:`${P}Under load (${b} + ${r.cpuLoadMsPerFrame} ms CPU per frame): p50 ${s.p50Ms} ms, p95 ${s.p95Ms} ms, p99 ${s.p99Ms} ms; ${u.droppedFrames} frames dropped at ${o} Hz and ${s.droppedFrames} at the display's ${a} Hz over ${Math.round(s.durationMs/100)/10} s.${t.timerResolutionMs>=0.5?` The page clock is clamped to ${t.timerResolutionMs} ms, so intervals read as whole milliseconds.`:""}`,data:c}}var E=5;function D(t){let{input:e}=t,n={eventTimeStampComparable:t.eventTimeStampComparable,eventTimingSupported:e.eventTimingSupported,timerResolutionMs:t.timerResolutionMs};if(!t.eventTimeStampComparable)return{status:"error",note:"event.timeStamp does not share performance.now()'s time origin, so event-to-frame time cannot be computed.",data:n};let r={pointerdown:e.pointerdown,pointermove:e.pointermove,keydown:e.keydown},o=Object.values(r).reduce((s,u)=>s+u.length,0);if(o===0)return{status:"requires-interaction",note:`${t.interaction.skipped?"The interactive window was skipped":`No trusted input reached the page in the ${t.interaction.windowMs/1000} s window`}, and synthetic events are not counted. Observable: event.timeStamp is on the performance.now() clock, timer resolution ${t.timerResolutionMs} ms, Event Timing API ${e.eventTimingSupported?"supported":"not supported"}.`,data:n};let a={...n},i=[];for(let[s,u]of Object.entries(r)){let c=w(u.map((b)=>b.toFrameMs)),P=w(u.map((b)=>b.toPostFrameMs));if(a[s]={toFrame:c,toPostFrame:P},c)i.push(`${s} p50 ${c.p50Ms} ms to next frame (${c.samples} ${c.samples===1?"sample":"samples"})`)}if(a.eventTiming=w(e.eventTimingDurations),o<E)return{status:"requires-interaction",note:`Only ${o} trusted input ${o===1?"event":"events"} arrived, fewer than the ${E} needed to summarise. Seen: ${i.join("; ")}. Re-run and use the panel's buttons and keys.`,data:a};return{status:"measured",note:`Event timestamp to the next rAF callback: ${i.join("; ")}. Real input from the OS; no synthetic events.`,data:a}}function B(t){let e=t.audio;if(!e.present)return{status:"unavailable",note:"window.AudioContext is undefined."};if(e.error||!e.initial)return{status:"error",note:`Creating an AudioContext failed: ${e.error??"unknown reason"}.`};let n=e.afterGesturedResume??e.afterUngesturedResume??e.initial,r={initial:e.initial,afterUngesturedResume:e.afterUngesturedResume??null,afterGesturedResume:e.afterGesturedResume??null},o=`baseLatency ${F(n.baseLatency)}, outputLatency ${F(n.outputLatency)} at ${n.sampleRate} Hz`,a=e.afterUngesturedResume?.state==="running"?"The context started without a user gesture":"The context stayed suspended without a user gesture";if(n.state!=="running")return{status:"requires-interaction",note:`${a}, so device latency was not observable. Reported before running: ${o}.`,data:r};if(n.baseLatency===null&&n.outputLatency===null)return{status:"unavailable",note:`${a}, but neither baseLatency nor outputLatency is implemented.`,data:r};return{status:"measured",note:`${a}. ${o}${n.outputLatency===null?" (outputLatency not implemented)":""}.`,data:r}}function U(t){let e=t.gamepad;if(!e.present)return{status:"unavailable",note:"navigator.getGamepads is undefined."};return{status:"available",note:e.connected>0?`navigator.getGamepads() is present and reported ${e.connected} controller(s).`:"navigator.getGamepads() is present; no controller was connected, so axis and button input was not exercised.",data:{...e,secureContext:t.isSecureContext}}}function G(t){let e=t.fullscreen,n=e.nativeBridge.outcome==="not-attempted"||e.nativeBridge.outcome==="no-api"?"":` Craft's own window.craft.window.setFullscreen(true), which needs no gesture: ${g(e.nativeBridge)}.`,r={...e};if(!e.elementApi&&!e.webkitElementApi)return{status:"unavailable",note:`Element.requestFullscreen and webkitRequestFullscreen are both undefined, so the standard Fullscreen API engines call does nothing.${n}`,data:r};let o=`Without a gesture: ${g(e.ungestured)}.`;switch(e.gestured.outcome){case"entered":return{status:"available",note:`requestFullscreen() from a real click entered fullscreen. ${o}${n}`,data:r};case"rejected":case"timeout":return{status:"denied",note:`requestFullscreen() from a real click: ${g(e.gestured)}. ${o}${n}`,data:r};default:return{status:"requires-interaction",note:`The API exists (fullscreenEnabled ${String(e.enabled)}) but needs a real click to test. ${o}${n}`,data:r}}}function W(t){let e=t.pointerLock,n={...e};if(!e.api)return{status:"unavailable",note:"Element.requestPointerLock is undefined.",data:n};let r=`Without a gesture: ${g(e.ungestured)}.`;switch(e.gestured.outcome){case"entered":return{status:"available",note:`requestPointerLock() from a real click locked the pointer. ${r}`,data:n};case"rejected":case"timeout":return{status:"denied",note:`requestPointerLock() from a real click: ${g(e.gestured)}. ${r}`,data:n};default:return{status:"requires-interaction",note:`The API exists but needs a real click to test. ${r}`,data:n}}}function de(t,e){return{schema:k,measuredAt:e.measuredAt,host:{...e.host,userAgent:t.userAgent},os:e.os,machine:{model:e.machine.model,cpu:e.machine.cpu,display:{...e.machine.display,devicePixelRatio:t.devicePixelRatio}},run:{idleMs:t.config.idleMs,loadMs:t.config.loadMs,cpuLoadMsPerFrame:t.config.cpuLoadMsPerFrame,triangles:t.frames.triangles,interactionWindowMs:t.interaction.windowMs,sawRealInput:t.interaction.sawRealInput},probes:{webgl2:_(t),webgpu:z(t),framePacing:j(t,e),inputLatency:D(t),audioLatency:B(t),gamepad:U(t),fullscreen:G(t),pointerLock:W(t)}}}import{readdirSync as V,readFileSync as J}from"fs";import{join as K}from"path";import{fileURLToPath as Y}from"url";var Q=Y(new URL("./results",import.meta.url));function v(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function he(t){return`${t.host.kind==="craft"?"craft":`browser-${v(t.host.name)}`}-${v(t.os.platform)}-${v(t.os.arch)}.json`}function we(t=Q){let e;try{e=V(t).filter((n)=>n.endsWith(".json")).sort()}catch{return[]}return e.map((n)=>{let r=K(t,n);return S(JSON.parse(J(r,"utf8")),r)})}var X="<!-- interactive-capability:begin -->",I="<!-- interactive-capability:end -->",y="buddy desktop:probe",Z={darwin:"macOS",linux:"Linux",win32:"Windows"};function L(t){return t.host.kind==="craft"?`Craft ${t.host.version}`:`${t.host.name} ${t.host.version}`}function N(t){return`${Z[t.os.platform]??t.os.name} ${t.os.version} ${t.os.arch}, ${t.machine.cpu} (${t.machine.model})`}function ee(t){return[...t].sort((e,n)=>{if(e.host.kind!==n.host.kind)return e.host.kind==="craft"?-1:1;return`${e.host.name} ${e.os.platform} ${e.os.arch}`.localeCompare(`${n.host.name} ${n.os.platform} ${n.os.arch}`)})}function p(t){return typeof t==="number"&&Number.isFinite(t)?t:null}function O(t,...e){let n=t;for(let r of e){if(typeof n!=="object"||n===null)return;n=n[r]}return n}function te(t,e){let n=e.data;if(e.status!=="measured"||!n)return null;switch(t){case"framePacing":{let r=p(n.rafHz),o=p(n.displayHz),a=p(O(n,"load","p99Ms"));if(r===null||o===null||a===null)return null;return`${r} Hz rAF on a ${o} Hz display, p99 ${a} ms under load`}case"inputLatency":{for(let r of["pointerdown","keydown","pointermove"]){let o=p(O(n,r,"toFrame","p50Ms"));if(o!==null)return`${r} p50 ${o} ms to next frame`}return null}case"audioLatency":{let r=n.afterGesturedResume??n.afterUngesturedResume,o=p(r?.baseLatency),a=p(r?.outputLatency),i=(s)=>s===null?"n/a":`${Math.round(s*1e4)/10} ms`;return`base ${i(o)}, output ${i(a)}`}default:return null}}function ne(t,e){if(!e)return"not probed";let n=te(t,e);return n?`**${e.status}** - ${n}`:`**${e.status}**`}function x(t){return t.replaceAll("|","\\|").replaceAll(`
|
|
5
|
-
`," ")}function Re(t){let e=ee(t),n=[X,"","<!-- Generated by `buddy docs:capabilities` from the records in"," storage/framework/core/desktop/src/probe/results/. Re-measure with",` \`${y} --record\`; do not edit this by hand. -->`,"","Whether a Craft window can host a game or other frame-driven content that somebody else","builds, measured by the probe harness in `@stacksjs/desktop-build`","([stacksjs/stacks#877](https://github.com/stacksjs/stacks/issues/877)). Ordinary browsers","on the same machine are measured alongside it for comparison.","","> **How current is this?** Unlike the driver tables above, these are measurements, not","> claims CI can re-check. Each column is one run on one machine, and says when, against","> which host version, on which OS and hardware. If your Craft is newer than the column,",`> the numbers describe something you are not running: re-measure with \`${y} --record\`.`,""];if(e.length===0)return n.push(`No measurement has been recorded yet. Run \`${y} --record\`.`,"",I),n.join(`
|
|
6
|
-
`);n.push(`| Probe | ${e.map((r)=>x(L(r))).join(" | ")} |`,`|---|${e.map(()=>"---").join("|")}|`);for(let[r,o]of f)n.push(`| ${o} | ${e.map((a)=>x(ne(r,a.probes[r]))).join(" | ")} |`);n.push(`| Measured on | ${e.map((r)=>x(N(r))).join(" | ")} |`,`| Measured at | ${e.map((r)=>r.measuredAt.slice(0,10)).join(" | ")} |`,"","- **available** / **measured** - the API exists and the probe exercised it.","- **unavailable** - the API is missing, or creating it failed.","- **requires-interaction** - needs a real click, key press or controller that the run did not have. What was still observable is in the notes; no number is invented.","- **denied** - requested from a real click and refused.","");for(let r of e){let o=r.machine.display.refreshHz===null?"unknown refresh rate":`${r.machine.display.refreshHz} Hz display`;n.push(`**${L(r)}** - ${N(r)}, ${o}`,"");for(let[a,i]of f){let s=r.probes[a];if(s)n.push(`- **${i}** - ${s.note}`)}n.push("")}return n.push("**Out of scope.** Stacks is not shipping a game engine, a physics engine, an audio mixer or","an asset pipeline. This section establishes what a Craft window gives an engine somebody","else wrote; it is not a plan to write one.","",I),n.join(`
|
|
7
|
-
`)}export{C as COMMON_REFRESH_RATES,re as DEFAULT_PROBE_PAGE_CONFIG,X as INTERACTIVE_BEGIN,I as INTERACTIVE_END,E as MIN_INPUT_SAMPLES,y as PROBE_COMMAND,f as PROBE_NAMES,Q as PROBE_RESULTS_DIR,k as PROBE_SCHEMA_VERSION,M as PROBE_STATUSES,S as assertProbeRecord,le as browserFromUserAgent,de as buildProbeRecord,q as droppedFrames,h as framePacing,te as headline,H as histogram,R as intervals,w as latency,we as loadProbeRecords,ee as orderRecords,ae as parseRefreshHz,d as percentile,he as probeRecordFileName,L as recordLabel,Re as renderInteractiveCapabilities,A as snapRefreshRate,T as validateProbeRecord};
|
|
1
|
+
export*from"./payload";export*from"./record";export*from"./records";export*from"./render";export*from"./schema";export*from"./stats";
|
package/dist/probe/page.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
var D=document.getElementById("probe-config"),l=JSON.parse(D?.textContent||"{}"),C=document.getElementById("status"),k=document.getElementById("interactive"),d=document.getElementById("scene"),A=[];function g(t){C.textContent=t,fetch("/progress",{method:"POST",body:t}).catch(()=>{})}function f(t){if(t instanceof Error)return`${t.name}: ${t.message}`;return String(t)}function w(t){return new Promise((e)=>setTimeout(e,t))}function T(){return new Promise((t)=>requestAnimationFrame(t))}function P(t,e,n){return Promise.race([t,w(e).then(()=>n)])}function q(){let t=Number.POSITIVE_INFINITY,e=performance.now(),n=e+50;while(e<n){let r=performance.now(),o=r-e;if(o>0&&o<t)t=o;e=r}return Math.round(t*1000)/1000}function _(){let t=new Event("probe");return Math.abs(t.timeStamp-performance.now())<1e4}function O(){try{let t=document.createElement("canvas").getContext("webgl2");if(!t)return{ok:!1,error:'getContext("webgl2") returned null'};let e=t.getExtension("WEBGL_debug_renderer_info");return{ok:!0,version:String(t.getParameter(t.VERSION)),shadingLanguageVersion:String(t.getParameter(t.SHADING_LANGUAGE_VERSION)),vendor:String(t.getParameter(t.VENDOR)),renderer:String(t.getParameter(t.RENDERER)),unmaskedVendor:e?String(t.getParameter(e.UNMASKED_VENDOR_WEBGL)):void 0,unmaskedRenderer:e?String(t.getParameter(e.UNMASKED_RENDERER_WEBGL)):void 0,maxTextureSize:Number(t.getParameter(t.MAX_TEXTURE_SIZE)),maxSamples:Number(t.getParameter(t.MAX_SAMPLES)),extensions:t.getSupportedExtensions()?.length??0}}catch(t){return{ok:!1,error:f(t)}}}async function G(){let t=navigator.gpu;if(!t)return{present:!1,adapter:!1,device:!1};try{let e=await P(t.requestAdapter(),5000,null);if(!e)return{present:!0,adapter:!1,device:!1,error:"requestAdapter() resolved null or timed out"};let n=e.info??{},r=!1,o;try{let i=await P(e.requestDevice(),5000,null);if(r=i!==null,i?.destroy?.(),!r)o="requestDevice() timed out"}catch(i){o=f(i)}return{present:!0,adapter:!0,device:r,error:o,vendor:n.vendor||void 0,architecture:n.architecture||void 0,description:n.description||void 0,maxTextureDimension2D:e.limits?.maxTextureDimension2D,features:e.features?.size}}catch(e){return{present:!0,adapter:!1,device:!1,error:f(e)}}}var N=`#version 300 es
|
|
1
|
+
const configElement=document.getElementById("probe-config"),config=JSON.parse(configElement?.textContent||"{}"),statusElement=document.getElementById("status"),panel=document.getElementById("interactive"),canvas=document.getElementById("scene"),errors=[];function say(message){statusElement.textContent=message;fetch("/progress",{method:"POST",body:message}).catch(()=>{})}function errorText(error){if(error instanceof Error)return`${error.name}: ${error.message}`;return String(error)}function sleep(ms){return new Promise((resolve)=>setTimeout(resolve,ms))}function nextFrame(){return new Promise((resolve)=>requestAnimationFrame(resolve))}function withTimeout(work,ms,fallback){return Promise.race([work,sleep(ms).then(()=>fallback)])}function timerResolution(){let smallest=Number.POSITIVE_INFINITY,last=performance.now();const deadline=last+50;while(last<deadline){const now=performance.now(),step=now-last;if(step>0&&step<smallest)smallest=step;last=now}return Math.round(smallest*1000)/1000}function eventTimeStampComparable(){const event=new Event("probe");return Math.abs(event.timeStamp-performance.now())<1e4}function probeWebgl2(){try{const gl=document.createElement("canvas").getContext("webgl2");if(!gl)return{ok:!1,error:'getContext("webgl2") returned null'};const debug=gl.getExtension("WEBGL_debug_renderer_info");return{ok:!0,version:String(gl.getParameter(gl.VERSION)),shadingLanguageVersion:String(gl.getParameter(gl.SHADING_LANGUAGE_VERSION)),vendor:String(gl.getParameter(gl.VENDOR)),renderer:String(gl.getParameter(gl.RENDERER)),unmaskedVendor:debug?String(gl.getParameter(debug.UNMASKED_VENDOR_WEBGL)):void 0,unmaskedRenderer:debug?String(gl.getParameter(debug.UNMASKED_RENDERER_WEBGL)):void 0,maxTextureSize:Number(gl.getParameter(gl.MAX_TEXTURE_SIZE)),maxSamples:Number(gl.getParameter(gl.MAX_SAMPLES)),extensions:gl.getSupportedExtensions()?.length??0}}catch(error){return{ok:!1,error:errorText(error)}}}async function probeWebgpu(){const gpu=navigator.gpu;if(!gpu)return{present:!1,adapter:!1,device:!1};try{const adapter=await withTimeout(gpu.requestAdapter(),5000,null);if(!adapter)return{present:!0,adapter:!1,device:!1,error:"requestAdapter() resolved null or timed out"};const info=adapter.info??{};let device=!1,error;try{const created=await withTimeout(adapter.requestDevice(),5000,null);device=created!==null;created?.destroy?.();if(!device)error="requestDevice() timed out"}catch(deviceError){error=errorText(deviceError)}return{present:!0,adapter:!0,device,error,vendor:info.vendor||void 0,architecture:info.architecture||void 0,description:info.description||void 0,maxTextureDimension2D:adapter.limits?.maxTextureDimension2D,features:adapter.features?.size}}catch(error){return{present:!0,adapter:!1,device:!1,error:errorText(error)}}}const VERTEX=`#version 300 es
|
|
3
2
|
layout(location = 0) in vec3 position;
|
|
4
3
|
layout(location = 1) in vec3 normal;
|
|
5
4
|
layout(location = 2) in vec4 offset;
|
|
@@ -16,8 +15,8 @@ void main() {
|
|
|
16
15
|
vec3 n = r * normal;
|
|
17
16
|
shade = vec3(0.35, 0.55, 0.95) * (0.35 + 0.65 * max(dot(n, normalize(vec3(0.4, 0.8, 0.6))), 0.0));
|
|
18
17
|
gl_Position = vec4(p.x / aspect, p.y, p.z * 0.5, 1.0);
|
|
19
|
-
}`,
|
|
18
|
+
}`,FRAGMENT=`#version 300 es
|
|
20
19
|
precision mediump float;
|
|
21
20
|
in vec3 shade;
|
|
22
21
|
out vec4 color;
|
|
23
|
-
void main() { color = vec4(shade, 1.0); }`;function U(){let t=[[[1,0,0],[0,1,0],[0,0,1]],[[-1,0,0],[0,0,1],[0,1,0]],[[0,1,0],[0,0,1],[1,0,0]],[[0,-1,0],[1,0,0],[0,0,1]],[[0,0,1],[1,0,0],[0,1,0]],[[0,0,-1],[0,1,0],[1,0,0]]],e=[];for(let[n,r,o]of t){let i=(c,s)=>[0,1,2].map((p)=>n[p]+c*r[p]+s*o[p]),a=[i(-1,-1),i(1,-1),i(1,1),i(-1,-1),i(1,1),i(-1,1)];for(let c of a)e.push(...c,...n)}return new Float32Array(e)}function V(t){let e=d.getContext("webgl2",{antialias:!0,powerPreference:"high-performance"});if(!e)return null;let n=(m,v)=>{let u=e.createShader(m);if(e.shaderSource(u,v),e.compileShader(u),!e.getShaderParameter(u,e.COMPILE_STATUS))throw Error(e.getShaderInfoLog(u)||"shader compile failed");return u},r=e.createProgram();if(e.attachShader(r,n(e.VERTEX_SHADER,N)),e.attachShader(r,n(e.FRAGMENT_SHADER,W)),e.linkProgram(r),!e.getProgramParameter(r,e.LINK_STATUS))throw Error(e.getProgramInfoLog(r)||"program link failed");let o=e.createVertexArray();e.bindVertexArray(o);let i=U();e.bindBuffer(e.ARRAY_BUFFER,e.createBuffer()),e.bufferData(e.ARRAY_BUFFER,i,e.STATIC_DRAW),e.enableVertexAttribArray(0),e.vertexAttribPointer(0,3,e.FLOAT,!1,24,0),e.enableVertexAttribArray(1),e.vertexAttribPointer(1,3,e.FLOAT,!1,24,12);let a=new Float32Array(t*4);for(let m=0;m<t;m++)a[m*4]=Math.random()*2-1,a[m*4+1]=Math.random()*2-1,a[m*4+2]=Math.random()*2-1,a[m*4+3]=Math.random()*Math.PI*2;e.bindBuffer(e.ARRAY_BUFFER,e.createBuffer()),e.bufferData(e.ARRAY_BUFFER,a,e.STATIC_DRAW),e.enableVertexAttribArray(2),e.vertexAttribPointer(2,4,e.FLOAT,!1,16,0),e.vertexAttribDivisor(2,1),e.useProgram(r),e.enable(e.DEPTH_TEST);let c=e.getUniformLocation(r,"time"),s=e.getUniformLocation(r,"aspect"),p=i.length/6;return{triangles:p/3*t,draw(m){let v=Math.floor(d.clientWidth*devicePixelRatio),u=Math.floor(d.clientHeight*devicePixelRatio);if(d.width!==v||d.height!==u)d.width=v,d.height=u;e.viewport(0,0,v,u),e.clearColor(0.04,0.05,0.08,1),e.clear(e.COLOR_BUFFER_BIT|e.DEPTH_BUFFER_BIT),e.uniform1f(c,m/1000),e.uniform1f(s,v/Math.max(1,u)),e.drawArraysInstanced(e.TRIANGLES,0,p,t)}}}function S(){let t=d.getContext("2d");return{triangles:0,draw(e){d.width=d.clientWidth,d.height=d.clientHeight,t.fillStyle="#0a0d14",t.fillRect(0,0,d.width,d.height),t.fillStyle="#5b8cf2";for(let n=0;n<2000;n++)t.fillRect((n*37+e/4)%d.width,n*53%d.height,6,6)}}}function H(t){let e=performance.now()+t;while(performance.now()<e);}async function R(t,e){let n=[],r=document.visibilityState!=="visible",o=()=>{if(document.visibilityState!=="visible")r=!0};document.addEventListener("visibilitychange",o),await T();let i=performance.now();return await new Promise((a)=>{let c=(s)=>{if(n.push(s),e?.(s),performance.now()-i<t)requestAnimationFrame(c);else a()};requestAnimationFrame(c)}),document.removeEventListener("visibilitychange",o),{timestamps:n,hidden:r}}var h=null;function E(t){return{state:t.state,baseLatency:typeof t.baseLatency==="number"?t.baseLatency:null,outputLatency:typeof t.outputLatency==="number"?t.outputLatency:null,sampleRate:t.sampleRate}}async function j(){let t=window.AudioContext;if(!t)return{present:!1};try{h=new t({latencyHint:"interactive"});let e=E(h);return await P(h.resume().catch(()=>{}),1500,void 0),await w(300),{present:!0,initial:e,afterUngesturedResume:E(h)}}catch(e){return{present:!0,error:f(e)}}}function F(t){let{requestFullscreen:e,webkitRequestFullscreen:n}=t;if(!e&&!n)return Promise.resolve({outcome:"no-api"});return new Promise((r)=>{let o=!1,i=(s)=>{if(o)return;o=!0,r(s)},a=()=>i({outcome:"entered",detail:"fullscreenchange"}),c=()=>i({outcome:"rejected",detail:"fullscreenerror"});document.addEventListener("fullscreenchange",a,{once:!0}),document.addEventListener("webkitfullscreenchange",a,{once:!0}),document.addEventListener("fullscreenerror",c,{once:!0}),document.addEventListener("webkitfullscreenerror",c,{once:!0});try{if(e)e.call(t).then(()=>i({outcome:"entered",detail:"promise resolved"}),(s)=>i({outcome:"rejected",detail:f(s)}));else n.call(t)}catch(s){i({outcome:"rejected",detail:f(s)})}setTimeout(()=>i({outcome:"timeout"}),3000)})}async function x(){let t=document.exitFullscreen;if(document.fullscreenElement&&t)await t.call(document).catch(()=>{})}function M(t){if(typeof t.requestPointerLock!=="function")return Promise.resolve({outcome:"no-api"});return new Promise((e)=>{let n=!1,r=(a)=>{if(n)return;n=!0,document.removeEventListener("pointerlockchange",o),document.removeEventListener("pointerlockerror",i),e(a)},o=()=>{if(document.pointerLockElement===t)r({outcome:"entered",detail:"pointerlockchange"})},i=()=>r({outcome:"rejected",detail:"pointerlockerror"});document.addEventListener("pointerlockchange",o),document.addEventListener("pointerlockerror",i);try{t.requestPointerLock()?.then?.(()=>o(),(c)=>r({outcome:"rejected",detail:f(c)}))}catch(a){r({outcome:"rejected",detail:f(a)})}setTimeout(()=>r({outcome:"timeout"}),3000)})}async function z(){let t=window.craft,e=t?.window?.setFullscreen;if(typeof e!=="function")return{outcome:"no-api"};let n=()=>`${innerWidth}x${innerHeight}`,r=n();try{await P(Promise.resolve(e.call(t.window,!0)),3000,void 0),await w(2000);let o=n(),i=o!==r&&innerWidth>=screen.width-2;return await P(Promise.resolve(e.call(t.window,!1)),3000,void 0),await w(1500),{outcome:i?"entered":"rejected",detail:i?"viewport grew to the screen":"viewport did not change",sizeBefore:r,sizeAfter:o}}catch(o){return{outcome:"rejected",detail:f(o),sizeBefore:r}}}var b={pointerdown:[],pointermove:[],keydown:[],eventTimingSupported:typeof PerformanceObserver<"u"&&(PerformanceObserver.supportedEntryTypes??[]).includes("event"),eventTimingDurations:[]},I=!1,L=!1;function y(t,e){if(!t.isTrusted)return;I=!0;let n=t.timeStamp;requestAnimationFrame(()=>{let r=performance.now()-n,o=new MessageChannel;o.port1.onmessage=()=>{e.push({toFrameMs:r,toPostFrameMs:performance.now()-n}),L=!1},o.port2.postMessage(0)})}function X(){let t=(o)=>y(o,b.pointerdown),e=(o)=>y(o,b.keydown),n=(o)=>{if(L||!o.isTrusted)return;L=!0,y(o,b.pointermove)};addEventListener("pointerdown",t,!0),addEventListener("keydown",e,!0),addEventListener("pointermove",n,!0);let r=null;if(b.eventTimingSupported){r=new PerformanceObserver((o)=>{for(let i of o.getEntries())b.eventTimingDurations.push(i.duration)});try{r.observe({type:"event",buffered:!0,durationThreshold:16})}catch{r=null}}return()=>{removeEventListener("pointerdown",t,!0),removeEventListener("keydown",e,!0),removeEventListener("pointermove",n,!0),r?.disconnect()}}async function Y(t){if(l.interactionWindowMs<=0){t.interaction.skipped=!0;return}k.hidden=!1;let e=document.getElementById("fullscreen-button"),n=document.getElementById("lock-button"),r=document.getElementById("done-button"),o=document.getElementById("countdown"),i=!1,a=!1,c=()=>{},s=new Promise((u)=>c=u),p=()=>{if(i&&a&&b.keydown.length>=5)c()};e.addEventListener("click",async(u)=>{if(!u.isTrusted||i)return;i=!0,e.setAttribute("disabled","");let B=h?.resume().catch(()=>{});if(t.fullscreen.gestured=await F(document.documentElement),await B,await w(300),h)t.audio.afterGesturedResume=E(h);await w(700),await x(),p()}),n.addEventListener("click",async(u)=>{if(!u.isTrusted||a)return;if(a=!0,n.setAttribute("disabled",""),t.pointerLock.gestured=await M(d),await w(500),document.pointerLockElement)document.exitPointerLock();p()}),addEventListener("keydown",()=>setTimeout(p,100)),r.addEventListener("click",(u)=>u.isTrusted&&c());let m=performance.now()+l.interactionWindowMs,v=setInterval(()=>{let u=Math.max(0,Math.ceil((m-performance.now())/1000));o.textContent=`${u}s left, ${b.keydown.length} key presses so far`},250);await Promise.race([s,w(l.interactionWindowMs)]),clearInterval(v),await w(200),k.hidden=!0}async function K(){let t={outcome:"not-attempted"},e=window.craft,n={config:l,userAgent:navigator.userAgent,devicePixelRatio,screen:{width:screen.width,height:screen.height},isSecureContext,timerResolutionMs:q(),eventTimeStampComparable:_(),bridge:{present:Boolean(e),setFullscreen:typeof e?.window?.setFullscreen==="function"},webgl2:{ok:!1},webgpu:{present:!1,adapter:!1,device:!1},frames:{idle:[],load:[],triangles:0,hiddenDuringSampling:!1,focused:!1},input:b,audio:{present:!1},gamepad:{present:typeof navigator.getGamepads==="function",connected:0,ids:[],connectedEventSeen:!1},fullscreen:{elementApi:typeof document.documentElement.requestFullscreen==="function",webkitElementApi:typeof document.documentElement.webkitRequestFullscreen==="function",enabled:typeof document.fullscreenEnabled==="boolean"?document.fullscreenEnabled:null,ungestured:t,gestured:t,nativeBridge:t},pointerLock:{api:typeof d.requestPointerLock==="function",ungestured:t,gestured:t},interaction:{windowMs:l.interactionWindowMs,sawRealInput:!1,skipped:!1},errors:A};addEventListener("gamepadconnected",()=>{n.gamepad.connectedEventSeen=!0});let r=X();g("Probing WebGL2 and WebGPU"),n.webgl2=O(),n.webgpu=await G(),g("Probing audio"),n.audio=await j(),g("Requesting fullscreen and pointer lock without a gesture (expected to be refused)"),n.fullscreen.ungestured=await F(document.documentElement),await x(),n.pointerLock.ungestured=await M(d),g(`Frame pacing: idle for ${l.idleMs/1000}s`);let o=await R(l.idleMs),i;try{i=V(l.instances)??S()}catch(s){A.push(`load scene: ${f(s)}`),i=S()}for(let s=0;s<10;s++)i.draw(await T());g(`Frame pacing: ${i.triangles.toLocaleString()} triangles + ${l.cpuLoadMsPerFrame} ms CPU per frame for ${l.loadMs/1000}s`);let a=await R(l.loadMs,(s)=>{H(l.cpuLoadMsPerFrame),i.draw(s)});n.frames={idle:o.timestamps,load:a.timestamps,triangles:i.triangles,hiddenDuringSampling:o.hidden||a.hidden,focused:document.hasFocus()},g("Waiting for real input (see the panel), or for the window to time out"),await Y(n),r();let c=typeof navigator.getGamepads==="function"?[...navigator.getGamepads()].filter(Boolean):[];if(n.gamepad.connected=c.length,n.gamepad.ids=c.map((s)=>s.id),l.tryNativeFullscreen&&n.bridge.setFullscreen)g("Trying Craft's native window fullscreen through window.craft"),n.fullscreen.nativeBridge=await z();n.interaction.sawRealInput=I,await h?.close().catch(()=>{}),g("Sending results"),await fetch("/results",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(n)}),g("Done. Results were sent to the runner; this window can close.")}K().catch((t)=>{let e=f(t);g(`Probe failed: ${e}`),fetch("/failed",{method:"POST",body:e}).catch(()=>{})});
|
|
22
|
+
void main() { color = vec4(shade, 1.0); }`;function cubeGeometry(){const faces=[[[1,0,0],[0,1,0],[0,0,1]],[[-1,0,0],[0,0,1],[0,1,0]],[[0,1,0],[0,0,1],[1,0,0]],[[0,-1,0],[1,0,0],[0,0,1]],[[0,0,1],[1,0,0],[0,1,0]],[[0,0,-1],[0,1,0],[1,0,0]]],out=[];for(const[n,u,v]of faces){const corner=(su,sv)=>[0,1,2].map((i)=>n[i]+su*u[i]+sv*v[i]),quad=[corner(-1,-1),corner(1,-1),corner(1,1),corner(-1,-1),corner(1,1),corner(-1,1)];for(const p of quad)out.push(...p,...n)}return new Float32Array(out)}function webglScene(instances){const gl=canvas.getContext("webgl2",{antialias:!0,powerPreference:"high-performance"});if(!gl)return null;const compile=(type,source)=>{const shader=gl.createShader(type);gl.shaderSource(shader,source);gl.compileShader(shader);if(!gl.getShaderParameter(shader,gl.COMPILE_STATUS))throw Error(gl.getShaderInfoLog(shader)||"shader compile failed");return shader},program=gl.createProgram();gl.attachShader(program,compile(gl.VERTEX_SHADER,VERTEX));gl.attachShader(program,compile(gl.FRAGMENT_SHADER,FRAGMENT));gl.linkProgram(program);if(!gl.getProgramParameter(program,gl.LINK_STATUS))throw Error(gl.getProgramInfoLog(program)||"program link failed");const vao=gl.createVertexArray();gl.bindVertexArray(vao);const geometry=cubeGeometry();gl.bindBuffer(gl.ARRAY_BUFFER,gl.createBuffer());gl.bufferData(gl.ARRAY_BUFFER,geometry,gl.STATIC_DRAW);gl.enableVertexAttribArray(0);gl.vertexAttribPointer(0,3,gl.FLOAT,!1,24,0);gl.enableVertexAttribArray(1);gl.vertexAttribPointer(1,3,gl.FLOAT,!1,24,12);const offsets=new Float32Array(instances*4);for(let i=0;i<instances;i++){offsets[i*4]=Math.random()*2-1;offsets[i*4+1]=Math.random()*2-1;offsets[i*4+2]=Math.random()*2-1;offsets[i*4+3]=Math.random()*Math.PI*2}gl.bindBuffer(gl.ARRAY_BUFFER,gl.createBuffer());gl.bufferData(gl.ARRAY_BUFFER,offsets,gl.STATIC_DRAW);gl.enableVertexAttribArray(2);gl.vertexAttribPointer(2,4,gl.FLOAT,!1,16,0);gl.vertexAttribDivisor(2,1);gl.useProgram(program);gl.enable(gl.DEPTH_TEST);const timeLocation=gl.getUniformLocation(program,"time"),aspectLocation=gl.getUniformLocation(program,"aspect"),vertices=geometry.length/6;return{triangles:vertices/3*instances,draw(time){const width=Math.floor(canvas.clientWidth*devicePixelRatio),height=Math.floor(canvas.clientHeight*devicePixelRatio);if(canvas.width!==width||canvas.height!==height){canvas.width=width;canvas.height=height}gl.viewport(0,0,width,height);gl.clearColor(0.04,0.05,0.08,1);gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);gl.uniform1f(timeLocation,time/1000);gl.uniform1f(aspectLocation,width/Math.max(1,height));gl.drawArraysInstanced(gl.TRIANGLES,0,vertices,instances)}}}function canvas2dScene(){const context=canvas.getContext("2d");return{triangles:0,draw(time){canvas.width=canvas.clientWidth;canvas.height=canvas.clientHeight;context.fillStyle="#0a0d14";context.fillRect(0,0,canvas.width,canvas.height);context.fillStyle="#5b8cf2";for(let i=0;i<2000;i++)context.fillRect((i*37+time/4)%canvas.width,i*53%canvas.height,6,6)}}}function busyWait(ms){const until=performance.now()+ms;while(performance.now()<until);}async function sampleFrames(durationMs,onFrame){const timestamps=[];let hidden=document.visibilityState!=="visible";const onVisibility=()=>{if(document.visibilityState!=="visible")hidden=!0};document.addEventListener("visibilitychange",onVisibility);await nextFrame();const start=performance.now();await new Promise((resolve)=>{const tick=(time)=>{timestamps.push(time);onFrame?.(time);if(performance.now()-start<durationMs)requestAnimationFrame(tick);else resolve()};requestAnimationFrame(tick)});document.removeEventListener("visibilitychange",onVisibility);return{timestamps,hidden}}let audioContext=null;function snapshot(context){return{state:context.state,baseLatency:typeof context.baseLatency==="number"?context.baseLatency:null,outputLatency:typeof context.outputLatency==="number"?context.outputLatency:null,sampleRate:context.sampleRate}}async function probeAudio(){const Constructor=window.AudioContext;if(!Constructor)return{present:!1};try{audioContext=new Constructor({latencyHint:"interactive"});const initial=snapshot(audioContext);await withTimeout(audioContext.resume().catch(()=>{}),1500,void 0);await sleep(300);return{present:!0,initial,afterUngesturedResume:snapshot(audioContext)}}catch(error){return{present:!0,error:errorText(error)}}}function requestFullscreen(target){const{requestFullscreen:standard,webkitRequestFullscreen:legacy}=target;if(!standard&&!legacy)return Promise.resolve({outcome:"no-api"});return new Promise((resolve)=>{let settled=!1;const finish=(outcome)=>{if(settled)return;settled=!0;resolve(outcome)},entered=()=>finish({outcome:"entered",detail:"fullscreenchange"}),failed=()=>finish({outcome:"rejected",detail:"fullscreenerror"});document.addEventListener("fullscreenchange",entered,{once:!0});document.addEventListener("webkitfullscreenchange",entered,{once:!0});document.addEventListener("fullscreenerror",failed,{once:!0});document.addEventListener("webkitfullscreenerror",failed,{once:!0});try{if(standard)standard.call(target).then(()=>finish({outcome:"entered",detail:"promise resolved"}),(error)=>finish({outcome:"rejected",detail:errorText(error)}));else legacy.call(target)}catch(error){finish({outcome:"rejected",detail:errorText(error)})}setTimeout(()=>finish({outcome:"timeout"}),3000)})}async function exitFullscreen(){const exit=document.exitFullscreen;if(document.fullscreenElement&&exit)await exit.call(document).catch(()=>{})}function requestPointerLock(target){if(typeof target.requestPointerLock!=="function")return Promise.resolve({outcome:"no-api"});return new Promise((resolve)=>{let settled=!1;const finish=(outcome)=>{if(settled)return;settled=!0;document.removeEventListener("pointerlockchange",changed);document.removeEventListener("pointerlockerror",failed);resolve(outcome)},changed=()=>{if(document.pointerLockElement===target)finish({outcome:"entered",detail:"pointerlockchange"})},failed=()=>finish({outcome:"rejected",detail:"pointerlockerror"});document.addEventListener("pointerlockchange",changed);document.addEventListener("pointerlockerror",failed);try{target.requestPointerLock()?.then?.(()=>changed(),(error)=>finish({outcome:"rejected",detail:errorText(error)}))}catch(error){finish({outcome:"rejected",detail:errorText(error)})}setTimeout(()=>finish({outcome:"timeout"}),3000)})}async function nativeFullscreen(){const bridge=window.craft,set=bridge?.window?.setFullscreen;if(typeof set!=="function")return{outcome:"no-api"};const size=()=>`${innerWidth}x${innerHeight}`,sizeBefore=size();try{await withTimeout(Promise.resolve(set.call(bridge.window,!0)),3000,void 0);await sleep(2000);const sizeAfter=size(),entered=sizeAfter!==sizeBefore&&innerWidth>=screen.width-2;await withTimeout(Promise.resolve(set.call(bridge.window,!1)),3000,void 0);await sleep(1500);return{outcome:entered?"entered":"rejected",detail:entered?"viewport grew to the screen":"viewport did not change",sizeBefore,sizeAfter}}catch(error){return{outcome:"rejected",detail:errorText(error),sizeBefore}}}const samples={pointerdown:[],pointermove:[],keydown:[],eventTimingSupported:typeof PerformanceObserver<"u"&&(PerformanceObserver.supportedEntryTypes??[]).includes("event"),eventTimingDurations:[]};let sawRealInput=!1,movePending=!1;function timeEvent(event,into){if(!event.isTrusted)return;sawRealInput=!0;const stamp=event.timeStamp;requestAnimationFrame(()=>{const toFrameMs=performance.now()-stamp,channel=new MessageChannel;channel.port1.onmessage=()=>{into.push({toFrameMs,toPostFrameMs:performance.now()-stamp});movePending=!1};channel.port2.postMessage(0)})}function listenForInput(){const onDown=(event)=>timeEvent(event,samples.pointerdown),onKey=(event)=>timeEvent(event,samples.keydown),onMove=(event)=>{if(movePending||!event.isTrusted)return;movePending=!0;timeEvent(event,samples.pointermove)};addEventListener("pointerdown",onDown,!0);addEventListener("keydown",onKey,!0);addEventListener("pointermove",onMove,!0);let observer=null;if(samples.eventTimingSupported){observer=new PerformanceObserver((list)=>{for(const entry of list.getEntries())samples.eventTimingDurations.push(entry.duration)});try{observer.observe({type:"event",buffered:!0,durationThreshold:16})}catch{observer=null}}return()=>{removeEventListener("pointerdown",onDown,!0);removeEventListener("keydown",onKey,!0);removeEventListener("pointermove",onMove,!0);observer?.disconnect()}}async function interactivePhase(payload){if(config.interactionWindowMs<=0){payload.interaction.skipped=!0;return}panel.hidden=!1;const fullscreenButton=document.getElementById("fullscreen-button"),lockButton=document.getElementById("lock-button"),doneButton=document.getElementById("done-button"),countdown=document.getElementById("countdown");let fullscreenDone=!1,lockDone=!1,finish=()=>{};const finished=new Promise((resolve)=>finish=resolve),maybeFinish=()=>{if(fullscreenDone&&lockDone&&samples.keydown.length>=5)finish()};fullscreenButton.addEventListener("click",async(event)=>{if(!event.isTrusted||fullscreenDone)return;fullscreenDone=!0;fullscreenButton.setAttribute("disabled","");const resume=audioContext?.resume().catch(()=>{});payload.fullscreen.gestured=await requestFullscreen(document.documentElement);await resume;await sleep(300);if(audioContext)payload.audio.afterGesturedResume=snapshot(audioContext);await sleep(700);await exitFullscreen();maybeFinish()});lockButton.addEventListener("click",async(event)=>{if(!event.isTrusted||lockDone)return;lockDone=!0;lockButton.setAttribute("disabled","");payload.pointerLock.gestured=await requestPointerLock(canvas);await sleep(500);if(document.pointerLockElement)document.exitPointerLock();maybeFinish()});addEventListener("keydown",()=>setTimeout(maybeFinish,100));doneButton.addEventListener("click",(event)=>event.isTrusted&&finish());const deadline=performance.now()+config.interactionWindowMs,timer=setInterval(()=>{const left=Math.max(0,Math.ceil((deadline-performance.now())/1000));countdown.textContent=`${left}s left, ${samples.keydown.length} key presses so far`},250);await Promise.race([finished,sleep(config.interactionWindowMs)]);clearInterval(timer);await sleep(200);panel.hidden=!0}async function run(){const notAttempted={outcome:"not-attempted"},bridge=window.craft,payload={config,userAgent:navigator.userAgent,devicePixelRatio,screen:{width:screen.width,height:screen.height},isSecureContext,timerResolutionMs:timerResolution(),eventTimeStampComparable:eventTimeStampComparable(),bridge:{present:Boolean(bridge),setFullscreen:typeof bridge?.window?.setFullscreen==="function"},webgl2:{ok:!1},webgpu:{present:!1,adapter:!1,device:!1},frames:{idle:[],load:[],triangles:0,hiddenDuringSampling:!1,focused:!1},input:samples,audio:{present:!1},gamepad:{present:typeof navigator.getGamepads==="function",connected:0,ids:[],connectedEventSeen:!1},fullscreen:{elementApi:typeof document.documentElement.requestFullscreen==="function",webkitElementApi:typeof document.documentElement.webkitRequestFullscreen==="function",enabled:typeof document.fullscreenEnabled==="boolean"?document.fullscreenEnabled:null,ungestured:notAttempted,gestured:notAttempted,nativeBridge:notAttempted},pointerLock:{api:typeof canvas.requestPointerLock==="function",ungestured:notAttempted,gestured:notAttempted},interaction:{windowMs:config.interactionWindowMs,sawRealInput:!1,skipped:!1},errors};addEventListener("gamepadconnected",()=>{payload.gamepad.connectedEventSeen=!0});const stopListening=listenForInput();say("Probing WebGL2 and WebGPU");payload.webgl2=probeWebgl2();payload.webgpu=await probeWebgpu();say("Probing audio");payload.audio=await probeAudio();say("Requesting fullscreen and pointer lock without a gesture (expected to be refused)");payload.fullscreen.ungestured=await requestFullscreen(document.documentElement);await exitFullscreen();payload.pointerLock.ungestured=await requestPointerLock(canvas);say(`Frame pacing: idle for ${config.idleMs/1000}s`);const idle=await sampleFrames(config.idleMs);let scene;try{scene=webglScene(config.instances)??canvas2dScene()}catch(error){errors.push(`load scene: ${errorText(error)}`);scene=canvas2dScene()}for(let i=0;i<10;i++)scene.draw(await nextFrame());say(`Frame pacing: ${scene.triangles.toLocaleString()} triangles + ${config.cpuLoadMsPerFrame} ms CPU per frame for ${config.loadMs/1000}s`);const load=await sampleFrames(config.loadMs,(time)=>{busyWait(config.cpuLoadMsPerFrame);scene.draw(time)});payload.frames={idle:idle.timestamps,load:load.timestamps,triangles:scene.triangles,hiddenDuringSampling:idle.hidden||load.hidden,focused:document.hasFocus()};say("Waiting for real input (see the panel), or for the window to time out");await interactivePhase(payload);stopListening();const pads=typeof navigator.getGamepads==="function"?[...navigator.getGamepads()].filter(Boolean):[];payload.gamepad.connected=pads.length;payload.gamepad.ids=pads.map((pad)=>pad.id);if(config.tryNativeFullscreen&&payload.bridge.setFullscreen){say("Trying Craft's native window fullscreen through window.craft");payload.fullscreen.nativeBridge=await nativeFullscreen()}payload.interaction.sawRealInput=sawRealInput;await audioContext?.close().catch(()=>{});say("Sending results");await fetch("/results",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(payload)});say("Done. Results were sent to the runner; this window can close.")}run().catch((error)=>{const message=errorText(error);say(`Probe failed: ${message}`);fetch("/failed",{method:"POST",body:message}).catch(()=>{})});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_PROBE_PAGE_CONFIG={idleMs:3000,loadMs:6000,cpuLoadMsPerFrame:6,instances:40000,interactionWindowMs:20000,tryNativeFullscreen:!0};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{PROBE_SCHEMA_VERSION}from"./schema";import{framePacing,intervals,latency,percentile,snapRefreshRate}from"./stats";export function browserFromUserAgent(userAgent){const patterns=[["Edge",/Edg\/([\d.]+)/],["Firefox",/Firefox\/([\d.]+)/],["Chrome",/Chrome\/([\d.]+)/],["Safari",/Version\/([\d.]+).*Safari\//],["WebKit",/AppleWebKit\/([\d.]+)/]];for(const[name,pattern]of patterns){const match=userAgent.match(pattern);if(match)return{name,version:match[1]}}return{name:"unknown",version:"unknown"}}function ms(seconds){return typeof seconds==="number"?`${Math.round(seconds*1e4)/10} ms`:"not reported"}function describeOutcome(outcome){return outcome.detail?`${outcome.outcome} (${outcome.detail})`:outcome.outcome}function webgl2(payload){const gl=payload.webgl2;if(!gl.ok)return{status:"unavailable",note:`No WebGL2 context: ${gl.error??"unknown reason"}.`,data:{...gl}};const renderer=gl.unmaskedRenderer||gl.renderer;return{status:"available",note:`WebGL2 context created (${gl.version}); renderer "${renderer}", max texture ${gl.maxTextureSize}, MSAA up to ${gl.maxSamples}x.`,data:{...gl}}}function webgpu(payload){const gpu=payload.webgpu;if(!gpu.present)return{status:"unavailable",note:"navigator.gpu is undefined.",data:{...gpu}};if(!gpu.adapter||!gpu.device)return{status:"unavailable",note:`navigator.gpu exists but ${gpu.adapter?"requestDevice()":"requestAdapter()"} failed: ${gpu.error??"no reason given"}.`,data:{...gpu}};return{status:"available",note:`Adapter (${[...new Set([gpu.vendor,gpu.architecture].filter(Boolean))].join(" ")||"unnamed adapter"}) and device created; maxTextureDimension2D ${gpu.maxTextureDimension2D??"unknown"}.`,data:{...gpu}}}function pacing(payload,facts){const{frames,config}=payload;if(frames.idle.length<3||frames.load.length<3)return{status:"error",note:`Too few animation frames to measure (${frames.idle.length} idle, ${frames.load.length} under load).`};const rafHz=snapRefreshRate(percentile(intervals(frames.idle),50)),displayHz=facts.machine.display.refreshHz??rafHz,idle=framePacing(frames.idle,displayHz),load=framePacing(frames.load,displayHz),loadAtRaf=framePacing(frames.load,rafHz),data={rafHz,displayHz,timerResolutionMs:payload.timerResolutionMs,triangles:frames.triangles,cpuLoadMsPerFrame:config.cpuLoadMsPerFrame,hiddenDuringSampling:frames.hiddenDuringSampling,idle,load,loadAtRafRate:{droppedFrames:loadAtRaf.droppedFrames,longFrames:loadAtRaf.longFrames}};if(frames.hiddenDuringSampling)return{status:"error",note:"The page was hidden while sampling, which throttles requestAnimationFrame; these numbers are not representative. Re-run with the window visible.",data};const cap=rafHz<displayHz-1?`requestAnimationFrame ran at ${rafHz} Hz on a ${displayHz} Hz display, so a game cannot reach the panel's rate. `:`requestAnimationFrame ran at the display's ${displayHz} Hz. `,work=frames.triangles>0?`${frames.triangles.toLocaleString("en-US")} triangles`:"a 2D canvas fallback";return{status:"measured",note:`${cap}Under load (${work} + ${config.cpuLoadMsPerFrame} ms CPU per frame): p50 ${load.p50Ms} ms, p95 ${load.p95Ms} ms, p99 ${load.p99Ms} ms; ${loadAtRaf.droppedFrames} frames dropped at ${rafHz} Hz and ${load.droppedFrames} at the display's ${displayHz} Hz over ${Math.round(load.durationMs/100)/10} s.${payload.timerResolutionMs>=0.5?` The page clock is clamped to ${payload.timerResolutionMs} ms, so intervals read as whole milliseconds.`:""}`,data}}export const MIN_INPUT_SAMPLES=5;function inputLatency(payload){const{input}=payload,context={eventTimeStampComparable:payload.eventTimeStampComparable,eventTimingSupported:input.eventTimingSupported,timerResolutionMs:payload.timerResolutionMs};if(!payload.eventTimeStampComparable)return{status:"error",note:"event.timeStamp does not share performance.now()'s time origin, so event-to-frame time cannot be computed.",data:context};const kinds={pointerdown:input.pointerdown,pointermove:input.pointermove,keydown:input.keydown},total=Object.values(kinds).reduce((sum,list)=>sum+list.length,0);if(total===0)return{status:"requires-interaction",note:`${payload.interaction.skipped?"The interactive window was skipped":`No trusted input reached the page in the ${payload.interaction.windowMs/1000} s window`}, and synthetic events are not counted. Observable: event.timeStamp is on the performance.now() clock, timer resolution ${payload.timerResolutionMs} ms, Event Timing API ${input.eventTimingSupported?"supported":"not supported"}.`,data:context};const summary={...context},parts=[];for(const[kind,list]of Object.entries(kinds)){const toFrame=latency(list.map((sample)=>sample.toFrameMs)),toPostFrame=latency(list.map((sample)=>sample.toPostFrameMs));summary[kind]={toFrame,toPostFrame};if(toFrame)parts.push(`${kind} p50 ${toFrame.p50Ms} ms to next frame (${toFrame.samples} ${toFrame.samples===1?"sample":"samples"})`)}summary.eventTiming=latency(input.eventTimingDurations);if(total<MIN_INPUT_SAMPLES)return{status:"requires-interaction",note:`Only ${total} trusted input ${total===1?"event":"events"} arrived, fewer than the ${MIN_INPUT_SAMPLES} needed to summarise. Seen: ${parts.join("; ")}. Re-run and use the panel's buttons and keys.`,data:summary};return{status:"measured",note:`Event timestamp to the next rAF callback: ${parts.join("; ")}. Real input from the OS; no synthetic events.`,data:summary}}function audio(payload){const a=payload.audio;if(!a.present)return{status:"unavailable",note:"window.AudioContext is undefined."};if(a.error||!a.initial)return{status:"error",note:`Creating an AudioContext failed: ${a.error??"unknown reason"}.`};const best=a.afterGesturedResume??a.afterUngesturedResume??a.initial,data={initial:a.initial,afterUngesturedResume:a.afterUngesturedResume??null,afterGesturedResume:a.afterGesturedResume??null},figures=`baseLatency ${ms(best.baseLatency)}, outputLatency ${ms(best.outputLatency)} at ${best.sampleRate} Hz`,unattended=a.afterUngesturedResume?.state==="running"?"The context started without a user gesture":"The context stayed suspended without a user gesture";if(best.state!=="running")return{status:"requires-interaction",note:`${unattended}, so device latency was not observable. Reported before running: ${figures}.`,data};if(best.baseLatency===null&&best.outputLatency===null)return{status:"unavailable",note:`${unattended}, but neither baseLatency nor outputLatency is implemented.`,data};return{status:"measured",note:`${unattended}. ${figures}${best.outputLatency===null?" (outputLatency not implemented)":""}.`,data}}function gamepad(payload){const g=payload.gamepad;if(!g.present)return{status:"unavailable",note:"navigator.getGamepads is undefined."};return{status:"available",note:g.connected>0?`navigator.getGamepads() is present and reported ${g.connected} controller(s).`:"navigator.getGamepads() is present; no controller was connected, so axis and button input was not exercised.",data:{...g,secureContext:payload.isSecureContext}}}function fullscreen(payload){const f=payload.fullscreen,native=f.nativeBridge.outcome==="not-attempted"||f.nativeBridge.outcome==="no-api"?"":` Craft's own window.craft.window.setFullscreen(true), which needs no gesture: ${describeOutcome(f.nativeBridge)}.`,data={...f};if(!f.elementApi&&!f.webkitElementApi)return{status:"unavailable",note:`Element.requestFullscreen and webkitRequestFullscreen are both undefined, so the standard Fullscreen API engines call does nothing.${native}`,data};const ungestured=`Without a gesture: ${describeOutcome(f.ungestured)}.`;switch(f.gestured.outcome){case"entered":return{status:"available",note:`requestFullscreen() from a real click entered fullscreen. ${ungestured}${native}`,data};case"rejected":case"timeout":return{status:"denied",note:`requestFullscreen() from a real click: ${describeOutcome(f.gestured)}. ${ungestured}${native}`,data};default:return{status:"requires-interaction",note:`The API exists (fullscreenEnabled ${String(f.enabled)}) but needs a real click to test. ${ungestured}${native}`,data}}}function pointerLock(payload){const p=payload.pointerLock,data={...p};if(!p.api)return{status:"unavailable",note:"Element.requestPointerLock is undefined.",data};const ungestured=`Without a gesture: ${describeOutcome(p.ungestured)}.`;switch(p.gestured.outcome){case"entered":return{status:"available",note:`requestPointerLock() from a real click locked the pointer. ${ungestured}`,data};case"rejected":case"timeout":return{status:"denied",note:`requestPointerLock() from a real click: ${describeOutcome(p.gestured)}. ${ungestured}`,data};default:return{status:"requires-interaction",note:`The API exists but needs a real click to test. ${ungestured}`,data}}}export function buildProbeRecord(payload,facts){return{schema:PROBE_SCHEMA_VERSION,measuredAt:facts.measuredAt,host:{...facts.host,userAgent:payload.userAgent},os:facts.os,machine:{model:facts.machine.model,cpu:facts.machine.cpu,display:{...facts.machine.display,devicePixelRatio:payload.devicePixelRatio}},run:{idleMs:payload.config.idleMs,loadMs:payload.config.loadMs,cpuLoadMsPerFrame:payload.config.cpuLoadMsPerFrame,triangles:payload.frames.triangles,interactionWindowMs:payload.interaction.windowMs,sawRealInput:payload.interaction.sawRealInput},probes:{webgl2:webgl2(payload),webgpu:webgpu(payload),framePacing:pacing(payload,facts),inputLatency:inputLatency(payload),audioLatency:audio(payload),gamepad:gamepad(payload),fullscreen:fullscreen(payload),pointerLock:pointerLock(payload)}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readdirSync,readFileSync}from"node:fs";import{join}from"node:path";import{fileURLToPath}from"node:url";import{assertProbeRecord}from"./schema";export const PROBE_RESULTS_DIR=fileURLToPath(new URL("./results",import.meta.url));function slug(value){return value.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}export function probeRecordFileName(record){return`${record.host.kind==="craft"?"craft":`browser-${slug(record.host.name)}`}-${slug(record.os.platform)}-${slug(record.os.arch)}.json`}export function loadProbeRecords(dir=PROBE_RESULTS_DIR){let files;try{files=readdirSync(dir).filter((file)=>file.endsWith(".json")).sort()}catch{return[]}return files.map((file)=>{const path=join(dir,file);return assertProbeRecord(JSON.parse(readFileSync(path,"utf8")),path)})}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{PROBE_NAMES}from"./schema";export const INTERACTIVE_BEGIN="<!-- interactive-capability:begin -->",INTERACTIVE_END="<!-- interactive-capability:end -->",PROBE_COMMAND="buddy desktop:probe";const OS_NAMES={darwin:"macOS",linux:"Linux",win32:"Windows"};export function recordLabel(record){return record.host.kind==="craft"?`Craft ${record.host.version}`:`${record.host.name} ${record.host.version}`}function environment(record){return`${OS_NAMES[record.os.platform]??record.os.name} ${record.os.version} ${record.os.arch}, ${record.machine.cpu} (${record.machine.model})`}export function orderRecords(records){return[...records].sort((a,b)=>{if(a.host.kind!==b.host.kind)return a.host.kind==="craft"?-1:1;return`${a.host.name} ${a.os.platform} ${a.os.arch}`.localeCompare(`${b.host.name} ${b.os.platform} ${b.os.arch}`)})}function number(value){return typeof value==="number"&&Number.isFinite(value)?value:null}function nested(data,...path){let current=data;for(const key of path){if(typeof current!=="object"||current===null)return;current=current[key]}return current}export function headline(name,probe){const data=probe.data;if(probe.status!=="measured"||!data)return null;switch(name){case"framePacing":{const raf=number(data.rafHz),display=number(data.displayHz),p99=number(nested(data,"load","p99Ms"));if(raf===null||display===null||p99===null)return null;return`${raf} Hz rAF on a ${display} Hz display, p99 ${p99} ms under load`}case"inputLatency":{for(const kind of["pointerdown","keydown","pointermove"]){const p50=number(nested(data,kind,"toFrame","p50Ms"));if(p50!==null)return`${kind} p50 ${p50} ms to next frame`}return null}case"audioLatency":{const snapshot=data.afterGesturedResume??data.afterUngesturedResume,base=number(snapshot?.baseLatency),output=number(snapshot?.outputLatency),format=(seconds)=>seconds===null?"n/a":`${Math.round(seconds*1e4)/10} ms`;return`base ${format(base)}, output ${format(output)}`}default:return null}}function cell(name,probe){if(!probe)return"not probed";const figure=headline(name,probe);return figure?`**${probe.status}** - ${figure}`:`**${probe.status}**`}function escapeCell(text){return text.replaceAll("|","\\|").replaceAll(`
|
|
2
|
+
`," ")}export function renderInteractiveCapabilities(records){const ordered=orderRecords(records),lines=[INTERACTIVE_BEGIN,"","<!-- Generated by `buddy docs:capabilities` from the records in"," storage/framework/core/desktop/src/probe/results/. Re-measure with",` \`${PROBE_COMMAND} --record\`; do not edit this by hand. -->`,"","Whether a Craft window can host a game or other frame-driven content that somebody else","builds, measured by the probe harness in `@stacksjs/desktop-build`","([stacksjs/stacks#877](https://github.com/stacksjs/stacks/issues/877)). Ordinary browsers","on the same machine are measured alongside it for comparison.","","> **How current is this?** Unlike the driver tables above, these are measurements, not","> claims CI can re-check. Each column is one run on one machine, and says when, against","> which host version, on which OS and hardware. If your Craft is newer than the column,",`> the numbers describe something you are not running: re-measure with \`${PROBE_COMMAND} --record\`.`,""];if(ordered.length===0){lines.push(`No measurement has been recorded yet. Run \`${PROBE_COMMAND} --record\`.`,"",INTERACTIVE_END);return lines.join(`
|
|
3
|
+
`)}lines.push(`| Probe | ${ordered.map((record)=>escapeCell(recordLabel(record))).join(" | ")} |`,`|---|${ordered.map(()=>"---").join("|")}|`);for(const[name,label]of PROBE_NAMES)lines.push(`| ${label} | ${ordered.map((record)=>escapeCell(cell(name,record.probes[name]))).join(" | ")} |`);lines.push(`| Measured on | ${ordered.map((record)=>escapeCell(environment(record))).join(" | ")} |`,`| Measured at | ${ordered.map((record)=>record.measuredAt.slice(0,10)).join(" | ")} |`,"","- **available** / **measured** - the API exists and the probe exercised it.","- **unavailable** - the API is missing, or creating it failed.","- **requires-interaction** - needs a real click, key press or controller that the run did not have. What was still observable is in the notes; no number is invented.","- **denied** - requested from a real click and refused.","");for(const record of ordered){const refresh=record.machine.display.refreshHz===null?"unknown refresh rate":`${record.machine.display.refreshHz} Hz display`;lines.push(`**${recordLabel(record)}** - ${environment(record)}, ${refresh}`,"");for(const[name,label]of PROBE_NAMES){const probe=record.probes[name];if(probe)lines.push(`- **${label}** - ${probe.note}`)}lines.push("")}lines.push("**Out of scope.** Stacks is not shipping a game engine, a physics engine, an audio mixer or","an asset pipeline. This section establishes what a Craft window gives an engine somebody","else wrote; it is not a plan to write one.","",INTERACTIVE_END);return lines.join(`
|
|
4
|
+
`)}
|
package/dist/probe/runner.js
CHANGED
|
@@ -1,108 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
var B=(e,t,n)=>()=>{if(e)try{t=e(e=0)}catch(r){n=[r]}if(n)throw n[0];return t};function Ze(e,t,n={}){if(!t.trim())throw Error("Invite token is required");let r=new URL("/invite",/^https?:\/\//.test(e)?e:`https://${e}`);if(r.searchParams.set("token",t),n.email)r.searchParams.set("email",n.email);if(n.team)r.searchParams.set("team",n.team);if(n.role)r.searchParams.set("role",n.role);if(n.expiresAt){let o=n.expiresAt instanceof Date?n.expiresAt:new Date(n.expiresAt);if(Number.isNaN(o.valueOf()))throw Error("Invite expiration must be a valid date");r.searchParams.set("expires",o.toISOString())}return r.toString()}async function rn(e,t){return Promise.all(e.map(async(n)=>{let r=typeof n==="string"?{email:n}:n;try{Xe(r.email);let o=await t.createToken(r),s=Ze(t.baseUrl,o,{email:r.email,team:r.team,role:r.role,expiresAt:t.expiresAt});return await t.send({recipient:r,url:s}),{recipient:r,url:s,delivered:!0}}catch(o){return{recipient:r,url:"",delivered:!1,error:o instanceof Error?o.message:String(o)}}}))}function Xe(e){if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))throw Error(`Invalid invite email: ${e}`)}import{createHash as Qe,createPublicKey as et,verify as tt}from"crypto";import{mkdir as nt,rename as rt,rm as ot,writeFile as st}from"fs/promises";import ce from"process";import{dirname as it}from"path";async function ln(e){me(e.manifestUrl);let t=await(e.fetcher||fetch)(e.manifestUrl,{headers:{Accept:"application/json"}});if(!t.ok)throw Error(`Desktop update check failed with HTTP ${t.status}`);let n=le(await t.json());return de(n,e.trustedKeys),pe(n,e.target),at(n.version,e.currentVersion)>0?n:null}async function dn(e,t,n=fetch,r,o){if(le(e),!r)throw Error("Desktop update staging requires trusted signing keys");de(e,r),pe(e,o),me(e.url);let s=await n(e.url);if(!s.ok)throw Error(`Desktop update download failed with HTTP ${s.status}`);let a=new Uint8Array(await s.arrayBuffer());if(a.byteLength!==e.size)throw Error(`Desktop update size mismatch: expected ${e.size}, received ${a.byteLength}`);if(Qe("sha256").update(a).digest("hex")!==e.sha256.toLowerCase())throw Error("Desktop update checksum mismatch");await nt(it(t),{recursive:!0});let u=`${t}.download-${crypto.randomUUID()}`;try{await st(u,a,{mode:448}),await rt(u,t)}catch(c){throw await ot(u,{force:!0}),c}return{manifest:e,path:t,bytes:a.byteLength}}function at(e,t){let n=V(e),r=V(t);for(let o=0;o<3;o++){let s=n[o]-r[o];if(s!==0)return Math.sign(s)}return 0}function le(e){if(!e||typeof e!=="object")throw Error("Desktop update manifest must be an object");let t=e;if(V(t.version||""),typeof t.url!=="string")throw Error("Desktop update manifest requires a URL");if(typeof t.sha256!=="string"||!/^[a-f0-9]{64}$/i.test(t.sha256))throw Error("Desktop update manifest requires a SHA-256 checksum");if(!Number.isInteger(t.size)||t.size<0)throw Error("Desktop update manifest requires a non-negative integer size");if(typeof t.sourceRevision!=="string"||!/^[0-9a-f]{40}$/.test(t.sourceRevision))throw Error("Desktop update manifest requires a full source revision");if(typeof t.platform!=="string"||!["darwin","linux","win32"].includes(t.platform))throw Error("Desktop update manifest requires a supported platform identifier");if(typeof t.architecture!=="string"||!/^[a-z0-9_-]+$/.test(t.architecture))throw Error("Desktop update manifest requires an architecture");if(typeof t.signingKeyId!=="string"||!/^[a-zA-Z0-9._-]+$/.test(t.signingKeyId))throw Error("Desktop update manifest requires a signing key ID");if(typeof t.signature!=="string"||!/^[A-Za-z0-9_-]+$/.test(t.signature))throw Error("Desktop update manifest requires an Ed25519 signature");return t}function ut(e){return Buffer.from(JSON.stringify({version:e.version,url:e.url,sha256:e.sha256.toLowerCase(),size:e.size,sourceRevision:e.sourceRevision,platform:e.platform,architecture:e.architecture,signingKeyId:e.signingKeyId,publishedAt:e.publishedAt||null,notes:e.notes||null}),"utf8")}function de(e,t){let n=t[e.signingKeyId];if(!n)throw Error(`Desktop update signing key is not trusted: ${e.signingKeyId}`);let r="ed25519-public:";if(!n.startsWith(r))throw Error("Desktop update signing key has an invalid encoding");try{let o=et({key:Buffer.from(n.slice(r.length),"base64url"),format:"der",type:"spki"}),s=Buffer.from(e.signature,"base64url");if(o.asymmetricKeyType!=="ed25519"||s.length!==64||!tt(null,ut(e),o,s))throw Error("invalid")}catch{throw Error("Desktop update manifest signature verification failed")}}function pe(e,t={platform:ce.platform,architecture:ce.arch}){if(e.platform!==t.platform||e.architecture!==t.architecture)throw Error(`Desktop update is built for ${e.platform}-${e.architecture}, not this app's ${t.platform}-${t.architecture}`)}function V(e){let t=e.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/);if(!t)throw Error(`Invalid semantic version: ${e}`);return[Number(t[1]),Number(t[2]),Number(t[3])]}function me(e){let t=new URL(e),n=["localhost","127.0.0.1","::1"].includes(t.hostname);if(t.protocol!=="https:"&&!(t.protocol==="http:"&&n))throw Error("Desktop updates require HTTPS outside loopback development")}var fe=()=>{};function lt(e=process.platform,t=process.arch){return ct.find((n)=>n.platform===e&&n.architecture===t)}function mn(e,t=process.platform,n=process.arch){let r=lt(t,n);if(!r)throw Error(`Desktop target ${t}/${n} is unsupported`);if(e==="stable"&&(r.status!=="stable"||r.signing!=="enforced"||r.platform==="darwin"&&r.notarization!=="enforced"||!r.installLaunchEvidence||!r.updateRollbackEvidence))throw Error(`Desktop target ${t}/${n} cannot be released as stable; complete stacksjs/stacks#2059, #2062, and #2063 first`);return r}var ct;var ge=B(()=>{ct=[{platform:"darwin",architecture:"arm64",status:"experimental",osVersions:["macOS 27.0 (local run, Craft v0.0.107 published archive)","macOS 15 runner (CI, Craft v0.0.48 from source)"],packageFormat:"DMG + PKG",signing:"pending",notarization:"pending",installLaunchEvidence:"https://github.com/stacksjs/stacks/blob/main/storage/framework/core/desktop/evidence/lifecycle-darwin-arm64.json",updateRollbackEvidence:"https://github.com/stacksjs/stacks/blob/main/storage/framework/core/desktop/evidence/lifecycle-darwin-arm64.json",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["The Stacks-built app bundle, DMG and PKG are unsigned while platform identities remain unprovisioned. The Craft runtime inside them is Developer ID-signed by Craft's own release workflow, which does not make the app signed.","Lifecycle evidence was re-run on 2026-10-03 against the published Craft v0.0.107 archive (craft-darwin-arm64.zip, sha256 98e1a7ae478a8900256add2884d05cc07aea2f3de7d550590d9458abd4fe7725) with `storage/framework/core/desktop/evidence/lifecycle.ts`: PKG and DMG install, a real Craft window launched from the installed bundle, update, a refused truncated update, rollback and uninstall all passed.","That run was local on macOS 27.0, not CI, and installed into the user domain (~/Applications) rather than /Applications. Gatekeeper was not exercised, since nothing carried a quarantine flag. The last CI lifecycle run for this target (https://github.com/stacksjs/stacks/actions/runs/29890804159, macOS 15 runner) used Craft v0.0.48 built from source."]},{platform:"darwin",architecture:"x64",status:"experimental",osVersions:["macOS 15 Intel runner"],packageFormat:"DMG + PKG",signing:"pending",notarization:"pending",installLaunchEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",updateRollbackEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["Lifecycle fixtures are unsigned while platform identities remain unprovisioned.","Lifecycle evidence was produced against Craft v0.0.48 built from source (bf75807). Craft now publishes a prebuilt craft-darwin-x64.zip with every release, and no Stacks lifecycle run has re-verified a prebuilt release yet."]},{platform:"linux",architecture:"x64",status:"experimental",osVersions:["Ubuntu 24.04 runner"],packageFormat:"DEB",signing:"pending",notarization:"not-applicable",installLaunchEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",updateRollbackEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["The lifecycle fixture is unsigned and no package repository support policy is published.","Lifecycle evidence was produced against Craft v0.0.48 built from source (bf75807). Craft now publishes a prebuilt craft-linux-x64.zip with every release, and no Stacks lifecycle run has re-verified a prebuilt release yet."]},{platform:"win32",architecture:"x64",status:"experimental",osVersions:["Windows Server 2025 runner"],packageFormat:"MSI + ZIP",signing:"pending",notarization:"not-applicable",installLaunchEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",updateRollbackEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["The lifecycle fixture is not Authenticode-signed.","Lifecycle evidence was produced against Craft v0.0.48 built from source (bf75807). Craft now publishes a prebuilt craft-windows-x64.zip with every release, and no Stacks lifecycle run has re-verified a prebuilt release yet."]}]});import{closeSync as be,existsSync as q,openSync as we,readSync as ye,statSync as dt}from"fs";import{dirname as pt,join as L}from"path";import I from"process";import{fileURLToPath as mt}from"url";import{craftBinaryNotFoundMessage as ft,resolveCraftBinary as gt}from"craft-native";function ke(e=I.env.CRAFT_BIN){return gt(e)}function ve(e=I.env.CRAFT_BIN,t=(n)=>Bun.which(n)){let n=ke(e);if(n!=="craft")return n;let r=t(n);if(!r)throw Error(ft(n));return r}function kn(e=I.cwd()){return q(L(e,xe))}function vn(e){return["build","--compile",...e.minify===!1?[]:["--minify"],"--no-compile-autoload-bunfig","--no-compile-autoload-dotenv",e.entry,"--outfile",e.outfile]}function xn(e=I.cwd()){let t=L(e,xe);if(q(t))return t;let n=pt(mt(import.meta.url)),o=[L(n,"launcher.ts"),L(n,"launcher.js")].find((s)=>q(s));if(!o)throw Error("Desktop launcher entrypoint not found. Reinstall @stacksjs/desktop - the package must ship dist/launcher.js.");return o}function y(e){return e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function G(e,t=""){if(typeof e==="string")return`<string>${y(e)}</string>`;if(typeof e==="boolean")return e?"<true/>":"<false/>";if(typeof e==="number"){if(!Number.isFinite(e))throw Error(`Unsupported Info.plist number: ${e}`);return Number.isInteger(e)?`<integer>${e}</integer>`:`<real>${e}</real>`}if(Array.isArray(e)){if(e.length===0)return"<array/>";return`<array>
|
|
3
|
-
${
|
|
4
|
-
`)}
|
|
5
|
-
${
|
|
6
|
-
|
|
7
|
-
`)}
|
|
8
|
-
|
|
9
|
-
${n.map(([o,s])=>` <key>${y(o)}</key>${G(s," ")}`).join(`
|
|
10
|
-
`)}`,ignored:t}}function Pn(e){let t=e.declared??{},{xml:n,ignored:r}=ht(t),o=(T)=>Object.hasOwn(t,T),s=` <!-- The window talks to something this app started on loopback, so an
|
|
11
|
-
exception for 127.0.0.1 is all it needs. NSAllowsArbitraryLoads would
|
|
12
|
-
additionally permit every unencrypted host on the internet. -->
|
|
13
|
-
<key>NSAppTransportSecurity</key>
|
|
14
|
-
<dict>
|
|
15
|
-
<key>NSAllowsLocalNetworking</key><true/>
|
|
16
|
-
<key>NSExceptionDomains</key>
|
|
17
|
-
<dict>
|
|
18
|
-
<key>127.0.0.1</key>
|
|
19
|
-
<dict><key>NSExceptionAllowsInsecureHTTPLoads</key><true/></dict>
|
|
20
|
-
</dict>
|
|
21
|
-
</dict>`,a=` <!-- The window loads a remote URL, so the app has to be allowed to reach it. -->
|
|
22
|
-
<key>NSAppTransportSecurity</key><dict><key>NSAllowsArbitraryLoads</key><true/></dict>`,i=o("LSMinimumSystemVersion")?"":` <key>LSMinimumSystemVersion</key><string>11.0</string>
|
|
23
|
-
`,u=o("NSHighResolutionCapable")?"":` <key>NSHighResolutionCapable</key><true/>
|
|
24
|
-
`,c=o("NSAppTransportSecurity")?"":e.ownsLauncher?` <!-- The window talks to something this app started on loopback, so an
|
|
25
|
-
exception for 127.0.0.1 is all it needs. NSAllowsArbitraryLoads would
|
|
26
|
-
additionally permit every unencrypted host on the internet. -->
|
|
27
|
-
<key>NSAppTransportSecurity</key>
|
|
28
|
-
<dict>
|
|
29
|
-
<key>NSAllowsLocalNetworking</key><true/>
|
|
30
|
-
<key>NSExceptionDomains</key>
|
|
31
|
-
<dict>
|
|
32
|
-
<key>127.0.0.1</key>
|
|
33
|
-
<dict><key>NSExceptionAllowsInsecureHTTPLoads</key><true/></dict>
|
|
34
|
-
</dict>
|
|
35
|
-
</dict>`:` <!-- The window loads a remote URL, so the app has to be allowed to reach it. -->
|
|
36
|
-
<key>NSAppTransportSecurity</key><dict><key>NSAllowsArbitraryLoads</key><true/></dict>`,d=e.iconFile?`
|
|
37
|
-
<key>CFBundleIconFile</key><string>${y(e.iconFile)}</string>`:"",l=y(e.appName),g=y(e.version);return{xml:`<?xml version="1.0" encoding="UTF-8"?>
|
|
38
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
39
|
-
<plist version="1.0">
|
|
40
|
-
<dict>
|
|
41
|
-
<key>CFBundleName</key><string>${l}</string>
|
|
42
|
-
<key>CFBundleDisplayName</key><string>${l}</string>
|
|
43
|
-
<key>CFBundleIdentifier</key><string>${y(e.bundleId)}</string>
|
|
44
|
-
<key>CFBundleVersion</key><string>${g}</string>
|
|
45
|
-
<key>CFBundleShortVersionString</key><string>${g}</string>
|
|
46
|
-
<key>CFBundleExecutable</key><string>${y(e.executable)}</string>
|
|
47
|
-
<key>CFBundlePackageType</key><string>APPL</string>
|
|
48
|
-
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
|
49
|
-
${i}${u}${c}${d}${n}
|
|
50
|
-
</dict>
|
|
51
|
-
</plist>
|
|
52
|
-
`,ignored:r}}function bt(e,t={}){if(!Number.isInteger(e)||e<1||e>65535)throw RangeError(`Invalid desktop development port: ${e}`);let n=t.url||I.env.APP_URL;if(!n)return"https://stacks.test";let r=/^https?:\/\//.test(n)?n:`https://${n}`;return new URL(r).toString().replace(/\/$/,"")}function wt(e,t={}){let n=[ke(t.craftBin),bt(e,t),"--title",t.title||"Stacks","--width",String(t.width||1400),"--height",String(t.height||900)];if(t.hotReload!==!1)n.push("--hot-reload");if(t.devTools===!1)n.push("--no-devtools");if(t.darkMode)n.push("--dark");if(t.systemTray)n.push("--system-tray");if(t.hideDockIcon)n.push("--hide-dock-icon");if(t.menubarOnly)n.push("--menubar-only");return n}function $n(e,t="stable"){if(!/^[a-z0-9-]+$/i.test(t))throw Error("Update channel may only contain letters, numbers, and hyphens");return new URL(`/desktop/updates/${t}.json`,/^https?:\/\//.test(e)?e:`https://${e}`).toString()}async function yt(e){Bun.spawn(e,{stdin:"inherit",stdout:"inherit",stderr:"inherit"}).unref()}async function Rn(e,t={},n=yt){return await n(wt(e,t)),!0}function Pt(e){try{let t=we(e,"r");try{let n=Buffer.alloc(4);if(ye(t,n,0,4,0)<4)return!1;return xt.has(n.readUInt32BE(0))}finally{be(t)}}catch{return!1}}function Sn(e){return Pt(e)?"MacOS":"Resources"}function $t(e={}){let t=e.override?.trim();if(t)return t;return(typeof e.declared==="string"?e.declared.trim():"")||"0.0.0"}function Rt(e){let t=e.override?.trim();if(t)return t;let n=e.appName.trim(),r=e.version?.trim();return r?`${n} ${r}`:`${n} Installer`}function f(e){if(typeof e!=="string")return;return e.trim()||void 0}function En(e={}){let t=e.flags??{},n=e.env??{},r=f(t.appName)??f(n.DESKTOP_APP_NAME)??f(e.manifestTitle)??f(n.APP_NAME)??"Stacks",o=$t({override:f(t.appVersion)??f(n.DESKTOP_APP_VERSION),declared:e.declaredVersion}),s=f(t.bundleId)??f(n.DESKTOP_BUNDLE_ID)??`sh.stacks.${r.toLowerCase().replace(/[^a-z0-9]+/g,"-")}`;return{appName:r,bundleId:s,version:o,volumeName:Rt({appName:r,version:o,override:f(t.volumeName)??f(n.DESKTOP_VOLUME_NAME)}),signingIdentity:f(t.signingIdentity)??f(n.DESKTOP_SIGNING_IDENTITY),notaryProfile:f(t.notaryProfile)??f(n.DESKTOP_NOTARY_PROFILE)}}function Dn(e){return["codesign","--force",e.identity==="-"?"--timestamp=none":"--timestamp","--options","runtime",...e.entitlements?["--entitlements",e.entitlements]:[],"--sign",e.identity,e.target]}function In(e,t=St){let n=t(e);if(!n||n.size<W)return!1;return n.tail.includes(kt)}function St(e){try{let{size:t}=dt(e);if(t<W)return{size:t,tail:""};let n=we(e,"r");try{let r=Math.min(vt,t),o=Buffer.alloc(r);return ye(n,o,0,r,t-r),{size:t,tail:o.toString("latin1")}}finally{be(n)}}catch{return null}}function Mn(e){if(e.length<2)return null;let t=[...e].sort((s,a)=>a.bytes-s.bytes),r=t.slice(1).length*W,o=t.map((s)=>`${s.name} (${(s.bytes/1024/1024).toFixed(1)} MB)`).join(", ");return`This bundle ships ${e.length} Bun-compiled executables: ${o}. Each embeds a full copy of the Bun runtime, so at least ${(r/1024/1024).toFixed(0)} MB of the bundle is the same runtime repeated. `+"Compiling one binary that dispatches on a subcommand \u2014 `MyApp agent`, `MyApp worker` \u2014 keeps the helpers as separate processes and pays for the runtime once."}var xe="app/Desktop/launcher.ts",he,kt="---- Bun!",vt=8388608,W=41943040,xt;var Pe=B(()=>{fe();ge();he=new Set(["CFBundleName","CFBundleDisplayName","CFBundleIdentifier","CFBundleVersion","CFBundleShortVersionString","CFBundleExecutable","CFBundlePackageType","CFBundleInfoDictionaryVersion","CFBundleIconFile"]);xt=new Set([4277009102,3472551422,4277009103,3489328638,3405691582,3199925962])});import{existsSync as De,mkdirSync as Et,writeFileSync as $e}from"fs";import Re from"os";import{dirname as Dt,join as O,relative as Se,resolve as It}from"path";import p from"process";import{fileURLToPath as Mt}from"url";function Ie(e){return e.replaceAll("</script","<\\/script")}function K(e,t){return`<!doctype html>
|
|
53
|
-
<html lang="en">
|
|
54
|
-
<head>
|
|
55
|
-
<meta charset="utf-8">
|
|
56
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
57
|
-
<title>Stacks interactive probe</title>
|
|
58
|
-
<style>
|
|
59
|
-
:root { color-scheme: dark; --bg: #0a0d14; --panel: #141a26; --text: #e6e9f0; --muted: #9aa3b5; --accent: #5b8cf2; }
|
|
60
|
-
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
61
|
-
#scene { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
|
|
62
|
-
#hud { position: fixed; left: 16px; right: 16px; top: 16px; padding: 12px 16px; background: color-mix(in srgb, var(--panel) 88%, transparent); border-radius: 10px; }
|
|
63
|
-
#hud h1 { margin: 0 0 4px; font-size: 15px; font-weight: 600; }
|
|
64
|
-
#status { color: var(--muted); margin: 0; }
|
|
65
|
-
#interactive { position: fixed; left: 16px; right: 16px; bottom: 16px; padding: 16px; background: var(--panel); border-radius: 10px; }
|
|
66
|
-
#interactive[hidden] { display: none; }
|
|
67
|
-
#interactive p { margin: 0 0 12px; }
|
|
68
|
-
#interactive ol { margin: 0 0 12px; padding-left: 20px; color: var(--muted); }
|
|
69
|
-
button { font: inherit; padding: 8px 14px; margin-right: 8px; border-radius: 8px; border: 1px solid var(--accent); background: transparent; color: var(--text); cursor: pointer; }
|
|
70
|
-
button[disabled] { opacity: 0.4; cursor: default; }
|
|
71
|
-
#countdown { color: var(--muted); margin-top: 10px; }
|
|
72
|
-
</style>
|
|
73
|
-
</head>
|
|
74
|
-
<body>
|
|
75
|
-
<canvas id="scene"></canvas>
|
|
76
|
-
<div id="hud">
|
|
77
|
-
<h1>Stacks interactive-content probe</h1>
|
|
78
|
-
<p id="status">Starting</p>
|
|
79
|
-
</div>
|
|
80
|
-
<div id="interactive" hidden>
|
|
81
|
-
<p>These probes need a real person. Synthetic events are ignored, so skipping them records them as needing interaction rather than inventing a number.</p>
|
|
82
|
-
<ol>
|
|
83
|
-
<li>Click <strong>Fullscreen + audio</strong>. It enters fullscreen briefly and resumes audio.</li>
|
|
84
|
-
<li>Click <strong>Pointer lock</strong>. It locks the pointer for half a second.</li>
|
|
85
|
-
<li>Press any key five times, and move the mouse over the window.</li>
|
|
86
|
-
</ol>
|
|
87
|
-
<button id="fullscreen-button" type="button">Fullscreen + audio</button>
|
|
88
|
-
<button id="lock-button" type="button">Pointer lock</button>
|
|
89
|
-
<button id="done-button" type="button">Done</button>
|
|
90
|
-
<div id="countdown"></div>
|
|
91
|
-
</div>
|
|
92
|
-
<script type="application/json" id="probe-config">${JSON.stringify(t).replaceAll("<","\\u003c")}</script>
|
|
93
|
-
<script type="module">${Ie(e)}</script>
|
|
94
|
-
</body>
|
|
95
|
-
</html>
|
|
96
|
-
`}var J={idleMs:3000,loadMs:6000,cpuLoadMsPerFrame:6,instances:40000,interactionWindowMs:20000,tryNativeFullscreen:!0};var Z=1,Y=["available","unavailable","measured","requires-interaction","denied","error"],w=[["webgl2","WebGL2"],["webgpu","WebGPU"],["framePacing","Frame pacing"],["inputLatency","Input latency"],["audioLatency","Audio latency"],["gamepad","Gamepad API"],["fullscreen","Fullscreen"],["pointerLock","Pointer lock"]];function h(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}function R(e){return typeof e==="string"&&e.trim().length>0}function Me(e){let t=[];if(!h(e))return["record is not an object"];if(e.schema!==1)t.push(`schema must be 1, got ${JSON.stringify(e.schema)}`);if(!R(e.measuredAt)||Number.isNaN(Date.parse(e.measuredAt)))t.push("measuredAt must be an ISO 8601 timestamp");let n=e.host;if(!h(n))t.push("host is missing");else{if(n.kind!=="craft"&&n.kind!=="browser")t.push(`host.kind must be craft or browser, got ${JSON.stringify(n.kind)}`);for(let i of["name","version","userAgent"])if(!R(n[i]))t.push(`host.${i} is required`);if(n.kind==="craft"&&R(n.version)&&!/^\d+\.\d+\.\d+/.test(n.version))t.push(`host.version must be a Craft release version, got ${JSON.stringify(n.version)}`)}let r=e.os;if(!h(r))t.push("os is missing");else for(let i of["platform","arch","name","version"])if(!R(r[i]))t.push(`os.${i} is required`);let o=e.machine;if(!h(o))t.push("machine is missing");else{for(let u of["model","cpu"])if(!R(o[u]))t.push(`machine.${u} is required`);let i=o.display;if(!h(i))t.push("machine.display is missing");else{if(i.refreshHz!==null&&!(typeof i.refreshHz==="number"&&i.refreshHz>0))t.push("machine.display.refreshHz must be a positive number or null");if(!(typeof i.devicePixelRatio==="number"&&i.devicePixelRatio>0))t.push("machine.display.devicePixelRatio must be a positive number")}}let s=e.run;if(!h(s))t.push("run is missing");else{for(let i of["idleMs","loadMs","cpuLoadMsPerFrame","triangles","interactionWindowMs"])if(!(typeof s[i]==="number"&&s[i]>=0))t.push(`run.${i} must be a non-negative number`);if(typeof s.sawRealInput!=="boolean")t.push("run.sawRealInput must be a boolean")}let a=e.probes;if(!h(a))return t.push("probes is missing"),t;for(let[i]of w){let u=a[i];if(!h(u)){t.push(`probes.${i} is missing`);continue}if(!Y.includes(u.status))t.push(`probes.${i}.status must be one of ${Y.join(", ")}, got ${JSON.stringify(u.status)}`);if(!R(u.note))t.push(`probes.${i}.note is required: say what was observed, even on success`);if(u.data!==void 0&&!h(u.data))t.push(`probes.${i}.data must be an object when present`);if(u.status==="measured"&&!h(u.data))t.push(`probes.${i} is measured but carries no data`)}for(let i of Object.keys(a))if(!w.some(([u])=>u===i))t.push(`probes.${i} is not a known probe`);return t}function A(e,t="probe record"){let n=Me(e);if(n.length>0)throw Error(`${t} is invalid:
|
|
97
|
-
- ${n.join(`
|
|
98
|
-
- `)}`);return e}var Ae=[24,30,48,50,60,72,75,90,100,120,144,165,240];function x(e,t){if(e.length===0)return null;if(!Number.isFinite(t)||t<0||t>100)throw RangeError(`percentile must be between 0 and 100, got ${t}`);let n=[...e].sort((u,c)=>u-c),r=t/100*(n.length-1),o=Math.floor(r),s=Math.ceil(r),a=n[o],i=n[s];return a+(i-a)*(r-o)}function _(e){let t=[];for(let n=1;n<e.length;n++)t.push(e[n]-e[n-1]);return t}function Fe(e,t=1,n=50){if(t<=0||!Number.isFinite(t))throw RangeError(`bucketMs must be positive, got ${t}`);if(!Number.isInteger(n)||n<1)throw RangeError(`buckets must be a positive integer, got ${n}`);let r=Array.from({length:n},()=>0),o=0;for(let s of e){let a=Math.floor(Math.max(0,s)/t);if(a>=n)o++;else r[a]++}return{bucketMs:t,counts:r,overflow:o}}function X(e,t=0.03){if(!(e>0))throw RangeError(`period must be positive, got ${e}`);let n=1000/e,r=n,o=Number.POSITIVE_INFINITY;for(let s of Ae){let a=Math.abs(n-s)/s;if(a<o)r=s,o=a}return o<=t?r:Math.round(n*100)/100}function Ce(e,t){if(!(t>0))throw RangeError(`period must be positive, got ${t}`);let n=0;for(let r of e)n+=Math.max(0,Math.round(r/t)-1);return n}function F(e,t){if(e.length<3)return null;if(!(t>0))throw RangeError(`targetHz must be positive, got ${t}`);let n=_(e),r=e[e.length-1]-e[0],o=1000/t,s=(a)=>Math.round(a*100)/100;return{frames:e.length,durationMs:s(r),fps:s(r>0?n.length*1000/r:0),p50Ms:s(x(n,50)),p95Ms:s(x(n,95)),p99Ms:s(x(n,99)),maxMs:s(Math.max(...n)),targetPeriodMs:s(o),targetHz:t,droppedFrames:Ce(n,o),longFrames:n.filter((a)=>a>o*1.5).length,histogram:Fe(n)}}function C(e){let t=e.filter((r)=>Number.isFinite(r)&&r>=0);if(t.length===0)return null;let n=(r)=>Math.round(r*100)/100;return{samples:t.length,p50Ms:n(x(t,50)),p95Ms:n(x(t,95)),maxMs:n(Math.max(...t))}}function Q(e){let t=e?.match(/@\s*([\d.]+)\s*Hz/i);if(!t)return null;let n=Number(t[1]);return Number.isFinite(n)&&n>0?n:null}function ne(e){let t=[["Edge",/Edg\/([\d.]+)/],["Firefox",/Firefox\/([\d.]+)/],["Chrome",/Chrome\/([\d.]+)/],["Safari",/Version\/([\d.]+).*Safari\//],["WebKit",/AppleWebKit\/([\d.]+)/]];for(let[n,r]of t){let o=e.match(r);if(o)return{name:n,version:o[1]}}return{name:"unknown",version:"unknown"}}function ee(e){return typeof e==="number"?`${Math.round(e*1e4)/10} ms`:"not reported"}function E(e){return e.detail?`${e.outcome} (${e.detail})`:e.outcome}function Le(e){let t=e.webgl2;if(!t.ok)return{status:"unavailable",note:`No WebGL2 context: ${t.error??"unknown reason"}.`,data:{...t}};let n=t.unmaskedRenderer||t.renderer;return{status:"available",note:`WebGL2 context created (${t.version}); renderer "${n}", max texture ${t.maxTextureSize}, MSAA up to ${t.maxSamples}x.`,data:{...t}}}function Ne(e){let t=e.webgpu;if(!t.present)return{status:"unavailable",note:"navigator.gpu is undefined.",data:{...t}};if(!t.adapter||!t.device)return{status:"unavailable",note:`navigator.gpu exists but ${t.adapter?"requestDevice()":"requestAdapter()"} failed: ${t.error??"no reason given"}.`,data:{...t}};return{status:"available",note:`Adapter (${[...new Set([t.vendor,t.architecture].filter(Boolean))].join(" ")||"unnamed adapter"}) and device created; maxTextureDimension2D ${t.maxTextureDimension2D??"unknown"}.`,data:{...t}}}function Oe(e,t){let{frames:n,config:r}=e;if(n.idle.length<3||n.load.length<3)return{status:"error",note:`Too few animation frames to measure (${n.idle.length} idle, ${n.load.length} under load).`};let o=X(x(_(n.idle),50)),s=t.machine.display.refreshHz??o,a=F(n.idle,s),i=F(n.load,s),u=F(n.load,o),c={rafHz:o,displayHz:s,timerResolutionMs:e.timerResolutionMs,triangles:n.triangles,cpuLoadMsPerFrame:r.cpuLoadMsPerFrame,hiddenDuringSampling:n.hiddenDuringSampling,idle:a,load:i,loadAtRafRate:{droppedFrames:u.droppedFrames,longFrames:u.longFrames}};if(n.hiddenDuringSampling)return{status:"error",note:"The page was hidden while sampling, which throttles requestAnimationFrame; these numbers are not representative. Re-run with the window visible.",data:c};let d=o<s-1?`requestAnimationFrame ran at ${o} Hz on a ${s} Hz display, so a game cannot reach the panel's rate. `:`requestAnimationFrame ran at the display's ${s} Hz. `,l=n.triangles>0?`${n.triangles.toLocaleString("en-US")} triangles`:"a 2D canvas fallback";return{status:"measured",note:`${d}Under load (${l} + ${r.cpuLoadMsPerFrame} ms CPU per frame): p50 ${i.p50Ms} ms, p95 ${i.p95Ms} ms, p99 ${i.p99Ms} ms; ${u.droppedFrames} frames dropped at ${o} Hz and ${i.droppedFrames} at the display's ${s} Hz over ${Math.round(i.durationMs/100)/10} s.${e.timerResolutionMs>=0.5?` The page clock is clamped to ${e.timerResolutionMs} ms, so intervals read as whole milliseconds.`:""}`,data:c}}var te=5;function Te(e){let{input:t}=e,n={eventTimeStampComparable:e.eventTimeStampComparable,eventTimingSupported:t.eventTimingSupported,timerResolutionMs:e.timerResolutionMs};if(!e.eventTimeStampComparable)return{status:"error",note:"event.timeStamp does not share performance.now()'s time origin, so event-to-frame time cannot be computed.",data:n};let r={pointerdown:t.pointerdown,pointermove:t.pointermove,keydown:t.keydown},o=Object.values(r).reduce((i,u)=>i+u.length,0);if(o===0)return{status:"requires-interaction",note:`${e.interaction.skipped?"The interactive window was skipped":`No trusted input reached the page in the ${e.interaction.windowMs/1000} s window`}, and synthetic events are not counted. Observable: event.timeStamp is on the performance.now() clock, timer resolution ${e.timerResolutionMs} ms, Event Timing API ${t.eventTimingSupported?"supported":"not supported"}.`,data:n};let s={...n},a=[];for(let[i,u]of Object.entries(r)){let c=C(u.map((l)=>l.toFrameMs)),d=C(u.map((l)=>l.toPostFrameMs));if(s[i]={toFrame:c,toPostFrame:d},c)a.push(`${i} p50 ${c.p50Ms} ms to next frame (${c.samples} ${c.samples===1?"sample":"samples"})`)}if(s.eventTiming=C(t.eventTimingDurations),o<te)return{status:"requires-interaction",note:`Only ${o} trusted input ${o===1?"event":"events"} arrived, fewer than the ${te} needed to summarise. Seen: ${a.join("; ")}. Re-run and use the panel's buttons and keys.`,data:s};return{status:"measured",note:`Event timestamp to the next rAF callback: ${a.join("; ")}. Real input from the OS; no synthetic events.`,data:s}}function Be(e){let t=e.audio;if(!t.present)return{status:"unavailable",note:"window.AudioContext is undefined."};if(t.error||!t.initial)return{status:"error",note:`Creating an AudioContext failed: ${t.error??"unknown reason"}.`};let n=t.afterGesturedResume??t.afterUngesturedResume??t.initial,r={initial:t.initial,afterUngesturedResume:t.afterUngesturedResume??null,afterGesturedResume:t.afterGesturedResume??null},o=`baseLatency ${ee(n.baseLatency)}, outputLatency ${ee(n.outputLatency)} at ${n.sampleRate} Hz`,s=t.afterUngesturedResume?.state==="running"?"The context started without a user gesture":"The context stayed suspended without a user gesture";if(n.state!=="running")return{status:"requires-interaction",note:`${s}, so device latency was not observable. Reported before running: ${o}.`,data:r};if(n.baseLatency===null&&n.outputLatency===null)return{status:"unavailable",note:`${s}, but neither baseLatency nor outputLatency is implemented.`,data:r};return{status:"measured",note:`${s}. ${o}${n.outputLatency===null?" (outputLatency not implemented)":""}.`,data:r}}function _e(e){let t=e.gamepad;if(!t.present)return{status:"unavailable",note:"navigator.getGamepads is undefined."};return{status:"available",note:t.connected>0?`navigator.getGamepads() is present and reported ${t.connected} controller(s).`:"navigator.getGamepads() is present; no controller was connected, so axis and button input was not exercised.",data:{...t,secureContext:e.isSecureContext}}}function Ue(e){let t=e.fullscreen,n=t.nativeBridge.outcome==="not-attempted"||t.nativeBridge.outcome==="no-api"?"":` Craft's own window.craft.window.setFullscreen(true), which needs no gesture: ${E(t.nativeBridge)}.`,r={...t};if(!t.elementApi&&!t.webkitElementApi)return{status:"unavailable",note:`Element.requestFullscreen and webkitRequestFullscreen are both undefined, so the standard Fullscreen API engines call does nothing.${n}`,data:r};let o=`Without a gesture: ${E(t.ungestured)}.`;switch(t.gestured.outcome){case"entered":return{status:"available",note:`requestFullscreen() from a real click entered fullscreen. ${o}${n}`,data:r};case"rejected":case"timeout":return{status:"denied",note:`requestFullscreen() from a real click: ${E(t.gestured)}. ${o}${n}`,data:r};default:return{status:"requires-interaction",note:`The API exists (fullscreenEnabled ${String(t.enabled)}) but needs a real click to test. ${o}${n}`,data:r}}}function je(e){let t=e.pointerLock,n={...t};if(!t.api)return{status:"unavailable",note:"Element.requestPointerLock is undefined.",data:n};let r=`Without a gesture: ${E(t.ungestured)}.`;switch(t.gestured.outcome){case"entered":return{status:"available",note:`requestPointerLock() from a real click locked the pointer. ${r}`,data:n};case"rejected":case"timeout":return{status:"denied",note:`requestPointerLock() from a real click: ${E(t.gestured)}. ${r}`,data:n};default:return{status:"requires-interaction",note:`The API exists but needs a real click to test. ${r}`,data:n}}}function re(e,t){return{schema:Z,measuredAt:t.measuredAt,host:{...t.host,userAgent:e.userAgent},os:t.os,machine:{model:t.machine.model,cpu:t.machine.cpu,display:{...t.machine.display,devicePixelRatio:e.devicePixelRatio}},run:{idleMs:e.config.idleMs,loadMs:e.config.loadMs,cpuLoadMsPerFrame:e.config.cpuLoadMsPerFrame,triangles:e.frames.triangles,interactionWindowMs:e.interaction.windowMs,sawRealInput:e.interaction.sawRealInput},probes:{webgl2:Le(e),webgpu:Ne(e),framePacing:Oe(e,t),inputLatency:Te(e),audioLatency:Be(e),gamepad:_e(e),fullscreen:Ue(e),pointerLock:je(e)}}}import{readdirSync as ze,readFileSync as He}from"fs";import{join as Ve}from"path";import{fileURLToPath as qe}from"url";var D=qe(new URL("./results",import.meta.url));function U(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function oe(e){return`${e.host.kind==="craft"?"craft":`browser-${U(e.host.name)}`}-${U(e.os.platform)}-${U(e.os.arch)}.json`}function Xt(e=D){let t;try{t=ze(e).filter((n)=>n.endsWith(".json")).sort()}catch{return[]}return t.map((n)=>{let r=Ve(e,n);return A(JSON.parse(He(r,"utf8")),r)})}var Ge="<!-- interactive-capability:begin -->",se="<!-- interactive-capability:end -->",j="buddy desktop:probe",We={darwin:"macOS",linux:"Linux",win32:"Windows"};function ie(e){return e.host.kind==="craft"?`Craft ${e.host.version}`:`${e.host.name} ${e.host.version}`}function ae(e){return`${We[e.os.platform]??e.os.name} ${e.os.version} ${e.os.arch}, ${e.machine.cpu} (${e.machine.model})`}function Ke(e){return[...e].sort((t,n)=>{if(t.host.kind!==n.host.kind)return t.host.kind==="craft"?-1:1;return`${t.host.name} ${t.os.platform} ${t.os.arch}`.localeCompare(`${n.host.name} ${n.os.platform} ${n.os.arch}`)})}function S(e){return typeof e==="number"&&Number.isFinite(e)?e:null}function ue(e,...t){let n=e;for(let r of t){if(typeof n!=="object"||n===null)return;n=n[r]}return n}function H(e,t){let n=t.data;if(t.status!=="measured"||!n)return null;switch(e){case"framePacing":{let r=S(n.rafHz),o=S(n.displayHz),s=S(ue(n,"load","p99Ms"));if(r===null||o===null||s===null)return null;return`${r} Hz rAF on a ${o} Hz display, p99 ${s} ms under load`}case"inputLatency":{for(let r of["pointerdown","keydown","pointermove"]){let o=S(ue(n,r,"toFrame","p50Ms"));if(o!==null)return`${r} p50 ${o} ms to next frame`}return null}case"audioLatency":{let r=n.afterGesturedResume??n.afterUngesturedResume,o=S(r?.baseLatency),s=S(r?.outputLatency),a=(i)=>i===null?"n/a":`${Math.round(i*1e4)/10} ms`;return`base ${a(o)}, output ${a(s)}`}default:return null}}function Je(e,t){if(!t)return"not probed";let n=H(e,t);return n?`**${t.status}** - ${n}`:`**${t.status}**`}function z(e){return e.replaceAll("|","\\|").replaceAll(`
|
|
99
|
-
`," ")}function tn(e){let t=Ke(e),n=[Ge,"","<!-- Generated by `buddy docs:capabilities` from the records in"," storage/framework/core/desktop/src/probe/results/. Re-measure with",` \`${j} --record\`; do not edit this by hand. -->`,"","Whether a Craft window can host a game or other frame-driven content that somebody else","builds, measured by the probe harness in `@stacksjs/desktop-build`","([stacksjs/stacks#877](https://github.com/stacksjs/stacks/issues/877)). Ordinary browsers","on the same machine are measured alongside it for comparison.","","> **How current is this?** Unlike the driver tables above, these are measurements, not","> claims CI can re-check. Each column is one run on one machine, and says when, against","> which host version, on which OS and hardware. If your Craft is newer than the column,",`> the numbers describe something you are not running: re-measure with \`${j} --record\`.`,""];if(t.length===0)return n.push(`No measurement has been recorded yet. Run \`${j} --record\`.`,"",se),n.join(`
|
|
100
|
-
`);n.push(`| Probe | ${t.map((r)=>z(ie(r))).join(" | ")} |`,`|---|${t.map(()=>"---").join("|")}|`);for(let[r,o]of w)n.push(`| ${o} | ${t.map((s)=>z(Je(r,s.probes[r]))).join(" | ")} |`);n.push(`| Measured on | ${t.map((r)=>z(ae(r))).join(" | ")} |`,`| Measured at | ${t.map((r)=>r.measuredAt.slice(0,10)).join(" | ")} |`,"","- **available** / **measured** - the API exists and the probe exercised it.","- **unavailable** - the API is missing, or creating it failed.","- **requires-interaction** - needs a real click, key press or controller that the run did not have. What was still observable is in the notes; no number is invented.","- **denied** - requested from a real click and refused.","");for(let r of t){let o=r.machine.display.refreshHz===null?"unknown refresh rate":`${r.machine.display.refreshHz} Hz display`;n.push(`**${ie(r)}** - ${ae(r)}, ${o}`,"");for(let[s,a]of w){let i=r.probes[s];if(i)n.push(`- **${a}** - ${i.note}`)}n.push("")}return n.push("**Out of scope.** Stacks is not shipping a game engine, a physics engine, an audio mixer or","an asset pipeline. This section establishes what a Craft window gives an engine somebody","else wrote; it is not a plan to write one.","",se),n.join(`
|
|
101
|
-
`)}function P(e){try{let t=Bun.spawnSync(e,{stdout:"pipe",stderr:"ignore"});return t.exitCode===0?t.stdout.toString().trim():null}catch{return null}}function At(e){return e?.match(/craft version\s+(\d+\.\d+\.\d+\S*)/i)?.[1]??null}function Ft(e){if(!e)return null;try{let n=(JSON.parse(e).SPDisplaysDataType??[]).flatMap((s)=>s.spdisplays_ndrvs??[]),r=n.find((s)=>s.spdisplays_main==="spdisplays_yes")??n[0];if(!r)return null;let o=r._spdisplays_resolution??"unknown";return{name:r._name??"unknown",resolution:o,refreshHz:Q(o)}}catch{return null}}function Ct(e,t,n){let r=p.platform,o=Lt[r]??r,s=Re.release(),a,i="unknown",u=Re.cpus()[0]?.model?.trim()||"unknown",c={name:"unknown",resolution:"unknown",refreshHz:null};if(r==="darwin")o="macOS",s=P(["sw_vers","-productVersion"])??s,a=P(["sw_vers","-buildVersion"])??void 0,i=P(["sysctl","-n","hw.model"])??i,u=P(["sysctl","-n","machdep.cpu.brand_string"])??u,c=Ft(P(["system_profiler","SPDisplaysDataType","-json"]))??c;else if(r==="linux")i=P(["cat","/sys/devices/virtual/dmi/id/product_name"])??i;let d=ne(n);return{measuredAt:new Date().toISOString(),host:e==="craft"?{kind:"craft",name:"craft",version:t??"unknown"}:{kind:"browser",name:d.name,version:d.version},os:{platform:r,arch:p.arch,name:o,version:s,...a?{build:a}:{}},machine:{model:i,cpu:u,display:c}}}var Lt={darwin:"macOS",linux:"Linux",win32:"Windows"};async function Nt(){let e=Dt(Mt(import.meta.url)),t=[O(e,"page.ts"),O(e,"page.js")].find(De);if(!t)throw Error("The probe page script is missing; reinstall @stacksjs/desktop-build.");let n=await Bun.build({entrypoints:[t],target:"browser",format:"esm",minify:!0});if(!n.success)throw Error(`Bundling the probe page failed:
|
|
102
|
-
${n.logs.map(String).join(`
|
|
103
|
-
`)}`);return await n.outputs[0].text()}function Ot(e,t){if(t===!1)return null;if(p.platform==="darwin")return typeof t==="string"?["open","-a",t,e]:["open",e];if(p.platform==="win32")return["cmd","/c","start","",e];return["xdg-open",e]}async function Tt(e){let t=e.log??((m)=>console.log(m)),n={...J,...e.page};if(e.mode==="browser")n.tryNativeFullscreen=!1;let r=null,o=null;if(e.mode==="craft"){await Promise.resolve().then(() => Pe());if(r=ve(e.craftBin),o=At(P([r,"--version"])),!o)throw Error(`Could not read a version from \`${r} --version\`; is it a Craft binary?`)}let s=K(await Nt(),n),a,i,u=new Promise((m,b)=>{a=m,i=b}),c=Bun.serve({hostname:"127.0.0.1",port:e.port??0,async fetch(m){let{pathname:b}=new URL(m.url);if(m.method==="POST"&&b==="/results")return a(await m.json()),new Response("ok");if(m.method==="POST"&&b==="/progress")return t(` page: ${await m.text()}`),new Response("ok");if(m.method==="POST"&&b==="/failed")return i(Error(`The probe page failed: ${await m.text()}`)),new Response("ok");if(b==="/")return new Response(s,{headers:{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}});return new Response("not found",{status:404})}}),d=c.url.href,l=null,g=()=>{l?.kill(),p.exit(130)};p.once("SIGINT",g),p.once("SIGTERM",g);let v=e.timeoutMs??n.idleMs+n.loadMs+n.interactionWindowMs+60000,T=setTimeout(()=>i(Error(`No results within ${Math.round(v/1000)} s. Was the window visible?`)),v);try{if(e.mode==="craft")t(`Opening ${d} in Craft ${o} (${r})`),l=Bun.spawn([r,d,"--title","Stacks interactive probe","--width","1280","--height","800"],{stdout:"ignore",stderr:"ignore"}),l.exited.then((M)=>i(Error(`Craft exited (code ${M}) before the probe finished`)));else{t(`Probe page: ${d}`);let M=Ot(d,e.open??!0);if(M)Bun.spawn(M,{stdout:"ignore",stderr:"ignore"});else t("Open that URL in the browser to measure, and keep its window visible.")}let m=await u,b=Ct(e.mode,o,m.userAgent);return A(re(m,b),"the probe result")}finally{clearTimeout(T),p.off("SIGINT",g),p.off("SIGTERM",g),l?.kill(),c.stop(!0)}}function Bt(e){let t=Math.max(...w.map(([,r])=>r.length)),n=[`${e.host.kind==="craft"?`Craft ${e.host.version}`:`${e.host.name} ${e.host.version}`} on ${e.os.name} ${e.os.version} (${e.machine.model}, ${e.machine.cpu}), display ${e.machine.display.refreshHz??"unknown"} Hz`];for(let[r,o]of w){let s=e.probes[r],a=H(r,s);n.push(` ${o.padEnd(t)} ${s.status}${a?` - ${a}`:""}`)}return n.join(`
|
|
104
|
-
`)}function k(e){let t=`--${e}=`;for(let n=0;n<p.argv.length;n++){let r=p.argv[n];if(r===`--${e}`){let o=p.argv[n+1];return o&&!o.startsWith("--")?o:!0}if(r.startsWith(t))return r.slice(t.length)}return}function N(e){let t=k(e);if(t===void 0||t===!0)return;let n=Number(t);if(!Number.isFinite(n)||n<0)throw Error(`--${e} must be a non-negative number of seconds`);return n*1000}var Ee=It(D,"../../../../../../..");async function _t(){let e=k("browser"),t=k("record")!==void 0,n=k("out"),r=k("cpu-load"),o=k("instances");if(t){if(Se(Ee,p.cwd()).startsWith("..")||!De(O(Ee,"storage/framework/core/buddy")))throw Error("--record writes into the Stacks framework repository; run it from that checkout, or use --out <file>.")}let s={},a=N("idle"),i=N("load"),u=N("interaction-window");if(a!==void 0)s.idleMs=a;if(i!==void 0)s.loadMs=i;if(u!==void 0)s.interactionWindowMs=u;if(typeof r==="string")s.cpuLoadMsPerFrame=Number(r);if(typeof o==="string")s.instances=Number(o);let c=typeof e==="string"?e:!0;if(k("no-open")!==void 0)c=!1;let d=k("craft-bin"),l=await Tt({mode:e===void 0?"craft":"browser",open:c,craftBin:typeof d==="string"?d:void 0,timeoutMs:N("timeout"),page:s});console.log(`
|
|
105
|
-
${Bt(l)}
|
|
106
|
-
`);let g=`${JSON.stringify(l,null,2)}
|
|
107
|
-
`;if(t){Et(D,{recursive:!0});let v=O(D,oe(l));$e(v,g),console.log(`Recorded ${Se(p.cwd(),v)}. Run \`buddy docs:capabilities\` to publish it.`)}if(typeof n==="string")$e(n,g),console.log(`Wrote ${n}`);if(!t&&typeof n!=="string")console.log(g)}if(import.meta.main)try{await _t()}catch(e){p.stderr.write(`${e instanceof Error?e.message:String(e)}
|
|
108
|
-
`),p.exit(1)}export{Ct as collectHostFacts,Ft as mainDisplayFromProfiler,At as parseCraftVersion,_t as run,Tt as runProbe,Bt as summarize};
|
|
1
|
+
import{existsSync,mkdirSync,writeFileSync}from"node:fs";import os from"node:os";import{dirname,join,relative,resolve}from"node:path";import process from"node:process";import{fileURLToPath}from"node:url";import{probePageHtml}from"./html";import{DEFAULT_PROBE_PAGE_CONFIG}from"./payload";import{browserFromUserAgent,buildProbeRecord}from"./record";import{PROBE_RESULTS_DIR,probeRecordFileName}from"./records";import{headline}from"./render";import{assertProbeRecord,PROBE_NAMES}from"./schema";import{parseRefreshHz}from"./stats";function capture(command){try{const result=Bun.spawnSync(command,{stdout:"pipe",stderr:"ignore"});return result.exitCode===0?result.stdout.toString().trim():null}catch{return null}}export function parseCraftVersion(output){return output?.match(/craft version\s+(\d+\.\d+\.\d+\S*)/i)?.[1]??null}export function mainDisplayFromProfiler(json){if(!json)return null;try{const displays=(JSON.parse(json).SPDisplaysDataType??[]).flatMap((gpu)=>gpu.spdisplays_ndrvs??[]),main=displays.find((display)=>display.spdisplays_main==="spdisplays_yes")??displays[0];if(!main)return null;const resolution=main._spdisplays_resolution??"unknown";return{name:main._name??"unknown",resolution,refreshHz:parseRefreshHz(resolution)}}catch{return null}}export function collectHostFacts(mode,craftVersion,userAgent){const platform=process.platform;let osName=OS_NAME[platform]??platform,osVersion=os.release(),build,model="unknown",cpu=os.cpus()[0]?.model?.trim()||"unknown",display={name:"unknown",resolution:"unknown",refreshHz:null};if(platform==="darwin"){osName="macOS";osVersion=capture(["sw_vers","-productVersion"])??osVersion;build=capture(["sw_vers","-buildVersion"])??void 0;model=capture(["sysctl","-n","hw.model"])??model;cpu=capture(["sysctl","-n","machdep.cpu.brand_string"])??cpu;display=mainDisplayFromProfiler(capture(["system_profiler","SPDisplaysDataType","-json"]))??display}else if(platform==="linux")model=capture(["cat","/sys/devices/virtual/dmi/id/product_name"])??model;const browser=browserFromUserAgent(userAgent);return{measuredAt:new Date().toISOString(),host:mode==="craft"?{kind:"craft",name:"craft",version:craftVersion??"unknown"}:{kind:"browser",name:browser.name,version:browser.version},os:{platform,arch:process.arch,name:osName,version:osVersion,...build?{build}:{}},machine:{model,cpu,display}}}const OS_NAME={darwin:"macOS",linux:"Linux",win32:"Windows"};async function bundlePageScript(){const here=dirname(fileURLToPath(import.meta.url)),entry=[join(here,"page.ts"),join(here,"page.js")].find(existsSync);if(!entry)throw Error("The probe page script is missing; reinstall @stacksjs/desktop-build.");const result=await Bun.build({entrypoints:[entry],target:"browser",format:"esm",minify:!0});if(!result.success)throw Error(`Bundling the probe page failed:
|
|
2
|
+
${result.logs.map(String).join(`
|
|
3
|
+
`)}`);return await result.outputs[0].text()}function openCommand(url,app){if(app===!1)return null;if(process.platform==="darwin")return typeof app==="string"?["open","-a",app,url]:["open",url];if(process.platform==="win32")return["cmd","/c","start","",url];return["xdg-open",url]}export async function runProbe(options){const log=options.log??((line)=>console.log(line)),config={...DEFAULT_PROBE_PAGE_CONFIG,...options.page};if(options.mode==="browser")config.tryNativeFullscreen=!1;let craftBin=null,craftVersion=null;if(options.mode==="craft"){const{resolveCraftExecutable}=await import("../index");craftBin=resolveCraftExecutable(options.craftBin);craftVersion=parseCraftVersion(capture([craftBin,"--version"]));if(!craftVersion)throw Error(`Could not read a version from \`${craftBin} --version\`; is it a Craft binary?`)}const html=probePageHtml(await bundlePageScript(),config);let settle,fail;const received=new Promise((resolve,reject)=>{settle=resolve;fail=reject}),server=Bun.serve({hostname:"127.0.0.1",port:options.port??0,async fetch(request){const{pathname}=new URL(request.url);if(request.method==="POST"&&pathname==="/results"){settle(await request.json());return new Response("ok")}if(request.method==="POST"&&pathname==="/progress"){log(` page: ${await request.text()}`);return new Response("ok")}if(request.method==="POST"&&pathname==="/failed"){fail(Error(`The probe page failed: ${await request.text()}`));return new Response("ok")}if(pathname==="/")return new Response(html,{headers:{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}});return new Response("not found",{status:404})}}),url=server.url.href;let craft=null;const interrupted=()=>{craft?.kill();process.exit(130)};process.once("SIGINT",interrupted);process.once("SIGTERM",interrupted);const budget=options.timeoutMs??config.idleMs+config.loadMs+config.interactionWindowMs+60000,timer=setTimeout(()=>fail(Error(`No results within ${Math.round(budget/1000)} s. Was the window visible?`)),budget);try{if(options.mode==="craft"){log(`Opening ${url} in Craft ${craftVersion} (${craftBin})`);craft=Bun.spawn([craftBin,url,"--title","Stacks interactive probe","--width","1280","--height","800"],{stdout:"ignore",stderr:"ignore"});craft.exited.then((code)=>fail(Error(`Craft exited (code ${code}) before the probe finished`)))}else{log(`Probe page: ${url}`);const command=openCommand(url,options.open??!0);if(command)Bun.spawn(command,{stdout:"ignore",stderr:"ignore"});else log("Open that URL in the browser to measure, and keep its window visible.")}const payload=await received,facts=collectHostFacts(options.mode,craftVersion,payload.userAgent);return assertProbeRecord(buildProbeRecord(payload,facts),"the probe result")}finally{clearTimeout(timer);process.off("SIGINT",interrupted);process.off("SIGTERM",interrupted);craft?.kill();server.stop(!0)}}export function summarize(record){const width=Math.max(...PROBE_NAMES.map(([,label])=>label.length)),lines=[`${record.host.kind==="craft"?`Craft ${record.host.version}`:`${record.host.name} ${record.host.version}`} on ${record.os.name} ${record.os.version} (${record.machine.model}, ${record.machine.cpu}), display ${record.machine.display.refreshHz??"unknown"} Hz`];for(const[name,label]of PROBE_NAMES){const probe=record.probes[name],figure=headline(name,probe);lines.push(` ${label.padEnd(width)} ${probe.status}${figure?` - ${figure}`:""}`)}return lines.join(`
|
|
4
|
+
`)}function flag(name){const prefix=`--${name}=`;for(let i=0;i<process.argv.length;i++){const arg=process.argv[i];if(arg===`--${name}`){const next=process.argv[i+1];return next&&!next.startsWith("--")?next:!0}if(arg.startsWith(prefix))return arg.slice(prefix.length)}return}function seconds(name){const value=flag(name);if(value===void 0||value===!0)return;const parsed=Number(value);if(!Number.isFinite(parsed)||parsed<0)throw Error(`--${name} must be a non-negative number of seconds`);return parsed*1000}const frameworkRoot=resolve(PROBE_RESULTS_DIR,"../../../../../../..");export async function run(){const browser=flag("browser"),record=flag("record")!==void 0,out=flag("out"),cpu=flag("cpu-load"),instances=flag("instances");if(record){if(relative(frameworkRoot,process.cwd()).startsWith("..")||!existsSync(join(frameworkRoot,"storage/framework/core/buddy")))throw Error("--record writes into the Stacks framework repository; run it from that checkout, or use --out <file>.")}const page={},idle=seconds("idle"),load=seconds("load"),window=seconds("interaction-window");if(idle!==void 0)page.idleMs=idle;if(load!==void 0)page.loadMs=load;if(window!==void 0)page.interactionWindowMs=window;if(typeof cpu==="string")page.cpuLoadMsPerFrame=Number(cpu);if(typeof instances==="string")page.instances=Number(instances);let open=typeof browser==="string"?browser:!0;if(flag("no-open")!==void 0)open=!1;const craftBin=flag("craft-bin"),result=await runProbe({mode:browser===void 0?"craft":"browser",open,craftBin:typeof craftBin==="string"?craftBin:void 0,timeoutMs:seconds("timeout"),page});console.log(`
|
|
5
|
+
${summarize(result)}
|
|
6
|
+
`);const json=`${JSON.stringify(result,null,2)}
|
|
7
|
+
`;if(record){mkdirSync(PROBE_RESULTS_DIR,{recursive:!0});const path=join(PROBE_RESULTS_DIR,probeRecordFileName(result));writeFileSync(path,json);console.log(`Recorded ${relative(process.cwd(),path)}. Run \`buddy docs:capabilities\` to publish it.`)}if(typeof out==="string"){writeFileSync(out,json);console.log(`Wrote ${out}`)}if(!record&&typeof out!=="string")console.log(json)}if(import.meta.main)try{await run()}catch(error){process.stderr.write(`${error instanceof Error?error.message:String(error)}
|
|
8
|
+
`);process.exit(1)}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export const PROBE_SCHEMA_VERSION=1,PROBE_STATUSES=["available","unavailable","measured","requires-interaction","denied","error"],PROBE_NAMES=[["webgl2","WebGL2"],["webgpu","WebGPU"],["framePacing","Frame pacing"],["inputLatency","Input latency"],["audioLatency","Audio latency"],["gamepad","Gamepad API"],["fullscreen","Fullscreen"],["pointerLock","Pointer lock"]];function isRecord(value){return typeof value==="object"&&value!==null&&!Array.isArray(value)}function nonEmptyString(value){return typeof value==="string"&&value.trim().length>0}export function validateProbeRecord(value){const errors=[];if(!isRecord(value))return["record is not an object"];if(value.schema!==PROBE_SCHEMA_VERSION)errors.push(`schema must be ${PROBE_SCHEMA_VERSION}, got ${JSON.stringify(value.schema)}`);if(!nonEmptyString(value.measuredAt)||Number.isNaN(Date.parse(value.measuredAt)))errors.push("measuredAt must be an ISO 8601 timestamp");const host=value.host;if(!isRecord(host))errors.push("host is missing");else{if(host.kind!=="craft"&&host.kind!=="browser")errors.push(`host.kind must be craft or browser, got ${JSON.stringify(host.kind)}`);for(const key of["name","version","userAgent"])if(!nonEmptyString(host[key]))errors.push(`host.${key} is required`);if(host.kind==="craft"&&nonEmptyString(host.version)&&!/^\d+\.\d+\.\d+/.test(host.version))errors.push(`host.version must be a Craft release version, got ${JSON.stringify(host.version)}`)}const os=value.os;if(!isRecord(os))errors.push("os is missing");else for(const key of["platform","arch","name","version"])if(!nonEmptyString(os[key]))errors.push(`os.${key} is required`);const machine=value.machine;if(!isRecord(machine))errors.push("machine is missing");else{for(const key of["model","cpu"])if(!nonEmptyString(machine[key]))errors.push(`machine.${key} is required`);const display=machine.display;if(!isRecord(display))errors.push("machine.display is missing");else{if(display.refreshHz!==null&&!(typeof display.refreshHz==="number"&&display.refreshHz>0))errors.push("machine.display.refreshHz must be a positive number or null");if(!(typeof display.devicePixelRatio==="number"&&display.devicePixelRatio>0))errors.push("machine.display.devicePixelRatio must be a positive number")}}const run=value.run;if(!isRecord(run))errors.push("run is missing");else{for(const key of["idleMs","loadMs","cpuLoadMsPerFrame","triangles","interactionWindowMs"])if(!(typeof run[key]==="number"&&run[key]>=0))errors.push(`run.${key} must be a non-negative number`);if(typeof run.sawRealInput!=="boolean")errors.push("run.sawRealInput must be a boolean")}const probes=value.probes;if(!isRecord(probes)){errors.push("probes is missing");return errors}for(const[name]of PROBE_NAMES){const probe=probes[name];if(!isRecord(probe)){errors.push(`probes.${name} is missing`);continue}if(!PROBE_STATUSES.includes(probe.status))errors.push(`probes.${name}.status must be one of ${PROBE_STATUSES.join(", ")}, got ${JSON.stringify(probe.status)}`);if(!nonEmptyString(probe.note))errors.push(`probes.${name}.note is required: say what was observed, even on success`);if(probe.data!==void 0&&!isRecord(probe.data))errors.push(`probes.${name}.data must be an object when present`);if(probe.status==="measured"&&!isRecord(probe.data))errors.push(`probes.${name} is measured but carries no data`)}for(const name of Object.keys(probes))if(!PROBE_NAMES.some(([known])=>known===name))errors.push(`probes.${name} is not a known probe`);return errors}export function assertProbeRecord(value,source="probe record"){const errors=validateProbeRecord(value);if(errors.length>0)throw Error(`${source} is invalid:
|
|
2
|
+
- ${errors.join(`
|
|
3
|
+
- `)}`);return value}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const COMMON_REFRESH_RATES=[24,30,48,50,60,72,75,90,100,120,144,165,240];export function percentile(values,p){if(values.length===0)return null;if(!Number.isFinite(p)||p<0||p>100)throw RangeError(`percentile must be between 0 and 100, got ${p}`);const sorted=[...values].sort((a,b)=>a-b),rank=p/100*(sorted.length-1),lower=Math.floor(rank),upper=Math.ceil(rank),low=sorted[lower],high=sorted[upper];return low+(high-low)*(rank-lower)}export function intervals(timestamps){const out=[];for(let i=1;i<timestamps.length;i++)out.push(timestamps[i]-timestamps[i-1]);return out}export function histogram(values,bucketMs=1,buckets=50){if(bucketMs<=0||!Number.isFinite(bucketMs))throw RangeError(`bucketMs must be positive, got ${bucketMs}`);if(!Number.isInteger(buckets)||buckets<1)throw RangeError(`buckets must be a positive integer, got ${buckets}`);const counts=Array.from({length:buckets},()=>0);let overflow=0;for(const value of values){const index=Math.floor(Math.max(0,value)/bucketMs);if(index>=buckets)overflow++;else counts[index]++}return{bucketMs,counts,overflow}}export function snapRefreshRate(periodMs,tolerance=0.03){if(!(periodMs>0))throw RangeError(`period must be positive, got ${periodMs}`);const measured=1000/periodMs;let best=measured,bestError=Number.POSITIVE_INFINITY;for(const rate of COMMON_REFRESH_RATES){const error=Math.abs(measured-rate)/rate;if(error<bestError){best=rate;bestError=error}}return bestError<=tolerance?best:Math.round(measured*100)/100}export function droppedFrames(frameIntervals,periodMs){if(!(periodMs>0))throw RangeError(`period must be positive, got ${periodMs}`);let dropped=0;for(const interval of frameIntervals)dropped+=Math.max(0,Math.round(interval/periodMs)-1);return dropped}export function framePacing(timestamps,targetHz){if(timestamps.length<3)return null;if(!(targetHz>0))throw RangeError(`targetHz must be positive, got ${targetHz}`);const frameIntervals=intervals(timestamps),durationMs=timestamps[timestamps.length-1]-timestamps[0],targetPeriodMs=1000/targetHz,round=(value)=>Math.round(value*100)/100;return{frames:timestamps.length,durationMs:round(durationMs),fps:round(durationMs>0?frameIntervals.length*1000/durationMs:0),p50Ms:round(percentile(frameIntervals,50)),p95Ms:round(percentile(frameIntervals,95)),p99Ms:round(percentile(frameIntervals,99)),maxMs:round(Math.max(...frameIntervals)),targetPeriodMs:round(targetPeriodMs),targetHz,droppedFrames:droppedFrames(frameIntervals,targetPeriodMs),longFrames:frameIntervals.filter((interval)=>interval>targetPeriodMs*1.5).length,histogram:histogram(frameIntervals)}}export function latency(samples){const usable=samples.filter((sample)=>Number.isFinite(sample)&&sample>=0);if(usable.length===0)return null;const round=(value)=>Math.round(value*100)/100;return{samples:usable.length,p50Ms:round(percentile(usable,50)),p95Ms:round(percentile(usable,95)),maxMs:round(Math.max(...usable))}}export function parseRefreshHz(resolution){const match=resolution?.match(/@\s*([\d.]+)\s*Hz/i);if(!match)return null;const hz=Number(match[1]);return Number.isFinite(hz)&&hz>0?hz:null}
|
package/dist/support.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const desktopSupportMatrix=[{platform:"darwin",architecture:"arm64",status:"experimental",osVersions:["macOS 27.0 (local run, Craft v0.0.107 published archive)","macOS 15 runner (CI, Craft v0.0.48 from source)"],packageFormat:"DMG + PKG",signing:"pending",notarization:"pending",installLaunchEvidence:"https://github.com/stacksjs/stacks/blob/main/storage/framework/core/desktop/evidence/lifecycle-darwin-arm64.json",updateRollbackEvidence:"https://github.com/stacksjs/stacks/blob/main/storage/framework/core/desktop/evidence/lifecycle-darwin-arm64.json",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["The Stacks-built app bundle, DMG and PKG are unsigned while platform identities remain unprovisioned. The Craft runtime inside them is Developer ID-signed by Craft's own release workflow, which does not make the app signed.","Lifecycle evidence was re-run on 2026-10-03 against the published Craft v0.0.107 archive (craft-darwin-arm64.zip, sha256 98e1a7ae478a8900256add2884d05cc07aea2f3de7d550590d9458abd4fe7725) with `storage/framework/core/desktop/evidence/lifecycle.ts`: PKG and DMG install, a real Craft window launched from the installed bundle, update, a refused truncated update, rollback and uninstall all passed.","That run was local on macOS 27.0, not CI, and installed into the user domain (~/Applications) rather than /Applications. Gatekeeper was not exercised, since nothing carried a quarantine flag. The last CI lifecycle run for this target (https://github.com/stacksjs/stacks/actions/runs/29890804159, macOS 15 runner) used Craft v0.0.48 built from source."]},{platform:"darwin",architecture:"x64",status:"experimental",osVersions:["macOS 15 Intel runner"],packageFormat:"DMG + PKG",signing:"pending",notarization:"pending",installLaunchEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",updateRollbackEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["Lifecycle fixtures are unsigned while platform identities remain unprovisioned.","Lifecycle evidence was produced against Craft v0.0.48 built from source (bf75807). Craft now publishes a prebuilt craft-darwin-x64.zip with every release, and no Stacks lifecycle run has re-verified a prebuilt release yet."]},{platform:"linux",architecture:"x64",status:"experimental",osVersions:["Ubuntu 24.04 runner"],packageFormat:"DEB",signing:"pending",notarization:"not-applicable",installLaunchEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",updateRollbackEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["The lifecycle fixture is unsigned and no package repository support policy is published.","Lifecycle evidence was produced against Craft v0.0.48 built from source (bf75807). Craft now publishes a prebuilt craft-linux-x64.zip with every release, and no Stacks lifecycle run has re-verified a prebuilt release yet."]},{platform:"win32",architecture:"x64",status:"experimental",osVersions:["Windows Server 2025 runner"],packageFormat:"MSI + ZIP",signing:"pending",notarization:"not-applicable",installLaunchEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",updateRollbackEvidence:"https://github.com/stacksjs/stacks/actions/runs/29890804159",packagingEvidence:"https://github.com/stacksjs/protocol/blob/main/evidence/craft.json",blockingIssues:["https://github.com/stacksjs/stacks/issues/2062"],limitations:["The lifecycle fixture is not Authenticode-signed.","Lifecycle evidence was produced against Craft v0.0.48 built from source (bf75807). Craft now publishes a prebuilt craft-windows-x64.zip with every release, and no Stacks lifecycle run has re-verified a prebuilt release yet."]}];export function desktopSupport(platform=process.platform,architecture=process.arch){return desktopSupportMatrix.find((row)=>row.platform===platform&&row.architecture===architecture)}export function assertDesktopReleaseChannel(channel,platform=process.platform,architecture=process.arch){const row=desktopSupport(platform,architecture);if(!row)throw Error(`Desktop target ${platform}/${architecture} is unsupported`);if(channel==="stable"&&(row.status!=="stable"||row.signing!=="enforced"||row.platform==="darwin"&&row.notarization!=="enforced"||!row.installLaunchEvidence||!row.updateRollbackEvidence))throw Error(`Desktop target ${platform}/${architecture} cannot be released as stable; complete stacksjs/stacks#2059, #2062, and #2063 first`);return row}
|
package/dist/updater.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash,createPublicKey,verify}from"node:crypto";import{mkdir,rename,rm,writeFile}from"node:fs/promises";import process from"node:process";import{dirname}from"node:path";export async function checkForDesktopUpdate(options){assertSecureUrl(options.manifestUrl);const response=await(options.fetcher||fetch)(options.manifestUrl,{headers:{Accept:"application/json"}});if(!response.ok)throw Error(`Desktop update check failed with HTTP ${response.status}`);const manifest=validateUpdateManifest(await response.json());verifyUpdateManifestSignature(manifest,options.trustedKeys);assertUpdateTarget(manifest,options.target);return compareVersions(manifest.version,options.currentVersion)>0?manifest:null}export async function stageDesktopUpdate(manifest,destination,fetcher=fetch,trustedKeys,target){validateUpdateManifest(manifest);if(!trustedKeys)throw Error("Desktop update staging requires trusted signing keys");verifyUpdateManifestSignature(manifest,trustedKeys);assertUpdateTarget(manifest,target);assertSecureUrl(manifest.url);const response=await fetcher(manifest.url);if(!response.ok)throw Error(`Desktop update download failed with HTTP ${response.status}`);const bytes=new Uint8Array(await response.arrayBuffer());if(bytes.byteLength!==manifest.size)throw Error(`Desktop update size mismatch: expected ${manifest.size}, received ${bytes.byteLength}`);if(createHash("sha256").update(bytes).digest("hex")!==manifest.sha256.toLowerCase())throw Error("Desktop update checksum mismatch");await mkdir(dirname(destination),{recursive:!0});const temporaryPath=`${destination}.download-${crypto.randomUUID()}`;try{await writeFile(temporaryPath,bytes,{mode:448});await rename(temporaryPath,destination)}catch(error){await rm(temporaryPath,{force:!0});throw error}return{manifest,path:destination,bytes:bytes.byteLength}}export function compareVersions(left,right){const leftParts=parseVersion(left),rightParts=parseVersion(right);for(let index=0;index<3;index++){const difference=leftParts[index]-rightParts[index];if(difference!==0)return Math.sign(difference)}return 0}export function validateUpdateManifest(input){if(!input||typeof input!=="object")throw Error("Desktop update manifest must be an object");const manifest=input;parseVersion(manifest.version||"");if(typeof manifest.url!=="string")throw Error("Desktop update manifest requires a URL");if(typeof manifest.sha256!=="string"||!/^[a-f0-9]{64}$/i.test(manifest.sha256))throw Error("Desktop update manifest requires a SHA-256 checksum");if(!Number.isInteger(manifest.size)||manifest.size<0)throw Error("Desktop update manifest requires a non-negative integer size");if(typeof manifest.sourceRevision!=="string"||!/^[0-9a-f]{40}$/.test(manifest.sourceRevision))throw Error("Desktop update manifest requires a full source revision");if(typeof manifest.platform!=="string"||!["darwin","linux","win32"].includes(manifest.platform))throw Error("Desktop update manifest requires a supported platform identifier");if(typeof manifest.architecture!=="string"||!/^[a-z0-9_-]+$/.test(manifest.architecture))throw Error("Desktop update manifest requires an architecture");if(typeof manifest.signingKeyId!=="string"||!/^[a-zA-Z0-9._-]+$/.test(manifest.signingKeyId))throw Error("Desktop update manifest requires a signing key ID");if(typeof manifest.signature!=="string"||!/^[A-Za-z0-9_-]+$/.test(manifest.signature))throw Error("Desktop update manifest requires an Ed25519 signature");return manifest}export function updateManifestPayload(manifest){return Buffer.from(JSON.stringify({version:manifest.version,url:manifest.url,sha256:manifest.sha256.toLowerCase(),size:manifest.size,sourceRevision:manifest.sourceRevision,platform:manifest.platform,architecture:manifest.architecture,signingKeyId:manifest.signingKeyId,publishedAt:manifest.publishedAt||null,notes:manifest.notes||null}),"utf8")}export function verifyUpdateManifestSignature(manifest,trustedKeys){const encodedKey=trustedKeys[manifest.signingKeyId];if(!encodedKey)throw Error(`Desktop update signing key is not trusted: ${manifest.signingKeyId}`);const prefix="ed25519-public:";if(!encodedKey.startsWith(prefix))throw Error("Desktop update signing key has an invalid encoding");try{const publicKey=createPublicKey({key:Buffer.from(encodedKey.slice(prefix.length),"base64url"),format:"der",type:"spki"}),signature=Buffer.from(manifest.signature,"base64url");if(publicKey.asymmetricKeyType!=="ed25519"||signature.length!==64||!verify(null,updateManifestPayload(manifest),publicKey,signature))throw Error("invalid")}catch{throw Error("Desktop update manifest signature verification failed")}}function assertUpdateTarget(manifest,target={platform:process.platform,architecture:process.arch}){if(manifest.platform!==target.platform||manifest.architecture!==target.architecture)throw Error(`Desktop update is built for ${manifest.platform}-${manifest.architecture}, not this app's ${target.platform}-${target.architecture}`)}function parseVersion(version){const match=version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/);if(!match)throw Error(`Invalid semantic version: ${version}`);return[Number(match[1]),Number(match[2]),Number(match[3])]}function assertSecureUrl(value){const url=new URL(value),isLoopback=["localhost","127.0.0.1","::1"].includes(url.hostname);if(url.protocol!=="https:"&&!(url.protocol==="http:"&&isLoopback))throw Error("Desktop updates require HTTPS outside loopback development")}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/desktop-build",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.242",
|
|
6
6
|
"description": "Build and launch Craft desktop applications for Stacks",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -39,6 +39,12 @@
|
|
|
39
39
|
"import": "./dist/index.js",
|
|
40
40
|
"default": "./dist/index.js"
|
|
41
41
|
},
|
|
42
|
+
"./probe": {
|
|
43
|
+
"types": "./dist/probe/index.d.ts",
|
|
44
|
+
"bun": "./dist/probe/index.js",
|
|
45
|
+
"import": "./dist/probe/index.js",
|
|
46
|
+
"default": "./dist/probe/index.js"
|
|
47
|
+
},
|
|
42
48
|
"./*": {
|
|
43
49
|
"types": "./dist/*.d.ts",
|
|
44
50
|
"bun": "./dist/*.js",
|
|
@@ -70,8 +76,8 @@
|
|
|
70
76
|
"craft-native": ">=0.0.135"
|
|
71
77
|
},
|
|
72
78
|
"devDependencies": {
|
|
73
|
-
"@stacksjs/build": "0.75.
|
|
79
|
+
"@stacksjs/build": "0.75.242",
|
|
74
80
|
"better-dx": "^0.2.24",
|
|
75
|
-
"@stacksjs/server": "0.75.
|
|
81
|
+
"@stacksjs/server": "0.75.242"
|
|
76
82
|
}
|
|
77
83
|
}
|