Read design files from the local checkout with a Forgejo fallback #1

Merged
arne merged 19 commits from feat/local-design-reads into main 2026-04-04 23:21:40 +02:00
Owner

Previewing design files through orbit's web dashboard required committing
and pushing to Forgejo for every change, then waiting for the API cache to
catch up before the preview reflected the update. That's a slow loop when
you're iterating on a stylesheet or a markdown spec.

This PR points orbit at the local working-tree checkout for design reads,
with the Forgejo API as a fallback when the checkout is unavailable. Set
ORBIT_SOURCE_ROOT to the directory containing your repo checkouts (e.g.,
~/source), and orbit resolves {sourceRoot}/{owner}/{slug}/design/ for
each registered project. Unset it to fall back to Forgejo-only reads.

The designs package

New designs.Source interface with three implementations. LocalSource
reads from the working tree and has traversal and symlink-escape guards —
it resolves symlinks and re-validates containment before reading to close
the TOCTOU window. ForgejoSource wraps the existing forgejo.Client.
ChainSource composes them, trying each in order and falling through on
ErrNotFound.

Web and API handlers now depend on the Source interface rather than
calling forgejo.Client directly for design reads. Production wiring in
main.go composes a ChainSource of LocalSource + ForgejoSource so
local wins when present.

Security

The design-preview route enforces an extension allowlist (HTML, CSS, JS,
images, fonts, markdown, text — 15 types total). Unsupported extensions
return 415. Every design response carries Cache-Control: no-store to
prevent any layer from caching stale content while iterating.

Also

Adds CLAUDE.md with the repo conventions that will grow alongside the
codebase: squash-merge policy, deployment notes, source layout.

Previewing design files through orbit's web dashboard required committing and pushing to Forgejo for every change, then waiting for the API cache to catch up before the preview reflected the update. That's a slow loop when you're iterating on a stylesheet or a markdown spec. This PR points orbit at the local working-tree checkout for design reads, with the Forgejo API as a fallback when the checkout is unavailable. Set `ORBIT_SOURCE_ROOT` to the directory containing your repo checkouts (e.g., `~/source`), and orbit resolves `{sourceRoot}/{owner}/{slug}/design/` for each registered project. Unset it to fall back to Forgejo-only reads. ## The `designs` package New `designs.Source` interface with three implementations. `LocalSource` reads from the working tree and has traversal and symlink-escape guards — it resolves symlinks and re-validates containment before reading to close the TOCTOU window. `ForgejoSource` wraps the existing `forgejo.Client`. `ChainSource` composes them, trying each in order and falling through on `ErrNotFound`. Web and API handlers now depend on the `Source` interface rather than calling `forgejo.Client` directly for design reads. Production wiring in `main.go` composes a `ChainSource` of `LocalSource` + `ForgejoSource` so local wins when present. ## Security The design-preview route enforces an extension allowlist (HTML, CSS, JS, images, fonts, markdown, text — 15 types total). Unsupported extensions return 415. Every design response carries `Cache-Control: no-store` to prevent any layer from caching stale content while iterating. ## Also Adds `CLAUDE.md` with the repo conventions that will grow alongside the codebase: squash-merge policy, deployment notes, source layout.
arne added 18 commits 2026-04-04 23:18:09 +02:00
Adds the approved design spec for reading design files directly from
the local source checkout (with Forgejo API fallback), eliminating
the commit-push loop during design prototyping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The design scaffold files (index.html, style.css, theme.js) used to
live as string literals in mcp/scaffold.go. Moves them into
mcp/templates/design/ as real files and walks an embed.FS to copy
them out, so they can be edited without touching Go code. Empty
fonts/ and specs/ subdirs are preserved with .gitkeep placeholders
that are skipped at copy time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Template uses only .Name (links go through orbit's preview route),
so File struct drops DownloadURL; directory filtering moves into
ForgejoSource so both sources only emit regular files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Task-by-task TDD plan covering the new designs package (Source
interface, LocalSource, ForgejoSource, ChainSource), config,
api/web handler wiring, template update, and deployment
verification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements the Source interface by reading design files from a local
{sourceRoot}/{owner}/{slug}/design/ tree, with traversal and symlink
escape guards on ReadFile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the Source interface via Forgejo contents API, filtering
directory entries and mapping 404 errors to ErrNotFound.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire designs.Source into api.Server; rewrite listDesigns and getDesign
to use s.designs instead of s.forgejo. Add Cache-Control: no-store to
both endpoints. Add handler tests with fakeDesigns test double.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In Go, a function returning a concrete pointer type (e.g. *LocalSource)
that returns nil produces a typed nil. When assigned to a Source interface
variable, the interface value is non-nil (it has a type but a nil data
pointer), so ChainSource's `if s != nil` guard passes and the source is
included — leading to a nil-pointer dereference on the first method call.
Changing NewLocalSource and NewForgejoSource to return Source means
`return nil` yields a true untyped nil interface value, which ChainSource
correctly filters out. Adds a regression test to chain_test.go that
constructs a chain from both disabled constructors and asserts ErrNotFound
is returned without panicking.
Switch ForgejoSource.ReadFile from GetFileContent (base64 contents API)
to GetRawFile (/raw/ endpoint) per spec; add binary-bytes regression
test. Remove unreachable nil-source guards in api/designs.go and
web/web.go since main always wires a non-nil ChainSource.
Establishes the squash-merge policy (one commit per PR on main),
design/plan flow, deployment notes, and source layout for this repo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
arne merged commit eea6816d0c into main 2026-04-04 23:21:40 +02:00
arne changed title from Local-checkout design reads to Read design files from the local checkout with a Forgejo fallback 2026-04-05 22:08:49 +02:00
arne changed title from Read design files from the local checkout with a Forgejo fallback to Read design files from the local checkout with a Forgejo fallback (test) 2026-04-05 22:21:30 +02:00
arne changed title from Read design files from the local checkout with a Forgejo fallback (test) to Read design files from the local checkout with a Forgejo fallback 2026-04-05 22:21:30 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
arne/orbit!1
No description provided.