Initial scaffold: tooling, plan, task briefs

- pnpm 10 workspace + TypeScript strict + ESLint flat + Prettier
- CLAUDE.md as canonical brief
- docs/tasks/ — 22 task briefs broken out by phase for sub-sessions
- docs/decisions/ — ADR scaffold

Implements task 01 (repo-init).
This commit is contained in:
ginnoir
2026-05-06 00:05:50 -05:00
commit b89690a9f2
40 changed files with 2282 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
+25
View File
@@ -0,0 +1,25 @@
# Public app URL (used for OIDC redirect URIs, share links, etc.)
NEXT_PUBLIC_APP_URL=https://fam.ginnoir.com
# Postgres
DATABASE_URL=postgres://famapp:famapp@localhost:5432/famapp
# Auth.js
AUTH_SECRET=replace-with-openssl-rand-base64-32
# OIDC (Authentik)
AUTH_OIDC_ISSUER=https://auth.ginnoir.com/application/o/famapp/
AUTH_OIDC_CLIENT_ID=replace-me
AUTH_OIDC_CLIENT_SECRET=replace-me
# Web Push (generate with: pnpm vapid:generate)
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_SUBJECT=mailto:you@example.com
# ntfy (optional fallback channel; leave blank to disable)
NTFY_URL=
NTFY_TOPIC=
# Logging
LOG_LEVEL=info
+9
View File
@@ -0,0 +1,9 @@
* text=auto eol=lf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.webp binary
*.woff binary
*.woff2 binary
+43
View File
@@ -0,0 +1,43 @@
# Dependencies
node_modules/
.pnpm-store/
# Build output
.next/
out/
dist/
build/
*.tsbuildinfo
# Env
.env
.env.local
.env.*.local
.env.production
# Logs
*.log
npm-debug.log*
pnpm-debug.log*
# OS
.DS_Store
Thumbs.db
desktop.ini
# Editors
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json.example
.idea/
# Coverage / test artifacts
coverage/
playwright-report/
test-results/
# Drizzle generated artifacts (migrations themselves are committed)
drizzle/meta/
# Backups
deploy/backups/data/
+1
View File
@@ -0,0 +1 @@
22
+5
View File
@@ -0,0 +1,5 @@
node_modules
.next
dist
drizzle
pnpm-lock.yaml
+9
View File
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always",
"endOfLine": "lf"
}
+165
View File
@@ -0,0 +1,165 @@
# famapp
Self-hosted family coordination web app for Matt and his wife. Replaces a commercial family app with a shared calendar, shopping/task lists, and notes/reminders. Designed to be **deliberately extensible** — Matt expects to bolt on niche features over time, so the architecture treats every feature as a module.
This file is the canonical brief. Read it at the start of every session before making changes. Sub-task briefs in [`docs/tasks/`](docs/tasks/) reference this file; do not duplicate its contents there.
---
## Goals
- Replace current family app: shared calendar, shopping list, task list, notes/reminders.
- Self-hosted on Matt's home server, accessible from outside the house with auth.
- Wife-friendly: installable PWA, passkey/SSO login, no extra apps to install for notifications.
- "Share anything" — most entities can produce a temporary public link for outsiders.
- Architecture optimized for adding many small features later without core changes.
---
## Stack (decided)
| Layer | Choice | Notes |
| ------------------ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Frontend + backend | Next.js 15 App Router + TypeScript | Single Node process, server actions for mutations |
| UI | Tailwind + shadcn/ui | |
| DB | Postgres 16 | Dedicated container per app (Matt's rule) |
| ORM | Drizzle | Lightweight, SQL-friendly, good for evolving schema |
| Auth/SSO | **Authentik** at `auth.ginnoir.com` | Picked over Pocket-ID because it ships forward-auth (Proxy Provider) for the rest of Matt's stack (Sonarr/Radarr/Prowlarr/NZBGet/qBittorrent/Tautulli/Overseerr/FreshRSS/ntfy) which has no native OIDC. famapp itself uses OIDC. |
| Push | Web Push (VAPID) | Works in installed PWAs incl. iOS Safari 16.4+. ntfy stays optional. |
| Realtime | Postgres `LISTEN/NOTIFY` → SSE | Simpler than WebSockets, fine for two users |
| Reverse proxy | Caddy (existing) | `fam.ginnoir.com` → famapp:3000, `auth.ginnoir.com` → authentik |
| Package manager | pnpm | Fast, content-addressed store, strict deps |
| Runtime | Node 22 LTS | In `node:22-alpine` container |
| Host | Ubuntu Server 24.04 x64, Docker + Compose | Existing self-host stack |
Domain: `fam.ginnoir.com`. SSO: `auth.ginnoir.com`.
---
## Architecture: extensibility-first
Every feature is a self-contained **module** under `src/modules/<name>/`. A module declares its tables, routes, dashboard contributions, quick-add actions, and supported share/reminder/search behaviors via a manifest. Core services (sharing, push, activity log, search, reminders) operate generically against any registered entity — adding a new module should not require touching core code.
```
src/
modules/
_core/ # auth, household, sharing, push, activity log, reminders
calendar/
schema.ts # drizzle tables
server/ # server actions + queries
components/
routes.ts # registers /calendar pages + API
dashboard.tsx # widget contributed to dashboard
manifest.ts # name, icon, nav, entity types, share/reminder handlers
lists/
notes/
app/ # Next.js routes — thin, mounts modules
lib/
```
### Core primitives every module gets
- **Entity registry.** Modules declare entity types; share-link, activity log, search, reminders all work against any registered entity.
- **Dashboard contribution API.** Each module exports a widget + priority. Dashboard composes whatever's installed.
- **Quick-add registry.** Modules register quick actions for the dashboard's `+` menu.
- **Share-link service.** `createShareLink(entityType, entityId, { expiresAt, capabilities })``fam.ginnoir.com/s/<token>`. Generic.
- **Notification bus.** `notify(userId, { title, body, url })` fans out to web push + in-app + (optional) ntfy.
- **Permissions.** Household-scoped by default. Share-tokens grant scoped read/write per entity.
- **Feature flags.** Env/db-driven for staging experiments.
**Rule for contributors (including future Sonnet sessions):** if a feature requires a change to `_core` to support a new entity type, that's a smell — extend the registry instead.
---
## Data model (v1)
- `users`, `households`, `household_members`
- `calendar_events` — title, start, end, all_day, location, notes, color, owner. `rrule` text column reserved (no recurrence in v1). `external_source`/`external_id` nullable for future Google/Apple sync.
- `lists` (type enum: `shopping` | `task` | future), `list_items`
- `notes` — title, body, pinned, remind_at
- `share_links` — entity_type, entity_id, token, capabilities jsonb, expires_at
- `activity_log` — entity_type, entity_id, actor, action, payload jsonb
- `push_subscriptions`
- `reminders` — entity_type, entity_id, fire_at, channel. Generic; used by notes/events/anything.
All entity tables include `household_id`. All mutations are scoped to the caller's household.
---
## Pages (v1)
- `/` Dashboard — today + next 3 days events, top of shopping list, open tasks, pinned notes, recent activity, quick-add `+`
- `/calendar` — month/week/day, drag-create
- `/lists/shopping`, `/lists/tasks` — defaults; create more
- `/notes`
- `/s/<token>` — public share viewer
- `/settings` — household, push toggles, share-link management
---
## Deploy shape
```yaml
# compose.yaml (sketch — final lives in /deploy)
services:
famapp: # node:22-alpine, Next.js
famapp-db: # postgres:16
authentik-server:
authentik-worker:
authentik-db: # postgres:16 (separate per Matt's rule)
authentik-redis:
```
Caddy: `fam.ginnoir.com``famapp:3000`. `auth.ginnoir.com``authentik-server:9000`.
Backups: nightly `pg_dump` to a host volume. Out of scope for phase 1 but reserve the cron slot.
---
## Build phases
1. **Scaffold** — repo init, Next.js + Drizzle + Tailwind + shadcn, module loader, compose stack, Caddy snippet, Authentik wired up, seeded household with both users.
2. **Core modules** — calendar, lists, notes (CRUD only).
3. **Dashboard + quick-add + activity log.**
4. **Sharing** — share-link service + `/s/<token>` viewer.
5. **Push notifications + reminders.**
6. **PWA polish** — manifest, icons, offline shell, install prompt.
7. **Hardening** — backups (pg_dump cron), rate limiting on share links, logging.
Tasks for each phase live in [`docs/tasks/`](docs/tasks/). Sub-sessions should pick up a task file, follow its scope, and stop at its acceptance criteria.
---
## Conventions
- **TypeScript strict.** No `any` without a written reason.
- **Server actions** for mutations; route handlers only for webhooks/SSE/share-link viewer.
- **Drizzle migrations** committed under `drizzle/`. Never edit a shipped migration — add a new one.
- **No comments** unless the _why_ is non-obvious. Names should carry intent.
- **Module isolation.** A module imports from `_core` and `lib/` only — never from a sibling module.
- **One module = one PR/commit boundary** when possible.
- **Tests:** Vitest for units (where it pays off), Playwright for one happy-path E2E per module. Don't write tests for trivial CRUD.
- **Secrets** via `.env` (gitignored) and `.env.example` (committed, no values).
---
## Out of scope for v1 (do not build)
- ICS export / Google / Apple / Outlook calendar sync (data model leaves room)
- Recurrence editor (column reserved, no UI)
- Native iOS/Android app (PWA only; architecture must not preclude it)
- Migration from any existing app (starting fresh)
- Forward-auth wiring for the rest of Matt's stack (Authentik is installed; wiring the \*arr apps is a separate later task)
- Multi-tenant / multi-household (schema has `household_id` but UI is single-household)
---
## Where things live
- **This brief:** `CLAUDE.md`
- **Task briefs for sub-sessions:** `docs/tasks/NN-name.md`
- **Architecture decisions worth preserving:** `docs/decisions/NNNN-title.md` (lightweight ADR — only when a non-obvious choice is made)
- **App code:** `src/`
- **Drizzle migrations:** `drizzle/`
- **Deploy:** `deploy/` (compose.yaml, Caddyfile snippet, Authentik bootstrap)
- **Memory (cross-session):** `C:\Users\MattC\.claude\projects\C--Users-MattC-Documents-famapp\memory\`
+7
View File
@@ -0,0 +1,7 @@
# famapp
Self-hosted family coordination: shared calendar, lists, notes.
- **Plan & architecture:** [`CLAUDE.md`](CLAUDE.md)
- **Task briefs (for sub-sessions):** [`docs/tasks/`](docs/tasks/)
- **Architecture decisions:** [`docs/decisions/`](docs/decisions/)
+25
View File
@@ -0,0 +1,25 @@
# Architecture decisions
Lightweight ADRs. Add one only when a non-obvious choice is made and the reasoning would be hard to reconstruct later. Keep each under ~300 words.
## Format
```
# NNNN — Title
Date: YYYY-MM-DD
Status: accepted | superseded by NNNN
## Context
What problem we faced.
## Decision
What we chose.
## Consequences
What this costs us, what it buys us.
```
## Index
- (none yet — add as decisions are made)
+42
View File
@@ -0,0 +1,42 @@
# 01 — Repo init & tooling
## Goal
Initialize the famapp repo with pnpm, TypeScript strict, ESLint/Prettier, and a clean baseline commit.
## Why
Every later task assumes the repo, lockfile, and lint config exist.
## Depends on
None.
## Scope
- `git init` in `C:\Users\MattC\Documents\famapp` with `main` as default branch.
- `package.json` (`"packageManager": "pnpm@<latest 10.x>"`, `"engines": { "node": ">=22" }`).
- `pnpm-workspace.yaml` (single root for now; leave room for workspaces).
- `tsconfig.json` — strict, `moduleResolution: "bundler"`, paths alias `@/* → src/*`.
- ESLint (flat config) + Prettier with sensible defaults; `eslint-config-next` deferred to task 02.
- `.gitignore` (Node, Next.js, env files, OS junk).
- `.env.example` with placeholders for: `DATABASE_URL`, `AUTH_OIDC_ISSUER`, `AUTH_OIDC_CLIENT_ID`, `AUTH_OIDC_CLIENT_SECRET`, `AUTH_SECRET`, `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, `NEXT_PUBLIC_APP_URL`.
- `README.md` — short pointer to `CLAUDE.md` and `docs/tasks/`.
- Editor: `.editorconfig`, `.nvmrc` (`22`).
## Out of scope
- Installing Next.js, Drizzle, Tailwind (those are in 02 and 03).
- Husky / lint-staged (defer; small two-person project doesn't need it yet).
## Acceptance criteria
- [ ] `pnpm install` succeeds on a clean clone.
- [ ] `pnpm lint` and `pnpm format:check` run (even if no files yet) and exit 0.
- [ ] `tsc --noEmit` exits 0.
- [ ] `git log` shows exactly one initial commit.
## Notes
- Use the latest pnpm 10.x.
- Flat ESLint config (`eslint.config.mjs`) — don't use the legacy `.eslintrc`.
+38
View File
@@ -0,0 +1,38 @@
# 02 — Next.js app skeleton
## Goal
Stand up a minimal Next.js 15 (App Router) app with Tailwind and shadcn/ui, rendering a placeholder dashboard.
## Why
Provides the shell the modules will plug into.
## Depends on
- 01 (repo init)
## Scope
- Install Next.js 15 + React 19 into the existing repo (do **not** use `create-next-app` to overwrite — add deps manually or with `--use-pnpm` into the existing dir).
- App Router under `src/app/` with a root layout and a `/` page that renders "famapp" plus a stub "Dashboard coming soon".
- Tailwind v4 (or latest stable) configured against `src/**`.
- shadcn/ui initialized; install `button`, `card`, `input`, `dialog` to start.
- Path alias `@/*` working at runtime + in the editor.
- `next.config.ts` with `reactStrictMode: true` and `output: "standalone"` (required for the Docker image in task 05).
## Out of scope
- Auth, DB, modules. Just the shell.
- Custom theming beyond shadcn defaults.
## Acceptance criteria
- [ ] `pnpm dev` serves `http://localhost:3000` showing the placeholder page.
- [ ] `pnpm build` succeeds with `output: "standalone"` producing `.next/standalone/`.
- [ ] `pnpm lint` (now extending `next/core-web-vitals`) passes.
- [ ] No `any` introduced.
## Notes
- shadcn CLI writes to `src/components/ui/`. Keep it there; modules will import from it.
+38
View File
@@ -0,0 +1,38 @@
# 03 — Drizzle + Postgres setup
## Goal
Wire Drizzle ORM to a Postgres 16 instance, with migrations and a working `db` client.
## Depends on
- 02 (Next.js skeleton)
## Scope
- Install `drizzle-orm`, `drizzle-kit`, `postgres` (the `postgres` driver, not `pg`).
- `drizzle.config.ts` pointing at `src/modules/**/schema.ts` and writing to `./drizzle/`.
- `src/lib/db.ts` exporting a singleton `db` client built from `DATABASE_URL`.
- Initial schema in `src/modules/_core/schema.ts`:
- `users` (id uuid pk, email unique, display_name, created_at)
- `households` (id uuid pk, name, created_at)
- `household_members` (household_id, user_id, role enum: `owner` | `member`, pk on both)
- pnpm scripts: `db:generate`, `db:migrate`, `db:studio`.
- `docker-compose.dev.yaml` at repo root with a `famapp-db` service for local dev (Postgres 16, exposed on 5432, named volume).
## Out of scope
- Module-specific tables (calendar/lists/notes ship in their own tasks).
- Seed data (in task 07).
## Acceptance criteria
- [ ] `docker compose -f docker-compose.dev.yaml up -d` starts Postgres locally.
- [ ] `pnpm db:generate && pnpm db:migrate` produces `drizzle/0000_*.sql` and applies it.
- [ ] A throwaway script can `import { db } from "@/lib/db"` and `select` from `users` (returning empty).
- [ ] `tsc --noEmit` passes.
## Notes
- Use the `postgres` driver — works in both Node and edge-light contexts and is what Drizzle docs prefer for serverless-shaped apps.
- Keep the dev compose file separate from the production `deploy/compose.yaml` (task 05).
+72
View File
@@ -0,0 +1,72 @@
# 04 — Module loader & registry
## Goal
Implement the module system that everything else hangs off: each module declares a manifest, the loader composes them at startup, and core services iterate the registry instead of hardcoding entity types.
## Why
This is the load-bearing extensibility piece. Get this wrong and every later module needs core changes.
## Depends on
- 03 (DB)
## Scope
### Module manifest type (`src/modules/_core/module.ts`)
```ts
export type ModuleManifest = {
id: string; // "calendar", "lists", "notes"
name: string; // human-readable
nav?: { href: string; label: string; icon?: string };
entities: EntityTypeRegistration[]; // see below
dashboardWidgets?: DashboardWidget[];
quickAdds?: QuickAddAction[];
};
export type EntityTypeRegistration = {
type: string; // "calendar.event", "lists.item", "notes.note"
label: { singular: string; plural: string };
share?: ShareCapabilities; // null = not shareable
reminder?: ReminderCapabilities; // null = not remindable
search?: SearchAdapter; // null = not searchable
resolveUrl: (id: string) => string; // canonical app URL
loadForShare?: (id: string) => Promise<unknown>; // payload for /s/<token>
};
```
### Registry (`src/modules/_core/registry.ts`)
- `registerModule(manifest)` and `getRegistry()` returning frozen views.
- `getEntityType(type)` lookup.
- Import-time side-effect free: modules are listed and registered explicitly in `src/modules/index.ts`.
### Loader (`src/modules/index.ts`)
- Imports each module's manifest and calls `registerModule`. Order is deterministic.
- Stub modules for now: `calendar`, `lists`, `notes` each export an empty-but-valid manifest so the loader has something to register.
### Wiring
- Root layout reads the registry to render nav.
- A throwaway `/debug/registry` page (dev-only) dumps the loaded registry as JSON.
## Out of scope
- Real implementations of share / reminder / search adapters (later phases).
- Dynamic plugin loading from disk. Modules are statically imported.
## Acceptance criteria
- [ ] `src/modules/index.ts` registers three stub modules.
- [ ] Nav renders entries from the registry, not from a hardcoded list.
- [ ] `/debug/registry` shows all three module manifests in dev.
- [ ] Adding a fourth stub module only requires creating its folder + adding one line to `src/modules/index.ts`.
- [ ] All types exported from `_core` so other modules can import without circulars.
## Notes
- Frozen objects (`Object.freeze`) on registry exposure are cheap insurance against accidental mutation.
- Resist the urge to make this a fancy DI container. A plain map is enough.
+63
View File
@@ -0,0 +1,63 @@
# 05 — Compose stack & Caddy
## Goal
Produce the production deploy artifacts: Dockerfile, `deploy/compose.yaml`, Caddy snippet, `.env.production.example`.
## Depends on
- 02 (so `output: "standalone"` exists)
- 03 (so DB service is defined)
## Scope
### Dockerfile (multi-stage)
- Stage 1: `node:22-alpine` + pnpm, install deps, build (`pnpm build`).
- Stage 2: `node:22-alpine` runtime, copy `.next/standalone`, `.next/static`, `public/`. Run as non-root. `CMD ["node", "server.js"]`.
### `deploy/compose.yaml`
Services:
- `famapp` (built from Dockerfile)
- `famapp-db` (postgres:16, volume `famapp_db_data`)
- `authentik-server`, `authentik-worker`, `authentik-db` (postgres:16, volume `authentik_db_data`), `authentik-redis` — leave fully configured but task 06 will tune env
All services on a `famapp_net` network. famapp depends on famapp-db. No host port exposure for the DBs/redis.
### Caddy snippet (`deploy/Caddyfile.snippet`)
```
fam.ginnoir.com {
reverse_proxy famapp:3000
}
auth.ginnoir.com {
reverse_proxy authentik-server:9000
}
```
Comment at the top: "Include from main Caddyfile or paste into the existing one."
### `.env.production.example`
All vars needed by the compose stack, with comments explaining each.
## Out of scope
- Actually running the stack against ginnoir.com (Matt does that).
- Backups, log shipping (later tasks).
## Acceptance criteria
- [ ] `docker build -t famapp .` succeeds.
- [ ] `docker compose -f deploy/compose.yaml config` validates without errors.
- [ ] famapp container starts against famapp-db when given a populated `.env`.
- [ ] Image size under ~300 MB.
- [ ] No secrets committed; only `.env.production.example`.
## Notes
- Use `pnpm fetch` + `pnpm install --offline` in the build stage for cache locality.
- `output: "standalone"` (set in task 02) means we copy `.next/standalone/server.js`, not run `next start`.
+53
View File
@@ -0,0 +1,53 @@
# 06 — Authentik install + OIDC integration
## Goal
Bring up Authentik in the compose stack and integrate famapp as an OIDC client. Logging in at `fam.ginnoir.com` redirects to `auth.ginnoir.com`, returns, and creates a session.
## Depends on
- 04 (registry, so we have a `users` table to map into)
- 05 (compose stack)
## Scope
### Authentik bootstrap
- Tune env in `deploy/compose.yaml` for authentik-server/worker/db/redis per Authentik's official docs.
- `deploy/authentik/README.md` — manual one-time steps Matt runs after first boot:
1. Visit `auth.ginnoir.com/if/flow/initial-setup/`, set the akadmin password.
2. Create an OIDC Provider for famapp (RS256, redirect URI `https://fam.ginnoir.com/api/auth/callback/authentik`).
3. Create an Application bound to it; copy client ID + secret into famapp's `.env`.
4. Create the two user accounts (Matt + wife) with passkeys.
### famapp OIDC client
- Install `next-auth@beta` (v5) — Auth.js.
- `src/lib/auth.ts` configures Auth.js with a generic OIDC provider pointed at `AUTH_OIDC_ISSUER`.
- On first login, upsert into `users` table by email; create-or-attach to the single seeded household (task 07 owns the seeding; this task assumes it).
- Middleware (`src/middleware.ts`) protects everything except `/s/*` (share viewer), `/api/auth/*`, and static assets.
- A signed-in user is available via a `getCurrentUser()` server helper.
### Login UX
- `/login` page with a single "Sign in with SSO" button.
- After login, redirect to `/`.
## Out of scope
- Forward-auth / Outpost wiring for the rest of Matt's stack (separate later task).
- Account self-service in famapp (Authentik owns identity).
- Automating Authentik provider creation via Terraform/blueprints — manual is fine for now; document it well.
## Acceptance criteria
- [ ] Hitting `/` while signed out redirects to `/login`.
- [ ] Signing in with an Authentik account creates/updates a row in `users` and returns to `/`.
- [ ] `getCurrentUser()` works in server components and server actions.
- [ ] `/s/<token>` is reachable signed-out (placeholder is fine — real viewer is task 31).
- [ ] Auth secret comes from `AUTH_SECRET` env, not committed.
## Notes
- Authentik's OIDC issuer URL takes the form `https://auth.ginnoir.com/application/o/<app-slug>/`. Document this in the README so Matt copies it correctly.
- Use Auth.js's database session strategy with the Drizzle adapter so the `users` row is the source of truth.
+29
View File
@@ -0,0 +1,29 @@
# 07 — Household seeding & session
## Goal
On first boot, create the single household and attach Matt + wife to it. Every authenticated request resolves a `householdId` cheaply.
## Depends on
- 06 (auth)
## Scope
- Migration / seed script (`pnpm db:seed`) that inserts one household named "Home" if none exists.
- On first OIDC login, attach the user to the seeded household as `owner` (Matt) or `member` (everyone else). Idempotent.
- `getCurrentSession()` server helper returns `{ user, household, role }`. Throws if no household membership.
- All later modules' queries take `householdId` as the scope key.
- `/settings/household` page showing household name, member list, and a "rename household" form (owner only).
## Out of scope
- Multi-household UI (schema supports it; UI is single-household only).
- Invite flows (Authentik provisions accounts; we just attach them).
## Acceptance criteria
- [ ] `pnpm db:seed` is idempotent — running twice does not create duplicate households.
- [ ] First user to sign in becomes `owner`; subsequent users become `member`.
- [ ] `getCurrentSession()` returns a typed object with `household.id`.
- [ ] `/settings/household` renders for both roles; rename only works for owner.
+57
View File
@@ -0,0 +1,57 @@
# 10 — Calendar module
## Goal
Implement the `calendar` module: shared events with month/week/day views and CRUD.
## Depends on
- 04 (module loader), 07 (household)
## Scope
### Schema (`src/modules/calendar/schema.ts`)
- `calendar_events`:
- `id` uuid pk
- `household_id` fk
- `title` text
- `start_at` timestamptz, `end_at` timestamptz
- `all_day` boolean
- `location` text nullable
- `notes` text nullable
- `color` text nullable
- `owner_id` fk users
- `rrule` text nullable (reserved; no UI)
- `external_source` text nullable, `external_id` text nullable (reserved)
- `created_at`, `updated_at`
### Server (`src/modules/calendar/server/`)
- `listEvents({ from, to })` — household-scoped, range query.
- `createEvent`, `updateEvent`, `deleteEvent` — server actions, validate with Zod.
### UI (`src/modules/calendar/components/`)
- `/calendar` page with month / week / day toggle. Recommended lib: **FullCalendar** (`@fullcalendar/react` + day/week/month plugins) — handles the heavy lifting; we own only the data layer.
- Click a day cell → create-event dialog. Click an event → edit dialog. Drag-resize updates `end_at`.
### Manifest
- Registers entity type `calendar.event` with `share` enabled (read-only by default), `reminder` enabled, `search` enabled (title + notes + location).
- Registers nav entry `/calendar`.
- Dashboard widget: next 3 days (built in task 20, but expose the data fetcher here).
- Quick-add: "New event" → opens create dialog.
## Out of scope
- Recurrence UI (rrule column reserved, ignored on read).
- External calendar sync.
- Free/busy aggregation across users.
## Acceptance criteria
- [ ] CRUD works end-to-end with optimistic updates.
- [ ] All queries scoped to `household_id`; cross-household leakage is impossible (verify with a second seeded household in a test).
- [ ] Manifest registers all four capabilities (entity, nav, widget data, quick-add).
- [ ] One Playwright happy-path test: create → see on calendar → edit → delete.
+48
View File
@@ -0,0 +1,48 @@
# 11 — Lists module (shopping + task)
## Goal
Implement the `lists` module supporting two list types out of the box (`shopping`, `task`), with the schema designed so additional types can be added later without migration.
## Depends on
- 04, 07
## Scope
### Schema
- `lists`: `id`, `household_id`, `type` (text — not an enum, to allow extension), `name`, `archived`, `created_at`.
- `list_items`: `id`, `list_id`, `text`, `done` boolean, `qty` text nullable, `notes` text nullable, `due_at` timestamptz nullable, `assignee_id` fk users nullable, `position` int (for ordering), `created_at`, `updated_at`.
Seed: one default list of each type per household on first access (idempotent).
### Server
- `listLists({ type? })`, `getList(id)`, `createList`, `renameList`, `archiveList`.
- `addItem`, `toggleItem`, `updateItem`, `deleteItem`, `reorderItems`.
### UI
- `/lists` index showing all lists grouped by type.
- `/lists/[id]` — fast keyboard-driven entry: focus stays in input, Enter adds, checkbox toggles, swipe-left (mobile) deletes.
- Realtime: SSE subscription on the list id; updates from the other user appear without refresh. (Implementation detail: Postgres `LISTEN/NOTIFY` channel `list:<id>`; thin SSE route handler bridges it. Document the pattern in `docs/decisions/`.)
### Manifest
- Entity type `lists.list` (shareable read+write via share token), `lists.item` (not directly shareable; inherits via list).
- Quick-adds: "Add to shopping", "Add to tasks" (each adds a single item to the default list of that type).
- Dashboard widget: top 5 unchecked shopping items + open tasks assigned to me.
## Out of scope
- Reordering across lists.
- Per-item images.
- Recurring tasks (use reminders module later if needed).
## Acceptance criteria
- [ ] Both default lists are auto-seeded.
- [ ] Adding an item on one device appears on the other within ~1s via SSE.
- [ ] `type` is `text` not `enum` — adding a third type works without a migration.
- [ ] Playwright: add item → check it off → archive list.
+30
View File
@@ -0,0 +1,30 @@
# 12 — Notes module
## Goal
Lightweight shared notes with optional reminders.
## Depends on
- 04, 07
## Scope
- Schema: `notes` (`id`, `household_id`, `author_id`, `title`, `body` markdown, `pinned` bool, `remind_at` timestamptz nullable, `created_at`, `updated_at`).
- `/notes` index: pinned first, then by updated_at desc.
- `/notes/[id]` editor — minimal markdown (use `@uiw/react-md-editor` or similar; live preview optional).
- `remind_at` writes a row to `reminders` (table comes from `_core`; reminder firing comes from task 41).
- Manifest: shareable, remindable, searchable (title + body).
- Dashboard widget: pinned notes.
## Out of scope
- Rich text / WYSIWYG.
- Attachments (separate later module).
- Collaborative editing.
## Acceptance criteria
- [ ] Create / edit / pin / delete works.
- [ ] Setting `remind_at` creates a `reminders` row scoped to entity `notes.note`.
- [ ] Markdown renders safely (no raw HTML injection).
+33
View File
@@ -0,0 +1,33 @@
# 20 — Dashboard composition
## Goal
The `/` landing page composes widgets contributed by installed modules. No hardcoded widget list.
## Depends on
- 10, 11, 12
## Scope
- `DashboardWidget` type already in `_core` (from task 04). Each widget exports `{ id, priority, render }`.
- `/` reads `getRegistry().dashboardWidgets`, sorts by priority desc, renders them in a responsive grid (1 col mobile, 2-3 cols desktop).
- Widgets implemented:
- **calendar.upcoming** — next 3 days of events
- **lists.shopping** — top 5 unchecked items in default shopping list
- **lists.tasks** — my open tasks (assigned to me, due soon first)
- **notes.pinned** — pinned notes
- **core.activity** — recent activity feed (depends on task 22; until then render a stub)
- Skeleton loaders while widgets load (each widget is a server component with `loading.tsx`-equivalent Suspense boundary).
- "+" floating action button opens the quick-add menu (task 21).
## Out of scope
- User-configurable widget order or hide/show (defer; pin a sane default order).
- Drag-rearrange.
## Acceptance criteria
- [ ] Removing a module from `src/modules/index.ts` cleanly removes its widgets — no errors, no blank slot.
- [ ] Widgets load in parallel (verify in network tab — no waterfall).
- [ ] Mobile layout (375px) is usable without horizontal scroll.
+26
View File
@@ -0,0 +1,26 @@
# 21 — Quick-add registry
## Goal
A single global `+` button on the dashboard (and a `cmd+k` palette anywhere) that lists actions contributed by modules.
## Depends on
- 20
## Scope
- `QuickAddAction` type: `{ id, label, icon?, shortcut?, run: () => void | Promise<void> }`.
- Modules register via manifest. Registry exposes a sorted list.
- UI: floating `+` on dashboard opens a sheet of actions; `cmd/ctrl+k` opens a command-palette-style modal (use `cmdk` package).
- Default actions: New event, Add to shopping, Add to tasks, New note.
## Out of scope
- Fuzzy search across data (search adapter is a later concern).
- Action history / recents.
## Acceptance criteria
- [ ] Adding a new module that registers a quick-add appears in both the FAB sheet and the cmd-k palette without touching core.
- [ ] Keyboard navigation works in the palette (arrows + enter + escape).
+26
View File
@@ -0,0 +1,26 @@
# 22 — Activity log
## Goal
Generic activity feed every module writes to and the dashboard reads from.
## Depends on
- 10, 11, 12
## Scope
- Schema: `activity_log` (`id`, `household_id`, `entity_type`, `entity_id`, `actor_id`, `action`, `payload` jsonb, `created_at`). Index on `(household_id, created_at desc)`.
- `_core` exports `logActivity({ entityType, entityId, action, payload })`. Reads `householdId` and `actorId` from current session.
- Calendar/lists/notes modules call it on create/update/delete.
- Dashboard widget `core.activity` shows last 20 entries with human-readable rendering. Each entity type's manifest provides a `renderActivity(entry)` function.
## Out of scope
- Per-user activity filters.
- Retention / pruning (no growth concerns at two-user scale).
## Acceptance criteria
- [ ] Every CRUD across the three core modules writes a row.
- [ ] Widget renders entries via the registry — no `if (entityType === ...)` branches in the widget itself.
+32
View File
@@ -0,0 +1,32 @@
# 30 — Share-link service
## Goal
A generic service that issues temporary, scoped, revocable share tokens for any registered entity.
## Depends on
- 04 (registry), 07 (household)
## Scope
- Schema: `share_links` (`id`, `household_id`, `entity_type`, `entity_id`, `token` unique, `capabilities` jsonb (e.g. `{ read: true, write: false }`), `created_by`, `expires_at` nullable, `revoked_at` nullable, `created_at`).
- `_core/share.ts`:
- `createShareLink(entityType, entityId, opts)``{ url, token, expiresAt }`.
- `resolveShareToken(token)``{ entityType, entityId, capabilities } | null` (rejects expired/revoked).
- `revokeShareLink(id)`.
- Verifies the entity type is registered and shareable per its manifest's `share` capability declaration.
- Token format: 32-byte URL-safe base64. Stored hashed (sha-256) — only the URL contains the raw token.
## Out of scope
- The viewer page (task 31).
- Rate limiting (task 61).
- Per-recipient access logs.
## Acceptance criteria
- [ ] Creating a link for an entity type with `share: undefined` throws.
- [ ] Tokens are stored hashed; raw token only returned at creation.
- [ ] Expired/revoked tokens return null.
- [ ] `/settings` shows active share links per entity with revoke buttons.
+27
View File
@@ -0,0 +1,27 @@
# 31 — Public share viewer (`/s/<token>`)
## Goal
Unauthenticated route that resolves a share token and renders the entity using its manifest's `loadForShare` adapter.
## Depends on
- 30
## Scope
- `/s/[token]/page.tsx` — server component. Calls `resolveShareToken`. If valid, calls the entity's `loadForShare(id)` and renders a per-type viewer component (each module exports a `<SharedView />`).
- If the token grants write capability, render an editable view (initially only relevant for lists — recipient can check items off).
- Friendly error page for invalid/expired/revoked tokens.
- Add `noindex` headers on the route.
## Out of scope
- Auth on the share page (deliberately public; security is the unguessable token).
- Comments / reactions from anonymous viewers.
## Acceptance criteria
- [ ] Calendar event share renders title/time/location/notes — read-only.
- [ ] Shopping list share with `write: true` lets a recipient toggle items; toggles round-trip back into the household (with `actor_id = null` in activity log, action `share.toggle`).
- [ ] Middleware does not gate `/s/*`.
+28
View File
@@ -0,0 +1,28 @@
# 40 — Web Push (VAPID)
## Goal
Subscribe browsers to web push and deliver notifications to them.
## Depends on
- 06 (auth), 50 (PWA shell — needed for iOS to allow push). If 50 isn't done, this task can land but iOS won't fire until then.
## Scope
- `pnpm add web-push` for server, use built-in `PushManager` on client.
- VAPID keys generated once (script: `pnpm vapid:generate`), stored in `.env` as `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY`. Document in README.
- Schema: `push_subscriptions` (`id`, `user_id`, `endpoint` unique, `p256dh`, `auth`, `user_agent`, `created_at`).
- Service worker registers on app load; on user opt-in (button in `/settings`), prompts permission and stores subscription via server action.
- Server helper `sendPush(userId, { title, body, url })` iterates subscriptions, removes any returning 404/410.
## Out of scope
- Notification preferences per category (defer).
- ntfy fallback (task 42).
## Acceptance criteria
- [ ] Opt-in flow works in Chrome desktop and mobile, plus iOS Safari (when PWA installed).
- [ ] Stale subscriptions are pruned on send failure.
- [ ] Sending a test notification from `/settings` reaches all of the current user's subscribed devices.
+28
View File
@@ -0,0 +1,28 @@
# 41 — Reminders engine
## Goal
A generic, entity-agnostic reminder system that fires at `fire_at` and routes through the notification bus.
## Depends on
- 40 (push), 42 (bus)
## Scope
- Schema: `reminders` (`id`, `household_id`, `entity_type`, `entity_id`, `fire_at` timestamptz, `channel` text default `'auto'`, `fired_at` timestamptz nullable, `created_by`).
- Worker: a long-running tick (every 30s) that selects due un-fired reminders, calls `notify(...)`, marks `fired_at`. Runs in the same Node process via `setInterval` guarded by a Postgres advisory lock to allow future horizontal scaling.
- `_core/reminders.ts` exposes `scheduleReminder(...)`, `cancelReminder(id)`, `listReminders(entityType, entityId)`.
- Notes module's `remind_at` field calls `scheduleReminder` on save and `cancelReminder` when cleared.
- Calendar module: optional "remind me" on event create (default 30 min before).
## Out of scope
- Recurring reminders (use rrule later).
- Snooze.
## Acceptance criteria
- [ ] A note with `remind_at = now + 1min` fires within 90s and triggers a push notification.
- [ ] Editing the note's `remind_at` updates the existing reminder, not creates duplicates.
- [ ] Worker survives DB blips (caught + logged, not crashed).
+28
View File
@@ -0,0 +1,28 @@
# 42 — Notification bus + ntfy adapter
## Goal
Single `notify()` entrypoint that fans out to web push, in-app inbox, and (optionally) ntfy.
## Depends on
- 40
## Scope
- `_core/notify.ts``notify(userId, { title, body, url, channels? })`.
- Channels: `push`, `inapp`, `ntfy`. Default: `['push', 'inapp']`.
- In-app inbox: `notifications` table (`id`, `user_id`, `title`, `body`, `url`, `read_at`, `created_at`); bell icon in header with unread count and dropdown.
- ntfy adapter: if `NTFY_URL` and `NTFY_TOPIC` env are set, also POSTs there. Off by default; Matt opts in.
- Per-user toggles in `/settings` for each channel.
## Out of scope
- Email notifications.
- Per-event-type routing rules (Matt can add later as a module).
## Acceptance criteria
- [ ] `notify()` with default channels delivers push + in-app and ignores ntfy.
- [ ] Setting `NTFY_URL`+`NTFY_TOPIC` and toggling on in settings adds ntfy delivery.
- [ ] Unread count is accurate and clears on read.
+28
View File
@@ -0,0 +1,28 @@
# 50 — Manifest, icons, install prompt
## Goal
Make famapp installable as a PWA on iOS and Android.
## Depends on
- 02
## Scope
- `public/manifest.webmanifest` with name, short_name, theme_color, background_color, display: `standalone`, start_url `/`.
- App icons (192, 384, 512, maskable). Source SVG committed; export script in `scripts/`.
- `apple-touch-icon` link in root layout.
- Service worker registration (next-pwa or hand-rolled — prefer hand-rolled to avoid coupling to a library that lags Next.js releases).
- Install prompt: a small banner on first visit on supported browsers, dismissible.
## Out of scope
- Offline caching (task 51).
- Background sync.
## Acceptance criteria
- [ ] Lighthouse "Installable" check passes.
- [ ] Installing on iOS 16.4+ home screen produces a standalone window with no Safari chrome.
- [ ] App icon shows correctly on Android home screen with maskable variant.
+28
View File
@@ -0,0 +1,28 @@
# 51 — Offline shell + service worker caching
## Goal
The app shell loads without network; cached data is shown with a "stale" indicator while fresh data loads.
## Depends on
- 50
## Scope
- Service worker uses Workbox-style strategies (hand-rolled, see task 50 rationale):
- **App shell** (HTML, JS, CSS): stale-while-revalidate.
- **API GETs**: network-first with 2s timeout, fall back to cache.
- **Mutations**: never cache; if offline, show a clear "you're offline" toast.
- Offline page for navigations that have no cache.
- Version SW on every build; show a "new version available, refresh" toast when a new SW takes control.
## Out of scope
- Background sync of queued mutations (defer; usually more trouble than worth at this scale).
## Acceptance criteria
- [ ] In Chrome DevTools "Offline", reloading the app shows the cached dashboard with a stale-indicator.
- [ ] Attempting to mutate while offline shows a clear error, doesn't silently drop.
- [ ] Deploying a new build prompts active sessions to refresh.
+27
View File
@@ -0,0 +1,27 @@
# 60 — Postgres backups (pg_dump cron)
## Goal
Nightly compressed pg_dump of `famapp-db` (and `authentik-db`) to a host volume, with a retention policy.
## Depends on
- 05
## Scope
- `deploy/backups/` directory committed.
- Add a `famapp-backup` service to compose: small Alpine container with `postgresql-client` + `cron` + a script that runs `pg_dump -Fc` against both DBs into `/backups/<db>/<YYYY-MM-DD>.dump`.
- Retention: keep last 14 daily, last 8 weekly, last 6 monthly. Pure shell, no fancy tooling.
- Document restore procedure in `deploy/backups/README.md`.
## Out of scope
- Off-site replication (Matt's call; he can rsync `/backups` elsewhere).
- Encryption at rest (handled at the disk layer).
## Acceptance criteria
- [ ] After a day, dump files exist for both DBs.
- [ ] `restore.sh <dump-file>` script exists and works against a fresh DB.
- [ ] Retention script keeps the right counts.
+24
View File
@@ -0,0 +1,24 @@
# 61 — Rate limiting on share links
## Goal
Prevent token brute-forcing on `/s/<token>`.
## Depends on
- 31
## Scope
- IP + token-prefix bucket. Reject after N failed lookups per minute per IP. In-memory LRU is fine for one Node process; document switch to Redis if multi-process arrives.
- Failed `resolveShareToken` calls increment the bucket; successful resolves do not.
- Generic `lib/rate-limit.ts` so other endpoints can use the same primitive.
## Out of scope
- Captcha.
## Acceptance criteria
- [ ] 50 bad tokens from one IP in a minute returns 429 thereafter.
- [ ] Legitimate access from another IP unaffected.
+27
View File
@@ -0,0 +1,27 @@
# 62 — Structured logging
## Goal
JSON logs for the app, easy to grep and ship later.
## Depends on
- 02
## Scope
- `pino` for app logs; pretty-print only in dev.
- Log levels via `LOG_LEVEL` env (default `info`).
- Request log middleware (method, path, status, duration, userId if present).
- Errors: capture stack + cause, never log secrets.
## Out of scope
- Log shipping (Loki/etc).
- Sentry/error tracking.
## Acceptance criteria
- [ ] Production logs are single-line JSON.
- [ ] No secret values appear anywhere in the log stream during a typical session.
- [ ] Request logs include duration and userId when authenticated.
+68
View File
@@ -0,0 +1,68 @@
# Task briefs
Each file here is a self-contained brief for a sub-session (typically Sonnet) to execute one chunk of work. Briefs assume the executor has read [`/CLAUDE.md`](../../CLAUDE.md) — do not restate the stack or architecture there.
## How to use a brief
1. Read `/CLAUDE.md` first.
2. Read the task file end-to-end.
3. Stop when **all** acceptance criteria pass. Do not bolt on extra scope.
4. If a brief turns out to be wrong, update the brief in the same commit as the code.
## Brief format
Every task file has these sections:
- **Goal** — one sentence.
- **Why** — what this unlocks; how it fits the larger plan.
- **Depends on** — task numbers that must be done first.
- **Scope** — bullet list of what to build.
- **Out of scope** — explicit non-goals to prevent drift.
- **Acceptance criteria** — checkable list. Done = all checked.
- **Notes** — gotchas, recommended libs, sketches.
## Phase index
### Phase 1 — Scaffold
- [01 — Repo init & tooling](01-repo-init.md)
- [02 — Next.js app skeleton](02-nextjs-skeleton.md)
- [03 — Drizzle + Postgres setup](03-drizzle-postgres.md)
- [04 — Module loader & registry](04-module-loader.md)
- [05 — Compose stack & Caddy](05-compose-caddy.md)
- [06 — Authentik install + OIDC integration](06-authentik-oidc.md)
- [07 — Household seeding & session](07-household-seed.md)
### Phase 2 — Core modules
- [10 — Calendar module](10-calendar-module.md)
- [11 — Lists module (shopping + task)](11-lists-module.md)
- [12 — Notes module](12-notes-module.md)
### Phase 3 — Dashboard & UX
- [20 — Dashboard composition](20-dashboard.md)
- [21 — Quick-add registry](21-quick-add.md)
- [22 — Activity log](22-activity-log.md)
### Phase 4 — Sharing
- [30 — Share-link service](30-share-links.md)
- [31 — Public share viewer (`/s/<token>`)](31-share-viewer.md)
### Phase 5 — Notifications & reminders
- [40 — Web Push (VAPID)](40-web-push.md)
- [41 — Reminders engine](41-reminders.md)
- [42 — Notification bus + ntfy adapter](42-notification-bus.md)
### Phase 6 — PWA polish
- [50 — Manifest, icons, install prompt](50-pwa-shell.md)
- [51 — Offline shell + service worker caching](51-offline.md)
### Phase 7 — Hardening
- [60 — Postgres backups (pg_dump cron)](60-backups.md)
- [61 — Rate limiting on share links](61-rate-limit.md)
- [62 — Structured logging](62-logging.md)
+33
View File
@@ -0,0 +1,33 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import globals from "globals";
export default tseslint.config(
{
ignores: [
"node_modules/**",
".next/**",
"dist/**",
"drizzle/**",
"*.config.js",
"*.config.mjs",
"*.config.cjs",
],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.browser,
},
},
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
},
);
+26
View File
@@ -0,0 +1,26 @@
{
"name": "famapp",
"version": "0.0.0",
"private": true,
"type": "module",
"packageManager": "pnpm@10.33.3",
"engines": {
"node": ">=22"
},
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^22.9.0",
"eslint": "^9.15.0",
"globals": "^15.12.0",
"prettier": "^3.3.3",
"typescript": "^5.6.3",
"typescript-eslint": "^8.15.0"
}
}
+990
View File
@@ -0,0 +1,990 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@eslint/js':
specifier: ^10.0.1
version: 10.0.1(eslint@9.39.4)
'@types/node':
specifier: ^22.9.0
version: 22.19.17
eslint:
specifier: ^9.15.0
version: 9.39.4
globals:
specifier: ^15.12.0
version: 15.15.0
prettier:
specifier: ^3.3.3
version: 3.8.3
typescript:
specifier: ^5.6.3
version: 5.9.3
typescript-eslint:
specifier: ^8.15.0
version: 8.59.2(eslint@9.39.4)(typescript@5.9.3)
packages:
'@eslint-community/eslint-utils@4.9.1':
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.12.2':
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/config-array@0.21.2':
resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/config-helpers@0.4.2':
resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/core@0.17.0':
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/eslintrc@3.3.5':
resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@10.0.1':
resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
peerDependencies:
eslint: ^10.0.0
peerDependenciesMeta:
eslint:
optional: true
'@eslint/js@9.39.4':
resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.7':
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/plugin-kit@0.4.1':
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
'@humanfs/node@0.16.8':
resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
engines: {node: '>=18.18.0'}
'@humanfs/types@0.15.0':
resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
engines: {node: '>=18.18.0'}
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
engines: {node: '>=12.22'}
'@humanwhocodes/retry@0.4.3':
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@22.19.17':
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
'@typescript-eslint/eslint-plugin@8.59.2':
resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.59.2
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/parser@8.59.2':
resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.59.2':
resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.59.2':
resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.59.2':
resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.59.2':
resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.59.2':
resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.59.2':
resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.59.2':
resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.59.2':
resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
hasBin: true
ajv@6.15.0:
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
brace-expansion@1.1.14:
resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==}
brace-expansion@5.0.5:
resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
engines: {node: 18 || 20 || >=22}
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
eslint-scope@8.4.0:
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-visitor-keys@4.2.1:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@5.0.1:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@9.39.4:
resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
peerDependencies:
jiti: '*'
peerDependenciesMeta:
jiti:
optional: true
espree@10.4.0:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
esquery@1.7.0:
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
engines: {node: '>=0.10'}
esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
engines: {node: '>=4.0'}
estraverse@5.3.0:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
flat-cache@4.0.1:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
globals@15.15.0:
resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
engines: {node: '>=18'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
ignore@7.0.5:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
prettier@3.8.3:
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
engines: {node: '>=14'}
hasBin: true
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
semver@7.7.4:
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
hasBin: true
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
ts-api-utils@2.5.0:
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
typescript-eslint@8.59.2:
resolution: {integrity: sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
snapshots:
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)':
dependencies:
eslint: 9.39.4
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
'@eslint/config-array@0.21.2':
dependencies:
'@eslint/object-schema': 2.1.7
debug: 4.4.3
minimatch: 3.1.5
transitivePeerDependencies:
- supports-color
'@eslint/config-helpers@0.4.2':
dependencies:
'@eslint/core': 0.17.0
'@eslint/core@0.17.0':
dependencies:
'@types/json-schema': 7.0.15
'@eslint/eslintrc@3.3.5':
dependencies:
ajv: 6.15.0
debug: 4.4.3
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.1.1
minimatch: 3.1.5
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
'@eslint/js@10.0.1(eslint@9.39.4)':
optionalDependencies:
eslint: 9.39.4
'@eslint/js@9.39.4': {}
'@eslint/object-schema@2.1.7': {}
'@eslint/plugin-kit@0.4.1':
dependencies:
'@eslint/core': 0.17.0
levn: 0.4.1
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/types': 0.15.0
'@humanfs/node@0.16.8':
dependencies:
'@humanfs/core': 0.19.2
'@humanfs/types': 0.15.0
'@humanwhocodes/retry': 0.4.3
'@humanfs/types@0.15.0': {}
'@humanwhocodes/module-importer@1.0.1': {}
'@humanwhocodes/retry@0.4.3': {}
'@types/estree@1.0.8': {}
'@types/json-schema@7.0.15': {}
'@types/node@22.19.17':
dependencies:
undici-types: 6.21.0
'@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.59.2
'@typescript-eslint/type-utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.59.2
eslint: 9.39.4
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.59.2
'@typescript-eslint/types': 8.59.2
'@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.59.2
debug: 4.4.3
eslint: 9.39.4
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.59.2(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3)
'@typescript-eslint/types': 8.59.2
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.59.2':
dependencies:
'@typescript-eslint/types': 8.59.2
'@typescript-eslint/visitor-keys': 8.59.2
'@typescript-eslint/tsconfig-utils@8.59.2(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.59.2(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.59.2
'@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3)
'@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3)
debug: 4.4.3
eslint: 9.39.4
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.59.2': {}
'@typescript-eslint/typescript-estree@8.59.2(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.59.2(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3)
'@typescript-eslint/types': 8.59.2
'@typescript-eslint/visitor-keys': 8.59.2
debug: 4.4.3
minimatch: 10.2.5
semver: 7.7.4
tinyglobby: 0.2.16
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.59.2(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4)
'@typescript-eslint/scope-manager': 8.59.2
'@typescript-eslint/types': 8.59.2
'@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3)
eslint: 9.39.4
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.59.2':
dependencies:
'@typescript-eslint/types': 8.59.2
eslint-visitor-keys: 5.0.1
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
acorn@8.16.0: {}
ajv@6.15.0:
dependencies:
fast-deep-equal: 3.1.3
fast-json-stable-stringify: 2.1.0
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
argparse@2.0.1: {}
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
brace-expansion@1.1.14:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@5.0.5:
dependencies:
balanced-match: 4.0.4
callsites@3.1.0: {}
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
concat-map@0.0.1: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
debug@4.4.3:
dependencies:
ms: 2.1.3
deep-is@0.1.4: {}
escape-string-regexp@4.0.0: {}
eslint-scope@8.4.0:
dependencies:
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-visitor-keys@3.4.3: {}
eslint-visitor-keys@4.2.1: {}
eslint-visitor-keys@5.0.1: {}
eslint@9.39.4:
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4)
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.2
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.5
'@eslint/js': 9.39.4
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.8
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
ajv: 6.15.0
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.3
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.7.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 8.0.0
find-up: 5.0.0
glob-parent: 6.0.2
ignore: 5.3.2
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
lodash.merge: 4.6.2
minimatch: 3.1.5
natural-compare: 1.4.0
optionator: 0.9.4
transitivePeerDependencies:
- supports-color
espree@10.4.0:
dependencies:
acorn: 8.16.0
acorn-jsx: 5.3.2(acorn@8.16.0)
eslint-visitor-keys: 4.2.1
esquery@1.7.0:
dependencies:
estraverse: 5.3.0
esrecurse@4.3.0:
dependencies:
estraverse: 5.3.0
estraverse@5.3.0: {}
esutils@2.0.3: {}
fast-deep-equal@3.1.3: {}
fast-json-stable-stringify@2.1.0: {}
fast-levenshtein@2.0.6: {}
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
path-exists: 4.0.0
flat-cache@4.0.1:
dependencies:
flatted: 3.4.2
keyv: 4.5.4
flatted@3.4.2: {}
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
globals@14.0.0: {}
globals@15.15.0: {}
has-flag@4.0.0: {}
ignore@5.3.2: {}
ignore@7.0.5: {}
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
resolve-from: 4.0.0
imurmurhash@0.1.4: {}
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
isexe@2.0.0: {}
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
json-buffer@3.0.1: {}
json-schema-traverse@0.4.1: {}
json-stable-stringify-without-jsonify@1.0.1: {}
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
type-check: 0.4.0
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
lodash.merge@4.6.2: {}
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.5
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.14
ms@2.1.3: {}
natural-compare@1.4.0: {}
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
fast-levenshtein: 2.0.6
levn: 0.4.1
prelude-ls: 1.2.1
type-check: 0.4.0
word-wrap: 1.2.5
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
path-exists@4.0.0: {}
path-key@3.1.1: {}
picomatch@4.0.4: {}
prelude-ls@1.2.1: {}
prettier@3.8.3: {}
punycode@2.3.1: {}
resolve-from@4.0.0: {}
semver@7.7.4: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
strip-json-comments@3.1.1: {}
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
ts-api-utils@2.5.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
typescript-eslint@8.59.2(eslint@9.39.4)(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3)
'@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@5.9.3)
eslint: 9.39.4
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
typescript@5.9.3: {}
undici-types@6.21.0: {}
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
which@2.0.2:
dependencies:
isexe: 2.0.0
word-wrap@1.2.5: {}
yocto-queue@0.1.0: {}
+2
View File
@@ -0,0 +1,2 @@
packages:
- "."
+1
View File
@@ -0,0 +1 @@
export {};
+29
View File
@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowJs": false,
"noEmit": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*", "*.ts", "*.mts", "*.cts"],
"exclude": ["node_modules", ".next", "dist", "drizzle"]
}