Update expo application
Build and Push Next App / quality (push) Successful in 1m27s
Build and Push Next App / build-next (push) Successful in 3m58s

This commit is contained in:
Gabriel Brown
2026-06-22 12:13:02 -04:00
parent ddce5efb13
commit 42f95530de
78 changed files with 5315 additions and 421 deletions
+375 -224
View File
@@ -1,269 +1,420 @@
# Spoon
<p align="center">
<img src="apps/next/public/favicon.png" alt="Spoon logo" width="96" height="96" />
</p>
Spoon is a self-hostable fork maintenance cockpit.
<h1 align="center">Spoon</h1>
Forking a project should not mean supporting it alone. Spoon tracks managed
forks, called **Spoons**, watches upstream for drift, automatically syncs clean
forks when it can, and opens durable **Threads** when upstream changes need
review, context, or code.
<p align="center">
<strong>Fork freely & keep them all intimately close to upstream.</strong>
</p>
This repository is the Spoon application itself, not a generic starter.
<p align="center">
Spoon is a self-hostable fork maintenance cockpit built around managed forks,
durable maintenance threads, and OpenCode-powered workspaces.
</p>
## What Spoon Does
<p align="center">
<a href="#what-this-is">What this is</a>
·
<a href="#product-model">Product model</a>
·
<a href="#architecture">Architecture</a>
·
<a href="#environment-reference">Environment</a>
</p>
- Tracks GitHub-backed managed forks and their upstream projects.
- Shows raw and effective drift, fork-only commits, pull requests, clone URLs,
additional remotes, sync history, and open maintenance work.
- Uses Threads as the product center for upstream reviews, merge conflicts,
ignored commits, user-requested changes, worker logs, and draft PR handoff.
- Auto-syncs clean behind forks when there are no fork-only commits.
- Creates maintenance threads when custom fork work means upstream changes need
a decision.
- Runs optional OpenCode-backed workspaces in isolated agent-job containers.
- Lets users configure encrypted AI provider profiles, Codex/OpenCode auth,
per-Spoon secrets, commands, and agent settings.
- Opens draft PRs for code changes instead of auto-merging custom forks.
---
## Current Scope
## What This Is
Implemented today:
Spoon is a private, actively evolving project for making forks less lonely to
maintain.
- Public Next.js landing page for Spoon's thread-first maintenance model.
- Authenticated web routes:
- `/dashboard`
- `/spoons`
- `/spoons/new`
- `/spoons/[spoonId]`
- `/spoons/[spoonId]/agent/[jobId]`
- `/threads`
- `/threads/[threadId]`
- `/settings/profile`
- `/settings/integrations`
- `/settings/ai-providers`
- Legacy `/updates` and `/agents` routes redirect into Threads.
- GitHub App connection, repository listing, fork creation, drift refresh,
commit/PR cache, and safe sync foundation.
- Thread-first maintenance model with ignored upstream changes and effective
drift.
- Optional `apps/agent-worker` service that claims queued jobs, clones the
current GitHub fork, starts an isolated workspace, exposes file browsing and
edits through server-side Next proxies, runs commands, and opens draft PRs.
- Browser workspace with persisted thread messages, file tree, Monaco editor
with optional Vim mode, diff view, command panel, logs, artifacts, and draft
PR actions.
- Encrypted per-user AI provider profiles and per-Spoon project secrets.
- Password auth and Authentik/GitHub OAuth through Convex Auth.
- Expo companion app shell with password and Authentik sign-in.
- Self-hosted local Convex using Postgres storage.
Forking a project is easy. Keeping that fork close to upstream after you add
custom changes is the hard part. Spoon treats a fork as an ongoing relationship:
it watches upstream, understands fork-only commits, automatically syncs clean
drift when it can, and opens a durable **Thread** when a decision needs context
or code.
Not implemented yet:
The application is currently GitHub-first. Future provider-neutral fields exist
in the data model, but GitHub is the active automation surface today.
- Automatic merge of custom/diverged forks.
- Git provider automation beyond GitHub.
- Additional remotes as push targets.
- Long-running service-stack orchestration inside agent jobs.
- Direct browser access to worker containers.
- Production mobile build/release setup.
## Highlights
- **Managed forks, called Spoons**
Track upstream metadata, fork metadata, clone URLs, extra remotes, sync
cadence, production-ref strategy, fork-only commits, and pull requests.
- **Thread-first maintenance**
Upstream updates, conflict review, ignore decisions, user-requested work,
worker output, and draft PR handoff all live inside Threads.
- **Clean drift auto-sync**
If upstream moves and the fork has no custom commits, Spoon can fast-forward
the fork without creating busywork.
- **Custom forks get context**
If the fork has custom commits, Spoon creates a maintenance thread rather than
pretending the update is trivial.
- **Effective drift**
Spoon keeps raw GitHub drift visible while also tracking ignored upstream
changes so irrelevant commits do not keep a fork permanently actionable.
- **OpenCode workspaces**
Agent work happens in an isolated workspace with a file tree, browser editor,
diff viewer, command panel, logs, artifacts, and draft PR actions.
- **User-owned providers and secrets**
AI provider profiles, Codex/OpenCode auth, and per-Spoon project secrets are
encrypted. Secrets are redacted from logs and refused from commits when
materialized into env files.
- **Draft PR handoff**
Code changes become branches and draft pull requests. Spoon does not
auto-merge custom forks behind the user's back.
## Product Model
<details open>
<summary><strong>Spoons</strong></summary>
A **Spoon** is a managed fork. It records the upstream project, the fork
repository, default branches, sync policy, extra remotes, current drift, cached
commits, cached pull requests, secrets, and agent settings.
Spoons are the durable project-level objects. They answer:
- What did I fork?
- Where does my fork live?
- How far has it drifted?
- Which commits are mine?
- Which upstream changes matter?
- What threads or PRs are open?
</details>
<details open>
<summary><strong>Threads</strong></summary>
A **Thread** is the durable place where Spoon talks about maintenance work.
Threads can be created by a user or by the system.
Common thread sources:
- `user_request`: user asks Spoon to change a fork.
- `upstream_update`: upstream moved and the fork needs review.
- `merge_conflict`: a sync conflict needs context or code.
- `manual_review`: user explicitly asks for a review.
- `system`: internal maintenance coordination.
Threads hold messages, status, outcomes, related sync runs, related jobs,
workspace links, draft PR links, and ignored upstream decisions.
</details>
<details open>
<summary><strong>Maintenance decisions</strong></summary>
Spoon's maintenance policy is intentionally conservative:
| Situation | Default action |
| ------------------------------------------ | ------------------------------------- |
| No fork-only commits and upstream is ahead | Auto-sync |
| Fork-only commits and upstream is ahead | Create a maintenance thread |
| Merge conflicts | Open or continue a workspace thread |
| Irrelevant upstream changes | Record an intentional ignore decision |
| Agent/code changes | Open a draft PR |
The goal is to keep forks close without hiding risk or skipping review when
custom work exists.
</details>
<details>
<summary><strong>OpenCode workspaces</strong></summary>
Spoon's optional agent worker is designed to run outside Convex actions. The
worker claims queued jobs, clones the current GitHub fork, creates a branch,
starts an isolated workspace, and exposes workspace operations to the Next app
through server-only API proxies.
Workspace capabilities:
- browse repository files
- edit files in a browser editor
- use optional Vim keybindings
- inspect diffs
- send thread messages to the agent
- run configured commands
- store logs and artifacts
- push a branch
- open a draft PR
The browser never receives worker tokens and never talks directly to the worker
or job container.
</details>
## Architecture
- `apps/next`: Next.js 16 web app and primary product UI.
- `apps/agent-worker`: optional server-side worker for OpenCode workspaces and
draft PR jobs.
- `apps/expo`: Expo companion app.
- `packages/backend/convex`: self-hosted Convex schema, functions, auth, and
HTTP routes.
- `packages/ui`: shared shadcn-based UI components.
- `tools`: shared ESLint, Prettier, Tailwind, TypeScript, and Vitest config.
- `docker`: local and production Compose files.
- `scripts`: environment, database, codegen, and CI helpers.
Core domain objects:
- `spoons`: managed fork records.
- `threads`: durable maintenance and work conversations.
- `threadMessages`: persisted thread messages.
- `syncRuns`: upstream checks, sync attempts, and maintenance decisions.
- `ignoredUpstreamChanges`: intentional ignore decisions that affect effective
drift.
- `gitConnections`: Git provider connection metadata.
- `agentJobs`: worker-executed workspace jobs and PR lifecycle.
- `agentJobEvents` and `agentJobArtifacts`: logs and structured job outputs.
- `agentWorkspaceChanges`: recorded file changes from user, agent, or command
activity.
- `spoonSecrets`: encrypted per-Spoon environment variables.
- `spoonAgentSettings`: per-Spoon runtime, branch, command, and env-file
settings.
- `aiProviderProfiles`: encrypted provider/auth profiles used by OpenCode.
## Local Setup
Requirements:
- Bun 1.3.10
- Node 22
- Docker or Podman
- Infisical CLI
```sh
bun install --frozen-lockfile
infisical login
infisical init
bun db:up
bun dev:next
```
Local services:
- Next.js: `http://localhost:3000`
- Convex API: `http://localhost:3210`
- Convex site HTTP routes: `http://localhost:3211`
- Convex dashboard: `http://localhost:6791`
- Convex Postgres: `localhost:5432`
Next and Expo run on the host. Local Convex runs in containers with Postgres
storage. Normal `bun db:up` never contacts staging; it starts local Postgres,
Convex, and the dashboard, generates a machine-local Convex admin key in
`.local/dev.generated.env` when needed, deploys functions/schema, and configures
local Convex Auth keys.
```sh
bun db:down # stop; preserve local data
bun db:down:wipe # remove local data volumes and generated admin key
```
Use staging services explicitly:
```sh
INFISICAL_ENV=staging bun dev:next
```
Run the optional local agent worker in a separate terminal:
```sh
bun dev:agent
```
The worker starts an internal HTTP API, defaulting to `http://localhost:3921`,
for server-side Next route handlers. The browser never receives the worker token
or talks to this API directly.
The Docker Compose local worker service is disabled by default behind the
`agent` profile. Build the job image before using Docker-backed jobs:
```sh
docker build -f docker/agent-job.Dockerfile -t spoon-agent-job:latest .
docker compose -f docker/compose.local.yml --profile agent up spoon-agent-worker
```
The job image includes the OpenCode CLI. Rebuild it after changes to
`docker/agent-job.Dockerfile`.
## Environment Model
Local `dev` and `staging` values come from Infisical through
`scripts/with-env`. App commands do not fall back to root `.env` files.
Generated local state belongs in:
<details open>
<summary><strong>Workspace layout</strong></summary>
```txt
.local/<environment>.generated.env
.
├── apps
│ ├── next # Next.js 16 web app and primary Spoon UI
│ ├── agent-worker # Optional OpenCode workspace / draft PR worker
│ └── expo # Expo companion app scaffold
├── packages
│ ├── backend # Convex backend package
│ │ └── convex # Schema, functions, auth, HTTP routes
│ └── ui # Shared shadcn-based UI components
├── tools # Shared lint, format, Tailwind, TS, Vitest config
├── docker # Compose files and worker/job Dockerfiles
└── scripts # Env, Convex, codegen, database, and CI helpers
```
CI uses Gitea-provided secrets or `CI_ENV_FILE` and must not call Infisical.
</details>
Useful helpers:
<details>
<summary><strong>Core tables</strong></summary>
| Table | Purpose |
| ------------------------ | --------------------------------------------------------- |
| `spoons` | Managed fork records |
| `threads` | Durable maintenance and work conversations |
| `threadMessages` | Messages inside threads |
| `syncRuns` | Upstream checks, sync attempts, and maintenance decisions |
| `ignoredUpstreamChanges` | Intentional ignore records that affect effective drift |
| `gitConnections` | Git provider connection metadata |
| `spoonRepositoryStates` | Latest cached upstream/fork state |
| `spoonCommits` | Cached upstream and fork-only commits |
| `spoonPullRequests` | Cached fork/upstream pull requests |
| `spoonSecrets` | Encrypted per-Spoon environment variables |
| `spoonAgentSettings` | Per-Spoon runtime, branch, command, and env-file settings |
| `aiProviderProfiles` | Encrypted provider/auth profiles used by OpenCode |
| `agentJobs` | Worker-executed workspace jobs and PR lifecycle |
| `agentJobEvents` | Append-only worker event log |
| `agentJobArtifacts` | Diffs, summaries, command output, PR body drafts |
| `agentWorkspaceChanges` | Recorded user, agent, and command file changes |
</details>
<details>
<summary><strong>Important routes</strong></summary>
| Route | Purpose |
| --------------------------------- | --------------------------------------- |
| `/` | Public product landing page |
| `/dashboard` | Maintenance overview |
| `/spoons` | Managed fork list |
| `/spoons/new` | Manual/GitHub Spoon creation |
| `/spoons/[spoonId]` | Spoon detail dashboard |
| `/spoons/[spoonId]/agent/[jobId]` | Interactive workspace |
| `/threads` | Global thread queue |
| `/threads/[threadId]` | Thread detail |
| `/settings/profile` | User profile settings |
| `/settings/integrations` | GitHub and service integration settings |
| `/settings/ai-providers` | AI/OpenCode provider profiles |
Legacy `/updates` and `/agents` routes redirect into `/threads`.
</details>
## Mobile App
<details open>
<summary><strong>Current Expo scope</strong></summary>
`apps/expo` is the mobile Spoon client. It is designed to mirror the core web
product without exposing worker internals or trying to turn a phone into the
primary code-editing surface.
The mobile app currently supports:
- password, GitHub, and Authentik sign-in
- Dashboard, Spoons, Threads, Workspace Review, and Settings tabs/screens
- manual Spoon creation and GitHub-assisted repository tracking
- Spoon detail views for overview, upstream commits, fork-only commits, PRs,
threads, settings, clone URLs, and additional remotes
- Spoon maintenance settings, agent settings, encrypted secrets, and bulk
`.env` paste import
- thread list/detail, message composer, resolve/cancel actions, and workspace
review links
- GitHub integration status and repository listing
- AI provider profile management, including Codex/OpenCode auth JSON
- read-only workspace review for job status, messages, diffs, events,
artifacts, and draft PR links
The mobile app intentionally does not currently support:
- live workspace file browsing/editing
- mobile command execution
- direct mobile calls to the agent worker HTTP API
- mobile access to worker/container tokens
- long-running app preview stacks
- production app-store/EAS release flow
Mobile workspace editing is deferred until worker authorization and mobile
editor UX are designed explicitly. For now, the phone is a strong review and
control surface; the browser remains the code workspace.
</details>
<details>
<summary><strong>Expo validation</strong></summary>
Useful mobile checks:
```sh
sh scripts/with-env dev -- <command>
sh scripts/export-env dev
bun sync:convex
bun sync:convex:staging
bun --filter @spoon/expo lint
bun --filter @spoon/expo typecheck
bun --filter @spoon/expo test:unit
bun --filter @spoon/expo test:component
```
### Convex Deployment Env
The Expo unit tests cover pure utilities such as `.env` parsing and formatting.
The component tests use a lightweight React Native mock layer to exercise shared
mobile controls, higher-value forms, and route smoke renders without booting a
native simulator.
Convex functions and HTTP actions read environment variables from the Convex
deployment environment, not directly from the host process. OAuth providers,
GitHub App credentials, UseSend, encryption keys, worker tokens, and Convex Auth
signing keys must be synced into the selected Convex deployment.
</details>
`packages/backend` runs `scripts/sync-convex-env` before `convex dev`, so
`bun dev:next`, `bun dev:backend`, and `bun db:up` sync the relevant Infisical
values into local Convex first. Run it manually when needed:
## Environment Reference
```sh
sh scripts/sync-convex-env dev
sh scripts/sync-convex-env staging
INFISICAL_ENV=staging bun sync:convex
```
This project is currently private, so this section is a reference for what the
application expects rather than public setup documentation.
For local `dev`, `JWT_PRIVATE_KEY`, `JWKS`, `SPOON_ENCRYPTION_KEY`,
`SPOON_WORKER_TOKEN`, and related generated values are created automatically if
they are not already present. The generated Convex admin key remains
machine-local in `.local/dev.generated.env`; do not put it in Infisical.
<details open>
<summary><strong>Public Next variables</strong></summary>
Local OAuth callback URLs:
| Variable | Used for |
| --------------------------------- | ------------------------------------------- |
| `NEXT_PUBLIC_SITE_URL` | Canonical Spoon web URL |
| `NEXT_PUBLIC_CONVEX_URL` | Convex client URL |
| `NEXT_PUBLIC_DEPLOYMENT_URL` | Convex dashboard/deployment URL when needed |
| `NEXT_PUBLIC_PLAUSIBLE_URL` | Plausible analytics endpoint |
| `NEXT_PUBLIC_SENTRY_DSN` | Browser Sentry DSN |
| `NEXT_PUBLIC_SENTRY_URL` | Sentry instance URL |
| `NEXT_PUBLIC_SENTRY_ORG` | Sentry organization |
| `NEXT_PUBLIC_SENTRY_PROJECT_NAME` | Sentry project name |
```txt
http://localhost:3211/api/auth/callback/authentik
http://localhost:3211/api/auth/callback/github
```
</details>
If GitHub App actions fail with `GITHUB_APP_PRIVATE_KEY is not configured`, add
the full PEM contents to Infisical as `GITHUB_APP_PRIVATE_KEY` and rerun the
sync command.
<details>
<summary><strong>Auth and email</strong></summary>
## Development
| Variable | Used for |
| ----------------------- | ----------------------------- |
| `SITE_URL` | Convex Auth site URL |
| `JWT_PRIVATE_KEY` | Convex Auth signing key |
| `JWKS` | Convex Auth JWKS |
| `AUTH_AUTHENTIK_ID` | Authentik OAuth client ID |
| `AUTH_AUTHENTIK_SECRET` | Authentik OAuth client secret |
| `AUTH_AUTHENTIK_ISSUER` | Authentik issuer URL |
| `AUTH_GITHUB_ID` | GitHub OAuth client ID |
| `AUTH_GITHUB_SECRET` | GitHub OAuth client secret |
| `USESEND_API_KEY` | UseSend API key |
| `USESEND_URL` | UseSend API URL |
| `USESEND_FROM_EMAIL` | Transactional email sender |
```sh
bun dev:next
bun dev:expo
bun dev:agent
```
</details>
Physical devices cannot resolve their own `localhost`; override the public
Convex URL with the development host's LAN address when testing Expo on-device.
<details>
<summary><strong>GitHub App</strong></summary>
Shared dependency versions belong in root catalogs. Edit the root catalog, run
`bun install`, then `bun lint:ws`. Do not run `bun update` inside a workspace.
| Variable | Used for |
| ---------------------------- | ---------------------------------- |
| `GITHUB_APP_ID` | GitHub App ID |
| `GITHUB_APP_CLIENT_ID` | GitHub App OAuth client ID |
| `GITHUB_APP_CLIENT_SECRET` | GitHub App OAuth client secret |
| `GITHUB_APP_PRIVATE_KEY` | GitHub App PEM private key |
| `GITHUB_APP_WEBHOOK_SECRET` | GitHub webhook verification secret |
| `GITHUB_APP_SLUG` | GitHub App slug |
| `GITHUB_APP_INSTALLATION_ID` | Default/local installation ID |
| `GITHUB_APP_OWNER` | Default/local installation owner |
## Validation
</details>
Routine checks:
<details>
<summary><strong>Convex, storage, and runtime</strong></summary>
```sh
bun lint:ws
bun format
bun lint
bun typecheck
bun run test
```
| Variable | Used for |
| ----------------------------------- | ----------------------------------------------- |
| `CONVEX_SELF_HOSTED_URL` | Self-hosted Convex API URL |
| `CONVEX_SELF_HOSTED_ADMIN_KEY` | Admin key for deploying/syncing Convex |
| `CONVEX_CLOUD_ORIGIN` | Convex backend origin |
| `CONVEX_SITE_ORIGIN` | Convex site-function origin |
| `CONVEX_SITE_URL` | Site URL seen by Convex Auth |
| `POSTGRES_URL` | Convex storage database URL |
| `SPOON_ENCRYPTION_KEY` | Encryption key for stored secrets/provider auth |
| `SPOON_WORKER_TOKEN` | Worker token for Convex worker mutations |
| `SPOON_AGENT_WORKER_URL` | Internal worker HTTP URL used by Next |
| `SPOON_AGENT_WORKER_HTTP_PORT` | Worker HTTP port |
| `SPOON_AGENT_WORKER_INTERNAL_TOKEN` | Server-only token for Next-to-worker proxy |
| `SPOON_AGENT_JOB_IMAGE` | Agent job container image |
| `SPOON_AGENT_RUNTIME` | Runtime mode, currently Docker/Podman-oriented |
| `SPOON_AGENT_MAX_CONCURRENT_JOBS` | Worker concurrency limit |
| `SPOON_AGENT_JOB_TIMEOUT_MS` | Job timeout |
| `SPOON_AGENT_WORKDIR` | Worker work directory |
| `SPOON_AGENT_NETWORK` | Optional job container network |
Full local gate without e2e:
</details>
```sh
SKIP_E2E=1 bun run ci:check
```
<details>
<summary><strong>Deployment and observability</strong></summary>
Local-stack smoke e2e:
| Variable | Used for |
| ----------------------- | --------------------------------- |
| `NODE_ENV` | Runtime environment |
| `SENTRY_AUTH_TOKEN` | Sentry source map/upload auth |
| `REDACT_LOGS_TO_CLIENT` | Convex log redaction setting |
| `DISABLE_BEACON` | Self-hosted Convex beacon setting |
| `DO_NOT_REQUIRE_SSL` | Self-hosted Convex SSL behavior |
| `CI_ENV_FILE` | CI-provided env file path |
```sh
bun test:e2e
```
</details>
`bun test:e2e` starts the isolated local stack when needed and stops it
afterward only when it was not already running.
## Current Status
Use `bun run test`, not bare `bun test`; bare `bun test` invokes Bun's built-in
test runner instead of the repo's Turbo/Vitest test script.
<details open>
<summary><strong>Implemented</strong></summary>
## Deployment
- Thread-first Next.js product shell
- GitHub App connection and fork creation foundation
- GitHub drift refresh, commit cache, PR cache, and sync-run history
- Effective drift and ignored upstream change records
- Global Threads page and Spoon-scoped Threads tab
- OpenCode-oriented agent worker and browser workspace foundation
- Monaco editor with optional Vim mode
- Diff viewer, command panel, worker logs, and artifacts
- Encrypted Spoon secrets and bulk `.env` import
- Encrypted AI provider profiles, including Codex/OpenCode auth support
- Authentik, GitHub, and password auth through Convex Auth
- Self-hosted Convex/Postgres deployment model
Production Compose runs the Next image, self-hosted Convex backend/dashboard,
and Postgres. The deployed Next image is expected to be named
`spoon-next:latest` in the Gitea registry.
</details>
Gitea runs the quality gate first, runs Convex codegen with deployment env,
builds the Next image from injected secrets or `CI_ENV_FILE`, then pushes SHA
and `latest` tags. CI never installs or invokes Infisical.
<details>
<summary><strong>Intentionally not done yet</strong></summary>
- Autonomous merging for custom/diverged forks
- Non-GitHub provider automation
- Pushing agent branches to additional remotes
- Long-running preview stacks for arbitrary forked projects
- Direct browser access to worker containers
- Public self-hosting setup documentation
- Production mobile release flow
</details>
## Notes
Spoon is built for a very specific maintenance problem: "I want to fork this
project, but I do not want to permanently become its maintenance team."
The current product direction is to make that maintenance visible, threaded,
reviewable, and increasingly automated where it is safe. Clean forks can stay
close automatically. Custom forks get context, workspace help, and draft PRs.