Apps (apps.fuzzyworld.net)
Each app lives at apps.fuzzyworld.net/<app>/ and is a static build uploaded by fw deploy.
Making a repo deploy as an app
Add to the app repo's package.json:
"scripts": { "deploy": "fw deploy" },
"fuzzyworld": {
"app": "my-game", // URL: apps.fuzzyworld.net/my-game/ (required)
"title": "My Game", // apps index
"description": "One line.", // apps index
"repo": "my-game", // DreamHost repo name, if different from app
"prebuild": ["pnpm gen-assets"], // commands before checks and build
"checks": ["typecheck", "test"], // scripts that must pass (default: whichever of typecheck/lint/test exist)
"build": "make public", // default: `<pm> run build`
"out": "dist", // default: dist, else public, else build
"keep": ["my-game.apk"], // files on the server a deploy must not delete or replace (e.g. a download built elsewhere)
"extra": ["sim/*.js"], // build-output files every later deploy keeps: uploaded and updated, never deleted (e.g. old rules versions)
"replay": "replay.html?report={record}" // a page of this app that plays a bug report's run record (see issues.md)
},
"devDependencies": { "fuzzyworld-tools": "git+https://code.fuzzyworld.net/fuzzyworld-tools.git#semver:^1.0.0" }
For vite, add the plugin. It sets base to /<app>/ for builds (the dev server is unchanged). FW_APP, set by
fw deploy --as, overrides the app name.
import fuzzyworld from 'fuzzyworld-tools/vite';
export default defineConfig({ plugins: [fuzzyworld()] });
fw deploy
Run it with pnpm run deploy or npm run deploy.
- Clone. Clones branch
mainofdh:git/<repo>.gitinto a temp dir. It builds what is pushed, never your working copy (--ref <branch>for another branch,--hereto build the current directory as is). - Install with the lockfile's package manager (
pnpm install --frozen-lockfile,npm ci,yarn install --frozen-lockfile). - Prebuild, checks, build. Any failure stops the deploy.
- Check the output. Every same-site URL in
index.html, the web manifest'sid/scope/start_url, and everyserviceWorker.register(...)script and scope must stay inside/<app>/. That keeps apps on the shared origin from capturing each other, and makes each one install separately on Android. Any problem stops the deploy. - Upload.
rsync --deletetoapps.fuzzyworld.net/<app>/. -keeppaths are excluded. -extrafiles are protected from--deleteby an rsync filter: uploaded and updated, never removed. - It writes.fuzzyworld.json(commit, time, and thereplaytemplate; not web-readable). - It refreshes the apps index and the repo's code page. - Print the URL.
--dry-run does everything except upload.
--as NAME publishes the app at apps.fuzzyworld.net/NAME/ instead: a second copy of the same repo (for example
explodorama-next built from main while /explodorama/ is deployed from a tag with --ref). The build gets
FW_APP=NAME, which the vite plugin uses for base, and the index title gets a suffix.
The apps index
A table ordered by the newest commit of each app's repo (not by deploy date: an app can be redeployed often without its code changing, so the deploy date has its own column).
Columns: screenshot thumbnail (if the runner has taken one), name, description, last commit, deployed, and a source link for apps from public repos. The code index on code.fuzzyworld.net is likewise a table of public repos.
Apps hosted elsewhere are listed from server/site-apps/extra.json (today: engye on Cloudflare Pages;
apps.fuzzyworld.net/engye/... redirects there).
Screenshots
After a successful pipeline on the default branch, an app is built the way fw deploy builds it, served at
/<app>/ inside a Playwright container with no network, and photographed. The pictures go to
apps.fuzzyworld.net/_shots/<app>/ and appear on the apps index and the repo's summary page. They need a runner
with the chrome tag (ci.md).
"fuzzyworld": {
"screenshots": [{ "path": "", "size": "phone", "wait": 1500 }, { "path": "?demo", "size": "desktop" }],
// sizes: phone 390×844, tablet 820×1180, desktop 1280×800. Default: one phone shot of the start page.
// or a command that writes its own PNGs to $FW_SHOTS_DIR, for apps that need clicks first:
// "screenshots": "pnpm shots",
// or false: no screenshots
}
Without runner screenshots, the images in a public repo's screenshots/ folder are shown instead.