Software Update
Reepolee projects start as copies of the open-source repository, and when a newer
version contains changes you want, you selectively adopt them from an upstream
checkout. The Software Update page (/software-update, renamed from /sync)
in the reeman app is the browser version of that flow: scan a local upstream
checkout, review every differing file with its diff and git context, and copy
only the files you choose. Its rules mirror the standalone Reesync
CLI - the same file set, the same .reesyncignore, and the
same guarantee that project-only files are never deleted.
Two consequences of the selective model are worth stating up front:
- Nothing is merged or interpreted. Software Update copies whole files; it never merges source code, applies schemas, or touches the database. It is a file-level sync, and you are the reviewer for every copy.
- It only copies into your project. The upstream checkout is read-only from the page's point of view. Files that exist only in your project are shown informationally and are never deleted (the page repeats this next to the Apply button).
The page lives under the reeman app (served on REEMAN_PORT, default 2339),
not the main app, and requires the system module. Start it with bun dev:all
(dev) or bun start:reeman (production).
Prerequisites
A sibling checkout of the upstream source outside your project. "Sibling" is a hard rule, not a convention: the page's source-directory validation rejects anything that is empty, missing, a nested subdirectory of your project, a parent of your project, the project itself, or not a direct sibling directory. Paths are resolved to canonical absolute paths (symlinks included) before the check, so a link beside the project cannot reach a distant tree. In practice this means:
cd /tmp
git clone https://github.com/reepolee/reepolee.git reepolee-upstream
Your project and reepolee-upstream are then siblings under /tmp. If the
checkout is a git repository, the page shows commit context (see
Git context); an unpacked archive works the
same otherwise.
The Flow
- Scan - pick the upstream directory in the folder selector (sibling
directories are listed by
/software-update/browse; the last validated source is remembered as the form prefill) and press Scan. - Review - the page walks both directories, hashes every regular file (SHA-256), and groups the differences by folder. Each entry shows its state badge, path, size, its per-file git commit context, and an on-demand diff.
- Select - check the files you want. Folder checkboxes select their children; a global checkbox selects everything selectable.
- Apply - the confirmation dialog restates how many new and modified files you are about to copy. Software Update then re-validates every selection against the live filesystem and copies - or skips stale files with a reason.
The review page never trusts what the browser posts beyond a scan id and
relative paths: every apply and diff re-resolves both sides against the
snapshot recorded at scan time. A scan snapshot expires after 30 minutes
(.reepolee/reesync/<scan_id>.json), after which the page asks you to rescan.
If the upstream checkout gained a newer commit than the one you scanned, a
banner tells you to rescan before reviewing.
File States
| State | Badge | Meaning | Selectable |
|---|---|---|---|
| New | green | The file exists upstream but not in your project | yes |
| Modified | amber | Content differs between the two sides (compared by hash, so a mere timestamp change is not a difference) | yes |
| Project-only | slate | Exists only in your project | no - informational, never deleted |
| Ignored | dimmed, pre-unchecked | Matched by .reesyncignore (see below) | no - unless unignored |
Filter buttons above the list toggle the view by state (new / modified / project-only / ignored / selected). "Show not ignored" hides ignored rows, and "Hide ignored" (on by default) collapses them out of the way entirely.
Diff Preview and Git Context
Every selectable row has a View diff toggle. Diffs are loaded lazily per
file (GET /software-update/diff) and rendered as a color-coded text diff with
a +/-/space prefix per line: green for additions, red for removals, ···
between hunks. Binary files and files too large for inline preview show a note
instead of a diff. Because the diff is computed at click time, the page first
re-hashes both sides and shows "File changed since scan - rescan to see an up
to date diff" if either side moved on.
When the upstream checkout is a git repository, each row also shows the last
commit that touched the file - short hash, subject, author, and date - and the
header shows the upstream's latest commit. That context answers why the file
changed before you decide to take it. Per-file commit info is resolved with
git log -1 -- <path> inside the upstream checkout only; an unpacked archive
simply omits the suffix. A commit-info endpoint exposes the same data to the
header.
The Ignore List: .reesyncignore
Your project's .reesyncignore marks files that are yours alone and should
never be pulled in. Rules match the standalone Reesync CLI:
- One glob pattern per line; blank lines and
#comments are ignored. - Patterns match project-root-relative forward-slash paths. Matched files stay visible but dimmed and pre-unchecked - never hidden. You still see that the upstream changed the file, you just do not copy it by accident.
- A missing
.reesyncignoreis not an error. Invalid patterns (unbalanced[...]or{...}) are detected and never match. - The file is yours - hidden files are not compared, so
.reesyncignorecan never sync over itself.
The page manages it in two ways:
- Per-file toggle - the row's Ignore / Unignore button adds or removes
an exact-path line and immediately rescans. A file matched by a broader glob
shows "Ignored by pattern
<pattern>" instead of an exact line, and is dimmed with no toggle - edit the pattern by hand to unignore it. - Ignore selected - select several rows and use the button; every selected path is added as an exact line and the scan refreshes.
The starter ships a .reesyncignore with common project-owned paths
(apps/main/**, config/ customisations, package.json, bun.lock, dotfiles,
and so on). Before adopting content, review project-owned files deliberately:
generated route folders, config/supported_locales.ts, SQL files and
translation data, and any component or static asset you have changed for your
own UI.
Apply: The Stale-Checked Copy
Apply re-validates each selected file immediately before copying, so a scan can never overwrite something blindly:
- The selection must still be present and selectable in the snapshot (not project-only, not ignored, not since removed).
- Containment is re-checked on both sides against the canonical roots.
- The source file must still hash to the value recorded at scan time -
otherwise it is skipped as
stale-source(the upstream moved on; rescan). - The destination file must still hash to the value recorded at scan time -
otherwise it is skipped as
stale-dest(you edited the file since scanning; the snapshot's expectation does not match, so it cannot overwrite your work).
Copies happen one file at a time, creating parent directories as needed, and
continue across per-file failures. The result is reported as a toast with the
counts: copied, stale (skipped), and failed. Apply runs under the reeman
global-busy lock (/__busy), so it cannot collide with another reeman action.
Reesync semantics apply to the copy: hidden files and build/dependency
directories (node_modules, target, vendor, dist, .next, .svelte-kit,
.cache, .output, and any dot-prefixed name) are never compared, and
project-only files are never deleted.
After Applying
The flow does not run your checks for you. Verify the changes the way you would after the CLI flow: run the project checks appropriate to the adopted files, and test the routes, forms, translations, and database paths they touch. See Upgrade Guide for a typical pass. Nothing here touches your database - apply SQL file changes through reeman's "Run SQL file" after reviewing them.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /software-update | The review page (a ?scan=id query loads a specific snapshot) |
| POST | /software-update/scan | Validate the source directory, diff both trees, store a snapshot, redirect to ?scan=id |
| POST | /software-update/ignore | Add or remove one exact .reesyncignore line, then rescan |
| POST | /software-update/ignore-selected | Add exact lines for every selected path, then rescan |
| POST | /software-update/apply | Stale-checked, contained copy of the selected files |
| GET | /software-update/diff | Lazy per-file diff/preview fragment (?scan=&path=) |
| GET | /software-update/browse | List sibling directories for the folder selector |
| GET | /software-update/commit-info | Upstream git head info for the header (?source=) |
Scan snapshots live under .reepolee/reesync/ (30-minute TTL, pruned on each
new scan); the remembered source path is .reepolee/reesync/last-source.json.
Every POST is CSRF-protected.
Software Update vs the Reesync CLI
| Software Update page | Reesync CLI | |
|---|---|---|
| Where | In-app, at /software-update in the reeman app | Standalone binary, run from the project root |
| File set | Identical - same exclusion rules, same .reesyncignore semantics | |
| Review | Browser UI: folder-grouped rows, per-file lazy diffs, filter buttons | Terminal tree: keyboard navigation, inline commit subjects, badges |
| Selecting | Checkboxes (file, folder, global), pre-unchecked when ignored | Space toggles, i adds an ignore line, pre-unchecked when ignored |
| Safety | Stale-checked copy re-hashes both sides at apply time; snapshots expire after 30 min | Copies the confirmed selection; no re-hash between review and copy |
| Requires | A running reeman app and a browser (works over SSH tunnels) | Nothing - usable on a server or in CI-style contexts with no web UI |
The CLI is the right tool when you have no browser handy; the page is the right tool when you want visual diffs, folder-level review, and the extra stale-check pass. Both write only the files you select, and neither ever deletes project-only files.