Update README.md & fix test
Build and Push Next App / quality (push) Successful in 1m40s
Build and Push Next App / build-next (push) Successful in 4m17s

This commit is contained in:
Gabriel Brown
2026-06-22 10:42:47 -04:00
parent 206b64176b
commit ddce5efb13
3 changed files with 107 additions and 89 deletions
+88 -87
View File
@@ -1,76 +1,102 @@
# Spoon # Spoon
Spoon is a self-hostable fork maintenance dashboard. Spoon is a self-hostable fork maintenance cockpit.
The product goal is simple: make it practical to fork a project, customize it, Forking a project should not mean supporting it alone. Spoon tracks managed
and still stay close to upstream. Spoon tracks managed forks, called forks, called **Spoons**, watches upstream for drift, automatically syncs clean
**Spoons**, and lays the foundation for upstream update checks, AI-assisted forks when it can, and opens durable **Threads** when upstream changes need
change review, and agent-authored merge requests. review, context, or code.
This repository is the Spoon application itself, not a generic starter. This repository is the Spoon application itself, not a generic starter.
## Current scope ## What Spoon Does
- 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
Implemented today: Implemented today:
- Public Spoon landing page in Next.js. - Public Next.js landing page for Spoon's thread-first maintenance model.
- Authenticated web dashboard routes: - Authenticated web routes:
- `/dashboard` - `/dashboard`
- `/spoons` - `/spoons`
- `/spoons/new` - `/spoons/new`
- `/updates`
- `/spoons/[spoonId]` - `/spoons/[spoonId]`
- `/settings` - `/spoons/[spoonId]/agent/[jobId]`
- Manual and GitHub-created Spoon records stored in Convex. - `/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, - GitHub App connection, repository listing, fork creation, drift refresh,
commit/PR cache, and safe manual sync foundation. commit/PR cache, and safe sync foundation.
- Per-user OpenAI settings for upstream compatibility review. - Thread-first maintenance model with ignored upstream changes and effective
- Per-Spoon encrypted project secrets and agent runtime settings. drift.
- Optional `apps/agent-worker` service that can claim queued jobs, clone the - Optional `apps/agent-worker` service that claims queued jobs, clones the
GitHub fork, keep an interactive workspace active, expose file browsing and current GitHub fork, starts an isolated workspace, exposes file browsing and
edits through a server-side proxy, run selected commands, call OpenCode or the edits through server-side Next proxies, runs commands, and opens draft PRs.
OpenAI direct fallback, push a branch, and open a draft PR. - Browser workspace with persisted thread messages, file tree, Monaco editor
- Browser agent workspace at `/spoons/[spoonId]/agent/[jobId]` with persisted with optional Vim mode, diff view, command panel, logs, artifacts, and draft
thread messages, file tree, Monaco editor with optional Vim mode, diff view, PR actions.
command panel, and draft PR actions. - Encrypted per-user AI provider profiles and per-Spoon project secrets.
- Password auth and Authentik OAuth through Convex Auth. - Password auth and Authentik/GitHub OAuth through Convex Auth.
- Expo companion app shell with password and Authentik sign-in. - Expo companion app shell with password and Authentik sign-in.
- Self-hosted local Convex using Postgres storage. - Self-hosted local Convex using Postgres storage.
Not implemented yet: Not implemented yet:
- Automatic merge. - Automatic merge of custom/diverged forks.
- Additional Git provider automation beyond preserving provider-neutral fields. - Git provider automation beyond GitHub.
- Additional remotes as push targets. - Additional remotes as push targets.
- Long-running service-stack orchestration inside agent jobs. - Long-running service-stack orchestration inside agent jobs.
- Direct browser access to agent containers. - Direct browser access to worker containers.
- Production mobile build/release setup. - Production mobile build/release setup.
## Architecture ## Architecture
- `apps/next`: Next.js 16 web app and primary product UI. - `apps/next`: Next.js 16 web app and primary product UI.
- `apps/agent-worker`: optional server-side worker for queued coding-agent jobs. - `apps/agent-worker`: optional server-side worker for OpenCode workspaces and
draft PR jobs.
- `apps/expo`: Expo companion app. - `apps/expo`: Expo companion app.
- `packages/backend/convex`: self-hosted Convex schema, functions, auth, and - `packages/backend/convex`: self-hosted Convex schema, functions, auth, and
HTTP routes. HTTP routes.
- `packages/ui`: shared shadcn-based UI components. - `packages/ui`: shared shadcn-based UI components.
- `tools`: shared ESLint, Prettier, Tailwind, TypeScript, and Vitest config. - `tools`: shared ESLint, Prettier, Tailwind, TypeScript, and Vitest config.
- `docker`: local and production Compose files. - `docker`: local and production Compose files.
- `scripts`: environment, database, and CI helpers. - `scripts`: environment, database, codegen, and CI helpers.
The core domain objects are: Core domain objects:
- `spoons`: managed fork records. - `spoons`: managed fork records.
- `gitConnections`: future Git provider connection metadata. - `threads`: durable maintenance and work conversations.
- `syncRuns`: future upstream checks, merge attempts, and AI reviews. - `threadMessages`: persisted thread messages.
- `agentRequests`: prompt-driven agent work requests. - `syncRuns`: upstream checks, sync attempts, and maintenance decisions.
- `agentJobs`: worker-executed coding-agent jobs and their PR lifecycle. - `ignoredUpstreamChanges`: intentional ignore decisions that affect effective
- `agentJobMessages`: persisted per-job agent workspace thread messages. drift.
- `agentWorkspaceChanges`: recorded user, agent, and command workspace changes. - `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. - `spoonSecrets`: encrypted per-Spoon environment variables.
- `spoonAgentSettings`: per-Spoon agent model, branch, and command settings. - `spoonAgentSettings`: per-Spoon runtime, branch, command, and env-file
settings.
- `aiProviderProfiles`: encrypted provider/auth profiles used by OpenCode.
## Local setup ## Local Setup
Requirements: Requirements:
@@ -98,8 +124,8 @@ Local services:
Next and Expo run on the host. Local Convex runs in containers with Postgres 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, storage. Normal `bun db:up` never contacts staging; it starts local Postgres,
Convex, and the dashboard, generates a machine-local Convex admin key in Convex, and the dashboard, generates a machine-local Convex admin key in
`.local/dev.generated.env` when needed, deploys functions/schema, and `.local/dev.generated.env` when needed, deploys functions/schema, and configures
configures local Convex Auth keys. local Convex Auth keys.
```sh ```sh
bun db:down # stop; preserve local data bun db:down # stop; preserve local data
@@ -118,9 +144,9 @@ Run the optional local agent worker in a separate terminal:
bun dev:agent bun dev:agent
``` ```
The worker also starts an internal HTTP API, defaulting to The worker starts an internal HTTP API, defaulting to `http://localhost:3921`,
`http://localhost:3921`, for server-side Next route handlers. The browser never for server-side Next route handlers. The browser never receives the worker token
receives the worker token or talks to this API directly. or talks to this API directly.
The Docker Compose local worker service is disabled by default behind the The Docker Compose local worker service is disabled by default behind the
`agent` profile. Build the job image before using Docker-backed jobs: `agent` profile. Build the job image before using Docker-backed jobs:
@@ -133,10 +159,10 @@ 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 The job image includes the OpenCode CLI. Rebuild it after changes to
`docker/agent-job.Dockerfile`. `docker/agent-job.Dockerfile`.
## Environment model ## Environment Model
Local `dev` and `staging` values come from Infisical through `scripts/with-env`. Local `dev` and `staging` values come from Infisical through
App commands do not fall back to root `.env` files. `scripts/with-env`. App commands do not fall back to root `.env` files.
Generated local state belongs in: Generated local state belongs in:
@@ -152,17 +178,19 @@ Useful helpers:
sh scripts/with-env dev -- <command> sh scripts/with-env dev -- <command>
sh scripts/export-env dev sh scripts/export-env dev
bun sync:convex bun sync:convex
bun sync:convex:staging
``` ```
### Convex deployment env ### Convex Deployment Env
Convex functions and HTTP actions read environment variables from the Convex Convex functions and HTTP actions read environment variables from the Convex
deployment environment, not directly from the host process. For OAuth providers, deployment environment, not directly from the host process. OAuth providers,
that means Infisical values must also be present in local Convex env. GitHub App credentials, UseSend, encryption keys, worker tokens, and Convex Auth
signing keys must be synced into the selected Convex deployment.
`packages/backend` runs `scripts/sync-convex-env` before `convex dev`, so `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 `bun dev:next`, `bun dev:backend`, and `bun db:up` sync the relevant Infisical
values into the selected Convex deployment first. Run it manually when needed: values into local Convex first. Run it manually when needed:
```sh ```sh
sh scripts/sync-convex-env dev sh scripts/sync-convex-env dev
@@ -170,41 +198,12 @@ sh scripts/sync-convex-env staging
INFISICAL_ENV=staging bun sync:convex INFISICAL_ENV=staging bun sync:convex
``` ```
The sync includes: 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.
```txt Local OAuth callback URLs:
AUTH_AUTHENTIK_ID
AUTH_AUTHENTIK_SECRET
AUTH_AUTHENTIK_ISSUER
AUTH_GITHUB_ID
AUTH_GITHUB_SECRET
GITHUB_APP_ID
GITHUB_APP_CLIENT_ID
GITHUB_APP_CLIENT_SECRET
GITHUB_APP_PRIVATE_KEY
GITHUB_APP_WEBHOOK_SECRET
GITHUB_APP_SLUG
GITHUB_APP_INSTALLATION_ID
GITHUB_APP_OWNER
SPOON_ENCRYPTION_KEY
SPOON_WORKER_TOKEN
SPOON_AGENT_WORKER_INTERNAL_TOKEN
SPOON_AGENT_WORKER_HTTP_PORT
SPOON_AGENT_WORKER_URL
USESEND_API_KEY
USESEND_URL
USESEND_FROM_EMAIL
JWT_PRIVATE_KEY
JWKS
SITE_URL
```
For local `dev`, `JWT_PRIVATE_KEY`, `JWKS`, `SPOON_ENCRYPTION_KEY`, and
`SPOON_WORKER_TOKEN` are generated automatically if they are not already present
in Convex. The generated Convex admin key remains machine-local in
`.local/dev.generated.env`; do not put it in Infisical.
The local OAuth callback URLs are:
```txt ```txt
http://localhost:3211/api/auth/callback/authentik http://localhost:3211/api/auth/callback/authentik
@@ -220,6 +219,7 @@ sync command.
```sh ```sh
bun dev:next bun dev:next
bun dev:expo bun dev:expo
bun dev:agent
``` ```
Physical devices cannot resolve their own `localhost`; override the public Physical devices cannot resolve their own `localhost`; override the public
@@ -260,9 +260,10 @@ test runner instead of the repo's Turbo/Vitest test script.
## Deployment ## Deployment
Production Compose keeps the self-hosted Convex backend/dashboard and expects Production Compose runs the Next image, self-hosted Convex backend/dashboard,
`POSTGRES_URL` to be a database-cluster URL without a database path. and Postgres. The deployed Next image is expected to be named
`spoon-next:latest` in the Gitea registry.
Gitea runs the quality gate first, builds the Next image from a temporary Gitea runs the quality gate first, runs Convex codegen with deployment env,
Gitea-secret env file, then pushes SHA and `latest` tags. CI never installs or builds the Next image from injected secrets or `CI_ENV_FILE`, then pushes SHA
invokes Infisical. and `latest` tags. CI never installs or invokes Infisical.
@@ -61,10 +61,27 @@ const steps: DemoStepConfig[] = [
}, },
]; ];
const getReducedMotionPreference = () => {
if (
typeof window === 'undefined' ||
typeof window.matchMedia !== 'function'
) {
return false;
}
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
};
const usePrefersReducedMotion = () => { const usePrefersReducedMotion = () => {
const [reducedMotion, setReducedMotion] = useState(true); const [reducedMotion, setReducedMotion] = useState(
getReducedMotionPreference,
);
useEffect(() => { useEffect(() => {
if (typeof window.matchMedia !== 'function') {
return;
}
const query = window.matchMedia('(prefers-reduced-motion: reduce)'); const query = window.matchMedia('(prefers-reduced-motion: reduce)');
const update = () => setReducedMotion(query.matches); const update = () => setReducedMotion(query.matches);
update(); update();
+1 -1
View File
@@ -25,7 +25,7 @@ describe('component test harness', () => {
render(<Hero />); render(<Hero />);
expect( expect(
screen.getByRole('heading', { screen.getByRole('heading', {
name: /make your forks intimately close to upstream\./i, name: /fork freely & keep them all intimately close to upstream\./i,
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });