diff --git a/.gitignore b/.gitignore index 5ffc648..026d65f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ .orch/ bin/ dist/ +node_modules/ +apps/web/dist/ +apps/web/.vite/ *.db coverage.out .DS_Store diff --git a/api/events.md b/api/events.md new file mode 100644 index 0000000..7e7a516 --- /dev/null +++ b/api/events.md @@ -0,0 +1,77 @@ +# Event Stream Contract + +## Status + +Planned for the next milestone. +The Phase 1 web skeleton does not implement the event stream yet. + +## Intended Endpoint + +- `GET /api/events/stream` + +## Transport Choice + +- start with Server-Sent Events rather than websockets +- keep the stream read-only and cursor-based +- let the frontend use events to invalidate or refresh TanStack Query caches + +## Query Parameters + +- `after_event_id`: optional monotonic cursor +- `run_id`: optional run filter for run-scoped pages +- `thread_id`: optional thread filter for timeline views + +## Event Envelope + +Each SSE message should carry one JSON object with this shape: + +```json +{ + "event_id": 42, + "event_type": "task_blocked", + "run_id": "run_web_001", + "task_id": "T1", + "thread_id": "thr_123", + "summary": "Need the API shape", + "payload": { + "source": "orch" + }, + "created_at": "2026-03-20T09:30:00Z" +} +``` + +## Expected First Event Types + +- `task_added` +- `task_ready` +- `task_dispatched` +- `task_running` +- `task_blocked` +- `task_answered` +- `task_done` +- `task_failed` +- `thread_claim` +- `thread_update` +- `thread_reply` +- `thread_result` +- `council_tallied` +- `council_report_persisted` + +## Cursor Rules + +- `event_id` is the only resume cursor +- the server should emit events in ascending `event_id` order +- reconnect clients should pass the last processed `event_id` +- the server should not depend on in-memory subscriptions for correctness + +## Frontend Usage + +- run detail pages should subscribe with `run_id` +- thread views may subscribe with `thread_id` +- the client should prefer refetch/invalidate behavior over complex local reducers in the first realtime slice + +## Backend Notes + +- source data should come from the existing shared `events` table +- `orchd` should treat the stream as a projection over persisted events, not as a separate ephemeral bus +- if filtering becomes expensive later, move filtering logic into a dedicated query layer without changing the wire contract diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..1312849 --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,363 @@ +openapi: 3.1.0 +info: + title: Orch Web API + version: 0.1.0 + summary: Initial read-only web API for the orchestration control plane. +servers: + - url: http://localhost:8080 +paths: + /health: + get: + summary: Health check + operationId: getHealth + responses: + '200': + description: Service is healthy + content: + application/json: + schema: + type: object + required: [status] + properties: + status: + type: string + example: ok + /api/runs: + get: + summary: List orchestration runs + operationId: listRuns + responses: + '200': + description: Run summaries + content: + application/json: + schema: + type: object + required: [runs] + properties: + runs: + type: array + items: + $ref: '#/components/schemas/RunListItem' + /api/runs/{runID}: + get: + summary: Get run detail + operationId: getRunDetail + parameters: + - $ref: '#/components/parameters/RunID' + responses: + '200': + description: Run detail including tasks and blocked-task summaries + content: + application/json: + schema: + type: object + required: [run] + properties: + run: + $ref: '#/components/schemas/RunDetail' + '404': + $ref: '#/components/responses/NotFound' + /api/runs/{runID}/tasks: + get: + summary: List tasks for a run + operationId: listRunTasks + parameters: + - $ref: '#/components/parameters/RunID' + responses: + '200': + description: Task list + content: + application/json: + schema: + type: object + required: [tasks] + properties: + tasks: + type: array + items: + $ref: '#/components/schemas/Task' + '404': + $ref: '#/components/responses/NotFound' + /api/runs/{runID}/blocked: + get: + summary: List blocked tasks for a run + operationId: listBlockedTasks + parameters: + - $ref: '#/components/parameters/RunID' + responses: + '200': + description: Blocked task list + content: + application/json: + schema: + type: object + required: [blocked] + properties: + blocked: + type: array + items: + $ref: '#/components/schemas/BlockedTask' + '404': + $ref: '#/components/responses/NotFound' + /api/threads/{threadID}: + get: + summary: Get thread timeline + operationId: getThreadDetail + parameters: + - $ref: '#/components/parameters/ThreadID' + responses: + '200': + description: Thread detail with messages and artifacts + content: + application/json: + schema: + type: object + required: [thread] + properties: + thread: + $ref: '#/components/schemas/ThreadDetail' + '404': + $ref: '#/components/responses/NotFound' +components: + parameters: + RunID: + name: runID + in: path + required: true + schema: + type: string + ThreadID: + name: threadID + in: path + required: true + schema: + type: string + responses: + NotFound: + description: Requested resource was not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + schemas: + ErrorEnvelope: + type: object + required: [error] + properties: + error: + type: object + required: [code, message] + properties: + code: + type: string + example: not_found + message: + type: string + Run: + type: object + required: [run_id, goal, summary, status, created_at, updated_at] + properties: + run_id: + type: string + goal: + type: string + summary: + type: string + status: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + RunListItem: + type: object + required: [run, task_counts, total_tasks] + properties: + run: + $ref: '#/components/schemas/Run' + task_counts: + type: object + additionalProperties: + type: integer + total_tasks: + type: integer + RunDetail: + type: object + required: [run, task_counts, total_tasks, tasks, blocked_tasks] + properties: + run: + $ref: '#/components/schemas/Run' + task_counts: + type: object + additionalProperties: + type: integer + total_tasks: + type: integer + tasks: + type: array + items: + $ref: '#/components/schemas/Task' + blocked_tasks: + type: array + items: + $ref: '#/components/schemas/BlockedTask' + Task: + type: object + required: + [run_id, task_id, title, summary, status, priority, acceptance_json, created_at, updated_at] + properties: + run_id: + type: string + task_id: + type: string + title: + type: string + summary: + type: string + status: + type: string + default_to: + type: string + priority: + type: string + acceptance_json: + description: Raw JSON acceptance criteria payload + latest_attempt_no: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + TaskAttempt: + type: object + required: [run_id, task_id, attempt_no, assigned_to, thread_id, status, created_at, updated_at] + properties: + run_id: + type: string + task_id: + type: string + attempt_no: + type: integer + assigned_to: + type: string + thread_id: + type: string + base_ref: + type: string + base_commit: + type: string + branch_name: + type: string + worktree_path: + type: string + workspace_status: + type: string + result_commit: + type: string + status: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + Artifact: + type: object + required: [artifact_id, message_id, path, kind, metadata_json, created_at] + properties: + artifact_id: + type: string + message_id: + type: string + path: + type: string + kind: + type: string + metadata_json: + description: Raw JSON artifact metadata + created_at: + type: string + format: date-time + Message: + type: object + required: + [message_id, thread_id, from_agent, to_agent, kind, summary, body, payload_json, created_at] + properties: + message_id: + type: string + thread_id: + type: string + from_agent: + type: string + to_agent: + type: string + kind: + type: string + summary: + type: string + body: + type: string + payload_json: + description: Raw JSON message payload + created_at: + type: string + format: date-time + artifacts: + type: array + items: + $ref: '#/components/schemas/Artifact' + Thread: + type: object + required: + [thread_id, run_id, task_id, subject, created_by, assigned_to, status, priority, created_at, updated_at] + properties: + thread_id: + type: string + run_id: + type: string + task_id: + type: string + subject: + type: string + created_by: + type: string + assigned_to: + type: string + status: + type: string + priority: + type: string + latest_message_id: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + ThreadDetail: + type: object + required: [thread, messages] + properties: + thread: + $ref: '#/components/schemas/Thread' + messages: + type: array + items: + $ref: '#/components/schemas/Message' + BlockedTask: + type: object + required: [task, attempt, question] + properties: + task: + $ref: '#/components/schemas/Task' + attempt: + $ref: '#/components/schemas/TaskAttempt' + question: + $ref: '#/components/schemas/Message' diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..359d8a1 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,12 @@ + + + + + + Orch Control Plane + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..103acf7 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,25 @@ +{ + "name": "@ai-workflow-skill/web", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@tanstack/react-query": "^5.91.2", + "@tanstack/react-router": "^1.167.5", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "^5.9.3", + "vite": "^8.0.1" + } +} diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx new file mode 100644 index 0000000..471d129 --- /dev/null +++ b/apps/web/src/app.tsx @@ -0,0 +1,104 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + Link, + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from '@tanstack/react-router'; + +const queryClient = new QueryClient(); + +const rootRoute = createRootRoute({ + component: RootLayout, +}); + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: HomePage, +}); + +const routeTree = rootRoute.addChildren([indexRoute]); + +const router = createRouter({ + routeTree, +}); + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router; + } +} + +export function App() { + return ( + + + + ); +} + +function RootLayout() { + return ( +
+
+
+

AI Workflow Skill

+ + Orch Control Plane + +
+

+ Phase 1 keeps the UI thin while the backend contract settles around + `orchd`. +

+
+
+ +
+
+ ); +} + +function HomePage() { + return ( +
+
+

Current slice

+

Read-only operator shell for a future multi-user web product.

+

+ The monorepo now has a dedicated React app, a Go HTTP service, and a + first API contract for runs, blocked work, and thread history. +

+
+ +
+

Backend spine

+ +
+ +
+

Frontend posture

+

+ React, Vite, TanStack Router, and TanStack Query are present now so + Phase 2 can focus on actual operator views instead of build plumbing. +

+
+ +
+

Next UI targets

+ +
+
+ ); +} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..6258229 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,17 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { App } from './app'; +import './styles.css'; + +const rootElement = document.getElementById('root'); + +if (!rootElement) { + throw new Error('Missing root element'); +} + +createRoot(rootElement).render( + + + , +); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css new file mode 100644 index 0000000..8c13677 --- /dev/null +++ b/apps/web/src/styles.css @@ -0,0 +1,154 @@ +:root { + color-scheme: light; + font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif; + line-height: 1.5; + font-weight: 400; + background: + radial-gradient(circle at top left, rgba(255, 196, 98, 0.32), transparent 28%), + radial-gradient(circle at bottom right, rgba(8, 145, 178, 0.16), transparent 26%), + linear-gradient(135deg, #f4efe3 0%, #f9f7f1 55%, #eef3f4 100%); + color: #172026; + --border: rgba(23, 32, 38, 0.12); + --panel: rgba(255, 255, 255, 0.82); + --panel-strong: rgba(255, 255, 255, 0.92); + --ink-soft: rgba(23, 32, 38, 0.7); + --accent: #c96f20; + --accent-deep: #0f766e; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +a { + color: inherit; + text-decoration: none; +} + +#root { + min-height: 100vh; +} + +.shell { + min-height: 100vh; + padding: 32px 20px 48px; +} + +.masthead { + display: grid; + gap: 18px; + align-items: end; + max-width: 1200px; + margin: 0 auto 28px; +} + +.brand { + display: inline-block; + font-family: "Iowan Old Style", "Palatino Linotype", serif; + font-size: clamp(2rem, 4vw, 3.2rem); + letter-spacing: -0.04em; +} + +.eyebrow { + margin: 0 0 10px; + text-transform: uppercase; + letter-spacing: 0.16em; + font-size: 0.78rem; + color: var(--accent-deep); +} + +.masthead-copy { + max-width: 34rem; + margin: 0; + color: var(--ink-soft); +} + +.content { + max-width: 1200px; + margin: 0 auto; +} + +.hero-grid { + display: grid; + gap: 18px; + grid-template-columns: repeat(12, minmax(0, 1fr)); +} + +.hero-card { + grid-column: span 12; + padding: 24px; + border: 1px solid var(--border); + border-radius: 24px; + background: var(--panel); + backdrop-filter: blur(12px); + box-shadow: 0 20px 50px rgba(23, 32, 38, 0.08); +} + +.hero-card-primary { + background: + linear-gradient(145deg, rgba(255, 255, 255, 0.92), rgba(255, 248, 237, 0.94)); +} + +.hero-card-secondary { + background: + linear-gradient(145deg, rgba(240, 249, 255, 0.95), rgba(240, 253, 250, 0.9)); +} + +.hero-card h1, +.hero-card h2 { + margin: 0 0 12px; + font-family: "Iowan Old Style", "Palatino Linotype", serif; + letter-spacing: -0.03em; +} + +.hero-card h1 { + font-size: clamp(2rem, 4.4vw, 4.4rem); + line-height: 0.94; + max-width: 12ch; +} + +.hero-card h2 { + font-size: 1.5rem; +} + +.lede, +.hero-card p { + margin: 0; + max-width: 48rem; + color: var(--ink-soft); +} + +.detail-list { + margin: 0; + padding-left: 18px; + color: var(--ink-soft); +} + +.detail-list li + li { + margin-top: 10px; +} + +@media (min-width: 840px) { + .masthead { + grid-template-columns: 1.2fr 0.8fr; + } + + .hero-card-primary { + grid-column: span 7; + min-height: 420px; + } + + .hero-card-secondary { + grid-column: span 5; + } + + .hero-card:not(.hero-card-primary):not(.hero-card-secondary) { + grid-column: span 6; + } +} diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..a3d0976 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src", "vite.config.ts"] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..9cfb0ce --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,14 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5173, + proxy: { + '/api': 'http://localhost:8080', + '/health': 'http://localhost:8080', + }, + }, +}); diff --git a/cmd/orchd/main.go b/cmd/orchd/main.go new file mode 100644 index 0000000..71a4948 --- /dev/null +++ b/cmd/orchd/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "context" + "errors" + "flag" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "ai-workflow-skill/internal/app" + "ai-workflow-skill/internal/db" + "ai-workflow-skill/internal/httpapi" +) + +func main() { + var ( + dbPath string + listen string + shutdown time.Duration + ) + + flag.StringVar(&dbPath, "db", ".agents/coord.db", "SQLite database path") + flag.StringVar(&listen, "listen", ":8080", "HTTP listen address") + flag.DurationVar(&shutdown, "shutdown-timeout", 5*time.Second, "Graceful shutdown timeout") + flag.Parse() + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + sqlDB, err := db.Open(ctx, dbPath) + if err != nil { + log.Fatalf("open database: %v", err) + } + defer sqlDB.Close() + + if err := db.ApplyMigrations(ctx, sqlDB); err != nil { + log.Fatalf("apply migrations: %v", err) + } + + webApp := app.NewWebService(sqlDB) + server := &http.Server{ + Addr: listen, + Handler: httpapi.NewRouter(webApp), + ReadHeaderTimeout: 5 * time.Second, + } + + go func() { + <-ctx.Done() + + shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdown) + defer cancel() + + if err := server.Shutdown(shutdownCtx); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Printf("http shutdown: %v", err) + } + }() + + log.Printf("orchd listening on %s", listen) + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("serve http api: %v", err) + } +} diff --git a/docs/implementation-roadmap.md b/docs/implementation-roadmap.md index 92da1c8..4284228 100644 --- a/docs/implementation-roadmap.md +++ b/docs/implementation-roadmap.md @@ -31,6 +31,9 @@ As of now: - a council-review skill forward-test plan directory now exists under `docs/tests/council-review-skill/`, with a shared execution contract and nine council workflow scenarios covering end-to-end flow, unanimous-only defaults, timeout/before-tally errors, explicit minority reporting, invalid report filters, strict tally semantics, malformed reviewer JSON, and target-file inputs - an execution-roadmap workflow now exists under `docs/roadmaps/active/` and `docs/roadmaps/archive/` for agent-level work traces and completion archives - a forward-looking web product monorepo plan now exists under `docs/web-product-monorepo.md`, defining the recommended React frontend, `chi` HTTP service, `cmd/orchd` entrypoint, and shared application/query layering for future web work +- the Phase 1 web-product skeleton is now in place, including root `pnpm` workspace files, a standalone React app under `apps/web`, an initial OpenAPI/events contract under `api/`, and a new `cmd/orchd` HTTP service backed by `internal/app`, `internal/query`, and `internal/httpapi` +- `orchd` now serves a minimal read-only web API with `chi`, including `/health`, runs list/detail, run task list, blocked-task list, and thread detail endpoints backed by the existing SQLite state +- HTTP tests now cover the initial read-only `orchd` slice, and the new frontend workspace builds successfully with `pnpm run web:build` - a repo-local `scripts/package_skill_clis.sh` packaging flow now builds bundled skill CLI assets for `inbox`, `orch`, and `council-review` - `orch` now implements `run init/show`, `task add`, `dep add`, `ready`, `dispatch`, `reconcile`, `wait`, `blocked`, `answer`, `retry`, `reassign`, `cancel`, `cleanup`, and `status` - `orch` can create runs, gate tasks through dependencies, dispatch work through `inbox`, reconcile worker thread state back into task state, answer blocked tasks, retry or reassign work, cancel tasks or runs, clean attempt worktrees, and create per-attempt Git worktrees during strict dispatch @@ -88,8 +91,9 @@ Current implementation status: - `Milestone 5: Strict Worktree Support` is complete - `Milestone 6: Waiting Primitives` is complete - `Milestone 7: Council Review` is complete +- `Milestone 8: Web Product Phase 1 Skeleton` is complete -The council review v1 surface is now complete, including final report rendering and metadata persistence. +The council review v1 surface is complete, and the first web-product skeleton now exists as a separate monorepo workspace plus read-only HTTP backend slice. ### Milestone 1: Go Skeleton @@ -367,16 +371,59 @@ Remaining: - none for the v1 council workflow +### Milestone 8: Web Product Phase 1 Skeleton + +Goal: + +- create the first durable web-product backbone without replacing the existing CLI workflows + +Add: + +- root `pnpm` workspace files +- `apps/web` +- `api/openapi.yaml` +- `api/events.md` +- `cmd/orchd` +- `internal/app` +- `internal/query` +- `internal/httpapi` + +Definition of done: + +- the repository contains the agreed monorepo skeleton +- `orchd` can serve a small read-only HTTP API against the existing database +- the frontend workspace builds and can evolve independently in later milestones + +Status: + +- completed + +Completed so far: + +- root `package.json`, `pnpm-workspace.yaml`, and `pnpm-lock.yaml` now define the monorepo JS workspace +- `apps/web` now contains a Vite + React + TypeScript + TanStack Router + TanStack Query frontend shell +- `cmd/orchd` now opens the shared SQLite database, applies migrations, and serves a `chi` router with graceful shutdown handling +- `internal/query` now exposes run list/detail, run tasks, blocked-task, and thread-detail read models for the web surface +- `internal/app` now provides a thin web-service boundary over the new read service +- `internal/httpapi` now owns HTTP routing, JSON/error helpers, and the initial read-only endpoints +- `api/openapi.yaml` now documents the implemented read-only endpoints and response shapes +- `api/events.md` now captures the planned SSE contract for the next realtime slice +- `go test ./...` covers the new HTTP slice, and `pnpm run web:build` succeeds for the frontend workspace + +Remaining: + +- Phase 2 should turn the frontend shell into actual run, task-board, blocked-queue, and thread-detail pages using the new HTTP contract + ## Immediate Next Task If a new agent is taking over now, the next concrete step should be: -1. treat `Milestone 7: Council Review` as complete unless a new user request introduces a new council capability -2. keep the authored inbox test-plan set in `docs/tests/inbox/` synchronized if future `orch` work changes shared CLI behavior -3. if the next milestone is the web product, use `docs/web-product-monorepo.md` as the starting implementation shape for the monorepo, `cmd/orchd`, and `apps/web` -4. choose the next milestone explicitly instead of reopening the completed council v1 slice +1. treat `Milestone 8: Web Product Phase 1 Skeleton` as complete unless a new user request reopens the backend skeleton itself +2. keep the authored inbox test-plan set in `docs/tests/inbox/` synchronized if future `orch` or web work changes shared CLI-visible behavior +3. start `Phase 2: Read-Only Web UI` on top of the existing `apps/web` and `orchd` contract, beginning with runs list, run detail, blocked queue, and thread timeline views +4. keep `api/openapi.yaml`, `api/events.md`, and `docs/web-product-monorepo.md` synchronized as the web surface expands -The inbox implementation and its human-readable test-plan set are already in place, and `orch` now supports the main scheduler loop plus the complete council start/wait/tally/report workflow, so any next step should be a new milestone rather than unfinished council v1 work. +The inbox implementation and its human-readable test-plan set are already in place, `orch` supports the main scheduler loop plus the complete council start/wait/tally/report workflow, and the web-product Phase 1 skeleton now exists, so the next step should be Phase 2 product surface work rather than reopening earlier milestones. ## Recommended Driver Choices diff --git a/docs/roadmaps/archive/web-product-phase1-skeleton.md b/docs/roadmaps/archive/web-product-phase1-skeleton.md new file mode 100644 index 0000000..ae91033 --- /dev/null +++ b/docs/roadmaps/archive/web-product-phase1-skeleton.md @@ -0,0 +1,74 @@ +# Web Product Phase 1 Skeleton + +## Status + +- `completed` + +## Owner + +- Codex + +## Started At + +- `2026-03-20` + +## Goal + +- Implement the first web-product milestone slice described in `docs/web-product-monorepo.md` by creating the monorepo skeleton, a minimal `orchd` HTTP service, and the first read-oriented backend boundaries. + +## Scope + +- add an active execution trace for this web implementation workstream +- add the initial monorepo workspace files and `apps/web` scaffold +- add `cmd/orchd`, `internal/httpapi`, `internal/app`, and `internal/query` +- expose a small read-only HTTP API for health, runs, run detail, blocked tasks, and thread detail +- add initial API contract docs under `api/` +- keep `docs/implementation-roadmap.md` synchronized with the new implementation state + +## Checklist + +- [x] create the active execution roadmap for the Phase 1 web skeleton workstream +- [x] scaffold the monorepo workspace files and `apps/web` +- [x] add `cmd/orchd` with a minimal `chi`-based HTTP server +- [x] introduce initial shared app/query boundaries for read-only web endpoints +- [x] add initial API contract documents under `api/` +- [x] validate the new slice with builds or targeted tests +- [x] update `docs/implementation-roadmap.md` +- [x] archive this execution roadmap with a completion summary if the slice is fully complete + +## Files + +- `docs/roadmaps/active/web-product-phase1-skeleton.md` +- `docs/implementation-roadmap.md` +- `docs/web-product-monorepo.md` +- `cmd/orchd/main.go` +- `internal/httpapi/` +- `internal/app/` +- `internal/query/` +- `internal/store/` +- `api/openapi.yaml` +- `api/events.md` +- `apps/web/` +- `package.json` +- `pnpm-workspace.yaml` + +## Decisions + +- implement a narrow read-only backend slice first instead of starting with frontend pages +- keep the first HTTP layer thin and let query/app packages own the web-facing backend boundary +- preserve the existing CLI entrypoints while introducing the new service + +## Blockers + +- none + +## Next Step + +- start Phase 2 on top of the new `apps/web` and `orchd` skeleton by wiring real runs, run-detail, blocked-queue, and thread-detail screens to the read-only API + +## Completion Summary + +- added the first web-product skeleton described in `docs/web-product-monorepo.md`, including root `pnpm` workspace files, a standalone React frontend shell under `apps/web`, initial API contract documents under `api/`, and a new `cmd/orchd` HTTP service +- introduced `internal/app`, `internal/query`, and `internal/httpapi` so the web backend has an explicit read-oriented boundary instead of letting handlers query storage ad hoc +- implemented and validated the first read-only HTTP endpoints for health, runs, run detail, run tasks, blocked tasks, and thread detail +- verified the slice with `go test ./...` and `pnpm run web:build`, then synchronized `docs/implementation-roadmap.md` diff --git a/go.mod b/go.mod index 698f24a..f66f5e4 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module ai-workflow-skill go 1.26 require ( + github.com/go-chi/chi/v5 v5.2.5 github.com/spf13/cobra v1.10.1 modernc.org/sqlite v1.40.1 ) diff --git a/go.sum b/go.sum index 91c8044..056a4f3 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/internal/app/web.go b/internal/app/web.go new file mode 100644 index 0000000..f8d66fa --- /dev/null +++ b/internal/app/web.go @@ -0,0 +1,39 @@ +package app + +import ( + "context" + "database/sql" + + "ai-workflow-skill/internal/query" + "ai-workflow-skill/internal/store" +) + +type WebService struct { + reads *query.ReadService +} + +func NewWebService(db *sql.DB) *WebService { + return &WebService{ + reads: query.NewReadService(db), + } +} + +func (s *WebService) ListRuns(ctx context.Context) ([]query.RunListItem, error) { + return s.reads.ListRuns(ctx) +} + +func (s *WebService) GetRunDetail(ctx context.Context, runID string) (query.RunDetail, error) { + return s.reads.GetRunDetail(ctx, runID) +} + +func (s *WebService) ListRunTasks(ctx context.Context, runID string) ([]store.Task, error) { + return s.reads.ListRunTasks(ctx, runID) +} + +func (s *WebService) ListBlockedTasks(ctx context.Context, runID string) ([]store.BlockedTask, error) { + return s.reads.ListBlockedTasks(ctx, runID) +} + +func (s *WebService) GetThreadDetail(ctx context.Context, threadID string) (store.ThreadDetail, error) { + return s.reads.GetThreadDetail(ctx, threadID) +} diff --git a/internal/httpapi/response.go b/internal/httpapi/response.go new file mode 100644 index 0000000..123691e --- /dev/null +++ b/internal/httpapi/response.go @@ -0,0 +1,58 @@ +package httpapi + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "ai-workflow-skill/internal/store" +) + +type errorEnvelope struct { + Error errorPayload `json:"error"` +} + +type errorPayload struct { + Code string `json:"code"` + Message string `json:"message"` +} + +func writeJSON(w http.ResponseWriter, status int, payload any) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(status) + + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + _ = enc.Encode(payload) +} + +func writeError(w http.ResponseWriter, err error) { + status, code := classifyError(err) + writeJSON(w, status, errorEnvelope{ + Error: errorPayload{ + Code: code, + Message: errorMessage(err), + }, + }) +} + +func classifyError(err error) (int, string) { + switch { + case errors.Is(err, store.ErrInvalidInput): + return http.StatusBadRequest, "invalid_input" + case errors.Is(err, store.ErrRunNotFound), errors.Is(err, store.ErrTaskNotFound), errors.Is(err, store.ErrThreadNotFound): + return http.StatusNotFound, "not_found" + case errors.Is(err, store.ErrInvalidState): + return http.StatusConflict, "invalid_state" + default: + return http.StatusInternalServerError, "internal_error" + } +} + +func errorMessage(err error) string { + if err == nil { + return "unknown error" + } + return fmt.Sprintf("%v", err) +} diff --git a/internal/httpapi/router.go b/internal/httpapi/router.go new file mode 100644 index 0000000..f581cac --- /dev/null +++ b/internal/httpapi/router.go @@ -0,0 +1,87 @@ +package httpapi + +import ( + "context" + "net/http" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + + "ai-workflow-skill/internal/query" + "ai-workflow-skill/internal/store" +) + +type readService interface { + ListRuns(ctx context.Context) ([]query.RunListItem, error) + GetRunDetail(ctx context.Context, runID string) (query.RunDetail, error) + ListRunTasks(ctx context.Context, runID string) ([]store.Task, error) + ListBlockedTasks(ctx context.Context, runID string) ([]store.BlockedTask, error) + GetThreadDetail(ctx context.Context, threadID string) (store.ThreadDetail, error) +} + +func NewRouter(service readService) http.Handler { + router := chi.NewRouter() + router.Use(middleware.RequestID) + router.Use(middleware.Recoverer) + router.Use(middleware.Timeout(30 * time.Second)) + + router.Get("/health", func(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]any{ + "status": "ok", + }) + }) + + router.Route("/api", func(r chi.Router) { + r.Get("/runs", func(w http.ResponseWriter, r *http.Request) { + runs, err := service.ListRuns(r.Context()) + if err != nil { + writeError(w, err) + return + } + writeJSON(w, http.StatusOK, map[string]any{"runs": runs}) + }) + + r.Get("/runs/{runID}", func(w http.ResponseWriter, r *http.Request) { + runID := chi.URLParam(r, "runID") + run, err := service.GetRunDetail(r.Context(), runID) + if err != nil { + writeError(w, err) + return + } + writeJSON(w, http.StatusOK, map[string]any{"run": run}) + }) + + r.Get("/runs/{runID}/tasks", func(w http.ResponseWriter, r *http.Request) { + runID := chi.URLParam(r, "runID") + tasks, err := service.ListRunTasks(r.Context(), runID) + if err != nil { + writeError(w, err) + return + } + writeJSON(w, http.StatusOK, map[string]any{"tasks": tasks}) + }) + + r.Get("/runs/{runID}/blocked", func(w http.ResponseWriter, r *http.Request) { + runID := chi.URLParam(r, "runID") + blocked, err := service.ListBlockedTasks(r.Context(), runID) + if err != nil { + writeError(w, err) + return + } + writeJSON(w, http.StatusOK, map[string]any{"blocked": blocked}) + }) + + r.Get("/threads/{threadID}", func(w http.ResponseWriter, r *http.Request) { + threadID := chi.URLParam(r, "threadID") + thread, err := service.GetThreadDetail(r.Context(), threadID) + if err != nil { + writeError(w, err) + return + } + writeJSON(w, http.StatusOK, map[string]any{"thread": thread}) + }) + }) + + return router +} diff --git a/internal/httpapi/router_test.go b/internal/httpapi/router_test.go new file mode 100644 index 0000000..f01c66f --- /dev/null +++ b/internal/httpapi/router_test.go @@ -0,0 +1,196 @@ +package httpapi + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + "time" + + "ai-workflow-skill/internal/app" + dbpkg "ai-workflow-skill/internal/db" + "ai-workflow-skill/internal/store" +) + +func TestRouterExposesReadOnlyWebEndpoints(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + dbPath := filepath.Join(t.TempDir(), "coord.db") + sqlDB, err := dbpkg.Open(ctx, dbPath) + if err != nil { + t.Fatalf("open db: %v", err) + } + defer sqlDB.Close() + + if err := dbpkg.ApplyMigrations(ctx, sqlDB); err != nil { + t.Fatalf("apply migrations: %v", err) + } + + orchStore := store.NewOrchStore(sqlDB) + inboxStore := store.NewInboxStore(sqlDB) + + _, err = orchStore.CreateRun(ctx, store.CreateRunInput{ + RunID: "run_web_001", + Goal: "Build the web control plane", + Summary: "Initial HTTP slice", + }) + if err != nil { + t.Fatalf("create run: %v", err) + } + + _, err = orchStore.AddTask(ctx, store.AddTaskInput{ + RunID: "run_web_001", + TaskID: "T1", + Title: "Implement read API", + Summary: "Expose run state over HTTP", + DefaultTo: "worker-a", + }) + if err != nil { + t.Fatalf("add task T1: %v", err) + } + + _, err = orchStore.AddTask(ctx, store.AddTaskInput{ + RunID: "run_web_001", + TaskID: "T2", + Title: "Build React shell", + Summary: "Scaffold the frontend workspace", + DefaultTo: "worker-b", + }) + if err != nil { + t.Fatalf("add task T2: %v", err) + } + + dispatch, err := orchStore.DispatchTask(ctx, store.DispatchInput{ + RunID: "run_web_001", + TaskID: "T1", + ToAgent: "worker-a", + Body: "Expose the initial HTTP API.", + }) + if err != nil { + t.Fatalf("dispatch task: %v", err) + } + + if _, err := inboxStore.ClaimThread(ctx, store.ClaimInput{ + ThreadID: dispatch.Attempt.ThreadID, + Agent: "worker-a", + LeaseSeconds: 300, + }); err != nil { + t.Fatalf("claim thread: %v", err) + } + + if _, _, err := inboxStore.UpdateThreadStatus(ctx, store.UpdateInput{ + ThreadID: dispatch.Attempt.ThreadID, + Agent: "worker-a", + Status: "blocked", + Summary: "Need the API shape", + Body: "Confirm whether run detail should include blocked tasks.", + }); err != nil { + t.Fatalf("mark thread blocked: %v", err) + } + + if _, err := orchStore.ReconcileRun(ctx, "run_web_001"); err != nil { + t.Fatalf("reconcile run: %v", err) + } + + handler := NewRouter(app.NewWebService(sqlDB)) + + assertStatusAndJSONField(t, handler, "/health", http.StatusOK, []string{"status"}, "ok") + assertStatusAndJSONField(t, handler, "/api/runs", http.StatusOK, []string{"runs", "0", "run", "run_id"}, "run_web_001") + assertStatusAndJSONField(t, handler, "/api/runs/run_web_001", http.StatusOK, []string{"run", "run", "run_id"}, "run_web_001") + assertStatusAndJSONField(t, handler, "/api/runs/run_web_001/tasks", http.StatusOK, []string{"tasks", "0", "task_id"}, "T1") + assertStatusAndJSONField(t, handler, "/api/runs/run_web_001/blocked", http.StatusOK, []string{"blocked", "0", "task", "task_id"}, "T1") + assertStatusAndJSONField(t, handler, "/api/threads/"+dispatch.Attempt.ThreadID, http.StatusOK, []string{"thread", "thread", "thread_id"}, dispatch.Attempt.ThreadID) +} + +func TestRouterMapsNotFoundErrors(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + dbPath := filepath.Join(t.TempDir(), "coord.db") + sqlDB, err := dbpkg.Open(ctx, dbPath) + if err != nil { + t.Fatalf("open db: %v", err) + } + defer sqlDB.Close() + + if err := dbpkg.ApplyMigrations(ctx, sqlDB); err != nil { + t.Fatalf("apply migrations: %v", err) + } + + handler := NewRouter(app.NewWebService(sqlDB)) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/runs/missing-run", nil) + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", rec.Code) + } + + var payload map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { + t.Fatalf("decode response: %v", err) + } + + code := nestedString(t, payload, "error", "code") + if code != "not_found" { + t.Fatalf("expected not_found error code, got %q", code) + } +} + +func assertStatusAndJSONField(t *testing.T, handler http.Handler, path string, wantStatus int, fieldPath []string, want string) { + t.Helper() + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, path, nil) + handler.ServeHTTP(rec, req) + + if rec.Code != wantStatus { + t.Fatalf("GET %s: expected status %d, got %d", path, wantStatus, rec.Code) + } + + var payload map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { + t.Fatalf("GET %s: decode response: %v", path, err) + } + + got := nestedString(t, payload, fieldPath...) + if got != want { + t.Fatalf("GET %s: expected %q at %v, got %q", path, want, fieldPath, got) + } +} + +func nestedString(t *testing.T, value any, path ...string) string { + t.Helper() + + current := value + for _, part := range path { + switch typed := current.(type) { + case map[string]any: + current = typed[part] + case []any: + if len(part) != 1 || part[0] < '0' || part[0] > '9' { + t.Fatalf("path segment %q is not a numeric index", part) + } + index := int(part[0] - '0') + if index >= len(typed) { + t.Fatalf("index %d out of range for path %v", index, path) + } + current = typed[index] + default: + t.Fatalf("unsupported type %T at path %v", current, path) + } + } + + got, ok := current.(string) + if !ok { + t.Fatalf("expected string at path %v, got %T", path, current) + } + return got +} diff --git a/internal/query/read_service.go b/internal/query/read_service.go new file mode 100644 index 0000000..4abb5eb --- /dev/null +++ b/internal/query/read_service.go @@ -0,0 +1,211 @@ +package query + +import ( + "context" + "database/sql" + "fmt" + "time" + + "ai-workflow-skill/internal/store" +) + +type ReadService struct { + db *sql.DB + orch *store.OrchStore + inbox *store.InboxStore +} + +type RunListItem struct { + Run store.Run `json:"run"` + TaskCounts map[string]int `json:"task_counts"` + TotalTasks int `json:"total_tasks"` +} + +type RunDetail struct { + Run store.Run `json:"run"` + TaskCounts map[string]int `json:"task_counts"` + TotalTasks int `json:"total_tasks"` + Tasks []store.Task `json:"tasks"` + BlockedTasks []store.BlockedTask `json:"blocked_tasks"` +} + +func NewReadService(db *sql.DB) *ReadService { + return &ReadService{ + db: db, + orch: store.NewOrchStore(db), + inbox: store.NewInboxStore(db), + } +} + +func (s *ReadService) ListRuns(ctx context.Context) ([]RunListItem, error) { + rows, err := s.db.QueryContext( + ctx, + `SELECT run_id, goal, summary, status, created_at, updated_at + FROM runs + ORDER BY updated_at DESC, created_at DESC`, + ) + if err != nil { + return nil, fmt.Errorf("query runs: %w", err) + } + defer rows.Close() + + var runs []store.Run + runIDs := make([]string, 0) + for rows.Next() { + var ( + run store.Run + createdAt, updated string + ) + if err := rows.Scan( + &run.RunID, + &run.Goal, + &run.Summary, + &run.Status, + &createdAt, + &updated, + ); err != nil { + return nil, fmt.Errorf("scan run list row: %w", err) + } + + run.CreatedAt = parseRFC3339(createdAt) + run.UpdatedAt = parseRFC3339(updated) + runs = append(runs, run) + runIDs = append(runIDs, run.RunID) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate runs: %w", err) + } + + countsByRunID, err := s.collectTaskCounts(ctx, runIDs) + if err != nil { + return nil, err + } + + items := make([]RunListItem, 0, len(runs)) + for _, run := range runs { + taskCounts := countsByRunID[run.RunID] + if taskCounts == nil { + taskCounts = map[string]int{} + } + + items = append(items, RunListItem{ + Run: run, + TaskCounts: taskCounts, + TotalTasks: totalTasks(taskCounts), + }) + } + + return items, nil +} + +func (s *ReadService) GetRunDetail(ctx context.Context, runID string) (RunDetail, error) { + overview, err := s.orch.GetRunOverview(ctx, runID) + if err != nil { + return RunDetail{}, err + } + + blocked, err := s.orch.ListBlockedTasks(ctx, runID) + if err != nil { + return RunDetail{}, err + } + + return RunDetail{ + Run: overview.Run, + TaskCounts: overview.TaskCounts, + TotalTasks: totalTasks(overview.TaskCounts), + Tasks: overview.Tasks, + BlockedTasks: blocked, + }, nil +} + +func (s *ReadService) ListRunTasks(ctx context.Context, runID string) ([]store.Task, error) { + detail, err := s.GetRunDetail(ctx, runID) + if err != nil { + return nil, err + } + return detail.Tasks, nil +} + +func (s *ReadService) ListBlockedTasks(ctx context.Context, runID string) ([]store.BlockedTask, error) { + return s.orch.ListBlockedTasks(ctx, runID) +} + +func (s *ReadService) GetThreadDetail(ctx context.Context, threadID string) (store.ThreadDetail, error) { + return s.inbox.GetThread(ctx, threadID) +} + +func (s *ReadService) collectTaskCounts(ctx context.Context, runIDs []string) (map[string]map[string]int, error) { + result := make(map[string]map[string]int, len(runIDs)) + if len(runIDs) == 0 { + return result, nil + } + + args := make([]any, 0, len(runIDs)) + for _, runID := range runIDs { + args = append(args, runID) + } + + rows, err := s.db.QueryContext( + ctx, + `SELECT run_id, status, COUNT(*) + FROM tasks + WHERE run_id IN (`+placeholders(len(runIDs))+`) + GROUP BY run_id, status`, + args..., + ) + if err != nil { + return nil, fmt.Errorf("query task counts for runs: %w", err) + } + defer rows.Close() + + for rows.Next() { + var ( + runID string + status string + count int + ) + if err := rows.Scan(&runID, &status, &count); err != nil { + return nil, fmt.Errorf("scan run task count: %w", err) + } + if result[runID] == nil { + result[runID] = make(map[string]int) + } + result[runID][status] = count + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate run task counts: %w", err) + } + + return result, nil +} + +func totalTasks(counts map[string]int) int { + total := 0 + for _, count := range counts { + total += count + } + return total +} + +func placeholders(count int) string { + if count <= 0 { + return "" + } + + buf := make([]byte, 0, count*2-1) + for i := 0; i < count; i++ { + if i > 0 { + buf = append(buf, ',') + } + buf = append(buf, '?') + } + return string(buf) +} + +func parseRFC3339(value string) time.Time { + parsed, err := time.Parse(time.RFC3339Nano, value) + if err != nil { + return time.Time{} + } + return parsed +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..058c1a9 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "ai-workflow-skill", + "private": true, + "packageManager": "pnpm@10.25.0", + "scripts": { + "web:dev": "pnpm --filter @ai-workflow-skill/web dev", + "web:build": "pnpm --filter @ai-workflow-skill/web build", + "web:preview": "pnpm --filter @ai-workflow-skill/web preview", + "web:typecheck": "pnpm --filter @ai-workflow-skill/web typecheck" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d728640 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,697 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + apps/web: + dependencies: + '@tanstack/react-query': + specifier: ^5.91.2 + version: 5.91.2(react@19.2.4) + '@tanstack/react-router': + specifier: ^1.167.5 + version: 1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + devDependencies: + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.1) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^8.0.1 + version: 8.0.1 + +packages: + + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@oxc-project/types@0.120.0': + resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} + + '@rolldown/binding-android-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.10': + resolution: {integrity: sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': + resolution: {integrity: sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': + resolution: {integrity: sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': + resolution: {integrity: sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': + resolution: {integrity: sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': + resolution: {integrity: sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': + resolution: {integrity: sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': + resolution: {integrity: sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.10': + resolution: {integrity: sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@tanstack/history@1.161.6': + resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} + engines: {node: '>=20.19'} + + '@tanstack/query-core@5.91.2': + resolution: {integrity: sha512-Uz2pTgPC1mhqrrSGg18RKCWT/pkduAYtxbcyIyKBhw7dTWjXZIzqmpzO2lBkyWr4hlImQgpu1m1pei3UnkFRWw==} + + '@tanstack/react-query@5.91.2': + resolution: {integrity: sha512-GClLPzbM57iFXv+FlvOUL56XVe00PxuTaVEyj1zAObhRiKF008J5vedmaq7O6ehs+VmPHe8+PUQhMuEyv8d9wQ==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-router@1.167.5': + resolution: {integrity: sha512-s1nP6l/7BYZfSwhoNbB7/rUmZ07q/AvkmhBoiDQl3tgy5dpb9Q1qjtIapYdvCOrao1aA/QCaWqxcbGc2Ct1bvQ==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.9.2': + resolution: {integrity: sha512-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.167.5': + resolution: {integrity: sha512-8fRgJ0zNJf77R4grCaJQ5Imatjyc4YT5v8rlsPkYYYeUlcFNLbuFRhLlAMdND9gRUMznpnbRDXngpTPgx2K7HQ==} + engines: {node: '>=20.19'} + hasBin: true + + '@tanstack/store@0.9.2': + resolution: {integrity: sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + cookie-es@2.0.0: + resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + isbot@5.1.36: + resolution: {integrity: sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ==} + engines: {node: '>=18'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + rolldown@1.0.0-rc.10: + resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + seroval-plugins@1.5.1: + resolution: {integrity: sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.1: + resolution: {integrity: sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==} + engines: {node: '>=10'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite@8.0.1: + resolution: {integrity: sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + +snapshots: + + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.120.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.10': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@tanstack/history@1.161.6': {} + + '@tanstack/query-core@5.91.2': {} + + '@tanstack/react-query@5.91.2(react@19.2.4)': + dependencies: + '@tanstack/query-core': 5.91.2 + react: 19.2.4 + + '@tanstack/react-router@1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/history': 1.161.6 + '@tanstack/react-store': 0.9.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-core': 1.167.5 + isbot: 5.1.36 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/react-store@0.9.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/store': 0.9.2 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + + '@tanstack/router-core@1.167.5': + dependencies: + '@tanstack/history': 1.161.6 + '@tanstack/store': 0.9.2 + cookie-es: 2.0.0 + seroval: 1.5.1 + seroval-plugins: 1.5.1(seroval@1.5.1) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/store@0.9.2': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@6.0.1(vite@8.0.1)': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.1 + + cookie-es@2.0.0: {} + + csstype@3.2.3: {} + + detect-libc@2.1.2: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + isbot@5.1.36: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + nanoid@3.3.11: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react@19.2.4: {} + + rolldown@1.0.0-rc.10: + dependencies: + '@oxc-project/types': 0.120.0 + '@rolldown/pluginutils': 1.0.0-rc.10 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.10 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.10 + '@rolldown/binding-darwin-x64': 1.0.0-rc.10 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.10 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.10 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.10 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.10 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.10 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.10 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10 + + scheduler@0.27.0: {} + + seroval-plugins@1.5.1(seroval@1.5.1): + dependencies: + seroval: 1.5.1 + + seroval@1.5.1: {} + + source-map-js@1.2.1: {} + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + vite@8.0.1: + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.3 + postcss: 8.5.8 + rolldown: 1.0.0-rc.10 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..852bf6b --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - apps/*