diff --git a/apps/agent-worker/src/worker.ts b/apps/agent-worker/src/worker.ts index 1bee109..47271ad 100644 --- a/apps/agent-worker/src/worker.ts +++ b/apps/agent-worker/src/worker.ts @@ -189,20 +189,30 @@ const detectPackageCommands = async ( scripts?: Record; }; const scripts = packageJson.scripts ?? {}; + const packageManager = (await fileExists(path.join(repoDir, 'bun.lock'))) + ? 'bun' + : (await fileExists(path.join(repoDir, 'pnpm-lock.yaml'))) + ? 'pnpm' + : (await fileExists(path.join(repoDir, 'yarn.lock'))) + ? 'yarn' + : 'npm'; + const runScript = (script: string) => + packageManager === 'npm' + ? `npm run ${script}` + : `${packageManager} run ${script}`; + return { - install: (await fileExists(path.join(repoDir, 'bun.lock'))) - ? 'bun install' - : (await fileExists(path.join(repoDir, 'pnpm-lock.yaml'))) - ? 'pnpm install' - : (await fileExists(path.join(repoDir, 'yarn.lock'))) - ? 'yarn install' - : 'npm install', + install: `${packageManager} install`, check: scripts.typecheck - ? 'npm run typecheck' + ? runScript('typecheck') : scripts.lint - ? 'npm run lint' + ? runScript('lint') : undefined, - test: scripts.test ? 'npm test' : undefined, + test: scripts.test + ? packageManager === 'npm' + ? 'npm test' + : `${packageManager} test` + : undefined, }; } catch { return {}; diff --git a/apps/next/public/favicon-light.png b/apps/next/public/favicon-light.png index ceaa592..8d5e0a1 100644 Binary files a/apps/next/public/favicon-light.png and b/apps/next/public/favicon-light.png differ diff --git a/apps/next/public/favicon.png b/apps/next/public/favicon.png index 7975cd5..8d5e0a1 100644 Binary files a/apps/next/public/favicon.png and b/apps/next/public/favicon.png differ diff --git a/apps/next/src/app/(app)/dashboard/page.tsx b/apps/next/src/app/(app)/dashboard/page.tsx index 63335a7..deee07f 100644 --- a/apps/next/src/app/(app)/dashboard/page.tsx +++ b/apps/next/src/app/(app)/dashboard/page.tsx @@ -5,13 +5,7 @@ import { MetricCard } from '@/components/dashboard/metric-card'; import { SpoonCard } from '@/components/spoons/spoon-card'; import { MaintenanceQueue } from '@/components/updates/maintenance-queue'; import { useQuery } from 'convex/react'; -import { - Bot, - GitBranch, - GitPullRequest, - RefreshCw, - ShieldCheck, -} from 'lucide-react'; +import { Bot, GitBranch, RefreshCw, ShieldCheck } from 'lucide-react'; import { api } from '@spoon/backend/convex/_generated/api.js'; import { Button, Card, CardContent, CardHeader, CardTitle } from '@spoon/ui'; @@ -50,17 +44,11 @@ const DashboardPage = () => {
- {
-

My Spoons

+

Spoons

Managed forks you want to keep close to their upstream projects.

diff --git a/apps/next/src/app/page.tsx b/apps/next/src/app/page.tsx index 1d76bcf..0966ba0 100644 --- a/apps/next/src/app/page.tsx +++ b/apps/next/src/app/page.tsx @@ -1,4 +1,11 @@ -import { Agents, CTA, Features, Hero, Workflow } from '@/components/landing'; +import { + Agents, + CTA, + Features, + Hero, + Security, + Workflow, +} from '@/components/landing'; const Home = () => (
@@ -6,6 +13,7 @@ const Home = () => ( +
); diff --git a/apps/next/src/components/app-shell/app-shell.tsx b/apps/next/src/components/app-shell/app-shell.tsx index 8980e7a..0f76e17 100644 --- a/apps/next/src/components/app-shell/app-shell.tsx +++ b/apps/next/src/components/app-shell/app-shell.tsx @@ -1,49 +1,12 @@ 'use client'; import type { ReactNode } from 'react'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { GitBranch, LayoutDashboard, RefreshCw, Settings } from 'lucide-react'; - -import { cn } from '@spoon/ui'; - -const navItems = [ - { href: '/dashboard', label: 'Dashboard', icon: LayoutDashboard }, - { href: '/spoons', label: 'My Spoons', icon: GitBranch }, - { href: '/updates', label: 'Updates', icon: RefreshCw }, - { href: '/settings/profile', label: 'Settings', icon: Settings }, -]; export const AppShell = ({ children }: { children: ReactNode }) => { - const pathname = usePathname(); return (
-
- -
{children}
+
+ {children}
); diff --git a/apps/next/src/components/brand/logo.tsx b/apps/next/src/components/brand/logo.tsx index 674e4bd..f53d906 100644 --- a/apps/next/src/components/brand/logo.tsx +++ b/apps/next/src/components/brand/logo.tsx @@ -1,16 +1,23 @@ +import Image from 'next/image'; import Link from 'next/link'; -import { Utensils } from 'lucide-react'; import { cn } from '@spoon/ui'; export const LogoMark = ({ className }: { className?: string }) => ( - + ); diff --git a/apps/next/src/components/landing/cta.tsx b/apps/next/src/components/landing/cta.tsx index 03a8988..dc37cc8 100644 --- a/apps/next/src/components/landing/cta.tsx +++ b/apps/next/src/components/landing/cta.tsx @@ -1,27 +1,35 @@ +'use client'; + import Link from 'next/link'; +import { useConvexAuth } from 'convex/react'; import { ArrowRight } from 'lucide-react'; import { Button } from '@spoon/ui'; -export const CTA = () => ( -
-
-
-

- Start your first Spoon -

-

- Create a manual managed fork record today. Provider connections, - scheduled checks, and AI merge request automation can build on the - same foundation. -

+export const CTA = () => { + const { isAuthenticated } = useConvexAuth(); + + return ( +
+
+
+
+

+ Keep the fork. Lose the maintenance dread. +

+

+ Create your first Spoon, connect GitHub, and make upstream drift + something you can see, review, and act on. +

+
+ +
- -
-
-); + + ); +}; diff --git a/apps/next/src/components/landing/features.tsx b/apps/next/src/components/landing/features.tsx index 4a1ca43..10a569e 100644 --- a/apps/next/src/components/landing/features.tsx +++ b/apps/next/src/components/landing/features.tsx @@ -1,100 +1,195 @@ import { Bot, - GitMerge, + Code2, + GitBranch, + GitCompare, + GitPullRequest, History, - SearchCheck, - ShieldCheck, - TriangleAlert, + KeyRound, + LockKeyhole, + RefreshCw, + ServerCog, + Sparkles, } from 'lucide-react'; -import { Card, CardContent, CardHeader, CardTitle } from '@spoon/ui'; +import { Badge } from '@spoon/ui'; -const maintenance = [ +const workflow = [ { - title: 'Upstream security fixes', + title: 'Connect GitHub', description: - 'Track the changes that land upstream so important fixes do not disappear into fork drift.', - icon: ShieldCheck, + 'Install the Spoon GitHub App so Spoon can read forks, compare branches, push agent branches, and open draft pull requests.', }, { - title: 'Conflict detection', + title: 'Create a Spoon', description: - 'Make update risk visible before a merge request reaches the fork you actually maintain.', - icon: TriangleAlert, + 'Register a managed fork with its upstream project, fork repository, default branches, sync cadence, and additional remote URLs.', }, { - title: 'AI-reviewed changes', + title: 'Watch drift', description: - 'Prepare for agent-assisted analysis that explains whether upstream changes affect your custom work.', - icon: SearchCheck, + 'Refresh GitHub state to see upstream commits waiting, fork-only commits, open pull requests, sync health, and merge history.', }, { - title: 'Merge request history', + title: 'Review safely', description: - 'Keep a durable timeline of upstream checks, review outcomes, and merge request decisions.', + 'Use AI compatibility reviews to summarize upstream changes, flag important files, and decide when a manual review is needed.', + }, + { + title: 'Ship through PRs', + description: + 'Queue agent jobs that work on fresh branches and open draft pull requests, while GitHub remains the source of truth.', + }, +]; + +const features = [ + { + title: 'Project dashboards', + description: + 'Each Spoon gets a focused dashboard with upstream drift, fork-only changes, pull requests, AI reviews, activity, settings, clone URLs, and extra remotes.', + icon: GitCompare, + }, + { + title: 'Upstream maintenance queue', + description: + 'The global dashboard makes it obvious which forks are up to date, behind, ahead, diverged, or waiting for review.', + icon: RefreshCw, + }, + { + title: 'Pull request visibility', + description: + 'Spoon caches fork pull requests and relevant upstream pull requests so maintenance decisions are tied to real GitHub activity.', + icon: GitPullRequest, + }, + { + title: 'AI compatibility review', + description: + 'OpenAI reviews upstream changes against fork-only commits and returns structured risk, summary, recommended action, and conflict signals.', + icon: Sparkles, + }, + { + title: 'Per-user AI settings', + description: + 'Users bring their own OpenAI API key, choose a review model, and set reasoning effort. Keys are encrypted before storage.', + icon: KeyRound, + }, + { + title: 'Agent job foundation', + description: + 'Spoon can queue coding-agent work per project with selected secrets, job settings, event logs, artifacts, and draft PR targets.', + icon: Bot, + }, +]; + +const builtFor = [ + { + title: 'Self-hosted by design', + description: + 'Run the app, Convex backend, Postgres, and optional agent worker on your own server so code automation stays under your control.', + icon: ServerCog, + }, + { + title: 'Secrets stay deliberate', + description: + 'Project secrets are per Spoon and selected per job. The agent runtime never receives every secret by default.', + icon: LockKeyhole, + }, + { + title: 'Outside work is expected', + description: + 'Spoon assumes you may push to GitHub directly, edit locally, or use another CI system. Refresh always starts from current GitHub state.', + icon: Code2, + }, + { + title: 'History stays inspectable', + description: + 'Sync runs, AI reviews, job logs, PR URLs, errors, and artifacts are stored so maintenance work is reviewable after the fact.', icon: History, }, ]; -export const Workflow = () => { - const steps = [ - 'Choose upstream', - 'Create a Spoon', - 'Customize your fork', - 'Track upstream', - 'Review and merge updates', - ]; - return ( -
-
-
-

- A fork workflow that keeps moving -

-

- Spoon starts with a provider-neutral model: upstream project, - managed fork, update checks, and reviewable merge requests. +export const Workflow = () => ( +

+
+
+ + Workflow + +

+ Forking should not mean drifting alone. +

+

+ Spoon treats a fork as an ongoing relationship with upstream. The + product keeps the original project, your custom work, and future + automation visible in one place. +

+
+ +
+
+ +

+ A Spoon is a managed fork +

+

+ It knows where upstream lives, where your fork lives, which branch + matters, what extra remotes you care about, and what rules should + govern updates. That gives maintenance a durable home instead of a + pile of one-off Git commands.

-
- {steps.map((step, index) => ( -
-

+ +

    + {workflow.map((step, index) => ( +
  1. + {String(index + 1).padStart(2, '0')} -

    -

    {step}

    -
+ +
+

{step.title}

+

+ {step.description} +

+
+ ))} -
+
-
- ); -}; +
+
+); export const Features = () => ( -
-
-

- Maintenance is the product -

-

- The first version establishes the dashboard surfaces and records that - future Git provider integrations and AI review jobs will use. +

+
+
+ + Product surface + +

+ Everything important about a fork, without opening six tabs. +

+
+

+ Spoon is not trying to replace GitHub. It is the layer that explains how + your fork relates to upstream and what should happen next.

-
- {maintenance.map(({ title, description, icon: Icon }) => ( - - - - {title} - - -

- {description} -

-
-
+ +
+ {features.map(({ title, description, icon: Icon }) => ( +
+
+ +

{title}

+
+

+ {description} +

+
))}
@@ -102,41 +197,92 @@ export const Features = () => ( export const Agents = () => (
-
+
-
- -
-

- Agent requests belong next to fork maintenance + + Agent work + +

+ The agent belongs inside the fork dashboard.

-

- Spoon is being shaped so a user can ask an agent to implement a - change, open a merge request against the managed fork, and still keep - upstream updates in view. This pass stores those requests without - running automation yet. +

+ The goal is simple: ask for a change, let a worker clone the current + fork, expose only the secrets you selected, run checks, push a branch, + and open a draft pull request. The first pieces are already modeled: + encrypted Spoon secrets, agent settings, queued jobs, logs, and + artifacts.

-
-
- -

Queued agent request

+ +
+
+
+ + + +
+

Draft PR agent flow

+

+ Built for review, not automatic merge. +

+
+
-

- “Add a project-specific onboarding flow, open a merge request, and - flag any upstream files this may affect.” -

-
-
- Target - feature/onboarding -
-
- Status - Queued -
+
+ {[ + ['Clone', 'Start from the current GitHub fork state.'], + ['Branch', 'Create a short-lived agent branch.'], + ['Edit', 'Apply focused changes with selected project context.'], + [ + 'Check', + 'Run configured install, lint, typecheck, or test steps.', + ], + ['Review', 'Open a draft pull request with logs and artifacts.'], + ].map(([phase, detail]) => ( +
+

{phase}

+

+ {detail} +

+
+ ))}
); + +export const Security = () => ( +
+
+
+ + Ownership + +

+ Useful because it respects how forks are really maintained. +

+

+ A fork can have local experiments, CI changes, private deployment + settings, and emergency upstream fixes all happening at once. Spoon + keeps those threads visible without pretending every change must come + through the app. +

+
+ +
+ {builtFor.map(({ title, description, icon: Icon }) => ( +
+
+ +

{title}

+
+

+ {description} +

+
+ ))} +
+
+
+); diff --git a/apps/next/src/components/landing/hero.tsx b/apps/next/src/components/landing/hero.tsx index 658a857..ad8b060 100644 --- a/apps/next/src/components/landing/hero.tsx +++ b/apps/next/src/components/landing/hero.tsx @@ -6,8 +6,10 @@ import { ArrowRight, Bot, CheckCircle2, + CircleDot, GitBranch, GitPullRequest, + KeyRound, ShieldCheck, } from 'lucide-react'; @@ -15,23 +17,23 @@ import { Badge, Button } from '@spoon/ui'; const previewRows = [ { - name: 'editor-spoon', - upstream: 'upstream/main', - status: 'Clean update', + name: 'gibsend', + upstream: 'usesend/usesend', + status: '3 upstream commits', icon: CheckCircle2, tone: 'text-emerald-600', }, { - name: 'billing-fork', - upstream: 'release/2026.06', - status: 'AI review queued', + name: 'internal-docs', + upstream: 'platform/docs', + status: 'AI review ready', icon: Bot, tone: 'text-teal-600', }, { - name: 'docs-platform', - upstream: 'main', - status: 'Needs review', + name: 'ops-console', + upstream: 'console/main', + status: 'fork-only changes', icon: GitPullRequest, tone: 'text-amber-600', }, @@ -41,19 +43,21 @@ export const Hero = () => { const { isAuthenticated } = useConvexAuth(); return (
-
+
- Self-hostable fork maintenance + Self-hostable fork maintenance cockpit

- Fork freely. Stay close to upstream. + Make your forks intimately close + to upstream.

- Spoon helps you customize upstream projects without inheriting the - full maintenance burden. Track drift, review update risk, and keep - managed forks ready for merge requests. + Spoon gives every important fork a living maintenance dashboard. + Track upstream drift, preserve your custom commits, review pull + requests, and queue AI-assisted work without losing sight of the + project you forked from.

+
+ {[ + 'GitHub App backed', + 'OpenAI key per user', + 'Draft PR workflow', + ].map((item) => ( + + + {item} + + ))} +
-
+
-

Spoon dashboard

+

Fork health

- Upstream status across managed forks + Current state across managed Spoons

- 3 active Spoons + Live GitHub sync
{[ - ['Updates', '4', '2 clean'], - ['Needs review', '1', 'conflict risk'], - ['Agents', '2', 'queued'], + ['Behind', '3', 'upstream commits'], + ['Fork-only', '12', 'custom changes'], + ['AI risk', 'Low', 'reviewed'], ].map(([label, value, note]) => (

{label}

{value}

@@ -100,7 +116,7 @@ export const Hero = () => { {previewRows.map(({ name, upstream, status, icon: Icon, tone }) => (
@@ -118,6 +134,20 @@ export const Hero = () => {
))}
+
+
+ +

+ User-owned OpenAI keys stay encrypted and selectable. +

+
+
+ +

+ Agent jobs are shaped around draft pull requests. +

+
+
diff --git a/apps/next/src/components/landing/index.tsx b/apps/next/src/components/landing/index.tsx index 80d5cac..4be82e6 100644 --- a/apps/next/src/components/landing/index.tsx +++ b/apps/next/src/components/landing/index.tsx @@ -1,3 +1,3 @@ export { Hero } from './hero'; -export { Agents, Features, Workflow } from './features'; +export { Agents, Features, Security, Workflow } from './features'; export { CTA } from './cta'; diff --git a/apps/next/src/components/layout/header/index.tsx b/apps/next/src/components/layout/header/index.tsx index cdad3ad..7e50b52 100644 --- a/apps/next/src/components/layout/header/index.tsx +++ b/apps/next/src/components/layout/header/index.tsx @@ -4,7 +4,14 @@ import type { ComponentProps } from 'react'; import Link from 'next/link'; import { SpoonLogo } from '@/components/brand/logo'; import { useConvexAuth } from 'convex/react'; -import { Bot, GitBranch, LayoutDashboard, ShieldCheck } from 'lucide-react'; +import { + GitBranch, + LayoutDashboard, + RefreshCw, + Settings, + ShieldCheck, + Sparkles, +} from 'lucide-react'; import { Button } from '@spoon/ui'; @@ -14,31 +21,46 @@ import { DesktopNavigation, MobileNavigation } from './navigation'; const Header = (headerProps: ComponentProps<'header'>) => { const { isAuthenticated } = useConvexAuth(); - const navItems: NavItem[] = [ - { - href: '/#workflow', - icon: GitBranch, - label: 'How it works', - }, - { - href: '/#maintenance', - icon: ShieldCheck, - label: 'Maintenance', - }, - { - href: '/#agents', - icon: Bot, - label: 'Agents', - }, - ]; - - if (isAuthenticated) { - navItems.push({ - href: '/dashboard', - icon: LayoutDashboard, - label: 'Dashboard', - }); - } + const navItems: NavItem[] = isAuthenticated + ? [ + { + href: '/dashboard', + icon: LayoutDashboard, + label: 'Dashboard', + }, + { + href: '/spoons', + icon: GitBranch, + label: 'My Spoons', + }, + { + href: '/updates', + icon: RefreshCw, + label: 'Updates', + }, + { + href: '/settings/profile', + icon: Settings, + label: 'Settings', + }, + ] + : [ + { + href: '/#workflow', + icon: GitBranch, + label: 'Workflow', + }, + { + href: '/#features', + icon: Sparkles, + label: 'Features', + }, + { + href: '/#security', + icon: ShieldCheck, + label: 'Security', + }, + ]; return (
) => {
{isAuthenticated ? ( ) : (
- setAgentModel(event.target.value)} - /> + onValueChange={(value) => setAgentModel(value as AgentModel)} + > + + + + + {modelOptions.map((option) => ( + + {option.label} + + ))} + +
@@ -160,27 +183,37 @@ export const SpoonAgentSettingsForm = ({ setInstallCommand(event.target.value)} /> +

+ Leave blank to inspect the repository and choose bun, pnpm, yarn, + or npm. +

setCheckCommand(event.target.value)} /> +

+ Leave blank to read package.json scripts after cloning. +

setTestCommand(event.target.value)} /> +

+ Leave blank to run the detected test script when one exists. +

+ + + + Add git remote + + Store another repository URL to copy from this Spoon. GitHub + remains the source of truth for upstream maintenance. + + +
+
+
+ + setLabel(event.target.value)} + /> +
+
+ + setRemoteName(event.target.value)} + /> +
+
+
+ + setUrl(event.target.value)} + /> +
+ + + +
+
+ {cloneUrl ? ( @@ -165,46 +230,6 @@ export const SpoonClonePanel = ({ spoon }: { spoon: Doc<'spoons'> }) => { ))}
) : null} -
-
-
- - setLabel(event.target.value)} - /> -
-
- - setRemoteName(event.target.value)} - /> -
-
-
- - setUrl(event.target.value)} - /> -
- -
); diff --git a/apps/next/src/components/spoons/spoon-detail-header.tsx b/apps/next/src/components/spoons/spoon-detail-header.tsx index 406ee3b..e169269 100644 --- a/apps/next/src/components/spoons/spoon-detail-header.tsx +++ b/apps/next/src/components/spoons/spoon-detail-header.tsx @@ -95,6 +95,11 @@ export const SpoonDetailHeader = ({ Fork metadata missing )}
+ {spoon.description ? ( +

+ {spoon.description} +

+ ) : null}