From ddce5efb13258afa0d41544c1d3b4104152e8f4e Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Mon, 22 Jun 2026 10:42:47 -0400 Subject: [PATCH] Update README.md & fix test --- README.md | 175 +++++++++--------- .../components/landing/product-story-demo.tsx | 19 +- apps/next/tests/component/render.test.tsx | 2 +- 3 files changed, 107 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index be4821f..581e9e1 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,102 @@ # 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, -and still stay close to upstream. Spoon tracks managed forks, called -**Spoons**, and lays the foundation for upstream update checks, AI-assisted -change review, and agent-authored merge requests. +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. 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: -- Public Spoon landing page in Next.js. -- Authenticated web dashboard routes: +- Public Next.js landing page for Spoon's thread-first maintenance model. +- Authenticated web routes: - `/dashboard` - `/spoons` - `/spoons/new` - - `/updates` - `/spoons/[spoonId]` - - `/settings` -- Manual and GitHub-created Spoon records stored in Convex. + - `/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 manual sync foundation. -- Per-user OpenAI settings for upstream compatibility review. -- Per-Spoon encrypted project secrets and agent runtime settings. -- Optional `apps/agent-worker` service that can claim queued jobs, clone the - GitHub fork, keep an interactive workspace active, expose file browsing and - edits through a server-side proxy, run selected commands, call OpenCode or the - OpenAI direct fallback, push a branch, and open a draft PR. -- Browser agent workspace at `/spoons/[spoonId]/agent/[jobId]` with persisted - thread messages, file tree, Monaco editor with optional Vim mode, diff view, - command panel, and draft PR actions. -- Password auth and Authentik OAuth through Convex Auth. + 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. Not implemented yet: -- Automatic merge. -- Additional Git provider automation beyond preserving provider-neutral fields. +- 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 agent containers. +- Direct browser access to worker containers. - Production mobile build/release setup. ## Architecture - `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. - `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, and CI helpers. +- `scripts`: environment, database, codegen, and CI helpers. -The core domain objects are: +Core domain objects: - `spoons`: managed fork records. -- `gitConnections`: future Git provider connection metadata. -- `syncRuns`: future upstream checks, merge attempts, and AI reviews. -- `agentRequests`: prompt-driven agent work requests. -- `agentJobs`: worker-executed coding-agent jobs and their PR lifecycle. -- `agentJobMessages`: persisted per-job agent workspace thread messages. -- `agentWorkspaceChanges`: recorded user, agent, and command workspace changes. +- `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 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: @@ -98,8 +124,8 @@ Local services: 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. +`.local/dev.generated.env` when needed, deploys functions/schema, and configures +local Convex Auth keys. ```sh bun db:down # stop; preserve local data @@ -118,9 +144,9 @@ Run the optional local agent worker in a separate terminal: bun dev:agent ``` -The worker also 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 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: @@ -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 `docker/agent-job.Dockerfile`. -## Environment model +## Environment Model -Local `dev` and `staging` values come from Infisical through `scripts/with-env`. -App commands do not fall back to root `.env` files. +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: @@ -152,17 +178,19 @@ Useful helpers: sh scripts/with-env dev -- sh scripts/export-env dev bun sync:convex +bun sync:convex:staging ``` -### Convex deployment env +### Convex Deployment Env Convex functions and HTTP actions read environment variables from the Convex -deployment environment, not directly from the host process. For OAuth providers, -that means Infisical values must also be present in local Convex env. +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. `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 the selected Convex deployment first. Run it manually when needed: +values into local Convex first. Run it manually when needed: ```sh sh scripts/sync-convex-env dev @@ -170,41 +198,12 @@ sh scripts/sync-convex-env staging 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 -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: +Local OAuth callback URLs: ```txt http://localhost:3211/api/auth/callback/authentik @@ -220,6 +219,7 @@ sync command. ```sh bun dev:next bun dev:expo +bun dev:agent ``` 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 -Production Compose keeps the self-hosted Convex backend/dashboard and expects -`POSTGRES_URL` to be a database-cluster URL without a database path. +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. -Gitea runs the quality gate first, builds the Next image from a temporary -Gitea-secret env file, then pushes SHA and `latest` tags. CI never installs or -invokes Infisical. +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. diff --git a/apps/next/src/components/landing/product-story-demo.tsx b/apps/next/src/components/landing/product-story-demo.tsx index 4ff6ede..855271c 100644 --- a/apps/next/src/components/landing/product-story-demo.tsx +++ b/apps/next/src/components/landing/product-story-demo.tsx @@ -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 [reducedMotion, setReducedMotion] = useState(true); + const [reducedMotion, setReducedMotion] = useState( + getReducedMotionPreference, + ); useEffect(() => { + if (typeof window.matchMedia !== 'function') { + return; + } + const query = window.matchMedia('(prefers-reduced-motion: reduce)'); const update = () => setReducedMotion(query.matches); update(); diff --git a/apps/next/tests/component/render.test.tsx b/apps/next/tests/component/render.test.tsx index 753aa41..f8cb1b7 100644 --- a/apps/next/tests/component/render.test.tsx +++ b/apps/next/tests/component/render.test.tsx @@ -25,7 +25,7 @@ describe('component test harness', () => { render(); expect( screen.getByRole('heading', { - name: /make your forks intimately close to upstream\./i, + name: /fork freely & keep them all intimately close to upstream\./i, }), ).toBeInTheDocument(); });