@stacksjs/desktop-build 0.75.40 → 0.75.42
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 +6 -6
- package/dist/probe/html.d.ts +2 -0
- package/dist/probe/index.d.ts +10 -0
- package/dist/probe/index.js +7 -0
- package/dist/probe/page.d.ts +1 -0
- package/dist/probe/page.js +23 -0
- package/dist/probe/payload.d.ts +113 -0
- package/dist/probe/record.d.ts +19 -0
- package/dist/probe/records.d.ts +7 -0
- package/dist/probe/render.d.ts +13 -0
- package/dist/probe/runner.d.ts +27 -0
- package/dist/probe/runner.js +66 -0
- package/dist/probe/schema.d.ts +88 -0
- package/dist/probe/stats.d.ts +75 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{closeSync as w,existsSync as u,openSync as y,readSync as v,statSync as P}from"fs";import{dirname as z,join as a}from"path";import o from"process";import{fileURLToPath as T}from"url";import{craftBinaryNotFoundMessage as N,resolveCraftBinary as _}from"craft-native";function E(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 Y(e,t){return Promise.all(e.map(async(r)=>{let n=typeof r==="string"?{email:r}:r;try{R(n.email);let i=await t.createToken(n),s=E(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 R(e){if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))throw Error(`Invalid invite email: ${e}`)}import{createHash as S,createPublicKey as I,verify as C}from"crypto";import{mkdir as A,rename as B,writeFile as U}from"fs/promises";import{dirname as L}from"path";async function re(e){k(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=h(await t.json());return m(r,e.trustedKeys),F(r.version,e.currentVersion)>0?r:null}async function ne(e,t,r=fetch,n){if(h(e),!n)throw Error("Desktop update staging requires trusted signing keys");m(e,n),k(e.url);let i=await r(e.url);if(!i.ok)throw Error(`Desktop update download failed with HTTP ${i.status}`);let s=new Uint8Array(await i.arrayBuffer());if(s.byteLength!==e.size)throw Error(`Desktop update size mismatch: expected ${e.size}, received ${s.byteLength}`);if(S("sha256").update(s).digest("hex")!==e.sha256.toLowerCase())throw Error("Desktop update checksum mismatch");await A(L(t),{recursive:!0});let g=`${t}.download-${crypto.randomUUID()}`;return await U(g,s,{mode:448}),await B(g,t),{manifest:e,path:t,bytes:s.byteLength}}function F(e,t){let r=c(e),n=c(t);for(let i=0;i<3;i++){let s=r[i]-n[i];if(s!==0)return Math.sign(s)}return 0}function h(e){if(!e||typeof e!=="object")throw Error("Desktop update manifest must be an object");let t=e;if(c(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 j(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 m(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=I({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||!C(null,j(e),i,s))throw Error("invalid")}catch{throw Error("Desktop update manifest signature verification failed")}}function c(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 k(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 O=[{platform:"darwin",architecture:"arm64",status:"experimental",osVersions:["macOS 15 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-arm64.zip with every release, and no Stacks lifecycle run has re-verified a prebuilt release yet."]},{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."]}];function M(e=process.platform,t=process.arch){return O.find((r)=>r.platform===e&&r.architecture===t)}function se(e,t=process.platform,r=process.arch){let n=M(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}function x(e=o.env.CRAFT_BIN){return _(e)}function de(e=o.env.CRAFT_BIN,t=(r)=>Bun.which(r)){let r=x(e);if(r!=="craft")return r;let n=t(r);if(!n)throw Error(N(r));return n}var D="app/Desktop/launcher.ts";function fe(e=o.cwd()){return u(a(e,D))}function ge(e){return["build","--compile",...e.minify===!1?[]:["--minify"],"--no-compile-autoload-bunfig","--no-compile-autoload-dotenv",e.entry,"--outfile",e.outfile]}function he(e=o.cwd()){let t=a(e,D);if(u(t))return t;let r=z(T(import.meta.url)),i=[a(r,"launcher.ts"),a(r,"launcher.js")].find((s)=>u(s));if(!i)throw Error("Desktop launcher entrypoint not found. Reinstall @stacksjs/desktop - the package must ship dist/launcher.js.");return i}var b=new Set(["CFBundleName","CFBundleDisplayName","CFBundleIdentifier","CFBundleVersion","CFBundleShortVersionString","CFBundleExecutable","CFBundlePackageType","CFBundleInfoDictionaryVersion","CFBundleIconFile"]);function p(e){return e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function l(e,t=""){if(typeof e==="string")return`<string>${p(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} ${
|
|
2
|
+
var c=(e,t,r)=>()=>{if(e)try{t=e(e=0)}catch(n){r=[n]}if(r)throw r[0];return t};function I(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 ne(e,t){return Promise.all(e.map(async(r)=>{let n=typeof r==="string"?{email:r}:r;try{A(n.email);let i=await t.createToken(n),s=I(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 A(e){if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))throw Error(`Invalid invite email: ${e}`)}import{createHash as B,createPublicKey as U,verify as L}from"crypto";import{mkdir as j,rename as F,writeFile as O}from"fs/promises";import{dirname as M}from"path";async function ce(e){w(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=m(await t.json());return k(r,e.trustedKeys),P(r.version,e.currentVersion)>0?r:null}async function ue(e,t,r=fetch,n){if(m(e),!n)throw Error("Desktop update staging requires trusted signing keys");k(e,n),w(e.url);let i=await r(e.url);if(!i.ok)throw Error(`Desktop update download failed with HTTP ${i.status}`);let s=new Uint8Array(await i.arrayBuffer());if(s.byteLength!==e.size)throw Error(`Desktop update size mismatch: expected ${e.size}, received ${s.byteLength}`);if(B("sha256").update(s).digest("hex")!==e.sha256.toLowerCase())throw Error("Desktop update checksum mismatch");await j(M(t),{recursive:!0});let h=`${t}.download-${crypto.randomUUID()}`;return await O(h,s,{mode:448}),await F(h,t),{manifest:e,path:t,bytes:s.byteLength}}function P(e,t){let r=u(e),n=u(t);for(let i=0;i<3;i++){let s=r[i]-n[i];if(s!==0)return Math.sign(s)}return 0}function m(e){if(!e||typeof e!=="object")throw Error("Desktop update manifest must be an object");let t=e;if(u(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 T(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 k(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=U({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||!L(null,T(e),i,s))throw Error("invalid")}catch{throw Error("Desktop update manifest signature verification failed")}}function u(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 w(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 b=()=>{};function N(e=process.platform,t=process.arch){return z.find((r)=>r.platform===e&&r.architecture===t)}function le(e,t=process.platform,r=process.arch){let n=N(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 y=c(()=>{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 x,existsSync as p,openSync as D,readSync as E,statSync as K}from"fs";import{dirname as _,join as a}from"path";import o from"process";import{fileURLToPath as q}from"url";import{craftBinaryNotFoundMessage as V,resolveCraftBinary as G}from"craft-native";function S(e=o.env.CRAFT_BIN){return G(e)}function we(e=o.env.CRAFT_BIN,t=(r)=>Bun.which(r)){let r=S(e);if(r!=="craft")return r;let n=t(r);if(!n)throw Error(V(r));return n}function be(e=o.cwd()){return p(a(e,C))}function ye(e){return["build","--compile",...e.minify===!1?[]:["--minify"],"--no-compile-autoload-bunfig","--no-compile-autoload-dotenv",e.entry,"--outfile",e.outfile]}function ve(e=o.cwd()){let t=a(e,C);if(p(t))return t;let r=_(q(import.meta.url)),i=[a(r,"launcher.ts"),a(r,"launcher.js")].find((s)=>p(s));if(!i)throw Error("Desktop launcher entrypoint not found. Reinstall @stacksjs/desktop - the package must ship dist/launcher.js.");return i}function l(e){return e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function d(e,t=""){if(typeof e==="string")return`<string>${l(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} ${d(n,`${t} `)}`).join(`
|
|
4
4
|
`)}
|
|
5
5
|
${t}</array>`}if(e&&typeof e==="object"){let r=Object.entries(e);if(r.length===0)return"<dict/>";return`<dict>
|
|
6
|
-
${r.map(([i,s])=>`${t} <key>${
|
|
6
|
+
${r.map(([i,s])=>`${t} <key>${l(i)}</key>${d(s,`${t} `)}`).join(`
|
|
7
7
|
`)}
|
|
8
|
-
${t}</dict>`}throw Error(`Unsupported Info.plist value: ${JSON.stringify(e)??String(e)}`)}function
|
|
9
|
-
${r.map(([i,s])=>` <key>${
|
|
10
|
-
`)}`,ignored:t}}function
|
|
8
|
+
${t}</dict>`}throw Error(`Unsupported Info.plist value: ${JSON.stringify(e)??String(e)}`)}function xe(e){let t=Object.keys(e).filter((i)=>v.has(i)),r=Object.entries(e).filter(([i])=>!v.has(i));if(r.length===0)return{xml:"",ignored:t};return{xml:`
|
|
9
|
+
${r.map(([i,s])=>` <key>${l(i)}</key>${d(s," ")}`).join(`
|
|
10
|
+
`)}`,ignored:t}}function W(e,t={}){if(!Number.isInteger(e)||e<1||e>65535)throw RangeError(`Invalid desktop development port: ${e}`);let r=t.url||o.env.APP_URL;if(!r)return"https://stacks.test";let n=/^https?:\/\//.test(r)?r:`https://${r}`;return new URL(n).toString().replace(/\/$/,"")}function H(e,t={}){let r=[S(t.craftBin),W(e,t),"--title",t.title||"Stacks","--width",String(t.width||1400),"--height",String(t.height||900)];if(t.hotReload!==!1)r.push("--hot-reload");if(t.devTools===!1)r.push("--no-devtools");if(t.darkMode)r.push("--dark");if(t.systemTray)r.push("--system-tray");if(t.hideDockIcon)r.push("--hide-dock-icon");if(t.menubarOnly)r.push("--menubar-only");return r}function De(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 Z(e){Bun.spawn(e,{stdin:"inherit",stdout:"inherit",stderr:"inherit"}).unref()}async function Ee(e,t={},r=Z){return await r(H(e,t)),!0}function Q(e){try{let t=D(e,"r");try{let r=Buffer.alloc(4);if(E(t,r,0,4,0)<4)return!1;return X.has(r.readUInt32BE(0))}finally{x(t)}}catch{return!1}}function Se(e){return Q(e)?"MacOS":"Resources"}function Ce(e){return["codesign","--force",e.identity==="-"?"--timestamp=none":"--timestamp","--options","runtime",...e.entitlements?["--entitlements",e.entitlements]:[],"--sign",e.identity,e.target]}function Re(e,t=ee){let r=t(e);if(!r||r.size<f)return!1;return r.tail.includes(J)}function ee(e){try{let{size:t}=K(e);if(t<f)return{size:t,tail:""};let r=D(e,"r");try{let n=Math.min(Y,t),i=Buffer.alloc(n);return E(r,i,0,n,t-n),{size:t,tail:i.toString("latin1")}}finally{x(r)}}catch{return null}}function Ie(e){if(e.length<2)return null;let t=[...e].sort((s,g)=>g.bytes-s.bytes),n=t.slice(1).length*f,i=t.map((s)=>`${s.name} (${(s.bytes/1024/1024).toFixed(1)} MB)`).join(", ");return`This bundle ships ${e.length} Bun-compiled executables: ${i}. Each embeds a full copy of the Bun runtime, so at least ${(n/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 C="app/Desktop/launcher.ts",v,J="---- Bun!",Y=8388608,f=41943040,X;var te=c(()=>{b();y();v=new Set(["CFBundleName","CFBundleDisplayName","CFBundleIdentifier","CFBundleVersion","CFBundleShortVersionString","CFBundleExecutable","CFBundlePackageType","CFBundleInfoDictionaryVersion","CFBundleIconFile"]);X=new Set([4277009102,3472551422,4277009103,3489328638,3405691582,3199925962])});te();export{J as BUN_EXECUTABLE_MARKER,v as RESERVED_PLIST_KEYS,C as USERLAND_LAUNCHER,le as assertDesktopReleaseChannel,Se as bundleDirectoryFor,ce as checkForDesktopUpdate,Ce as codesignArgs,P as compareVersions,H as craftDevCommand,I as createInviteLink,De as createUpdateManifestUrl,Ie as describeRuntimeDuplication,ye as desktopLauncherCompileArgs,N as desktopSupport,z as desktopSupportMatrix,be as hasUserlandDesktopLauncher,Q as isMachO,Re as looksLikeBunExecutable,Ee as openDevWindow,d as plistValue,xe as renderUserlandPlistEntries,S as resolveCraftBinary,we as resolveCraftExecutable,ve as resolveDesktopLauncher,W as resolveDevWindowUrl,ne as sendDesktopInvites,ue as stageDesktopUpdate,T as updateManifestPayload,m as validateUpdateManifest,k as verifyUpdateManifestSignature,l as xmlEscape};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The interactive-content probe (stacksjs/stacks#877): the record schema, the
|
|
2
|
+
// statistics behind it, and the capability-matrix section rendered from it.
|
|
3
|
+
// The runner and the page script are entrypoints of their own, not exports:
|
|
4
|
+
// one opens a native window and the other only runs in a browser.
|
|
5
|
+
export * from './payload';
|
|
6
|
+
export * from './record';
|
|
7
|
+
export * from './records';
|
|
8
|
+
export * from './render';
|
|
9
|
+
export * from './schema';
|
|
10
|
+
export * from './stats';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @bun
|
|
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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @bun
|
|
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
|
|
3
|
+
layout(location = 0) in vec3 position;
|
|
4
|
+
layout(location = 1) in vec3 normal;
|
|
5
|
+
layout(location = 2) in vec4 offset;
|
|
6
|
+
uniform float time;
|
|
7
|
+
uniform float aspect;
|
|
8
|
+
out vec3 shade;
|
|
9
|
+
mat3 rotation(float a, float b) {
|
|
10
|
+
float ca = cos(a), sa = sin(a), cb = cos(b), sb = sin(b);
|
|
11
|
+
return mat3(cb, 0.0, -sb, sa * sb, ca, sa * cb, ca * sb, -sa, ca * cb);
|
|
12
|
+
}
|
|
13
|
+
void main() {
|
|
14
|
+
mat3 r = rotation(time + offset.w, time * 0.7 + offset.w * 2.0);
|
|
15
|
+
vec3 p = r * position * 0.012 + offset.xyz;
|
|
16
|
+
vec3 n = r * normal;
|
|
17
|
+
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
|
+
gl_Position = vec4(p.x / aspect, p.y, p.z * 0.5, 1.0);
|
|
19
|
+
}`,W=`#version 300 es
|
|
20
|
+
precision mediump float;
|
|
21
|
+
in vec3 shade;
|
|
22
|
+
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(()=>{})});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export declare const DEFAULT_PROBE_PAGE_CONFIG: ProbePageConfig;
|
|
2
|
+
/**
|
|
3
|
+
* What the probe page posts back to its runner (stacksjs/stacks#877).
|
|
4
|
+
*
|
|
5
|
+
* Raw observations only. The page does no statistics and makes no judgement:
|
|
6
|
+
* `buildProbeRecord` in `record.ts` turns this into a published record, which is
|
|
7
|
+
* where the arithmetic and the status rules live and are unit-tested.
|
|
8
|
+
*/
|
|
9
|
+
/** Configuration the runner embeds in the page. */
|
|
10
|
+
export declare interface ProbePageConfig {
|
|
11
|
+
idleMs: number
|
|
12
|
+
loadMs: number
|
|
13
|
+
cpuLoadMsPerFrame: number
|
|
14
|
+
instances: number
|
|
15
|
+
interactionWindowMs: number
|
|
16
|
+
tryNativeFullscreen: boolean
|
|
17
|
+
}
|
|
18
|
+
/** The result of asking for fullscreen or pointer lock. */
|
|
19
|
+
export declare interface RequestOutcome {
|
|
20
|
+
outcome: 'entered' | 'rejected' | 'no-api' | 'timeout' | 'not-attempted'
|
|
21
|
+
detail?: string
|
|
22
|
+
}
|
|
23
|
+
/** One input event, timed to the frame that could first show it. */
|
|
24
|
+
export declare interface InputSample {
|
|
25
|
+
toFrameMs: number
|
|
26
|
+
toPostFrameMs: number
|
|
27
|
+
}
|
|
28
|
+
export declare interface AudioSnapshot {
|
|
29
|
+
state: string
|
|
30
|
+
baseLatency: number | null
|
|
31
|
+
outputLatency: number | null
|
|
32
|
+
sampleRate: number
|
|
33
|
+
}
|
|
34
|
+
export declare interface ProbePagePayload {
|
|
35
|
+
config: ProbePageConfig
|
|
36
|
+
userAgent: string
|
|
37
|
+
devicePixelRatio: number
|
|
38
|
+
screen: { width: number, height: number }
|
|
39
|
+
isSecureContext: boolean
|
|
40
|
+
timerResolutionMs: number
|
|
41
|
+
eventTimeStampComparable: boolean
|
|
42
|
+
bridge: { present: boolean, setFullscreen: boolean }
|
|
43
|
+
webgl2: {
|
|
44
|
+
ok: boolean
|
|
45
|
+
error?: string
|
|
46
|
+
version?: string
|
|
47
|
+
shadingLanguageVersion?: string
|
|
48
|
+
vendor?: string
|
|
49
|
+
renderer?: string
|
|
50
|
+
unmaskedVendor?: string
|
|
51
|
+
unmaskedRenderer?: string
|
|
52
|
+
maxTextureSize?: number
|
|
53
|
+
maxSamples?: number
|
|
54
|
+
extensions?: number
|
|
55
|
+
}
|
|
56
|
+
webgpu: {
|
|
57
|
+
present: boolean
|
|
58
|
+
adapter: boolean
|
|
59
|
+
device: boolean
|
|
60
|
+
error?: string
|
|
61
|
+
vendor?: string
|
|
62
|
+
architecture?: string
|
|
63
|
+
description?: string
|
|
64
|
+
maxTextureDimension2D?: number
|
|
65
|
+
features?: number
|
|
66
|
+
}
|
|
67
|
+
frames: {
|
|
68
|
+
idle: number[]
|
|
69
|
+
load: number[]
|
|
70
|
+
triangles: number
|
|
71
|
+
hiddenDuringSampling: boolean
|
|
72
|
+
focused: boolean
|
|
73
|
+
}
|
|
74
|
+
input: {
|
|
75
|
+
pointerdown: InputSample[]
|
|
76
|
+
pointermove: InputSample[]
|
|
77
|
+
keydown: InputSample[]
|
|
78
|
+
eventTimingSupported: boolean
|
|
79
|
+
eventTimingDurations: number[]
|
|
80
|
+
}
|
|
81
|
+
audio: {
|
|
82
|
+
present: boolean
|
|
83
|
+
error?: string
|
|
84
|
+
initial?: AudioSnapshot
|
|
85
|
+
afterUngesturedResume?: AudioSnapshot
|
|
86
|
+
afterGesturedResume?: AudioSnapshot
|
|
87
|
+
}
|
|
88
|
+
gamepad: {
|
|
89
|
+
present: boolean
|
|
90
|
+
connected: number
|
|
91
|
+
ids: string[]
|
|
92
|
+
connectedEventSeen: boolean
|
|
93
|
+
}
|
|
94
|
+
fullscreen: {
|
|
95
|
+
elementApi: boolean
|
|
96
|
+
webkitElementApi: boolean
|
|
97
|
+
enabled: boolean | null
|
|
98
|
+
ungestured: RequestOutcome
|
|
99
|
+
gestured: RequestOutcome
|
|
100
|
+
nativeBridge: RequestOutcome & { sizeBefore?: string, sizeAfter?: string }
|
|
101
|
+
}
|
|
102
|
+
pointerLock: {
|
|
103
|
+
api: boolean
|
|
104
|
+
ungestured: RequestOutcome
|
|
105
|
+
gestured: RequestOutcome
|
|
106
|
+
}
|
|
107
|
+
interaction: {
|
|
108
|
+
windowMs: number
|
|
109
|
+
sawRealInput: boolean
|
|
110
|
+
skipped: boolean
|
|
111
|
+
}
|
|
112
|
+
errors: string[]
|
|
113
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { InteractiveProbeRecord } from './schema';
|
|
2
|
+
import type { ProbePagePayload } from './payload';
|
|
3
|
+
/** The browser and version a user agent string names, best effort. */
|
|
4
|
+
export declare function browserFromUserAgent(userAgent: string): { name: string, version: string };
|
|
5
|
+
/** Combine the page's observations with the runner's host facts into a record. */
|
|
6
|
+
export declare function buildProbeRecord(payload: ProbePagePayload, facts: ProbeHostFacts): InteractiveProbeRecord;
|
|
7
|
+
/** Fewer trusted input events than this is reported, not summarised. */
|
|
8
|
+
export declare const MIN_INPUT_SAMPLES: 5;
|
|
9
|
+
/** What the runner knows about the host that the page cannot see. */
|
|
10
|
+
export declare interface ProbeHostFacts {
|
|
11
|
+
measuredAt: string
|
|
12
|
+
host: { kind: 'craft' | 'browser', name: string, version: string }
|
|
13
|
+
os: InteractiveProbeRecord['os']
|
|
14
|
+
machine: {
|
|
15
|
+
model: string
|
|
16
|
+
cpu: string
|
|
17
|
+
display: { name: string, resolution: string, refreshHz: number | null }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { InteractiveProbeRecord } from './schema';
|
|
2
|
+
/** The file a record is stored under. Version is deliberately not part of it. */
|
|
3
|
+
export declare function probeRecordFileName(record: Pick<InteractiveProbeRecord, 'host' | 'os'>): string;
|
|
4
|
+
/** Every record in `dir`, validated, in file-name order. Throws naming the bad file. */
|
|
5
|
+
export declare function loadProbeRecords(dir?: string): InteractiveProbeRecord[];
|
|
6
|
+
/** The checked-in records, beside this module in the framework repository. */
|
|
7
|
+
export declare const PROBE_RESULTS_DIR: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InteractiveProbeRecord, ProbeName, ProbeResult } from './schema';
|
|
2
|
+
/** The column heading for one record: the host and its version. */
|
|
3
|
+
export declare function recordLabel(record: InteractiveProbeRecord): string;
|
|
4
|
+
/** Records in the order the page shows them: Craft first, then browsers, each by name. */
|
|
5
|
+
export declare function orderRecords(records: readonly InteractiveProbeRecord[]): InteractiveProbeRecord[];
|
|
6
|
+
/** The one figure worth putting in a table cell, when the probe has one. */
|
|
7
|
+
export declare function headline(name: ProbeName, probe: ProbeResult): string | null;
|
|
8
|
+
/** The generated block, markers included. */
|
|
9
|
+
export declare function renderInteractiveCapabilities(records: readonly InteractiveProbeRecord[]): string;
|
|
10
|
+
export declare const INTERACTIVE_BEGIN: '<!-- interactive-capability:begin -->';
|
|
11
|
+
export declare const INTERACTIVE_END: '<!-- interactive-capability:end -->';
|
|
12
|
+
/** The command that produces a record, named once so the page and the CLI agree. */
|
|
13
|
+
export declare const PROBE_COMMAND: 'buddy desktop:probe';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { InteractiveProbeRecord } from './schema';
|
|
2
|
+
import type { ProbeHostFacts } from './record';
|
|
3
|
+
import type { ProbePageConfig } from './payload';
|
|
4
|
+
/** `craft version 0.0.105` (the first line of `craft --version`) to `0.0.105`. */
|
|
5
|
+
export declare function parseCraftVersion(output: string | null): string | null;
|
|
6
|
+
/**
|
|
7
|
+
* The main display from `system_profiler SPDisplaysDataType -json`, without
|
|
8
|
+
* the serial number and vendor identifiers it also reports.
|
|
9
|
+
*/
|
|
10
|
+
export declare function mainDisplayFromProfiler(json: string | null): DisplayFacts | null;
|
|
11
|
+
/** Everything about the host the page cannot see. */
|
|
12
|
+
export declare function collectHostFacts(mode: 'craft' | 'browser', craftVersion: string | null, userAgent: string): ProbeHostFacts;
|
|
13
|
+
/** Serve the page, run it, and return the validated record. */
|
|
14
|
+
export declare function runProbe(options: ProbeRunOptions): Promise<InteractiveProbeRecord>;
|
|
15
|
+
/** A plain-text summary, one line per probe. */
|
|
16
|
+
export declare function summarize(record: InteractiveProbeRecord): string;
|
|
17
|
+
export declare function run(): Promise<void>;
|
|
18
|
+
export declare interface ProbeRunOptions {
|
|
19
|
+
mode: 'craft' | 'browser'
|
|
20
|
+
craftBin?: string
|
|
21
|
+
open?: boolean | string
|
|
22
|
+
page?: Partial<ProbePageConfig>
|
|
23
|
+
timeoutMs?: number
|
|
24
|
+
port?: number
|
|
25
|
+
log?: (line: string) => void
|
|
26
|
+
}
|
|
27
|
+
declare interface DisplayFacts { name: string, resolution: string, refreshHz: number | null }
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var O=(e,t,n)=>()=>{if(e)try{t=e(e=0)}catch(r){n=[r]}if(n)throw n[0];return t};function Ke(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 s=n.expiresAt instanceof Date?n.expiresAt:new Date(n.expiresAt);if(Number.isNaN(s.valueOf()))throw Error("Invite expiration must be a valid date");r.searchParams.set("expires",s.toISOString())}return r.toString()}async function Zt(e,t){return Promise.all(e.map(async(n)=>{let r=typeof n==="string"?{email:n}:n;try{Je(r.email);let s=await t.createToken(r),o=Ke(t.baseUrl,s,{email:r.email,team:r.team,role:r.role,expiresAt:t.expiresAt});return await t.send({recipient:r,url:o}),{recipient:r,url:o,delivered:!0}}catch(s){return{recipient:r,url:"",delivered:!1,error:s instanceof Error?s.message:String(s)}}}))}function Je(e){if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))throw Error(`Invalid invite email: ${e}`)}import{createHash as Ze,createPublicKey as Ye,verify as Xe}from"crypto";import{mkdir as Qe,rename as et,writeFile as tt}from"fs/promises";import{dirname as nt}from"path";async function tn(e){ce(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=ae(await t.json());return ue(n,e.trustedKeys),rt(n.version,e.currentVersion)>0?n:null}async function nn(e,t,n=fetch,r){if(ae(e),!r)throw Error("Desktop update staging requires trusted signing keys");ue(e,r),ce(e.url);let s=await n(e.url);if(!s.ok)throw Error(`Desktop update download failed with HTTP ${s.status}`);let o=new Uint8Array(await s.arrayBuffer());if(o.byteLength!==e.size)throw Error(`Desktop update size mismatch: expected ${e.size}, received ${o.byteLength}`);if(Ze("sha256").update(o).digest("hex")!==e.sha256.toLowerCase())throw Error("Desktop update checksum mismatch");await Qe(nt(t),{recursive:!0});let i=`${t}.download-${crypto.randomUUID()}`;return await tt(i,o,{mode:448}),await et(i,t),{manifest:e,path:t,bytes:o.byteLength}}function rt(e,t){let n=_(e),r=_(t);for(let s=0;s<3;s++){let o=n[s]-r[s];if(o!==0)return Math.sign(o)}return 0}function ae(e){if(!e||typeof e!=="object")throw Error("Desktop update manifest must be an object");let t=e;if(_(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 st(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 ue(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 s=Ye({key:Buffer.from(n.slice(r.length),"base64url"),format:"der",type:"spki"}),o=Buffer.from(e.signature,"base64url");if(s.asymmetricKeyType!=="ed25519"||o.length!==64||!Xe(null,st(e),s,o))throw Error("invalid")}catch{throw Error("Desktop update manifest signature verification failed")}}function _(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 ce(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 le=()=>{};function it(e=process.platform,t=process.arch){return ot.find((n)=>n.platform===e&&n.architecture===t)}function sn(e,t=process.platform,n=process.arch){let r=it(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 ot;var de=O(()=>{ot=[{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 me,existsSync as z,openSync as fe,readSync as ge,statSync as at}from"fs";import{dirname as ut,join as F}from"path";import E from"process";import{fileURLToPath as ct}from"url";import{craftBinaryNotFoundMessage as lt,resolveCraftBinary as dt}from"craft-native";function he(e=E.env.CRAFT_BIN){return dt(e)}function be(e=E.env.CRAFT_BIN,t=(n)=>Bun.which(n)){let n=he(e);if(n!=="craft")return n;let r=t(n);if(!r)throw Error(lt(n));return r}function pn(e=E.cwd()){return z(F(e,we))}function mn(e){return["build","--compile",...e.minify===!1?[]:["--minify"],"--no-compile-autoload-bunfig","--no-compile-autoload-dotenv",e.entry,"--outfile",e.outfile]}function fn(e=E.cwd()){let t=F(e,we);if(z(t))return t;let n=ut(ct(import.meta.url)),s=[F(n,"launcher.ts"),F(n,"launcher.js")].find((o)=>z(o));if(!s)throw Error("Desktop launcher entrypoint not found. Reinstall @stacksjs/desktop - the package must ship dist/launcher.js.");return s}function H(e){return e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function q(e,t=""){if(typeof e==="string")return`<string>${H(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((r)=>`${t} ${q(r,`${t} `)}`).join(`
|
|
4
|
+
`)}
|
|
5
|
+
${t}</array>`}if(e&&typeof e==="object"){let n=Object.entries(e);if(n.length===0)return"<dict/>";return`<dict>
|
|
6
|
+
${n.map(([s,o])=>`${t} <key>${H(s)}</key>${q(o,`${t} `)}`).join(`
|
|
7
|
+
`)}
|
|
8
|
+
${t}</dict>`}throw Error(`Unsupported Info.plist value: ${JSON.stringify(e)??String(e)}`)}function gn(e){let t=Object.keys(e).filter((s)=>pe.has(s)),n=Object.entries(e).filter(([s])=>!pe.has(s));if(n.length===0)return{xml:"",ignored:t};return{xml:`
|
|
9
|
+
${n.map(([s,o])=>` <key>${H(s)}</key>${q(o," ")}`).join(`
|
|
10
|
+
`)}`,ignored:t}}function pt(e,t={}){if(!Number.isInteger(e)||e<1||e>65535)throw RangeError(`Invalid desktop development port: ${e}`);let n=t.url||E.env.APP_URL;if(!n)return"https://stacks.test";let r=/^https?:\/\//.test(n)?n:`https://${n}`;return new URL(r).toString().replace(/\/$/,"")}function mt(e,t={}){let n=[he(t.craftBin),pt(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 hn(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 ft(e){Bun.spawn(e,{stdin:"inherit",stdout:"inherit",stderr:"inherit"}).unref()}async function bn(e,t={},n=ft){return await n(mt(e,t)),!0}function wt(e){try{let t=fe(e,"r");try{let n=Buffer.alloc(4);if(ge(t,n,0,4,0)<4)return!1;return bt.has(n.readUInt32BE(0))}finally{me(t)}}catch{return!1}}function wn(e){return wt(e)?"MacOS":"Resources"}function yn(e){return["codesign","--force",e.identity==="-"?"--timestamp=none":"--timestamp","--options","runtime",...e.entitlements?["--entitlements",e.entitlements]:[],"--sign",e.identity,e.target]}function kn(e,t=yt){let n=t(e);if(!n||n.size<W)return!1;return n.tail.includes(gt)}function yt(e){try{let{size:t}=at(e);if(t<W)return{size:t,tail:""};let n=fe(e,"r");try{let r=Math.min(ht,t),s=Buffer.alloc(r);return ge(n,s,0,r,t-r),{size:t,tail:s.toString("latin1")}}finally{me(n)}}catch{return null}}function vn(e){if(e.length<2)return null;let t=[...e].sort((o,a)=>a.bytes-o.bytes),r=t.slice(1).length*W,s=t.map((o)=>`${o.name} (${(o.bytes/1024/1024).toFixed(1)} MB)`).join(", ");return`This bundle ships ${e.length} Bun-compiled executables: ${s}. 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 we="app/Desktop/launcher.ts",pe,gt="---- Bun!",ht=8388608,W=41943040,bt;var ye=O(()=>{le();de();pe=new Set(["CFBundleName","CFBundleDisplayName","CFBundleIdentifier","CFBundleVersion","CFBundleShortVersionString","CFBundleExecutable","CFBundlePackageType","CFBundleInfoDictionaryVersion","CFBundleIconFile"]);bt=new Set([4277009102,3472551422,4277009103,3489328638,3405691582,3199925962])});import{existsSync as Re,mkdirSync as kt,writeFileSync as ke}from"fs";import ve from"os";import{dirname as vt,join as L,relative as xe,resolve as xt}from"path";import l from"process";import{fileURLToPath as Pt}from"url";function Se(e){return e.replaceAll("</script","<\\/script")}function G(e,t){return`<!doctype html>
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="utf-8">
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
15
|
+
<title>Stacks interactive probe</title>
|
|
16
|
+
<style>
|
|
17
|
+
:root { color-scheme: dark; --bg: #0a0d14; --panel: #141a26; --text: #e6e9f0; --muted: #9aa3b5; --accent: #5b8cf2; }
|
|
18
|
+
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
19
|
+
#scene { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
|
|
20
|
+
#hud { position: fixed; left: 16px; right: 16px; top: 16px; padding: 12px 16px; background: color-mix(in srgb, var(--panel) 88%, transparent); border-radius: 10px; }
|
|
21
|
+
#hud h1 { margin: 0 0 4px; font-size: 15px; font-weight: 600; }
|
|
22
|
+
#status { color: var(--muted); margin: 0; }
|
|
23
|
+
#interactive { position: fixed; left: 16px; right: 16px; bottom: 16px; padding: 16px; background: var(--panel); border-radius: 10px; }
|
|
24
|
+
#interactive[hidden] { display: none; }
|
|
25
|
+
#interactive p { margin: 0 0 12px; }
|
|
26
|
+
#interactive ol { margin: 0 0 12px; padding-left: 20px; color: var(--muted); }
|
|
27
|
+
button { font: inherit; padding: 8px 14px; margin-right: 8px; border-radius: 8px; border: 1px solid var(--accent); background: transparent; color: var(--text); cursor: pointer; }
|
|
28
|
+
button[disabled] { opacity: 0.4; cursor: default; }
|
|
29
|
+
#countdown { color: var(--muted); margin-top: 10px; }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<canvas id="scene"></canvas>
|
|
34
|
+
<div id="hud">
|
|
35
|
+
<h1>Stacks interactive-content probe</h1>
|
|
36
|
+
<p id="status">Starting</p>
|
|
37
|
+
</div>
|
|
38
|
+
<div id="interactive" hidden>
|
|
39
|
+
<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>
|
|
40
|
+
<ol>
|
|
41
|
+
<li>Click <strong>Fullscreen + audio</strong>. It enters fullscreen briefly and resumes audio.</li>
|
|
42
|
+
<li>Click <strong>Pointer lock</strong>. It locks the pointer for half a second.</li>
|
|
43
|
+
<li>Press any key five times, and move the mouse over the window.</li>
|
|
44
|
+
</ol>
|
|
45
|
+
<button id="fullscreen-button" type="button">Fullscreen + audio</button>
|
|
46
|
+
<button id="lock-button" type="button">Pointer lock</button>
|
|
47
|
+
<button id="done-button" type="button">Done</button>
|
|
48
|
+
<div id="countdown"></div>
|
|
49
|
+
</div>
|
|
50
|
+
<script type="application/json" id="probe-config">${JSON.stringify(t).replaceAll("<","\\u003c")}</script>
|
|
51
|
+
<script type="module">${Se(e)}</script>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
54
|
+
`}var V={idleMs:3000,loadMs:6000,cpuLoadMsPerFrame:6,instances:40000,interactionWindowMs:20000,tryNativeFullscreen:!0};var J=1,K=["available","unavailable","measured","requires-interaction","denied","error"],b=[["webgl2","WebGL2"],["webgpu","WebGPU"],["framePacing","Frame pacing"],["inputLatency","Input latency"],["audioLatency","Audio latency"],["gamepad","Gamepad API"],["fullscreen","Fullscreen"],["pointerLock","Pointer lock"]];function f(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}function x(e){return typeof e==="string"&&e.trim().length>0}function Ee(e){let t=[];if(!f(e))return["record is not an object"];if(e.schema!==1)t.push(`schema must be 1, got ${JSON.stringify(e.schema)}`);if(!x(e.measuredAt)||Number.isNaN(Date.parse(e.measuredAt)))t.push("measuredAt must be an ISO 8601 timestamp");let n=e.host;if(!f(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(!x(n[i]))t.push(`host.${i} is required`);if(n.kind==="craft"&&x(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(!f(r))t.push("os is missing");else for(let i of["platform","arch","name","version"])if(!x(r[i]))t.push(`os.${i} is required`);let s=e.machine;if(!f(s))t.push("machine is missing");else{for(let u of["model","cpu"])if(!x(s[u]))t.push(`machine.${u} is required`);let i=s.display;if(!f(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 o=e.run;if(!f(o))t.push("run is missing");else{for(let i of["idleMs","loadMs","cpuLoadMsPerFrame","triangles","interactionWindowMs"])if(!(typeof o[i]==="number"&&o[i]>=0))t.push(`run.${i} must be a non-negative number`);if(typeof o.sawRealInput!=="boolean")t.push("run.sawRealInput must be a boolean")}let a=e.probes;if(!f(a))return t.push("probes is missing"),t;for(let[i]of b){let u=a[i];if(!f(u)){t.push(`probes.${i} is missing`);continue}if(!K.includes(u.status))t.push(`probes.${i}.status must be one of ${K.join(", ")}, got ${JSON.stringify(u.status)}`);if(!x(u.note))t.push(`probes.${i}.note is required: say what was observed, even on success`);if(u.data!==void 0&&!f(u.data))t.push(`probes.${i}.data must be an object when present`);if(u.status==="measured"&&!f(u.data))t.push(`probes.${i} is measured but carries no data`)}for(let i of Object.keys(a))if(!b.some(([u])=>u===i))t.push(`probes.${i} is not a known probe`);return t}function I(e,t="probe record"){let n=Ee(e);if(n.length>0)throw Error(`${t} is invalid:
|
|
55
|
+
- ${n.join(`
|
|
56
|
+
- `)}`);return e}var Me=[24,30,48,50,60,72,75,90,100,120,144,165,240];function y(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),s=Math.floor(r),o=Math.ceil(r),a=n[s],i=n[o];return a+(i-a)*(r-s)}function T(e){let t=[];for(let n=1;n<e.length;n++)t.push(e[n]-e[n-1]);return t}function Ie(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),s=0;for(let o of e){let a=Math.floor(Math.max(0,o)/t);if(a>=n)s++;else r[a]++}return{bucketMs:t,counts:r,overflow:s}}function Z(e,t=0.03){if(!(e>0))throw RangeError(`period must be positive, got ${e}`);let n=1000/e,r=n,s=Number.POSITIVE_INFINITY;for(let o of Me){let a=Math.abs(n-o)/o;if(a<s)r=o,s=a}return s<=t?r:Math.round(n*100)/100}function Ae(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 A(e,t){if(e.length<3)return null;if(!(t>0))throw RangeError(`targetHz must be positive, got ${t}`);let n=T(e),r=e[e.length-1]-e[0],s=1000/t,o=(a)=>Math.round(a*100)/100;return{frames:e.length,durationMs:o(r),fps:o(r>0?n.length*1000/r:0),p50Ms:o(y(n,50)),p95Ms:o(y(n,95)),p99Ms:o(y(n,99)),maxMs:o(Math.max(...n)),targetPeriodMs:o(s),targetHz:t,droppedFrames:Ae(n,s),longFrames:n.filter((a)=>a>s*1.5).length,histogram:Ie(n)}}function D(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(y(t,50)),p95Ms:n(y(t,95)),maxMs:n(Math.max(...t))}}function Y(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 ee(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 s=e.match(r);if(s)return{name:n,version:s[1]}}return{name:"unknown",version:"unknown"}}function X(e){return typeof e==="number"?`${Math.round(e*1e4)/10} ms`:"not reported"}function R(e){return e.detail?`${e.outcome} (${e.detail})`:e.outcome}function De(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 Fe(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 Ce(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 s=Z(y(T(n.idle),50)),o=t.machine.display.refreshHz??s,a=A(n.idle,o),i=A(n.load,o),u=A(n.load,s),c={rafHz:s,displayHz:o,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=s<o-1?`requestAnimationFrame ran at ${s} Hz on a ${o} Hz display, so a game cannot reach the panel's rate. `:`requestAnimationFrame ran at the display's ${o} Hz. `,p=n.triangles>0?`${n.triangles.toLocaleString("en-US")} triangles`:"a 2D canvas fallback";return{status:"measured",note:`${d}Under load (${p} + ${r.cpuLoadMsPerFrame} ms CPU per frame): p50 ${i.p50Ms} ms, p95 ${i.p95Ms} ms, p99 ${i.p99Ms} ms; ${u.droppedFrames} frames dropped at ${s} Hz and ${i.droppedFrames} at the display's ${o} 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 Q=5;function Le(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},s=Object.values(r).reduce((i,u)=>i+u.length,0);if(s===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 o={...n},a=[];for(let[i,u]of Object.entries(r)){let c=D(u.map((p)=>p.toFrameMs)),d=D(u.map((p)=>p.toPostFrameMs));if(o[i]={toFrame:c,toPostFrame:d},c)a.push(`${i} p50 ${c.p50Ms} ms to next frame (${c.samples} ${c.samples===1?"sample":"samples"})`)}if(o.eventTiming=D(t.eventTimingDurations),s<Q)return{status:"requires-interaction",note:`Only ${s} trusted input ${s===1?"event":"events"} arrived, fewer than the ${Q} needed to summarise. Seen: ${a.join("; ")}. Re-run and use the panel's buttons and keys.`,data:o};return{status:"measured",note:`Event timestamp to the next rAF callback: ${a.join("; ")}. Real input from the OS; no synthetic events.`,data:o}}function Oe(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},s=`baseLatency ${X(n.baseLatency)}, outputLatency ${X(n.outputLatency)} at ${n.sampleRate} Hz`,o=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:`${o}, so device latency was not observable. Reported before running: ${s}.`,data:r};if(n.baseLatency===null&&n.outputLatency===null)return{status:"unavailable",note:`${o}, but neither baseLatency nor outputLatency is implemented.`,data:r};return{status:"measured",note:`${o}. ${s}${n.outputLatency===null?" (outputLatency not implemented)":""}.`,data:r}}function Te(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 Ne(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: ${R(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 s=`Without a gesture: ${R(t.ungestured)}.`;switch(t.gestured.outcome){case"entered":return{status:"available",note:`requestFullscreen() from a real click entered fullscreen. ${s}${n}`,data:r};case"rejected":case"timeout":return{status:"denied",note:`requestFullscreen() from a real click: ${R(t.gestured)}. ${s}${n}`,data:r};default:return{status:"requires-interaction",note:`The API exists (fullscreenEnabled ${String(t.enabled)}) but needs a real click to test. ${s}${n}`,data:r}}}function Be(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: ${R(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: ${R(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 te(e,t){return{schema:J,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:De(e),webgpu:Fe(e),framePacing:Ce(e,t),inputLatency:Le(e),audioLatency:Oe(e),gamepad:Te(e),fullscreen:Ne(e),pointerLock:Be(e)}}}import{readdirSync as je,readFileSync as Ue}from"fs";import{join as _e}from"path";import{fileURLToPath as ze}from"url";var S=ze(new URL("./results",import.meta.url));function N(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function ne(e){return`${e.host.kind==="craft"?"craft":`browser-${N(e.host.name)}`}-${N(e.os.platform)}-${N(e.os.arch)}.json`}function Wt(e=S){let t;try{t=je(e).filter((n)=>n.endsWith(".json")).sort()}catch{return[]}return t.map((n)=>{let r=_e(e,n);return I(JSON.parse(Ue(r,"utf8")),r)})}var He="<!-- interactive-capability:begin -->",re="<!-- interactive-capability:end -->",B="buddy desktop:probe",qe={darwin:"macOS",linux:"Linux",win32:"Windows"};function se(e){return e.host.kind==="craft"?`Craft ${e.host.version}`:`${e.host.name} ${e.host.version}`}function oe(e){return`${qe[e.os.platform]??e.os.name} ${e.os.version} ${e.os.arch}, ${e.machine.cpu} (${e.machine.model})`}function We(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 P(e){return typeof e==="number"&&Number.isFinite(e)?e:null}function ie(e,...t){let n=e;for(let r of t){if(typeof n!=="object"||n===null)return;n=n[r]}return n}function U(e,t){let n=t.data;if(t.status!=="measured"||!n)return null;switch(e){case"framePacing":{let r=P(n.rafHz),s=P(n.displayHz),o=P(ie(n,"load","p99Ms"));if(r===null||s===null||o===null)return null;return`${r} Hz rAF on a ${s} Hz display, p99 ${o} ms under load`}case"inputLatency":{for(let r of["pointerdown","keydown","pointermove"]){let s=P(ie(n,r,"toFrame","p50Ms"));if(s!==null)return`${r} p50 ${s} ms to next frame`}return null}case"audioLatency":{let r=n.afterGesturedResume??n.afterUngesturedResume,s=P(r?.baseLatency),o=P(r?.outputLatency),a=(i)=>i===null?"n/a":`${Math.round(i*1e4)/10} ms`;return`base ${a(s)}, output ${a(o)}`}default:return null}}function Ge(e,t){if(!t)return"not probed";let n=U(e,t);return n?`**${t.status}** - ${n}`:`**${t.status}**`}function j(e){return e.replaceAll("|","\\|").replaceAll(`
|
|
57
|
+
`," ")}function Kt(e){let t=We(e),n=[He,"","<!-- Generated by `buddy docs:capabilities` from the records in"," storage/framework/core/desktop/src/probe/results/. Re-measure with",` \`${B} --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 \`${B} --record\`.`,""];if(t.length===0)return n.push(`No measurement has been recorded yet. Run \`${B} --record\`.`,"",re),n.join(`
|
|
58
|
+
`);n.push(`| Probe | ${t.map((r)=>j(se(r))).join(" | ")} |`,`|---|${t.map(()=>"---").join("|")}|`);for(let[r,s]of b)n.push(`| ${s} | ${t.map((o)=>j(Ge(r,o.probes[r]))).join(" | ")} |`);n.push(`| Measured on | ${t.map((r)=>j(oe(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 s=r.machine.display.refreshHz===null?"unknown refresh rate":`${r.machine.display.refreshHz} Hz display`;n.push(`**${se(r)}** - ${oe(r)}, ${s}`,"");for(let[o,a]of b){let i=r.probes[o];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.","",re),n.join(`
|
|
59
|
+
`)}function k(e){try{let t=Bun.spawnSync(e,{stdout:"pipe",stderr:"ignore"});return t.exitCode===0?t.stdout.toString().trim():null}catch{return null}}function Rt(e){return e?.match(/craft version\s+(\d+\.\d+\.\d+\S*)/i)?.[1]??null}function $t(e){if(!e)return null;try{let n=(JSON.parse(e).SPDisplaysDataType??[]).flatMap((o)=>o.spdisplays_ndrvs??[]),r=n.find((o)=>o.spdisplays_main==="spdisplays_yes")??n[0];if(!r)return null;let s=r._spdisplays_resolution??"unknown";return{name:r._name??"unknown",resolution:s,refreshHz:Y(s)}}catch{return null}}function St(e,t,n){let r=l.platform,s=Et[r]??r,o=ve.release(),a,i="unknown",u=ve.cpus()[0]?.model?.trim()||"unknown",c={name:"unknown",resolution:"unknown",refreshHz:null};if(r==="darwin")s="macOS",o=k(["sw_vers","-productVersion"])??o,a=k(["sw_vers","-buildVersion"])??void 0,i=k(["sysctl","-n","hw.model"])??i,u=k(["sysctl","-n","machdep.cpu.brand_string"])??u,c=$t(k(["system_profiler","SPDisplaysDataType","-json"]))??c;else if(r==="linux")i=k(["cat","/sys/devices/virtual/dmi/id/product_name"])??i;let d=ee(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:l.arch,name:s,version:o,...a?{build:a}:{}},machine:{model:i,cpu:u,display:c}}}var Et={darwin:"macOS",linux:"Linux",win32:"Windows"};async function Mt(){let e=vt(Pt(import.meta.url)),t=[L(e,"page.ts"),L(e,"page.js")].find(Re);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:
|
|
60
|
+
${n.logs.map(String).join(`
|
|
61
|
+
`)}`);return await n.outputs[0].text()}function It(e,t){if(t===!1)return null;if(l.platform==="darwin")return typeof t==="string"?["open","-a",t,e]:["open",e];if(l.platform==="win32")return["cmd","/c","start","",e];return["xdg-open",e]}async function At(e){let t=e.log??((m)=>console.log(m)),n={...V,...e.page};if(e.mode==="browser")n.tryNativeFullscreen=!1;let r=null,s=null;if(e.mode==="craft"){await Promise.resolve().then(() => ye());if(r=be(e.craftBin),s=Rt(k([r,"--version"])),!s)throw Error(`Could not read a version from \`${r} --version\`; is it a Craft binary?`)}let o=G(await Mt(),n),a,i,u=new Promise((m,h)=>{a=m,i=h}),c=Bun.serve({hostname:"127.0.0.1",port:e.port??0,async fetch(m){let{pathname:h}=new URL(m.url);if(m.method==="POST"&&h==="/results")return a(await m.json()),new Response("ok");if(m.method==="POST"&&h==="/progress")return t(` page: ${await m.text()}`),new Response("ok");if(m.method==="POST"&&h==="/failed")return i(Error(`The probe page failed: ${await m.text()}`)),new Response("ok");if(h==="/")return new Response(o,{headers:{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}});return new Response("not found",{status:404})}}),d=c.url.href,p=null,g=()=>{p?.kill(),l.exit(130)};l.once("SIGINT",g),l.once("SIGTERM",g);let v=e.timeoutMs??n.idleMs+n.loadMs+n.interactionWindowMs+60000,$e=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 ${s} (${r})`),p=Bun.spawn([r,d,"--title","Stacks interactive probe","--width","1280","--height","800"],{stdout:"ignore",stderr:"ignore"}),p.exited.then((M)=>i(Error(`Craft exited (code ${M}) before the probe finished`)));else{t(`Probe page: ${d}`);let M=It(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,h=St(e.mode,s,m.userAgent);return I(te(m,h),"the probe result")}finally{clearTimeout($e),l.off("SIGINT",g),l.off("SIGTERM",g),p?.kill(),c.stop(!0)}}function Dt(e){let t=Math.max(...b.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,s]of b){let o=e.probes[r],a=U(r,o);n.push(` ${s.padEnd(t)} ${o.status}${a?` - ${a}`:""}`)}return n.join(`
|
|
62
|
+
`)}function w(e){let t=`--${e}=`;for(let n=0;n<l.argv.length;n++){let r=l.argv[n];if(r===`--${e}`){let s=l.argv[n+1];return s&&!s.startsWith("--")?s:!0}if(r.startsWith(t))return r.slice(t.length)}return}function C(e){let t=w(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 Pe=xt(S,"../../../../../../..");async function Ft(){let e=w("browser"),t=w("record")!==void 0,n=w("out"),r=w("cpu-load"),s=w("instances");if(t){if(xe(Pe,l.cwd()).startsWith("..")||!Re(L(Pe,"storage/framework/core/buddy")))throw Error("--record writes into the Stacks framework repository; run it from that checkout, or use --out <file>.")}let o={},a=C("idle"),i=C("load"),u=C("interaction-window");if(a!==void 0)o.idleMs=a;if(i!==void 0)o.loadMs=i;if(u!==void 0)o.interactionWindowMs=u;if(typeof r==="string")o.cpuLoadMsPerFrame=Number(r);if(typeof s==="string")o.instances=Number(s);let c=typeof e==="string"?e:!0;if(w("no-open")!==void 0)c=!1;let d=w("craft-bin"),p=await At({mode:e===void 0?"craft":"browser",open:c,craftBin:typeof d==="string"?d:void 0,timeoutMs:C("timeout"),page:o});console.log(`
|
|
63
|
+
${Dt(p)}
|
|
64
|
+
`);let g=`${JSON.stringify(p,null,2)}
|
|
65
|
+
`;if(t){kt(S,{recursive:!0});let v=L(S,ne(p));ke(v,g),console.log(`Recorded ${xe(l.cwd(),v)}. Run \`buddy docs:capabilities\` to publish it.`)}if(typeof n==="string")ke(n,g),console.log(`Wrote ${n}`);if(!t&&typeof n!=="string")console.log(g)}if(import.meta.main)try{await Ft()}catch(e){l.stderr.write(`${e instanceof Error?e.message:String(e)}
|
|
66
|
+
`),l.exit(1)}export{St as collectHostFacts,$t as mainDisplayFromProfiler,Rt as parseCraftVersion,Ft as run,At as runProbe,Dt as summarize};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every way `value` fails to be an `InteractiveProbeRecord`, as readable
|
|
3
|
+
* messages. An empty array means it is valid.
|
|
4
|
+
*
|
|
5
|
+
* Hand-written rather than a schema library because the desktop package has no
|
|
6
|
+
* validation dependency and this is the one shape it checks.
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateProbeRecord(value: unknown): string[];
|
|
9
|
+
/** Throw with every validation error at once, or return the value typed. */
|
|
10
|
+
export declare function assertProbeRecord(value: unknown, source?: string): InteractiveProbeRecord;
|
|
11
|
+
/**
|
|
12
|
+
* The shape of one interactive-content measurement (stacksjs/stacks#877).
|
|
13
|
+
*
|
|
14
|
+
* A record is data, not prose: it says when it was taken, against which host
|
|
15
|
+
* (a Craft window or a browser) at which version, on which OS and machine. That
|
|
16
|
+
* is what lets it go stale visibly - a reader sees "Craft 0.0.105, macOS 27.0,
|
|
17
|
+
* 2026-10-02" beside every number and can tell how far that is from what they
|
|
18
|
+
* run.
|
|
19
|
+
*
|
|
20
|
+
* Every probe carries a status and a note. A probe that could not run - input
|
|
21
|
+
* latency with nobody at the keyboard, pointer lock without a user gesture, a
|
|
22
|
+
* gamepad with none attached - says `requires-interaction` and what was still
|
|
23
|
+
* observable. It never carries a number it did not measure.
|
|
24
|
+
*/
|
|
25
|
+
export declare const PROBE_SCHEMA_VERSION: 1;
|
|
26
|
+
export declare const PROBE_STATUSES: readonly ProbeStatus[];
|
|
27
|
+
/** Probe order, and the label each is published under. */
|
|
28
|
+
export declare const PROBE_NAMES: ReadonlyArray<[ProbeName, string]>;
|
|
29
|
+
export declare interface ProbeResult {
|
|
30
|
+
status: ProbeStatus
|
|
31
|
+
note: string
|
|
32
|
+
data?: Record<string, unknown>
|
|
33
|
+
}
|
|
34
|
+
export declare interface InteractiveProbeRecord {
|
|
35
|
+
schema: typeof PROBE_SCHEMA_VERSION
|
|
36
|
+
measuredAt: string
|
|
37
|
+
host: {
|
|
38
|
+
kind: 'craft' | 'browser'
|
|
39
|
+
name: string
|
|
40
|
+
version: string
|
|
41
|
+
userAgent: string
|
|
42
|
+
}
|
|
43
|
+
os: {
|
|
44
|
+
platform: string
|
|
45
|
+
arch: string
|
|
46
|
+
name: string
|
|
47
|
+
version: string
|
|
48
|
+
build?: string
|
|
49
|
+
}
|
|
50
|
+
machine: {
|
|
51
|
+
model: string
|
|
52
|
+
cpu: string
|
|
53
|
+
display: {
|
|
54
|
+
name: string
|
|
55
|
+
resolution: string
|
|
56
|
+
refreshHz: number | null
|
|
57
|
+
devicePixelRatio: number
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
run: {
|
|
61
|
+
idleMs: number
|
|
62
|
+
loadMs: number
|
|
63
|
+
cpuLoadMsPerFrame: number
|
|
64
|
+
triangles: number
|
|
65
|
+
interactionWindowMs: number
|
|
66
|
+
sawRealInput: boolean
|
|
67
|
+
}
|
|
68
|
+
probes: Record<ProbeName, ProbeResult>
|
|
69
|
+
}
|
|
70
|
+
export type ProbeStatus = | 'available'
|
|
71
|
+
/** The API is absent, or creating it failed. */
|
|
72
|
+
| 'unavailable'
|
|
73
|
+
/** Numbers were taken; see `data`. */
|
|
74
|
+
| 'measured'
|
|
75
|
+
/** Needs real input, a user gesture or hardware that this run did not have. */
|
|
76
|
+
| 'requires-interaction'
|
|
77
|
+
/** Requested with a real user gesture and refused. */
|
|
78
|
+
| 'denied'
|
|
79
|
+
/** The probe threw; `note` carries the message. */
|
|
80
|
+
| 'error';
|
|
81
|
+
export type ProbeName = | 'webgl2'
|
|
82
|
+
| 'webgpu'
|
|
83
|
+
| 'framePacing'
|
|
84
|
+
| 'inputLatency'
|
|
85
|
+
| 'audioLatency'
|
|
86
|
+
| 'gamepad'
|
|
87
|
+
| 'fullscreen'
|
|
88
|
+
| 'pointerLock';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `p`th percentile (0..100) of `values`, by linear interpolation between
|
|
3
|
+
* closest ranks - the same definition as NumPy's default and a spreadsheet's
|
|
4
|
+
* PERCENTILE. Returns null for an empty input rather than inventing a zero.
|
|
5
|
+
*/
|
|
6
|
+
export declare function percentile(values: readonly number[], p: number): number | null;
|
|
7
|
+
/** Consecutive differences of a monotonic series of timestamps. */
|
|
8
|
+
export declare function intervals(timestamps: readonly number[]): number[];
|
|
9
|
+
/** Fixed-width histogram of `values`, with an explicit overflow bucket. */
|
|
10
|
+
export declare function histogram(values: readonly number[], bucketMs?: number, buckets?: number): Histogram;
|
|
11
|
+
/**
|
|
12
|
+
* Snap a measured frame period to the nearest common refresh rate when it is
|
|
13
|
+
* within `tolerance` of one, so 8.33 ms reads as 120 Hz rather than 120.05 Hz.
|
|
14
|
+
* Returns the unsnapped rate when nothing is close.
|
|
15
|
+
*/
|
|
16
|
+
export declare function snapRefreshRate(periodMs: number, tolerance?: number): number;
|
|
17
|
+
/**
|
|
18
|
+
* Frames that should have been presented and were not.
|
|
19
|
+
*
|
|
20
|
+
* An interval of 2.0 periods means one frame was skipped; 1.4 periods is late
|
|
21
|
+
* but not a skipped frame. Rounding is the usual convention (it matches what a
|
|
22
|
+
* frame-time graph shows as a missing bar).
|
|
23
|
+
*/
|
|
24
|
+
export declare function droppedFrames(frameIntervals: readonly number[], periodMs: number): number;
|
|
25
|
+
/**
|
|
26
|
+
* Summarise a run of `requestAnimationFrame` timestamps against a target rate.
|
|
27
|
+
*
|
|
28
|
+
* `targetHz` should be the display's refresh rate when it is known, because
|
|
29
|
+
* that is the rate a game wants. Pass the rate rAF itself runs at to count
|
|
30
|
+
* drops against what the web view actually delivers - the two differ when the
|
|
31
|
+
* web view caps rAF below the display (WebKit's 60 Hz cap on ProMotion panels).
|
|
32
|
+
*/
|
|
33
|
+
export declare function framePacing(timestamps: readonly number[], targetHz: number): FramePacingStats | null;
|
|
34
|
+
/** Percentile summary of latency samples, or null when there were none. */
|
|
35
|
+
export declare function latency(samples: readonly number[]): LatencyStats | null;
|
|
36
|
+
/**
|
|
37
|
+
* The refresh rate in a `system_profiler SPDisplaysDataType` resolution string
|
|
38
|
+
* such as `1512 x 982 @ 120.00Hz`, or null when it names none.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseRefreshHz(resolution: string | undefined): number | null;
|
|
41
|
+
/**
|
|
42
|
+
* The arithmetic behind the interactive-content probe (stacksjs/stacks#877).
|
|
43
|
+
*
|
|
44
|
+
* Kept pure and DOM-free so the numbers the probe page reports can be checked
|
|
45
|
+
* without a window: the page collects raw `requestAnimationFrame` timestamps and
|
|
46
|
+
* event-to-frame deltas, and everything that turns those into a histogram, a
|
|
47
|
+
* percentile or a dropped-frame count lives here, where a unit test can reach it.
|
|
48
|
+
*/
|
|
49
|
+
/** Refresh rates a display can plausibly run at, used to snap a measured period. */
|
|
50
|
+
export declare const COMMON_REFRESH_RATES: readonly number[];
|
|
51
|
+
export declare interface Histogram {
|
|
52
|
+
bucketMs: number
|
|
53
|
+
counts: number[]
|
|
54
|
+
overflow: number
|
|
55
|
+
}
|
|
56
|
+
export declare interface FramePacingStats {
|
|
57
|
+
frames: number
|
|
58
|
+
durationMs: number
|
|
59
|
+
fps: number
|
|
60
|
+
p50Ms: number
|
|
61
|
+
p95Ms: number
|
|
62
|
+
p99Ms: number
|
|
63
|
+
maxMs: number
|
|
64
|
+
targetPeriodMs: number
|
|
65
|
+
targetHz: number
|
|
66
|
+
droppedFrames: number
|
|
67
|
+
longFrames: number
|
|
68
|
+
histogram: Histogram
|
|
69
|
+
}
|
|
70
|
+
export declare interface LatencyStats {
|
|
71
|
+
samples: number
|
|
72
|
+
p50Ms: number
|
|
73
|
+
p95Ms: number
|
|
74
|
+
maxMs: number
|
|
75
|
+
}
|
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.42",
|
|
6
6
|
"description": "Build and launch Craft desktop applications for Stacks",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -67,11 +67,11 @@
|
|
|
67
67
|
"prepublishOnly": "bun run build"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"craft-native": ">=0.0.
|
|
70
|
+
"craft-native": ">=0.0.107"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
-
"@stacksjs/build": "0.75.
|
|
73
|
+
"@stacksjs/build": "0.75.42",
|
|
74
74
|
"better-dx": "^0.2.24",
|
|
75
|
-
"@stacksjs/server": "0.75.
|
|
75
|
+
"@stacksjs/server": "0.75.42"
|
|
76
76
|
}
|
|
77
77
|
}
|