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

  1. 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.
  2. 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.
  3. Select - check the files you want. Folder checkboxes select their children; a global checkbox selects everything selectable.
  4. 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

StateBadgeMeaningSelectable
NewgreenThe file exists upstream but not in your projectyes
ModifiedamberContent differs between the two sides (compared by hash, so a mere timestamp change is not a difference)yes
Project-onlyslateExists only in your projectno - informational, never deleted
Ignoreddimmed, pre-uncheckedMatched 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 .reesyncignore is not an error. Invalid patterns (unbalanced [...] or {...}) are detected and never match.
  • The file is yours - hidden files are not compared, so .reesyncignore can 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:

  1. The selection must still be present and selectable in the snapshot (not project-only, not ignored, not since removed).
  2. Containment is re-checked on both sides against the canonical roots.
  3. 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).
  4. 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

MethodPathPurpose
GET/software-updateThe review page (a ?scan=id query loads a specific snapshot)
POST/software-update/scanValidate the source directory, diff both trees, store a snapshot, redirect to ?scan=id
POST/software-update/ignoreAdd or remove one exact .reesyncignore line, then rescan
POST/software-update/ignore-selectedAdd exact lines for every selected path, then rescan
POST/software-update/applyStale-checked, contained copy of the selected files
GET/software-update/diffLazy per-file diff/preview fragment (?scan=&path=)
GET/software-update/browseList sibling directories for the folder selector
GET/software-update/commit-infoUpstream 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 pageReesync CLI
WhereIn-app, at /software-update in the reeman appStandalone binary, run from the project root
File setIdentical - same exclusion rules, same .reesyncignore semantics
ReviewBrowser UI: folder-grouped rows, per-file lazy diffs, filter buttonsTerminal tree: keyboard navigation, inline commit subjects, badges
SelectingCheckboxes (file, folder, global), pre-unchecked when ignoredSpace toggles, i adds an ignore line, pre-unchecked when ignored
SafetyStale-checked copy re-hashes both sides at apply time; snapshots expire after 30 minCopies the confirmed selection; no re-hash between review and copy
RequiresA 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.