fuzzyworld-tools / files / README.md
README.md
623239b7 2026-10-26 Pavel Safronov | 1 | # fuzzyworld-tools |
| 2 | ||
bb7e1f57 2026-11-06 Pavel Safronov | 3 | Tooling for Pavel's self-hosted code and apps on DreamHost. It replaces GitLab (code, CI, Pages) and WordPress |
| 4 | (the blog) with static files plus a few helper machines. | |
623239b7 2026-10-26 Pavel Safronov | 5 | |
bb7e1f57 2026-11-06 Pavel Safronov | 6 | **The one rule: DreamHost only serves files.** There is no app server. Pages are generated when you push, and |
| 7 | Apache just sends them. Work that needs a real machine (CI, screenshots) runs on a *runner* (a home TrueNAS box, or | |
| 8 | the laptop when it is idle), which pulls jobs from DreamHost over ssh. No runner is a normal state: jobs wait. | |
623239b7 2026-10-26 Pavel Safronov | 9 | |
bb7e1f57 2026-11-06 Pavel Safronov | 10 | The roadmap and backlog are in [PLAN.md](PLAN.md). |
| 11 | ||
| 12 | ## What it provides | |
| 13 | ||
| 14 | | Site | What it is | Details | | |
| 15 | |---|---|---| | |
| 16 | | **code.fuzzyworld.net** | Browse pages and read-only `git clone` for public repos: every unmerged branch, syntax highlighting, history, blame, CI results, bug-report issues, AI-usage charts | [docs/code-site.md](docs/code-site.md) | | |
| 17 | | **apps.fuzzyworld.net** | Deployed web apps, one per path (`/bubbles/`, `/slide/`, ...), published with `fw deploy`, with CI screenshots | [docs/apps.md](docs/apps.md) | | |
| 18 | | **writing.fuzzyworld.net** | The writing blog: a static site built from Markdown by `fw blog`, with comments by email | [docs/blog.md](docs/blog.md) | | |
| 19 | | **VS Code in the browser** | Any public repo, read-only, at `apps.fuzzyworld.net/vscode/?repo=<name>` | [docs/operations.md](docs/operations.md#vs-code-in-the-browser) | | |
| 20 | | **CI runners** | Run each push's pipeline (`.fw-ci.yml`) in Docker, like a GitLab runner | [docs/ci.md](docs/ci.md) | | |
| 21 | | **Admin panel** | `fw admin`: a local web page for bug reports, blog comments and deploy state | [docs/admin.md](docs/admin.md) | | |
1758548c 2026-10-27 Pavel Safronov | 22 | |
| 23 | ## Philosophy | |
| 24 | ||
| 25 | Things made with AI should be shared freely with the world. The code and apps here were made with AI, so they | |
| 26 | are MIT-licensed: use them, change them, share them. | |
| 27 | ||
| 28 | My conversations with the AI are different. My words and my way of working are mine. I publish a conversation | |
2754717a 2026-10-30 Pavel Safronov | 29 | only when I choose to. |
1758548c 2026-10-27 Pavel Safronov | 30 | |
bb7e1f57 2026-11-06 Pavel Safronov | 31 | The same text is on [/why-i-left-gitlab/](https://code.fuzzyworld.net/why-i-left-gitlab/). |
623239b7 2026-10-26 Pavel Safronov | 32 | |
e9fdc5db 2026-11-01 Pavel Safronov | 33 | ## Hosting tiers |
| 34 | ||
bb7e1f57 2026-11-06 Pavel Safronov | 35 | Each feature needs a certain kind of host. A tier includes the ones above it, so you can pick what your hosting allows. |
e9fdc5db 2026-11-01 Pavel Safronov | 36 | |
| 37 | | Tier | The host must offer | Features | | |
bb7e1f57 2026-11-06 Pavel Safronov | 38 | |---|---|---| |
e9fdc5db 2026-11-01 Pavel Safronov | 39 | | **T0, files only** (ftp only) | serving static files | the built blog, Atom feeds, the landing page, the generated code and CI pages, VS Code in the browser | |
bb7e1f57 2026-11-06 Pavel Safronov | 40 | | **T1, files + jobs** (ftp + python) | ssh, cron, git hooks, a mailbox reachable by IMAP (or on the same host); nothing runs when a page is requested | page generation on push, the CI queue and runner API, cleanup jobs, comments and bug reports by email, the `fw` commands | |
| 41 | | **T2, files + an endpoint** (ftp + php/cgi) | a script that answers web requests | the comment form (`post.php`): written, **not enabled, not installed** | | |
e9fdc5db 2026-11-01 Pavel Safronov | 42 | | **T3, always-on process** | a long-running server | nothing; out of scope | |
| 43 | ||
bb7e1f57 2026-11-06 Pavel Safronov | 44 | T2 is new attack surface (code that runs on a web request, as the same user as everything else on the account), |
| 45 | so comments are accepted by email only. To use the form you would host `server/comments/post.php` on its own | |
| 46 | subdomain and set `comments_form` in the blog config. | |
e9fdc5db 2026-11-01 Pavel Safronov | 47 | |
bb7e1f57 2026-11-06 Pavel Safronov | 48 | ## Quick start |
f9ca0752 2026-10-27 Pavel Safronov | 49 | |
bb7e1f57 2026-11-06 Pavel Safronov | 50 | First create the repo on DreamHost (private unless `--public`): |
| 51 | ||
| 52 | ```bash | |
| 53 | fw new my-game --public --description "One line." | |
| 54 | ``` | |
| 55 | ||
| 56 | Then make it an app (details and every option: [docs/apps.md](docs/apps.md)): | |
f9ca0752 2026-10-27 Pavel Safronov | 57 | |
| 58 | ```jsonc | |
bb7e1f57 2026-11-06 Pavel Safronov | 59 | // package.json of the app repo |
f9ca0752 2026-10-27 Pavel Safronov | 60 | "scripts": { "deploy": "fw deploy" }, |
bb7e1f57 2026-11-06 Pavel Safronov | 61 | "fuzzyworld": { "app": "my-game", "title": "My Game", "description": "One line." }, |
45b1fe38 2026-10-27 Pavel Safronov | 62 | "devDependencies": { "fuzzyworld-tools": "git+https://code.fuzzyworld.net/fuzzyworld-tools.git#semver:^1.0.0" } |
f9ca0752 2026-10-27 Pavel Safronov | 63 | ``` |
| 64 | ||
bb7e1f57 2026-11-06 Pavel Safronov | 65 | ```bash |
| 66 | git push dh:git/my-game.git # `dh` is the SSH alias in ~/.ssh/config | |
| 67 | npm run deploy # clone main, install, check, build, upload, print the URL | |
f9ca0752 2026-10-27 Pavel Safronov | 68 | ``` |
| 69 | ||
bb7e1f57 2026-11-06 Pavel Safronov | 70 | Use `fw` outside an app repo (for example on a second laptop): |
f9ca0752 2026-10-27 Pavel Safronov | 71 | |
bb7e1f57 2026-11-06 Pavel Safronov | 72 | ```bash |
| 73 | npm install -g "git+https://code.fuzzyworld.net/fuzzyworld-tools.git#semver:^1.0.0" | |
| 74 | ``` | |
f9ca0752 2026-10-27 Pavel Safronov | 75 | |
623239b7 2026-10-26 Pavel Safronov | 76 | ## Commands |
| 77 | ||
bb7e1f57 2026-11-06 Pavel Safronov | 78 | `bin/fw` runs some commands on the laptop (`deploy`, `repoint`, `usage`, `admin`, `blog`) and forwards the rest to |
| 79 | DreamHost (`ssh dh ~/bin/fw ...`). | |
| 80 | ||
| 81 | ### Repos | |
623239b7 2026-10-26 Pavel Safronov | 82 | |
| 83 | | Command | What it does | | |
| 84 | |---|---| | |
| 85 | | `fw list` | every repo, its visibility and last commit | | |
| 86 | | `fw new <name> [--public] [--description TEXT]` | create `~/git/<name>.git` on DreamHost; **private unless `--public`** | | |
bb7e1f57 2026-11-06 Pavel Safronov | 87 | | `fw public <name>` / `fw private <name>` | publish the source on code.fuzzyworld.net right away / unpublish it (pages, clone URL and index entry are removed) | |
623239b7 2026-10-26 Pavel Safronov | 88 | | `fw describe <name> TEXT` | set the description shown on the index and summary page | |
| 89 | | `fw publish <name>` | refresh one repo's public copy and pages (the push hook does this) | | |
bb7e1f57 2026-11-06 Pavel Safronov | 90 | | `fw publish-all` | refresh every repo, printing `[n/total] name (seconds)`, then the index and the CI dashboard | |
| 91 | | `fw repoint [DIR] [--https] [--dry-run]` | for each repo in DIR whose `origin` is on gitlab.com: rename it `gitlab`, add `origin` on DreamHost (or the read-only https URL with `--https`, public repos only), fetch, retarget branch tracking. Changes only remotes, and checks that uncommitted work, HEAD and stashes are unchanged | | |
623239b7 2026-10-26 Pavel Safronov | 92 | |
bb7e1f57 2026-11-06 Pavel Safronov | 93 | ### Apps and deploys |
623239b7 2026-10-26 Pavel Safronov | 94 | |
bb7e1f57 2026-11-06 Pavel Safronov | 95 | | Command | What it does | |
623239b7 2026-10-26 Pavel Safronov | 96 | |---|---| |
bb7e1f57 2026-11-06 Pavel Safronov | 97 | | `fw deploy [--ref B] [--as NAME] [--here] [--dry-run]` | build and publish this repo's app ([docs/apps.md](docs/apps.md)) | |
| 98 | | `fw deploys [--json]` | what is deployed where: the deploy ledger and each deployed app against its repo's default branch. **Known gap: `bin/fw` does not forward it yet (see [Known gaps](#known-gaps)); until then run `ssh dh '~/bin/fw deploys'`.** | | |
1758548c 2026-10-27 Pavel Safronov | 99 | |
bb7e1f57 2026-11-06 Pavel Safronov | 100 | ### CI |
55e5fc3e 2026-11-04 Pavel Safronov | 101 | |
bb7e1f57 2026-11-06 Pavel Safronov | 102 | | Command | What it does | |
55e5fc3e 2026-11-04 Pavel Safronov | 103 | |---|---| |
bb7e1f57 2026-11-06 Pavel Safronov | 104 | | `fw ci <name> [branch]` | queue a CI run of a branch's latest commit (default: the default branch) | |
| 105 | | `fw ci-list` | queued and running CI jobs | | |
| 106 | | `fw ci-watch` | follow CI in the terminal (private repos too): running jobs with each job's state, the queue, the last 5 results; ends when nothing is queued or running | | |
55e5fc3e 2026-11-04 Pavel Safronov | 107 | |
bb7e1f57 2026-11-06 Pavel Safronov | 108 | ### Bug reports and comments |
55e5fc3e 2026-11-04 Pavel Safronov | 109 | |
bb7e1f57 2026-11-06 Pavel Safronov | 110 | | Command | What it does | |
| 111 | |---|---| | |
| 112 | | `fw issues <repo> public\|reviewed\|private` | set who sees the repo's bug-report pages (default private) and republish them | | |
| 113 | | `fw issues list\|show\|record\|approve\|hide\|close\|reopen\|publish\|publish-all\|ingest\|imap-check` | moderate bug reports ([docs/issues.md](docs/issues.md)) | | |
| 114 | | `fw comments list\|show\|approve\|reject\|remove\|untrust\|publish\|export\|ingest\|imap-check` | moderate blog comments ([docs/blog.md](docs/blog.md#comments-by-email)) | | |
55e5fc3e 2026-11-04 Pavel Safronov | 115 | |
bb7e1f57 2026-11-06 Pavel Safronov | 116 | ### Laptop-side tools |
55e5fc3e 2026-11-04 Pavel Safronov | 117 | |
bb7e1f57 2026-11-06 Pavel Safronov | 118 | | Command | What it does | |
| 119 | |---|---| | |
| 120 | | `fw admin [--port N] [--no-open] [--code-dir DIR]` | the local admin web page ([docs/admin.md](docs/admin.md)) | | |
| 121 | | `fw blog list\|build\|preview\|deploy <config>` | the static blog ([docs/blog.md](docs/blog.md)) | | |
| 122 | | `fw blog watch <config> [--once] [--dry-run]`, `fw blog watch install\|uninstall <config>` | mirror the blog source into git and auto-deploy safe edits (one machine only) | | |
| 123 | | `fw blog import-wp <config> --wp URL`, `fw blog check <config>` | one-time WordPress import / compare the built site with the WordPress text | | |
| 124 | | `fw usage [--all\|--now\|--trailers [--session ID]]` | Claude Code token use per session; `--trailers` prints commit-message trailers ([docs/usage.md](docs/usage.md)) | | |
55e5fc3e 2026-11-04 Pavel Safronov | 125 | |
bb7e1f57 2026-11-06 Pavel Safronov | 126 | `npm run` scripts in this repo: `dev` (preview the pages locally), `deploy` and `deploy-status` (deploy this repo's own |
| 127 | server by default; `-- all` or a target name adds the runner and VS Code web; [docs/operations.md](docs/operations.md#install-and-update)), `list-repos` | |
| 128 | (`fw list`) and `prepare-usage-for-pr` (`fw usage --trailers`). | |
55e5fc3e 2026-11-04 Pavel Safronov | 129 | |
bb7e1f57 2026-11-06 Pavel Safronov | 130 | ## Visibility |
85702b14 2026-11-04 Pavel Safronov | 131 | |
bb7e1f57 2026-11-06 Pavel Safronov | 132 | Each bare repo stores its own setting: `git config fuzzyworld.visibility public|private`. |
| 133 | **Anything other than exactly `public` is private**, including a missing value, so a repo is never published by | |
| 134 | accident. A private repo has no pages, no clone URL and no index entry, and pushing to it publishes nothing. | |
55e5fc3e 2026-11-04 Pavel Safronov | 135 | |
bb7e1f57 2026-11-06 Pavel Safronov | 136 | Other settings in the repo config: |
85702b14 2026-11-04 Pavel Safronov | 137 | |
bb7e1f57 2026-11-06 Pavel Safronov | 138 | | Key | Meaning | |
| 139 | |---|---| | |
| 140 | | `fuzzyworld.issues` | `public`, `reviewed`, or anything else = private: who sees bug-report pages ([docs/issues.md](docs/issues.md)) | | |
| 141 | | `fuzzyworld.description` | the one-line description | | |
| 142 | | `fuzzyworld.ci` | `off`: pushes queue no CI jobs | | |
| 143 | | `fuzzyworld.gitlab-id`, `fuzzyworld.gitlab-path` | for repos migrated from GitLab | | |
85702b14 2026-11-04 Pavel Safronov | 144 | |
bb7e1f57 2026-11-06 Pavel Safronov | 145 | ## Repository layout |
85702b14 2026-11-04 Pavel Safronov | 146 | |
bb7e1f57 2026-11-06 Pavel Safronov | 147 | | Folder | Contents | |
| 148 | |---|---| | |
| 149 | | `bin/` | `fw` (laptop side) and `deploy-tools` | | |
| 150 | | `lib/` | laptop-side programs: `deploy.mjs`, `admin.py`, `usage.py`, `wp_import.py`, `repoint.sh` | | |
| 151 | | `server/` | everything installed on DreamHost: `fw`, `gen.py` (page generator), `issues.py`, `comments.py`, `blog.py`, `blog_watch.py`, `deploys.py`, `runner-api`, hooks, jobs, site templates | | |
| 152 | | `runner/` | the CI runner (`fw-runner.py`), its installers and the screenshot script | | |
| 153 | | `vite/` | the vite plugin for apps (`fuzzyworld-tools/vite`) | | |
| 154 | | `vscode-web/` | VS Code in the browser: installer and the `fw-fs` extension | | |
| 155 | | `tests/` | the test suite | | |
85702b14 2026-11-04 Pavel Safronov | 156 | |
bb7e1f57 2026-11-06 Pavel Safronov | 157 | Where things live on DreamHost: [docs/operations.md](docs/operations.md#layout-on-dreamhost). |
85702b14 2026-11-04 Pavel Safronov | 158 | |
bb7e1f57 2026-11-06 Pavel Safronov | 159 | ## Security in one paragraph |
0e603cf7 2026-10-31 Pavel Safronov | 160 | |
bb7e1f57 2026-11-06 Pavel Safronov | 161 | The site never executes anything from a repo: pages are pre-generated HTML with all repo content escaped, only |
| 162 | image files are copied raw, scripts are denied by `.htaccess`, and the code site sends | |
| 163 | `Content-Security-Policy: script-src 'none'`. The full list is in [docs/code-site.md](docs/code-site.md#security). | |
0e603cf7 2026-10-31 Pavel Safronov | 164 | |
bb7e1f57 2026-11-06 Pavel Safronov | 165 | ## Documentation |
0e603cf7 2026-10-31 Pavel Safronov | 166 | |
bb7e1f57 2026-11-06 Pavel Safronov | 167 | | File | Covers | |
| 168 | |---|---| | |
| 169 | | [docs/code-site.md](docs/code-site.md) | page URLs, branches, licences, local preview, security | | |
| 170 | | [docs/apps.md](docs/apps.md) | `package.json` keys, `fw deploy`, the vite plugin, the apps index, screenshots | | |
| 171 | | [docs/ci.md](docs/ci.md) | queue, runners, `.fw-ci.yml`, secrets, watching CI | | |
| 172 | | [docs/issues.md](docs/issues.md) | bug reports by email: modes, privacy rules, run records, replay links | | |
| 173 | | [docs/admin.md](docs/admin.md) | `fw admin`: security model and pages | | |
| 174 | | [docs/blog.md](docs/blog.md) | `fw blog`, comments by email, the watcher, the WordPress import | | |
| 175 | | [docs/usage.md](docs/usage.md) | Claude Code usage trailers, cost estimates, usage charts | | |
| 176 | | [docs/operations.md](docs/operations.md) | DreamHost layout, scheduled jobs, installing and updating, VS Code in the browser, requirements | | |
| 177 | | [PLAN.md](PLAN.md) | roadmap, decisions, backlog | | |
| 178 | ||
| 179 | ## Tests | |
623239b7 2026-10-26 Pavel Safronov | 180 | |
| 181 | ```bash | |
bb7e1f57 2026-11-06 Pavel Safronov | 182 | python3 -m unittest discover tests # the whole suite: pages, blog, comments, issues, runner, admin, deploys |
| 183 | node --test tests/deploy.test.mjs # fw deploy's config, rsync filters and replay-template checks | |
623239b7 2026-10-26 Pavel Safronov | 184 | ``` |
| 185 | ||
bb7e1f57 2026-11-06 Pavel Safronov | 186 | ## Known gaps |
1758548c 2026-10-27 Pavel Safronov | 187 | |
bb7e1f57 2026-11-06 Pavel Safronov | 188 | - `fw deploys` is documented above but `bin/fw` on the laptop rejects it ("unknown command"). `fw admin` is not affected. |
| 189 | - The npm package does not ship `server/` (`files` in `package.json`), but `fw blog` (including `import-wp` and `check`) needs it, and `fw usage --trailers` reads `server/prices.json` (without it the trailers lose the `@over100k` split). So these work fully only from a checkout of this repo. | |
f9ca0752 2026-10-27 Pavel Safronov | 190 | |
bb7e1f57 2026-11-06 Pavel Safronov | 191 | Other problems found are tracked in the global queue, not here. |