From 3d9988ee3ab58f063d0d63db6fab3d49d9473680 Mon Sep 17 00:00:00 2001 From: RedStar Date: Sat, 17 Oct 2026 17:20:47 +0200 Subject: [PATCH 01/19] docs: plan the v2 restructuring and record its decisions Diagnoses the current dashboard, auth, source-control and deployment problems, proposes the supastarter-style workspace layout, and records the maintainer's answers on layout, ESM, licensing, billing, forges and deployment targets. --- .../2026-10-17-code-zero-v2-brainstorm.md | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 docs/plans/2026-10-17-code-zero-v2-brainstorm.md diff --git a/docs/plans/2026-10-17-code-zero-v2-brainstorm.md b/docs/plans/2026-10-17-code-zero-v2-brainstorm.md new file mode 100644 index 0000000..405ef36 --- /dev/null +++ b/docs/plans/2026-10-17-code-zero-v2-brainstorm.md @@ -0,0 +1,319 @@ +# Code Zero v2 — brainstorming e piani di ristrutturazione + +Stato: **decisioni prese** (2026-10-17, vedi §10). Scritta prima di toccare codice: ogni sezione termina con le +decisioni prese per default e le domande aperte. Le risposte alle domande in fondo cambiano il piano +esecutivo (docs/plans/-.md), che verrà scritto solo dopo. + +Fonti consultate: stato del repo su `feat/dashboard-live-control` (c51178d), `docs/PLAN.md` +(2026-09-05), supastarter.dev/nuxt, forges.link (introduction, installation, providers, data-model, +webhooks, env, capability-matrix, github), pullfrog (repo, action.yml, package.json, docs), +Blacksmith [code]smith (overview, pricing), Dokploy (pricing, blog licenza), harlan-agent-kit, Better +Auth (passkey, 2fa, admin), skilld.dev. + +--- + +## 0. Diagnosi: cosa non va oggi + +| Area | Problema osservato | Evidenza | +| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| Dashboard | Le viste esistono solo per `/`, `/audit`, `/organizations`; nessuna pagina per repository, runner, modelli, findings, impostazioni, membri, sicurezza. | `apps/dashboard/app/pages` | +| Dashboard | Logica di controllo (`poller`, `store`, `overview`, `access`, `repositories`) vive in `server/utils` dell'app, non in `packages/api`: non testabile fuori da Nitro e non riusabile da un worker o dall'Action. | `apps/dashboard/server/utils/*` (11 file) | +| Dashboard | `modules/*` dell'app sono moduli Nuxt "feature" ma condividono tipi duplicati con `packages/api` (`types/dashboard.ts`, `types/audit.ts`). | `apps/dashboard/modules/*/types` | +| Auth | Organizzazioni, inviti e device flow ci sono, ma dietro flag ambiente valutati **a build time** (`authConfigFromEnvironment()` in `nuxt.config.ts`): cambiare policy richiede rebuild. Mancano passkey, 2FA, admin/impersonation, ruoli org con permessi. | `nuxt.config.ts`, `packages/auth/src/auth.ts` | +| Auth | Gli inviti passano da `@octopi-ai/better-enrollment` **e** dal plugin `organization` di Better Auth: due modelli (invite/inviteUse + invitation) per lo stesso concetto. | `packages/database/src/schema/{enrollment,organization}.ts` | +| Source control | Adapter GitHub/GitLab/Bitbucket/Gitea scritti a mano (16 file + 9 test), con un solo endpoint webhook GitHub nell'app. Da sostituire con `forges`. | `packages/source-control/src/providers`, `server/routes/webhooks/github.post.ts` | +| Repo | `packages/github/` è una cartella residua (solo `dist`, nessun `package.json`, non tracciata): da rimuovere. | `git ls-files packages/github` vuoto | +| Esecuzione | Il poller e l'esecuzione dei run girano **dentro il processo Nitro**: impossibile su serverless (Cloudflare/Vercel), e un run lungo muore con l'HMR in dev. `docs/PLAN.md` lo segnala come rischio. | `server/plugins/poller.ts`, `docs/PLAN.md` "Rischi" | +| Store | Task e audit in KV (`fs-lite`/Upstash) mentre auth è in Postgres: due store, nessuna transazione, `list()` legge tutto a ogni overview. | `packages/api/src/storage.ts`, `docs/PLAN.md` | +| Config | Quattro file `.env` per quattro processi più `code-zero.deployment.yml`: corretto per principio, ma l'onboarding self-host è lungo. Nessun `docker compose up` che funzioni da solo. | `.env.example`, `apps/dashboard/.env.example` | +| Stile | Token oklch in `main.css` + `uno.theme.ts` ben fatti, ma duplicati fra dashboard e marketing; nessun `packages/ui`, nessun `DESIGN.md`. | `apps/dashboard/app/assets/css/main.css`, `docs/PLAN.md` "Cosa resta" | +| Business | Nessun concetto di edizione (self-hosted vs cloud), piani, entitlement, billing. `@better-auth/infra` è l'unico hook "cloud". | `packages/auth/src/auth.ts` | + +Cosa invece **è solido e si tiene**: `packages/agent` (state machine), `packages/runner` (boundary +di esecuzione e sandbox), `packages/models`, `packages/config`, `packages/shared`, toolchain (aube, +turbo, tsdown, oxlint/oxfmt, knip, tsgo), i18n centralizzato, mail con Maizzle, i token di design. + +Decisione di fondo: **"quasi da zero" = riscrivere il bordo (apps, api, auth, database, +source-control) e la struttura del workspace; non riscrivere il runtime.** Il runtime ha test +deterministici e nessuna dipendenza da HTTP: rifarlo è costo senza guadagno. + +--- + +## 1. Struttura target del workspace (modello supastarter) + +``` +apps/ + web/ # dashboard + auth + billing UI + API host (era apps/dashboard) + marketing/ # sito pubblico, prerender, copy in packages/i18n + docs/ # Docus, include docs/*.md + mail-preview/ # Maizzle dev-only +packages/ + core/ # runtime: agent, runner, models, config, shared (resta com'è, 5 pacchetti) + forge/ # NUOVO: wrapper di `forges` + contratti Code Zero (sostituisce source-control) + database/ # schema Drizzle (auth + org + billing + tasks + audit + repos), migrazioni + auth/ # Better Auth options factory: org, passkey, 2FA, admin, magic link, device, bearer + api/ # oRPC router + control-plane operations + scheduler (senza Nitro) + worker/ # NUOVO: esecuzione dei run fuori dal processo HTTP (Docker/Node long-running) + action/ # NUOVO: GitHub Action / GitLab CI component autonomi (modello pullfrog) + edition/ # NUOVO: entitlement, piani, feature flag self-hosted vs cloud + payments/ # NUOVO (solo cloud): provider Stripe/Polar/Lemonsqueezy dietro un contratto + ui/ # NUOVO: token oklch, UnoCSS preset, componenti base condivisi (web+marketing+docs) + mail/ # template transazionali (resta) + i18n/ # locali (resta) + cli/ # `zero` (resta; parla con api via oRPC client) + build-env/ # resta +tooling/ + oxc/ # resta + tsconfig/ # NUOVO: tsconfig.base per app, pacchetto, nuxt-layer +docs/ # architettura, provider, piani +.agents/skills/ # skill del progetto (vedi §7) +``` + +Note: + +- `agent/runner/models/config/shared` si spostano sotto `packages/core/*` (Q1); i nomi npm non + cambiano, quindi gli import restano identici. +- `apps/dashboard` diventa `apps/web` (Q2). +- Un pacchetto per capability, mai `utils`. Regola già in `code-zero-architecture`; si conferma. + +--- + +## 2. Piano A — Auth e organizzazioni complete + +Obiettivo: parità con supastarter su auth, con la policy di Code Zero (observe by default, fail +closed). + +**Plugin Better Auth da registrare in `packages/auth`:** + +| Capacità | Plugin | Note | +| ---------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Email+password, verifica email, reset | core | già presente | +| OAuth GitHub, GitLab, Google | core `socialProviders` | GitLab/Google sono nuovi; GitHub serve anche per collegare l'account al forge | +| Magic link | `magicLink` | via `packages/mail` | +| Passkey | `@better-auth/passkey` | tabella `passkey`; rpID = host della web app | +| 2FA TOTP + backup codes + trusted device | `twoFactor` | `issuer: "Code Zero"`, OTP via mail opzionale | +| Organizzazioni, membri, inviti, team | `organization` | ruoli `owner/admin/member` + `createAccessControl` con statement Code Zero: `repository:{read,write,configure}`, `task:{create,approve,cancel}`, `policy:{write}`, `billing:{manage}`, `member:{invite,remove}` | +| Admin globale, ban, impersonation | `admin` | sostituisce il check `role=admin` manuale in `packages/api/src/access.ts` | +| API key per CI/macchine | `apiKey` | sostituisce `CODE_ZERO_CONTROL_PLANE_TOKENS` (token in env): chiavi per-organizzazione, revocabili dalla UI, con rate limit | +| Device flow per `zero login` | `deviceAuthorization` + `bearer` | già presente | +| Multi-sessione, ultimo metodo di login | `multiSession`, `lastLoginMethod` | già presenti | +| OAuth proxy per preview | `oAuthProxy` | già presente | +| Cloud: sentinel/dash | `@better-auth/infra` | solo edizione cloud (vedi Piano F) | + +**Decisioni:** + +- **Tenere** `@octopi-ai/better-enrollment` per gli inviti (Q3): è più completo degli inviti base + di Better Auth (link pubblici con limite d'uso, scadenza, prova di possesso della mailbox). Il + plugin `organization` resta per org, membri e ruoli; il suo flusso di invito nativo non si espone + nella UI, così il modello utente è uno solo. +- Policy auth letta **a runtime** dal file di deployment (`code-zero.deployment.yml: auth:`) e dal + DB (impostazioni istanza), non più a build time. La pagina di login chiede al server quali metodi + sono attivi (`auth.capabilities` in oRPC) invece di leggerli da `appConfig`. +- Multi-tenant per default: ogni risorsa (repository, task, policy, chiave API, audit) ha + `organizationId`. Self-hosted single-user = un'org creata al primo avvio (onboarding wizard). + +**Pagine web (apps/web):** +`/login`, `/signup`, `/forgot`, `/reset`, `/verify`, `/two-factor`, `/passkey`, +`/invite/[id]`, `/onboarding`, `/settings/account`, `/settings/security` (passkey, 2FA, sessioni), +`/settings/api-keys`, `/org/[slug]/settings/{general,members,billing,policies}`, `/admin/{users,orgs,audit}`. + +--- + +## 3. Piano B — Stile e UI condivisi + +- Creare `packages/ui`: `theme.css` (i token oklch attuali, light/dark), `uno.preset.ts` + (shortcuts `panel`, `btn-*`, `input-field`, `label-upper`, preset a11y/rtl), componenti base in + Vue (`CzButton`, `CzPanel`, `CzBadge`, `CzTable`, `CzDialog`, `CzForm*`, `CzEmpty`, `CzTimeline`), + esportati come **Nuxt layer** (`packages/ui/nuxt.config.ts`) così web, marketing e docs lo + estendono con `extends: ['@code-zero/ui']`. +- **Non** adottare Nuxt UI: lo stile corrente è UnoCSS + token custom e il piano 2026-09 lo + conferma; Nuxt UI porterebbe un secondo sistema di token. (Q4) +- `DESIGN.md` nella root del pacchetto: contratto verificabile (griglia 32px, raggio 0, font Inter, + accent verde oklch, nessun gradiente oltre `--cz-grid`), usato dalla skill + `wolfstar-agent-kit:nuxt-frontend-review` prima di ogni merge UI. +- Skill UI da usare in esecuzione: `impeccable` (audit/polish), `web-design-guidelines` (review), + `accessibility`, `core-web-vitals`/`performance` (marketing e docs), `baseline-ui` (deslop), + `nuxt-frontend-design` del kit. Già installate in `.agents/skills`. + +--- + +## 4. Piano C — Multi-forge con `forges` + +`forges` (ESM-only, Node ≥ 22.18, zero dipendenze, `fetch` globale) copre GitHub, GitLab, +Bitbucket, Forgejo, Gitea, Gitee, Azure DevOps, Cursor Origin, Tangled, Pushin, Cloudflare +Artifacts, con `forge.can('writes.comment')` per degradare in modo esplicito, `webhooks.ingest` +per la verifica firma, `forges/env` per la configurazione, `forges/fake` e `forges/testing` per i +test deterministici. + +**Nuovo pacchetto `packages/forge`** (sostituisce `packages/source-control`): + +``` +src/ + index.ts # createForgeRegistry(config) → { get(kind, instance), list() } + contracts.ts # ChangeRequestRef, ReviewEvent, RunOutcome, StatusPublication (si tengono: shared le usa) + ingest.ts # ForgeEvent (forges) → ReviewTrigger/ReviewEvent di Code Zero; filtro ignoreAuthors/allowBots + publish.ts # RunOutcome → checks.report (fallback commit status), writes.comment con l'evidence + issue-to-pr.ts # contents.commitFiles({createFrom}) + writes.create(kind:'pull_request') + capabilities.ts # mappa forge.can(...) → ProviderCapabilities (il contratto resta onesto) + env.ts # providersFromEnv + variabili CODE_ZERO_* legacy → FORGES_* (compat) + fake.ts # re-export di forges/fake con fixture Code Zero +``` + +- Un endpoint webhook per forge: `apps/web/server/routes/webhooks/[forge].post.ts` → + `registry.get(forge).webhooks.ingest({headers, body})`. +- Credenziali: per-organizzazione nel DB (`forge_connection`: kind, instance, baseUrl, auth + cifrata, webhookSecret) **oppure** da env `FORGES_*` per il self-host minimale. Cifratura con + chiave `CODE_ZERO_ENCRYPTION_KEY` (AES-GCM, `node:crypto`/WebCrypto, funziona su Workers). +- GitHub App (appId/privateKey/installationId) come percorso consigliato per cloud; PAT per + self-host. +- Tutti i pacchetti diventano **ESM-only** (Q5), allineati a `forges` che non pubblica CJS. +- Matrice capacità da esporre in `docs/source-control-providers.md` generata dalla matrice di + forges, non mantenuta a mano. + +--- + +## 5. Piano D — `packages/action`: esecuzione autonoma senza dashboard (modello pullfrog) + +Il pacchetto rende Code Zero usabile come **GitHub Action** (e componente GitLab CI) senza alcun +control plane: il run gira nel CI del repository target con la chiave del cliente. + +``` +packages/action/ + action.yml # using: node24, main: dist/entry.mjs, post: dist/post.mjs, post-if: always() + src/entry.ts # legge inputs, risolve trigger (@code-zero mention, label, PR aperta, CI rossa, prompt) + src/post.ts # revoca token, carica evidence come artifact, chiude il commento di progresso + src/run.ts # compone agent+runner(local)+models+forge(github token del job) → runTask + src/inputs.ts # schema zod degli input + src/report.ts # commento di progresso aggiornato + check run con l'evidence + templates/code-zero.yml # workflow pronto, scritto da `zero init --action` +``` + +Input (allineati a pullfrog ma con la policy Code Zero): `mode` (observe|suggest|fix, default +observe), `prompt`/`prompt_file`, `model`, `timeout`, `push` (disabled|restricted|enabled), `shell`, +`output_schema`, `token`. Output: `result`, `evidence_path`. + +Decisioni: + +- **Come pullfrog** (Q6): l'Action offre due harness. `code-zero` (il runtime interno, default per + `observe`) e gli agent esterni `claude-code`, `codex`, `opencode`, scelti dal modello + configurato. Gli agent esterni girano con token del job a scadenza, env ripulito, shell e push + governati dagli input `shell`/`push`, e l'evidence passa sempre dal verificatore Code Zero. +- Token GitHub del job (`${{ github.token }}`) usato tramite `forges/github` con `auth.token`. +- Il **worker** (`packages/worker`) e l'Action condividono `runTask` da `packages/api` (o un nuovo + `packages/engine` se `api` deve restare senza dipendenze dal runtime: oggi `api` importa già + `agent/runner/models`, quindi si usa `api`). +- Opzionale: l'Action può **riportare** al control plane (`CODE_ZERO_URL` + API key) per + comparire nella dashboard. Questo è anche il modo in cui una dashboard **serverless** esegue + run: li delega all'Action o al worker (Piano G). + +--- + +## 6. Piano E — Coerenza del codice + +- Un solo stile di modulo: ESM, `exports` espliciti, `tsdown` con `scripts/tsdown.config.ts`. +- Una sola fonte dei tipi wire: gli schemi zod del router oRPC in `packages/api`; le app importano + `RouterClient`; sparisce `modules/*/types`. +- Un solo store: Postgres via Drizzle per **tutto** (task, eventi, audit, repository, connessioni + forge, chiavi). Il KV resta solo per cache/lock (claim dei webhook) con driver unstorage. + Cloudflare è solo per il cloud managed (Q7) e usa Postgres via Hyperdrive; D1 **no**. Il self-host + serverless (Vercel, Netlify) usa Postgres gestito (Neon, Supabase, RDS). +- Convenzioni: `packages//src/index.ts` esporta solo contratti e factory; nessun `src/` cross + import; test accanto al sorgente; `README.md` per pacchetto con "owns / never does". +- Skill `wolfstar-agent-kit:pkg-conform` e `improve-ts-pkg-architecture` come gate in CI + (`scripts/check-repository.mjs` già esiste: estendere). +- Hook del kit (modello harlan-agent-kit): `aube`-only guard, `check` prima di commit/push, PR + solo via skill `pr`. Da installare in `.claude/hooks.json`. + +--- + +## 7. Piano F — Modello di business (Dokploy + pullfrog allo stato iniziale) + +| Edizione | Chi | Cosa include | Prezzo (ipotesi) | +| ---------------------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| **Self-hosted (OSS, Apache-2.0)** | tutti | tutto il runtime, dashboard, auth completa, multi-forge, Action, worker Docker, 1 org illimitata | gratis | +| **Cloud Free** | personal, repo pubblici | come self-hosted, hosting gestito, 30 run/mese | gratis (come pullfrog) | +| **Cloud Pro** | team, repo privati | run illimitati a costo per run o per seat, GitHub App condivisa, sandbox gestite, 2FA obbligatoria, audit 90 gg | 30 $/org/mese + costo modello pass-through (pullfrog) | +| **Enterprise** (cloud dedicato o self-hosted con supporto) | aziende | SSO/SAML+SCIM, RBAC fine, audit illimitato, white-label, SLA, supporto prioritario, istanza dedicata | contatto (Q10) | + +Decisioni: + +- Licenza: **tutto Apache-2.0** (Q8), incluse le feature Enterprise. Si monetizza hosting, + supporto, SLA e contratti, non il codice. +- `packages/edition`: `edition()` → `'self-hosted' | 'cloud'` da env; `entitlements(org)` → + limiti (run/mese, seat, retention) letti dal piano; in self-hosted ritorna sempre "illimitato". + Tutto il gating passa da qui, mai da `if (process.env.X)` sparsi. +- `packages/payments`: contratto `PaymentProvider` (`createCheckout`, `portal`, `webhook`) con + **solo Stripe** (Q9); tabelle `subscription`, `usage_event`. Caricato solo in cloud. +- Metering: ogni run scrive un `usage_event` (token in/out, minuti sandbox, provider); in cloud + alimenta la fattura, in self-host alimenta la pagina "Costi" della dashboard. + +--- + +## 8. Piano G — Deploy: Docker e serverless (Cloudflare, Vercel, Netlify) + +Il vincolo è uno: **l'esecuzione dei run (git, comandi, sandbox) non può vivere in una funzione +serverless**. Quindi il sistema si divide in due processi con un contratto chiaro: + +``` +apps/web (Nitro) packages/worker (Node/Docker) packages/action (CI del cliente) + UI, auth, oRPC ───► claim task da Postgres (SKIP LOCKED) run in GitHub Actions / GitLab CI + webhook ingest esegue con runner local/sandbox riporta con API key (opzionale) + scheduler: enqueue pubblica evidence via forge + SSE/polling overview heartbeat → runner visibili in UI +``` + +| Target | apps/web | Esecuzione | Store | +| --------------------------------------- | -------------------------- | ----------------------------------------------------------------- | ---------------------- | +| Docker Compose (self-host default) | container `web` | container `worker` (scala a N) | `postgres` nel compose | +| Node bare-metal | `.output/` | `zero worker` dalla CLI | Postgres esterno | +| Vercel | preset `vercel` | Vercel Sandbox adapter **oppure** worker remoto **oppure** Action | Neon/Postgres | +| Cloudflare (**solo cloud managed**, Q7) | preset `cloudflare-module` | Cloudflare Sandbox/Containers adapter | Hyperdrive→Postgres | +| Netlify | preset `netlify` | worker remoto o Action | Postgres | + +- `docker-compose.yml` + `Dockerfile` multi-stage (web, worker) nella root; `zero doctor` verifica + tutto. "Primo avvio" = `docker compose up` → onboarding wizard a `/onboarding`. +- Il poller attuale diventa un task del worker (o un Cron Trigger su Cloudflare / Vercel Cron che + chiama `POST /api/v1/internal/poll` firmato). +- ViteHub resta per KV/Queue dove già c'è; non è un requisito. + +--- + +## 9. Sequenza proposta (fasi, non ancora task) + +| Fase | Contenuto | Dipende da | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | +| 0 | Pulizia (`packages/github`), `tooling/tsconfig`, rinomina `apps/web`, `packages/ui` layer con i token attuali | — | +| 1 | `packages/database` v2: schema unico (auth+org+passkey+2FA+admin+apiKey, repository, forge_connection, task, task_event, audit, usage_event, subscription), migrazioni; `packages/edition` | 0 | +| 2 | `packages/auth` v2 + pagine auth/settings/org/admin in `apps/web`; onboarding wizard | 1 | +| 3 | `packages/forge` su `forges`; webhook per forge; rimozione `source-control` | 1 | +| 4 | `packages/api` v2: router per dominio (`auth`, `orgs`, `repos`, `forges`, `tasks`, `runs`, `audit`, `billing`), scheduler su Postgres, nessun Nitro dentro; `packages/worker` | 1, 3 | +| 5 | `apps/web` dashboard: pagine repository, task, run, approvals, runner, modelli, costi, policy; SSE | 2, 4 | +| 6 | `packages/action` + template workflow + `zero init --action` | 4 | +| 7 | Docker Compose, Dockerfile, preset Cloudflare/Vercel verificati, docs deployment | 4, 5 | +| 8 | `packages/payments` + piani cloud + metering UI | 1, 5 | +| 9 | Marketing e docs: pricing reale, pagina "self-host vs cloud", confronto con Codesmith/pullfrog | 8 | + +Ogni fase chiude con `aube run check:repo && lint:ci && typecheck && test && build` verdi e una PR +separata (refactor e feature in PR distinte). + +--- + +## 10. Decisioni (risposte del 2026-10-17) + +| # | Tema | Decisione | +| --- | ------------------ | ---------------------------------------------------------------------------------------------------------- | +| Q1 | Layout runtime | Spostare `agent/runner/models/config/shared` in `packages/core/*`. I nomi npm restano `@code-zero/`. | +| Q2 | Nome app | `apps/dashboard` diventa `apps/web` (`@code-zero/web`). | +| Q3 | Inviti | Tenere `@octopi-ai/better-enrollment`: migliore degli inviti base di Better Auth. | +| Q4 | UI kit | UnoCSS e token attuali in `packages/ui`, niente Nuxt UI. | +| Q5 | Moduli | Tutti i pacchetti ESM-only. | +| Q6 | Action | Come pullfrog: harness `code-zero`, `claude-code`, `codex`, `opencode`. | +| Q7 | Cloudflare | Solo per il cloud managed. Il self-host gira su Docker e su serverless (Vercel, Netlify, Node). | +| Q8 | Licenza | Tutto Apache-2.0. | +| Q9 | Pagamenti | Stripe. | +| Q10 | Prezzi | Free 30 run/mese, Pro 30 $/org/mese + pass-through, più Enterprise. | +| Q11 | Forge | GitHub, GitLab, Gitea/Forgejo, Bitbucket, più Azure DevOps. | +| Q12 | Skill | Dal catalogo skilld.dev, le migliori per progetto, UI, performance, codebase e stile. | +| Q13 | Chat panel | Dopo la v2. | +| Q14 | Slack/Discord, MCP | Dopo la v2. | +| Q15 | Branch | `feat/v2-foundation` da `origin/main`. | From c555d8ea952f2da05394f5180c2b6e595a8e7898 Mon Sep 17 00:00:00 2001 From: RedStar Date: Sat, 17 Oct 2026 17:20:51 +0200 Subject: [PATCH 02/19] chore(skills): add v2 stack skills from the skilld registry Pins Better Auth, nuxt-better-auth, Vue, UnoCSS, Vitest, Vue testing, Postgres/Drizzle, Stripe, taste, modern CSS, code review, GitHub Actions, Dockerfile, Cloudflare and anti-slop skills to their source commits. --- .../better-auth-best-practices/SKILL.md | 192 + .agents/skills/better-auth-security/SKILL.md | 432 + .agents/skills/cloudflare/SKILL.md | 151 + .../references/ai-gateway/README.md | 27 + .../references/ai-gateway/configuration.md | 18 + .../references/ai-gateway/dynamic-routing.md | 15 + .../references/ai-gateway/features.md | 22 + .../references/ai-gateway/sdk-integration.md | 19 + .../references/ai-gateway/troubleshooting.md | 16 + .../cloudflare/references/ai-search/README.md | 25 + .../cloudflare/references/ai-search/api.md | 13 + .../references/ai-search/configuration.md | 16 + .../references/ai-search/gotchas.md | 13 + .../references/ai-search/patterns.md | 15 + .../cloudflare/references/bindings/README.md | 16 + .../cloudflare/references/bindings/api.md | 19 + .../references/bindings/configuration.md | 20 + .../cloudflare/references/bindings/gotchas.md | 18 + .../references/bindings/patterns.md | 20 + .../references/browser-rendering/README.md | 18 + .../references/browser-rendering/api.md | 12 + .../browser-rendering/configuration.md | 12 + .../references/browser-rendering/gotchas.md | 15 + .../references/browser-rendering/patterns.md | 12 + .../skills/cloudflare/references/c3/README.md | 112 + .../skills/cloudflare/references/c3/api.md | 70 + .../cloudflare/references/c3/configuration.md | 81 + .../cloudflare/references/c3/gotchas.md | 92 + .../cloudflare/references/c3/patterns.md | 80 + .../references/containers/README.md | 23 + .../cloudflare/references/containers/api.md | 13 + .../references/containers/configuration.md | 15 + .../references/containers/gotchas.md | 16 + .../references/containers/patterns.md | 18 + .../references/cron-triggers/README.md | 20 + .../references/cron-triggers/api.md | 16 + .../references/cron-triggers/configuration.md | 16 + .../references/cron-triggers/gotchas.md | 17 + .../references/cron-triggers/patterns.md | 18 + .../skills/cloudflare/references/d1/README.md | 15 + .../skills/cloudflare/references/d1/api.md | 18 + .../cloudflare/references/d1/configuration.md | 18 + .../cloudflare/references/d1/gotchas.md | 18 + .../cloudflare/references/d1/patterns.md | 20 + .../references/hyperdrive/README.md | 27 + .../cloudflare/references/hyperdrive/api.md | 26 + .../references/hyperdrive/configuration.md | 27 + .../references/hyperdrive/gotchas.md | 20 + .../references/hyperdrive/patterns.md | 15 + .../cloudflare/references/images/README.md | 12 + .../cloudflare/references/images/api.md | 13 + .../references/images/configuration.md | 17 + .../cloudflare/references/images/gotchas.md | 16 + .../cloudflare/references/images/patterns.md | 16 + .../skills/cloudflare/references/kv/README.md | 15 + .../skills/cloudflare/references/kv/api.md | 18 + .../cloudflare/references/kv/configuration.md | 16 + .../cloudflare/references/kv/gotchas.md | 17 + .../cloudflare/references/kv/patterns.md | 21 + .../cloudflare/references/miniflare/README.md | 22 + .../cloudflare/references/miniflare/api.md | 16 + .../references/miniflare/configuration.md | 16 + .../references/miniflare/gotchas.md | 15 + .../references/miniflare/patterns.md | 19 + .../references/observability/README.md | 27 + .../references/observability/api.md | 20 + .../references/observability/configuration.md | 23 + .../references/observability/gotchas.md | 25 + .../references/observability/patterns.md | 14 + .../references/pages-functions/README.md | 20 + .../references/pages-functions/api.md | 13 + .../pages-functions/configuration.md | 14 + .../references/pages-functions/gotchas.md | 17 + .../references/pages-functions/patterns.md | 16 + .../cloudflare/references/pages/README.md | 20 + .../skills/cloudflare/references/pages/api.md | 13 + .../references/pages/configuration.md | 16 + .../cloudflare/references/pages/gotchas.md | 19 + .../cloudflare/references/pages/patterns.md | 15 + .../cloudflare/references/queues/README.md | 24 + .../cloudflare/references/queues/api.md | 16 + .../references/queues/configuration.md | 20 + .../cloudflare/references/queues/gotchas.md | 18 + .../cloudflare/references/queues/patterns.md | 21 + .../skills/cloudflare/references/r2/README.md | 26 + .../skills/cloudflare/references/r2/api.md | 16 + .../cloudflare/references/r2/configuration.md | 19 + .../cloudflare/references/r2/gotchas.md | 17 + .../cloudflare/references/r2/patterns.md | 18 + .../references/secrets-store/README.md | 22 + .../references/secrets-store/api.md | 21 + .../references/secrets-store/configuration.md | 25 + .../references/secrets-store/gotchas.md | 19 + .../references/secrets-store/patterns.md | 27 + .../references/smart-placement/README.md | 138 + .../references/smart-placement/api.md | 183 + .../smart-placement/configuration.md | 196 + .../references/smart-placement/gotchas.md | 174 + .../references/smart-placement/patterns.md | 183 + .../references/static-assets/README.md | 21 + .../references/static-assets/api.md | 16 + .../references/static-assets/configuration.md | 16 + .../references/static-assets/gotchas.md | 18 + .../references/static-assets/patterns.md | 17 + .../cloudflare/references/stream/README.md | 22 + .../cloudflare/references/stream/api-live.md | 18 + .../cloudflare/references/stream/api.md | 21 + .../references/stream/configuration.md | 18 + .../cloudflare/references/stream/gotchas.md | 18 + .../cloudflare/references/stream/patterns.md | 22 + .../references/tail-workers/README.md | 21 + .../cloudflare/references/tail-workers/api.md | 13 + .../references/tail-workers/configuration.md | 15 + .../references/tail-workers/gotchas.md | 16 + .../references/tail-workers/patterns.md | 15 + .../cloudflare/references/tunnel/README.md | 20 + .../cloudflare/references/tunnel/api.md | 14 + .../references/tunnel/configuration.md | 14 + .../cloudflare/references/tunnel/gotchas.md | 15 + .../references/tunnel/networking.md | 14 + .../cloudflare/references/tunnel/patterns.md | 15 + .../cloudflare/references/vectorize/README.md | 22 + .../cloudflare/references/vectorize/api.md | 18 + .../references/vectorize/configuration.md | 19 + .../references/vectorize/gotchas.md | 15 + .../references/vectorize/patterns.md | 24 + .../cloudflare/references/waf/README.md | 21 + .../skills/cloudflare/references/waf/api.md | 17 + .../references/waf/configuration.md | 18 + .../cloudflare/references/waf/gotchas.md | 19 + .../cloudflare/references/waf/patterns.md | 17 + .../references/workers-ai/README.md | 24 + .../cloudflare/references/workers-ai/api.md | 13 + .../references/workers-ai/configuration.md | 16 + .../references/workers-ai/gotchas.md | 15 + .../references/workers-ai/patterns.md | 14 + .../cloudflare/references/workflows/README.md | 23 + .../cloudflare/references/workflows/api.md | 17 + .../references/workflows/configuration.md | 18 + .../references/workflows/gotchas.md | 19 + .../references/workflows/patterns.md | 30 + .agents/skills/dockerfile-validator/SKILL.md | 297 + .../examples/.dockerignore.example | 34 + .../examples/bad-example.Dockerfile | 37 + .../examples/golang-distroless.Dockerfile | 50 + .../examples/good-example.Dockerfile | 52 + .../examples/python-optimized.Dockerfile | 53 + .../examples/security-issues.Dockerfile | 42 + .../references/docker_best_practices.md | 348 + .../references/optimization_guide.md | 473 ++ .../references/security_checklist.md | 208 + .../scripts/dockerfile-validate.sh | 699 ++ .../scripts/test_validate.sh | 35 + .../copy-before-yarn-lock-read.Dockerfile | 6 + .../fixtures/copy-before-yarn.Dockerfile | 6 + .../fixtures/from-platform-nonroot.Dockerfile | 7 + .../tests/test_regression.sh | 164 + .../skills/github-actions-generator/SKILL.md | 350 + .../templates/action/composite/action.yml | 82 + .../assets/templates/action/docker/Dockerfile | 25 + .../assets/templates/action/docker/action.yml | 42 + .../templates/action/docker/entrypoint.sh | 27 + .../templates/action/javascript/action.yml | 33 + .../templates/action/javascript/index.js | 50 + .../templates/action/javascript/package.json | 27 + .../templates/workflow/basic_workflow.yml | 242 + .../templates/workflow/reusable_workflow.yml | 106 + .../examples/README.md | 147 + .../actions/setup-node-cached/action.yml | 93 + .../examples/caching/docker-buildkit.yml | 256 + .../examples/security/dependency-review.yml | 62 + .../examples/security/sbom-attestation.yml | 119 + .../examples/triggers/chatops-commands.yml | 475 ++ .../examples/triggers/repository-dispatch.yml | 418 + .../triggers/workflow-orchestration.yml | 404 + .../examples/workflows/docker-build-push.yml | 68 + .../examples/workflows/go-ci.yml | 161 + .../examples/workflows/monorepo-ci.yml | 340 + .../workflows/multi-environment-deploy.yml | 406 + .../examples/workflows/nodejs-ci.yml | 122 + .../examples/workflows/python-ci.yml | 157 + .../examples/workflows/scheduled-tasks.yml | 376 + .../references/advanced-triggers.md | 917 +++ .../references/best-practices.md | 755 ++ .../references/common-actions.md | 715 ++ .../references/custom-actions.md | 320 + .../references/expressions-and-contexts.md | 688 ++ .../references/modern-features.md | 421 + .../scripts/test_generator.sh | 344 + .agents/skills/install-anti-slop/SKILL.md | 127 + .../assets/anti-slop/effect/index.ts | 21 + .../rules/no-manual-effect-error-tag.ts | 52 + .../effect/rules/no-manual-tag-comparison.ts | 45 + .../rules/no-manual-tagged-construction.ts | 37 + .../rules/no-service-constructor-imports.ts | 52 + .../effect/rules/prefer-effect-match.ts | 54 + .../anti-slop/effect/shared/tagged-values.ts | 97 + .../assets/anti-slop/index.ts | 47 + .../anti-slop/rules/no-array-filter-map.ts | 28 + .../rules/no-chained-type-assertions.ts | 77 + .../no-conditional-empty-object-spread.ts | 49 + .../rules/no-known-value-widening.ts | 415 + .../anti-slop/rules/no-module-mocking.ts | 80 + .../anti-slop/rules/no-object-parameters.ts | 83 + .../rules/no-reduce-accumulator-copy.ts | 109 + .../anti-slop/rules/no-reflect-apply.ts | 28 + .../assets/anti-slop/rules/no-reflect-get.ts | 28 + .../anti-slop/rules/no-runtime-typeof.ts | 77 + .../rules/no-shape-in-symbol-names.ts | 46 + .../anti-slop/rules/no-unknown-parameters.ts | 69 + .../anti-slop/rules/no-unknown-returns.ts | 82 + .../rules/no-unknown-type-aliases.ts | 54 + .../rules/no-unsafe-dictionary-type.ts | 154 + .../anti-slop/rules/no-widen-then-assert.ts | 366 + .../rules/require-readable-spacing.ts | 80 + ...quire-safety-comment-for-type-assertion.ts | 131 + .../assets/anti-slop/shared/array-method.ts | 94 + .../anti-slop/shared/dictionary-types.ts | 515 ++ .../anti-slop/shared/function-parameters.ts | 49 + .../shared/lexical-type-parameters.ts | 61 + .../assets/anti-slop/shared/reflect-method.ts | 24 + .../assets/anti-slop/shared/scope.ts | 15 + .../anti-slop/shared/type-alias-resolution.ts | 250 + .../anti-slop/vendor/eslint-stylistic/LICENSE | 22 + .../vendor/eslint-stylistic/UPSTREAM.md | 28 + .../eslint-stylistic/padding-line-ast.ts | 51 + .../padding-line-between-statements.ts | 929 +++ .../padding-line-options.d.ts | 87 + .../install-anti-slop/references/update.md | 101 + .../install-anti-slop/scripts/install.mjs | 21 + .agents/skills/modern-css/SKILL.md | 50 + .../skills/modern-css/references/ANIMATION.md | 613 ++ .../skills/modern-css/references/CASCADE.md | 559 ++ .../modern-css/references/CHEATSHEET.md | 625 ++ .agents/skills/modern-css/references/COLOR.md | 541 ++ .../modern-css/references/COMPONENTS.md | 554 ++ .../skills/modern-css/references/LAYOUT.md | 585 ++ .../modern-css/references/PERFORMANCE.md | 614 ++ .../skills/modern-css/references/SCROLL.md | 597 ++ .../skills/modern-css/references/SELECTORS.md | 436 + .../skills/modern-css/references/TOKENS.md | 732 ++ .agents/skills/nuxt-better-auth/SKILL.md | 37 + .../references/client-auth.md | 119 + .../references/client-only.md | 40 + .../nuxt-better-auth/references/database.md | 42 + .../references/installation.md | 94 + .../nuxt-better-auth/references/plugins.md | 74 + .../references/route-protection.md | 87 + .../references/server-auth.md | 98 + .../nuxt-better-auth/references/types.md | 44 + .agents/skills/postgres-drizzle/SKILL.md | 41 + .../postgres-drizzle/references/CHEATSHEET.md | 439 + .../postgres-drizzle/references/MIGRATIONS.md | 589 ++ .../references/PERFORMANCE.md | 589 ++ .../postgres-drizzle/references/POSTGRES.md | 660 ++ .../postgres-drizzle/references/QUERIES.md | 803 ++ .../postgres-drizzle/references/RELATIONS.md | 742 ++ .../postgres-drizzle/references/SCHEMA.md | 605 ++ .agents/skills/postgres/SKILL.md | 49 + .../postgres/references/backup-recovery.md | 41 + .../postgres/references/index-optimization.md | 111 + .../skills/postgres/references/indexing.md | 61 + .../references/memory-management-ops.md | 39 + .../skills/postgres/references/monitoring.md | 59 + .../postgres/references/mvcc-transactions.md | 38 + .../skills/postgres/references/mvcc-vacuum.md | 41 + .../references/optimization-checklist.md | 19 + .../postgres/references/partitioning.md | 79 + .../references/pgbouncer-configuration.md | 45 + .../references/process-architecture.md | 46 + .../references/ps-cli-api-insights.md | 53 + .../postgres/references/ps-cli-commands.md | 72 + .../references/ps-connection-pooling.md | 81 + .../postgres/references/ps-connections.md | 38 + .../postgres/references/ps-extensions.md | 27 + .../skills/postgres/references/ps-insights.md | 62 + .../postgres/references/query-patterns.md | 80 + .../skills/postgres/references/replication.md | 49 + .../postgres/references/schema-design.md | 66 + .../postgres/references/storage-layout.md | 41 + .../postgres/references/wal-operations.md | 42 + .../skills/requesting-code-review/SKILL.md | 95 + .../requesting-code-review/code-reviewer.md | 198 + .agents/skills/skilld-lock.yaml | 119 + .agents/skills/stripe-best-practices/SKILL.md | 79 + .../references/billing.md | 77 + .../references/connect.md | 173 + .../references/payments.md | 102 + .../references/security.md | 121 + .../stripe-best-practices/references/tax.md | 173 + .../references/treasury.md | 16 + .agents/skills/taste-skill/README.md | 81 + .agents/skills/taste-skill/SKILL.md | 414 + .agents/skills/taste-skill/assets/base.css | 100 + .agents/skills/taste-skill/assets/base.js | 37 + .agents/skills/taste-skill/evals/evals.json | 48 + .../taste-skill/library/directions.json | 1078 +++ .../skills/taste-skill/library/heroes.json | 894 ++ .../skills/taste-skill/library/motions.json | 57 + .../skills/taste-skill/library/palettes.json | 1485 ++++ .../skills/taste-skill/library/photos.json | 7240 ++++++++++++++++ .../skills/taste-skill/library/sections.json | 4136 ++++++++++ .agents/skills/taste-skill/library/sites.json | 7274 +++++++++++++++++ .agents/skills/taste-skill/library/type.json | 2024 +++++ .../skills/taste-skill/references/colour.md | 141 + .agents/skills/taste-skill/references/copy.md | 177 + .../skills/taste-skill/references/critique.md | 171 + .../skills/taste-skill/references/imagery.md | 377 + .../skills/taste-skill/references/layout.md | 178 + .../skills/taste-skill/references/motion.md | 292 + .../skills/taste-skill/references/research.md | 306 + .../skills/taste-skill/references/tells.md | 115 + .agents/skills/taste-skill/references/type.md | 170 + .agents/skills/taste-skill/scripts/assets.mjs | 865 ++ .agents/skills/taste-skill/scripts/draw.mjs | 525 ++ .../skills/taste-skill/scripts/inspect.mjs | 205 + .../skills/taste-skill/scripts/lib/cdp.mjs | 205 + .../skills/taste-skill/scripts/lib/probe.mjs | 569 ++ .../skills/taste-skill/scripts/lib/rules.mjs | 137 + .agents/skills/taste-skill/scripts/lint.mjs | 108 + .agents/skills/unocss/GENERATION.md | 5 + .agents/skills/unocss/SKILL.md | 65 + .../skills/unocss/references/core-config.md | 194 + .../unocss/references/core-extracting.md | 137 + .../skills/unocss/references/core-layers.md | 104 + .../unocss/references/core-processors.md | 85 + .../skills/unocss/references/core-rules.md | 166 + .../skills/unocss/references/core-safelist.md | 158 + .../unocss/references/core-shortcuts.md | 89 + .../skills/unocss/references/core-theme.md | 172 + .../skills/unocss/references/core-variants.md | 175 + .../unocss/references/integrations-nuxt.md | 199 + .../unocss/references/integrations-vite.md | 290 + .../unocss/references/preset-attributify.md | 142 + .../skills/unocss/references/preset-icons.md | 184 + .../skills/unocss/references/preset-mini.md | 158 + .../unocss/references/preset-rem-to-px.md | 97 + .../skills/unocss/references/preset-tagify.md | 134 + .../unocss/references/preset-typography.md | 95 + .../unocss/references/preset-web-fonts.md | 125 + .../skills/unocss/references/preset-wind3.md | 194 + .../skills/unocss/references/preset-wind4.md | 247 + .../references/transformer-attributify-jsx.md | 156 + .../references/transformer-compile-class.md | 128 + .../references/transformer-directives.md | 165 + .../references/transformer-variant-group.md | 97 + .agents/skills/vitest/GENERATION.md | 5 + .agents/skills/vitest/SKILL.md | 55 + .../references/advanced-environments.md | 272 + .../vitest/references/advanced-projects.md | 341 + .../references/advanced-type-testing.md | 239 + .../skills/vitest/references/advanced-vi.md | 290 + .agents/skills/vitest/references/core-cli.md | 189 + .../skills/vitest/references/core-config.md | 216 + .../skills/vitest/references/core-describe.md | 195 + .../skills/vitest/references/core-expect.md | 311 + .../skills/vitest/references/core-hooks.md | 244 + .../skills/vitest/references/core-test-api.md | 267 + .../references/features-benchmarking.md | 100 + .../vitest/references/features-concurrency.md | 223 + .../vitest/references/features-context.md | 163 + .../vitest/references/features-coverage.md | 226 + .../vitest/references/features-filtering.md | 212 + .../vitest/references/features-mocking.md | 334 + .../vitest/references/features-reporters.md | 110 + .../vitest/references/features-snapshots.md | 253 + .../vitest/references/features-test-tags.md | 123 + .../vue-testing-best-practices/SKILL.md | 29 + .../reference/async-component-testing.md | 163 + .../reference/teleport-testing-complexity.md | 158 + .../testing-async-await-flushpromises.md | 175 + .../testing-browser-vs-node-runners.md | 208 + .../testing-component-blackbox-approach.md | 144 + .../testing-composables-helper-wrapper.md | 238 + .../testing-e2e-playwright-recommended.md | 242 + .../reference/testing-no-snapshot-only.md | 197 + .../reference/testing-pinia-store-setup.md | 228 + .../testing-suspense-async-components.md | 229 + .../testing-vitest-recommended-for-vue.md | 204 + .agents/skills/vue/GENERATION.md | 5 + .agents/skills/vue/SKILL.md | 84 + .../vue/references/advanced-patterns.md | 326 + .../skills/vue/references/core-new-apis.md | 275 + .../vue/references/script-setup-macros.md | 209 + .claude/skills/better-auth-best-practices | 1 + .claude/skills/better-auth-security | 1 + .claude/skills/cloudflare | 1 + .claude/skills/dockerfile-validator | 1 + .claude/skills/github-actions-generator | 1 + .claude/skills/install-anti-slop | 1 + .claude/skills/modern-css | 1 + .claude/skills/nuxt-better-auth | 1 + .claude/skills/postgres | 1 + .claude/skills/postgres-drizzle | 1 + .claude/skills/requesting-code-review | 1 + .claude/skills/stripe-best-practices | 1 + .claude/skills/taste-skill | 1 + .claude/skills/unocss | 1 + .claude/skills/vitest | 1 + .claude/skills/vue | 1 + .claude/skills/vue-testing-best-practices | 1 + 401 files changed, 74473 insertions(+) create mode 100644 .agents/skills/better-auth-best-practices/SKILL.md create mode 100644 .agents/skills/better-auth-security/SKILL.md create mode 100644 .agents/skills/cloudflare/SKILL.md create mode 100644 .agents/skills/cloudflare/references/ai-gateway/README.md create mode 100644 .agents/skills/cloudflare/references/ai-gateway/configuration.md create mode 100644 .agents/skills/cloudflare/references/ai-gateway/dynamic-routing.md create mode 100644 .agents/skills/cloudflare/references/ai-gateway/features.md create mode 100644 .agents/skills/cloudflare/references/ai-gateway/sdk-integration.md create mode 100644 .agents/skills/cloudflare/references/ai-gateway/troubleshooting.md create mode 100644 .agents/skills/cloudflare/references/ai-search/README.md create mode 100644 .agents/skills/cloudflare/references/ai-search/api.md create mode 100644 .agents/skills/cloudflare/references/ai-search/configuration.md create mode 100644 .agents/skills/cloudflare/references/ai-search/gotchas.md create mode 100644 .agents/skills/cloudflare/references/ai-search/patterns.md create mode 100644 .agents/skills/cloudflare/references/bindings/README.md create mode 100644 .agents/skills/cloudflare/references/bindings/api.md create mode 100644 .agents/skills/cloudflare/references/bindings/configuration.md create mode 100644 .agents/skills/cloudflare/references/bindings/gotchas.md create mode 100644 .agents/skills/cloudflare/references/bindings/patterns.md create mode 100644 .agents/skills/cloudflare/references/browser-rendering/README.md create mode 100644 .agents/skills/cloudflare/references/browser-rendering/api.md create mode 100644 .agents/skills/cloudflare/references/browser-rendering/configuration.md create mode 100644 .agents/skills/cloudflare/references/browser-rendering/gotchas.md create mode 100644 .agents/skills/cloudflare/references/browser-rendering/patterns.md create mode 100644 .agents/skills/cloudflare/references/c3/README.md create mode 100644 .agents/skills/cloudflare/references/c3/api.md create mode 100644 .agents/skills/cloudflare/references/c3/configuration.md create mode 100644 .agents/skills/cloudflare/references/c3/gotchas.md create mode 100644 .agents/skills/cloudflare/references/c3/patterns.md create mode 100644 .agents/skills/cloudflare/references/containers/README.md create mode 100644 .agents/skills/cloudflare/references/containers/api.md create mode 100644 .agents/skills/cloudflare/references/containers/configuration.md create mode 100644 .agents/skills/cloudflare/references/containers/gotchas.md create mode 100644 .agents/skills/cloudflare/references/containers/patterns.md create mode 100644 .agents/skills/cloudflare/references/cron-triggers/README.md create mode 100644 .agents/skills/cloudflare/references/cron-triggers/api.md create mode 100644 .agents/skills/cloudflare/references/cron-triggers/configuration.md create mode 100644 .agents/skills/cloudflare/references/cron-triggers/gotchas.md create mode 100644 .agents/skills/cloudflare/references/cron-triggers/patterns.md create mode 100644 .agents/skills/cloudflare/references/d1/README.md create mode 100644 .agents/skills/cloudflare/references/d1/api.md create mode 100644 .agents/skills/cloudflare/references/d1/configuration.md create mode 100644 .agents/skills/cloudflare/references/d1/gotchas.md create mode 100644 .agents/skills/cloudflare/references/d1/patterns.md create mode 100644 .agents/skills/cloudflare/references/hyperdrive/README.md create mode 100644 .agents/skills/cloudflare/references/hyperdrive/api.md create mode 100644 .agents/skills/cloudflare/references/hyperdrive/configuration.md create mode 100644 .agents/skills/cloudflare/references/hyperdrive/gotchas.md create mode 100644 .agents/skills/cloudflare/references/hyperdrive/patterns.md create mode 100644 .agents/skills/cloudflare/references/images/README.md create mode 100644 .agents/skills/cloudflare/references/images/api.md create mode 100644 .agents/skills/cloudflare/references/images/configuration.md create mode 100644 .agents/skills/cloudflare/references/images/gotchas.md create mode 100644 .agents/skills/cloudflare/references/images/patterns.md create mode 100644 .agents/skills/cloudflare/references/kv/README.md create mode 100644 .agents/skills/cloudflare/references/kv/api.md create mode 100644 .agents/skills/cloudflare/references/kv/configuration.md create mode 100644 .agents/skills/cloudflare/references/kv/gotchas.md create mode 100644 .agents/skills/cloudflare/references/kv/patterns.md create mode 100644 .agents/skills/cloudflare/references/miniflare/README.md create mode 100644 .agents/skills/cloudflare/references/miniflare/api.md create mode 100644 .agents/skills/cloudflare/references/miniflare/configuration.md create mode 100644 .agents/skills/cloudflare/references/miniflare/gotchas.md create mode 100644 .agents/skills/cloudflare/references/miniflare/patterns.md create mode 100644 .agents/skills/cloudflare/references/observability/README.md create mode 100644 .agents/skills/cloudflare/references/observability/api.md create mode 100644 .agents/skills/cloudflare/references/observability/configuration.md create mode 100644 .agents/skills/cloudflare/references/observability/gotchas.md create mode 100644 .agents/skills/cloudflare/references/observability/patterns.md create mode 100644 .agents/skills/cloudflare/references/pages-functions/README.md create mode 100644 .agents/skills/cloudflare/references/pages-functions/api.md create mode 100644 .agents/skills/cloudflare/references/pages-functions/configuration.md create mode 100644 .agents/skills/cloudflare/references/pages-functions/gotchas.md create mode 100644 .agents/skills/cloudflare/references/pages-functions/patterns.md create mode 100644 .agents/skills/cloudflare/references/pages/README.md create mode 100644 .agents/skills/cloudflare/references/pages/api.md create mode 100644 .agents/skills/cloudflare/references/pages/configuration.md create mode 100644 .agents/skills/cloudflare/references/pages/gotchas.md create mode 100644 .agents/skills/cloudflare/references/pages/patterns.md create mode 100644 .agents/skills/cloudflare/references/queues/README.md create mode 100644 .agents/skills/cloudflare/references/queues/api.md create mode 100644 .agents/skills/cloudflare/references/queues/configuration.md create mode 100644 .agents/skills/cloudflare/references/queues/gotchas.md create mode 100644 .agents/skills/cloudflare/references/queues/patterns.md create mode 100644 .agents/skills/cloudflare/references/r2/README.md create mode 100644 .agents/skills/cloudflare/references/r2/api.md create mode 100644 .agents/skills/cloudflare/references/r2/configuration.md create mode 100644 .agents/skills/cloudflare/references/r2/gotchas.md create mode 100644 .agents/skills/cloudflare/references/r2/patterns.md create mode 100644 .agents/skills/cloudflare/references/secrets-store/README.md create mode 100644 .agents/skills/cloudflare/references/secrets-store/api.md create mode 100644 .agents/skills/cloudflare/references/secrets-store/configuration.md create mode 100644 .agents/skills/cloudflare/references/secrets-store/gotchas.md create mode 100644 .agents/skills/cloudflare/references/secrets-store/patterns.md create mode 100644 .agents/skills/cloudflare/references/smart-placement/README.md create mode 100644 .agents/skills/cloudflare/references/smart-placement/api.md create mode 100644 .agents/skills/cloudflare/references/smart-placement/configuration.md create mode 100644 .agents/skills/cloudflare/references/smart-placement/gotchas.md create mode 100644 .agents/skills/cloudflare/references/smart-placement/patterns.md create mode 100644 .agents/skills/cloudflare/references/static-assets/README.md create mode 100644 .agents/skills/cloudflare/references/static-assets/api.md create mode 100644 .agents/skills/cloudflare/references/static-assets/configuration.md create mode 100644 .agents/skills/cloudflare/references/static-assets/gotchas.md create mode 100644 .agents/skills/cloudflare/references/static-assets/patterns.md create mode 100644 .agents/skills/cloudflare/references/stream/README.md create mode 100644 .agents/skills/cloudflare/references/stream/api-live.md create mode 100644 .agents/skills/cloudflare/references/stream/api.md create mode 100644 .agents/skills/cloudflare/references/stream/configuration.md create mode 100644 .agents/skills/cloudflare/references/stream/gotchas.md create mode 100644 .agents/skills/cloudflare/references/stream/patterns.md create mode 100644 .agents/skills/cloudflare/references/tail-workers/README.md create mode 100644 .agents/skills/cloudflare/references/tail-workers/api.md create mode 100644 .agents/skills/cloudflare/references/tail-workers/configuration.md create mode 100644 .agents/skills/cloudflare/references/tail-workers/gotchas.md create mode 100644 .agents/skills/cloudflare/references/tail-workers/patterns.md create mode 100644 .agents/skills/cloudflare/references/tunnel/README.md create mode 100644 .agents/skills/cloudflare/references/tunnel/api.md create mode 100644 .agents/skills/cloudflare/references/tunnel/configuration.md create mode 100644 .agents/skills/cloudflare/references/tunnel/gotchas.md create mode 100644 .agents/skills/cloudflare/references/tunnel/networking.md create mode 100644 .agents/skills/cloudflare/references/tunnel/patterns.md create mode 100644 .agents/skills/cloudflare/references/vectorize/README.md create mode 100644 .agents/skills/cloudflare/references/vectorize/api.md create mode 100644 .agents/skills/cloudflare/references/vectorize/configuration.md create mode 100644 .agents/skills/cloudflare/references/vectorize/gotchas.md create mode 100644 .agents/skills/cloudflare/references/vectorize/patterns.md create mode 100644 .agents/skills/cloudflare/references/waf/README.md create mode 100644 .agents/skills/cloudflare/references/waf/api.md create mode 100644 .agents/skills/cloudflare/references/waf/configuration.md create mode 100644 .agents/skills/cloudflare/references/waf/gotchas.md create mode 100644 .agents/skills/cloudflare/references/waf/patterns.md create mode 100644 .agents/skills/cloudflare/references/workers-ai/README.md create mode 100644 .agents/skills/cloudflare/references/workers-ai/api.md create mode 100644 .agents/skills/cloudflare/references/workers-ai/configuration.md create mode 100644 .agents/skills/cloudflare/references/workers-ai/gotchas.md create mode 100644 .agents/skills/cloudflare/references/workers-ai/patterns.md create mode 100644 .agents/skills/cloudflare/references/workflows/README.md create mode 100644 .agents/skills/cloudflare/references/workflows/api.md create mode 100644 .agents/skills/cloudflare/references/workflows/configuration.md create mode 100644 .agents/skills/cloudflare/references/workflows/gotchas.md create mode 100644 .agents/skills/cloudflare/references/workflows/patterns.md create mode 100644 .agents/skills/dockerfile-validator/SKILL.md create mode 100644 .agents/skills/dockerfile-validator/examples/.dockerignore.example create mode 100644 .agents/skills/dockerfile-validator/examples/bad-example.Dockerfile create mode 100644 .agents/skills/dockerfile-validator/examples/golang-distroless.Dockerfile create mode 100644 .agents/skills/dockerfile-validator/examples/good-example.Dockerfile create mode 100644 .agents/skills/dockerfile-validator/examples/python-optimized.Dockerfile create mode 100644 .agents/skills/dockerfile-validator/examples/security-issues.Dockerfile create mode 100644 .agents/skills/dockerfile-validator/references/docker_best_practices.md create mode 100644 .agents/skills/dockerfile-validator/references/optimization_guide.md create mode 100644 .agents/skills/dockerfile-validator/references/security_checklist.md create mode 100644 .agents/skills/dockerfile-validator/scripts/dockerfile-validate.sh create mode 100644 .agents/skills/dockerfile-validator/scripts/test_validate.sh create mode 100644 .agents/skills/dockerfile-validator/tests/fixtures/copy-before-yarn-lock-read.Dockerfile create mode 100644 .agents/skills/dockerfile-validator/tests/fixtures/copy-before-yarn.Dockerfile create mode 100644 .agents/skills/dockerfile-validator/tests/fixtures/from-platform-nonroot.Dockerfile create mode 100644 .agents/skills/dockerfile-validator/tests/test_regression.sh create mode 100644 .agents/skills/github-actions-generator/SKILL.md create mode 100644 .agents/skills/github-actions-generator/assets/templates/action/composite/action.yml create mode 100644 .agents/skills/github-actions-generator/assets/templates/action/docker/Dockerfile create mode 100644 .agents/skills/github-actions-generator/assets/templates/action/docker/action.yml create mode 100644 .agents/skills/github-actions-generator/assets/templates/action/docker/entrypoint.sh create mode 100644 .agents/skills/github-actions-generator/assets/templates/action/javascript/action.yml create mode 100644 .agents/skills/github-actions-generator/assets/templates/action/javascript/index.js create mode 100644 .agents/skills/github-actions-generator/assets/templates/action/javascript/package.json create mode 100644 .agents/skills/github-actions-generator/assets/templates/workflow/basic_workflow.yml create mode 100644 .agents/skills/github-actions-generator/assets/templates/workflow/reusable_workflow.yml create mode 100644 .agents/skills/github-actions-generator/examples/README.md create mode 100644 .agents/skills/github-actions-generator/examples/actions/setup-node-cached/action.yml create mode 100644 .agents/skills/github-actions-generator/examples/caching/docker-buildkit.yml create mode 100644 .agents/skills/github-actions-generator/examples/security/dependency-review.yml create mode 100644 .agents/skills/github-actions-generator/examples/security/sbom-attestation.yml create mode 100644 .agents/skills/github-actions-generator/examples/triggers/chatops-commands.yml create mode 100644 .agents/skills/github-actions-generator/examples/triggers/repository-dispatch.yml create mode 100644 .agents/skills/github-actions-generator/examples/triggers/workflow-orchestration.yml create mode 100644 .agents/skills/github-actions-generator/examples/workflows/docker-build-push.yml create mode 100644 .agents/skills/github-actions-generator/examples/workflows/go-ci.yml create mode 100644 .agents/skills/github-actions-generator/examples/workflows/monorepo-ci.yml create mode 100644 .agents/skills/github-actions-generator/examples/workflows/multi-environment-deploy.yml create mode 100644 .agents/skills/github-actions-generator/examples/workflows/nodejs-ci.yml create mode 100644 .agents/skills/github-actions-generator/examples/workflows/python-ci.yml create mode 100644 .agents/skills/github-actions-generator/examples/workflows/scheduled-tasks.yml create mode 100644 .agents/skills/github-actions-generator/references/advanced-triggers.md create mode 100644 .agents/skills/github-actions-generator/references/best-practices.md create mode 100644 .agents/skills/github-actions-generator/references/common-actions.md create mode 100644 .agents/skills/github-actions-generator/references/custom-actions.md create mode 100644 .agents/skills/github-actions-generator/references/expressions-and-contexts.md create mode 100644 .agents/skills/github-actions-generator/references/modern-features.md create mode 100644 .agents/skills/github-actions-generator/scripts/test_generator.sh create mode 100644 .agents/skills/install-anti-slop/SKILL.md create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/effect/index.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-effect-error-tag.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tag-comparison.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tagged-construction.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-service-constructor-imports.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/effect/rules/prefer-effect-match.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/effect/shared/tagged-values.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/index.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-array-filter-map.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-chained-type-assertions.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-conditional-empty-object-spread.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-known-value-widening.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-module-mocking.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-object-parameters.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-reduce-accumulator-copy.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-reflect-apply.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-reflect-get.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-runtime-typeof.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-shape-in-symbol-names.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-parameters.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-returns.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-type-aliases.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-unsafe-dictionary-type.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/no-widen-then-assert.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/require-readable-spacing.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/rules/require-safety-comment-for-type-assertion.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/shared/array-method.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/shared/dictionary-types.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/shared/function-parameters.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/shared/lexical-type-parameters.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/shared/reflect-method.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/shared/scope.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/shared/type-alias-resolution.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/LICENSE create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/UPSTREAM.md create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts create mode 100644 .agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts create mode 100644 .agents/skills/install-anti-slop/references/update.md create mode 100644 .agents/skills/install-anti-slop/scripts/install.mjs create mode 100644 .agents/skills/modern-css/SKILL.md create mode 100644 .agents/skills/modern-css/references/ANIMATION.md create mode 100644 .agents/skills/modern-css/references/CASCADE.md create mode 100644 .agents/skills/modern-css/references/CHEATSHEET.md create mode 100644 .agents/skills/modern-css/references/COLOR.md create mode 100644 .agents/skills/modern-css/references/COMPONENTS.md create mode 100644 .agents/skills/modern-css/references/LAYOUT.md create mode 100644 .agents/skills/modern-css/references/PERFORMANCE.md create mode 100644 .agents/skills/modern-css/references/SCROLL.md create mode 100644 .agents/skills/modern-css/references/SELECTORS.md create mode 100644 .agents/skills/modern-css/references/TOKENS.md create mode 100644 .agents/skills/nuxt-better-auth/SKILL.md create mode 100644 .agents/skills/nuxt-better-auth/references/client-auth.md create mode 100644 .agents/skills/nuxt-better-auth/references/client-only.md create mode 100644 .agents/skills/nuxt-better-auth/references/database.md create mode 100644 .agents/skills/nuxt-better-auth/references/installation.md create mode 100644 .agents/skills/nuxt-better-auth/references/plugins.md create mode 100644 .agents/skills/nuxt-better-auth/references/route-protection.md create mode 100644 .agents/skills/nuxt-better-auth/references/server-auth.md create mode 100644 .agents/skills/nuxt-better-auth/references/types.md create mode 100644 .agents/skills/postgres-drizzle/SKILL.md create mode 100644 .agents/skills/postgres-drizzle/references/CHEATSHEET.md create mode 100644 .agents/skills/postgres-drizzle/references/MIGRATIONS.md create mode 100644 .agents/skills/postgres-drizzle/references/PERFORMANCE.md create mode 100644 .agents/skills/postgres-drizzle/references/POSTGRES.md create mode 100644 .agents/skills/postgres-drizzle/references/QUERIES.md create mode 100644 .agents/skills/postgres-drizzle/references/RELATIONS.md create mode 100644 .agents/skills/postgres-drizzle/references/SCHEMA.md create mode 100644 .agents/skills/postgres/SKILL.md create mode 100644 .agents/skills/postgres/references/backup-recovery.md create mode 100644 .agents/skills/postgres/references/index-optimization.md create mode 100644 .agents/skills/postgres/references/indexing.md create mode 100644 .agents/skills/postgres/references/memory-management-ops.md create mode 100644 .agents/skills/postgres/references/monitoring.md create mode 100644 .agents/skills/postgres/references/mvcc-transactions.md create mode 100644 .agents/skills/postgres/references/mvcc-vacuum.md create mode 100644 .agents/skills/postgres/references/optimization-checklist.md create mode 100644 .agents/skills/postgres/references/partitioning.md create mode 100644 .agents/skills/postgres/references/pgbouncer-configuration.md create mode 100644 .agents/skills/postgres/references/process-architecture.md create mode 100644 .agents/skills/postgres/references/ps-cli-api-insights.md create mode 100644 .agents/skills/postgres/references/ps-cli-commands.md create mode 100644 .agents/skills/postgres/references/ps-connection-pooling.md create mode 100644 .agents/skills/postgres/references/ps-connections.md create mode 100644 .agents/skills/postgres/references/ps-extensions.md create mode 100644 .agents/skills/postgres/references/ps-insights.md create mode 100644 .agents/skills/postgres/references/query-patterns.md create mode 100644 .agents/skills/postgres/references/replication.md create mode 100644 .agents/skills/postgres/references/schema-design.md create mode 100644 .agents/skills/postgres/references/storage-layout.md create mode 100644 .agents/skills/postgres/references/wal-operations.md create mode 100644 .agents/skills/requesting-code-review/SKILL.md create mode 100644 .agents/skills/requesting-code-review/code-reviewer.md create mode 100644 .agents/skills/stripe-best-practices/SKILL.md create mode 100644 .agents/skills/stripe-best-practices/references/billing.md create mode 100644 .agents/skills/stripe-best-practices/references/connect.md create mode 100644 .agents/skills/stripe-best-practices/references/payments.md create mode 100644 .agents/skills/stripe-best-practices/references/security.md create mode 100644 .agents/skills/stripe-best-practices/references/tax.md create mode 100644 .agents/skills/stripe-best-practices/references/treasury.md create mode 100644 .agents/skills/taste-skill/README.md create mode 100644 .agents/skills/taste-skill/SKILL.md create mode 100644 .agents/skills/taste-skill/assets/base.css create mode 100644 .agents/skills/taste-skill/assets/base.js create mode 100644 .agents/skills/taste-skill/evals/evals.json create mode 100644 .agents/skills/taste-skill/library/directions.json create mode 100644 .agents/skills/taste-skill/library/heroes.json create mode 100644 .agents/skills/taste-skill/library/motions.json create mode 100644 .agents/skills/taste-skill/library/palettes.json create mode 100644 .agents/skills/taste-skill/library/photos.json create mode 100644 .agents/skills/taste-skill/library/sections.json create mode 100644 .agents/skills/taste-skill/library/sites.json create mode 100644 .agents/skills/taste-skill/library/type.json create mode 100644 .agents/skills/taste-skill/references/colour.md create mode 100644 .agents/skills/taste-skill/references/copy.md create mode 100644 .agents/skills/taste-skill/references/critique.md create mode 100644 .agents/skills/taste-skill/references/imagery.md create mode 100644 .agents/skills/taste-skill/references/layout.md create mode 100644 .agents/skills/taste-skill/references/motion.md create mode 100644 .agents/skills/taste-skill/references/research.md create mode 100644 .agents/skills/taste-skill/references/tells.md create mode 100644 .agents/skills/taste-skill/references/type.md create mode 100644 .agents/skills/taste-skill/scripts/assets.mjs create mode 100644 .agents/skills/taste-skill/scripts/draw.mjs create mode 100644 .agents/skills/taste-skill/scripts/inspect.mjs create mode 100644 .agents/skills/taste-skill/scripts/lib/cdp.mjs create mode 100644 .agents/skills/taste-skill/scripts/lib/probe.mjs create mode 100644 .agents/skills/taste-skill/scripts/lib/rules.mjs create mode 100644 .agents/skills/taste-skill/scripts/lint.mjs create mode 100644 .agents/skills/unocss/GENERATION.md create mode 100644 .agents/skills/unocss/SKILL.md create mode 100644 .agents/skills/unocss/references/core-config.md create mode 100644 .agents/skills/unocss/references/core-extracting.md create mode 100644 .agents/skills/unocss/references/core-layers.md create mode 100644 .agents/skills/unocss/references/core-processors.md create mode 100644 .agents/skills/unocss/references/core-rules.md create mode 100644 .agents/skills/unocss/references/core-safelist.md create mode 100644 .agents/skills/unocss/references/core-shortcuts.md create mode 100644 .agents/skills/unocss/references/core-theme.md create mode 100644 .agents/skills/unocss/references/core-variants.md create mode 100644 .agents/skills/unocss/references/integrations-nuxt.md create mode 100644 .agents/skills/unocss/references/integrations-vite.md create mode 100644 .agents/skills/unocss/references/preset-attributify.md create mode 100644 .agents/skills/unocss/references/preset-icons.md create mode 100644 .agents/skills/unocss/references/preset-mini.md create mode 100644 .agents/skills/unocss/references/preset-rem-to-px.md create mode 100644 .agents/skills/unocss/references/preset-tagify.md create mode 100644 .agents/skills/unocss/references/preset-typography.md create mode 100644 .agents/skills/unocss/references/preset-web-fonts.md create mode 100644 .agents/skills/unocss/references/preset-wind3.md create mode 100644 .agents/skills/unocss/references/preset-wind4.md create mode 100644 .agents/skills/unocss/references/transformer-attributify-jsx.md create mode 100644 .agents/skills/unocss/references/transformer-compile-class.md create mode 100644 .agents/skills/unocss/references/transformer-directives.md create mode 100644 .agents/skills/unocss/references/transformer-variant-group.md create mode 100644 .agents/skills/vitest/GENERATION.md create mode 100644 .agents/skills/vitest/SKILL.md create mode 100644 .agents/skills/vitest/references/advanced-environments.md create mode 100644 .agents/skills/vitest/references/advanced-projects.md create mode 100644 .agents/skills/vitest/references/advanced-type-testing.md create mode 100644 .agents/skills/vitest/references/advanced-vi.md create mode 100644 .agents/skills/vitest/references/core-cli.md create mode 100644 .agents/skills/vitest/references/core-config.md create mode 100644 .agents/skills/vitest/references/core-describe.md create mode 100644 .agents/skills/vitest/references/core-expect.md create mode 100644 .agents/skills/vitest/references/core-hooks.md create mode 100644 .agents/skills/vitest/references/core-test-api.md create mode 100644 .agents/skills/vitest/references/features-benchmarking.md create mode 100644 .agents/skills/vitest/references/features-concurrency.md create mode 100644 .agents/skills/vitest/references/features-context.md create mode 100644 .agents/skills/vitest/references/features-coverage.md create mode 100644 .agents/skills/vitest/references/features-filtering.md create mode 100644 .agents/skills/vitest/references/features-mocking.md create mode 100644 .agents/skills/vitest/references/features-reporters.md create mode 100644 .agents/skills/vitest/references/features-snapshots.md create mode 100644 .agents/skills/vitest/references/features-test-tags.md create mode 100644 .agents/skills/vue-testing-best-practices/SKILL.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/async-component-testing.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-vitest-recommended-for-vue.md create mode 100644 .agents/skills/vue/GENERATION.md create mode 100644 .agents/skills/vue/SKILL.md create mode 100644 .agents/skills/vue/references/advanced-patterns.md create mode 100644 .agents/skills/vue/references/core-new-apis.md create mode 100644 .agents/skills/vue/references/script-setup-macros.md create mode 120000 .claude/skills/better-auth-best-practices create mode 120000 .claude/skills/better-auth-security create mode 120000 .claude/skills/cloudflare create mode 120000 .claude/skills/dockerfile-validator create mode 120000 .claude/skills/github-actions-generator create mode 120000 .claude/skills/install-anti-slop create mode 120000 .claude/skills/modern-css create mode 120000 .claude/skills/nuxt-better-auth create mode 120000 .claude/skills/postgres create mode 120000 .claude/skills/postgres-drizzle create mode 120000 .claude/skills/requesting-code-review create mode 120000 .claude/skills/stripe-best-practices create mode 120000 .claude/skills/taste-skill create mode 120000 .claude/skills/unocss create mode 120000 .claude/skills/vitest create mode 120000 .claude/skills/vue create mode 120000 .claude/skills/vue-testing-best-practices diff --git a/.agents/skills/better-auth-best-practices/SKILL.md b/.agents/skills/better-auth-best-practices/SKILL.md new file mode 100644 index 0000000..74195ce --- /dev/null +++ b/.agents/skills/better-auth-best-practices/SKILL.md @@ -0,0 +1,192 @@ +--- +name: better-auth-best-practices +description: Configure Better Auth server and client, set up database adapters, manage sessions, add plugins, and handle environment variables. Use when users mention Better Auth, betterauth, auth.ts, or need to set up TypeScript authentication with email/password, OAuth, or plugin configuration. +--- + +# Better Auth Integration Guide + +## Documentation Version + +Use documentation that matches the Better Auth version installed in the project. APIs and plugin names can differ across maintained release lines. + +1. Prefer a version explicitly named by the user. +2. Otherwise, inspect the resolved `better-auth` version in the lockfile, falling back to the package manifest when no lockfile is available. +3. When the Better Auth MCP is available, call `get_doc` with `/llms.txt` to resolve that package version to a documentation identifier. Pass the identifier to every `search_docs` call and pass result paths to `get_doc` unchanged. +4. Without MCP, start at [better-auth.com/llms.txt](https://better-auth.com/llms.txt) and follow the matching version index. +5. Use the latest documentation only when the project version cannot be determined or the user explicitly asks about the latest release or an upgrade. + +When planning an upgrade, separate guidance for the currently installed version from guidance for the target version. + +--- + +## Setup Workflow + +1. Install: `npm install better-auth` +2. Set env vars: `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` +3. Create `auth.ts` with database + config +4. Create route handler for your framework +5. Run migrations: + - **Built-in adapter:** `npx auth@latest migrate` + - **Drizzle:** `npx auth@latest generate --output src/db/auth-schema.ts` then `npx drizzle-kit push` (dev) or `npx drizzle-kit generate && npx drizzle-kit migrate` (prod) + - **Prisma:** `npx auth@latest generate --output prisma/schema.prisma` then `npx prisma migrate dev` +6. Verify: call `GET /api/auth/ok` — should return `{ status: "ok" }` + +--- + +## Quick Reference + +### Environment Variables +- `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32` +- `BETTER_AUTH_URL` - Base URL (e.g., `https://example.com`) + +Only define `baseURL`/`secret` in config if env vars are NOT set. + +### File Location +CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path. + +### CLI Commands +- `npx auth@latest migrate` - Apply schema (built-in adapter) +- `npx auth@latest generate` - Generate schema for Prisma/Drizzle +- `npx auth@latest mcp --cursor` - Add MCP to AI tools + +**Re-run after adding/changing plugins.** + +--- + +## Core Config Options + +| Option | Notes | +|--------|-------| +| `appName` | Optional display name | +| `baseURL` | Only if `BETTER_AUTH_URL` not set | +| `basePath` | Default `/api/auth`. Set `/` for root. | +| `secret` | Only if `BETTER_AUTH_SECRET` not set | +| `database` | Required for most features. See adapters docs. | +| `secondaryStorage` | Redis/KV for sessions & rate limits | +| `emailAndPassword` | `{ enabled: true }` to activate | +| `socialProviders` | `{ google: { clientId, clientSecret }, ... }` | +| `plugins` | Array of plugins | +| `trustedOrigins` | CSRF whitelist | + +--- + +## Database + +**Direct connections:** Pass `pg.Pool`, `mysql2` pool, `better-sqlite3`, or `bun:sqlite` instance. For Postgres, also supports `postgres` (postgres.js) and `@neondatabase/serverless`. + +**ORM adapters:** Import from `better-auth/adapters/drizzle`, `better-auth/adapters/prisma`, `better-auth/adapters/mongodb`. + +**Drizzle provider values:** `"pg"` (PostgreSQL), `"mysql"` (MySQL), `"sqlite"` (SQLite). Must match the driver used. + +**Critical:** Better Auth uses adapter model names, NOT underlying table names. If Prisma model is `User` mapping to table `users`, use `modelName: "user"` (Prisma reference), not `"users"`. + +--- + +## Session Management + +**Storage priority:** +1. If `secondaryStorage` defined → sessions go there (not DB) +2. Set `session.storeSessionInDatabase: true` to also persist to DB +3. No database + `cookieCache` → fully stateless mode + +**Cookie cache strategies:** +- `compact` (default) - Base64url + HMAC. Smallest. +- `jwt` - Standard JWT. Readable but signed. +- `jwe` - Encrypted. Maximum security. + +**Key options:** `session.expiresIn` (default 7 days), `session.updateAge` (refresh interval), `session.cookieCache.maxAge`, `session.cookieCache.version` (change to invalidate all sessions). + +--- + +## User & Account Config + +**User:** `user.modelName`, `user.fields` (column mapping), `user.additionalFields`, `user.changeEmail.enabled` (disabled by default), `user.deleteUser.enabled` (disabled by default). + +**Account:** `account.modelName`, `account.accountLinking.enabled`, `account.storeAccountCookie` (for stateless OAuth). + +**Required for registration:** `email` and `name` fields. + +--- + +## Email Flows + +- `emailVerification.sendVerificationEmail` - Must be defined for verification to work +- `emailVerification.sendOnSignUp` / `sendOnSignIn` - Auto-send triggers +- `emailAndPassword.sendResetPassword` - Password reset email handler + +--- + +## Security + +**In `advanced`:** +- `useSecureCookies` - Force HTTPS cookies +- `disableCSRFCheck` - ⚠️ Security risk +- `disableOriginCheck` - ⚠️ Security risk +- `crossSubDomainCookies.enabled` - Share cookies across subdomains +- `ipAddress.ipAddressHeaders` - Custom IP headers for proxies +- `database.generateId` - Custom ID generation or `"serial"`/`"uuid"`/`false` + +**Rate limiting:** `rateLimit.enabled`, `rateLimit.window`, `rateLimit.max`, `rateLimit.storage` ("memory" | "database" | "secondary-storage"). + +--- + +## Hooks + +**Endpoint hooks:** `hooks.before` / `hooks.after` - Array of `{ matcher, handler }`. Use `createAuthMiddleware`. Access `ctx.path`, `ctx.context.returned` (after), `ctx.context.session`. + +**Database hooks:** `databaseHooks.user.create.before/after`, same for `session`, `account`. Useful for adding default values or post-creation actions. + +**Hook context (`ctx.context`):** `session`, `secret`, `authCookies`, `password.hash()`/`verify()`, `adapter`, `internalAdapter`, `generateId()`, `tables`, `baseURL`. + +--- + +## Plugins + +**Import from dedicated paths for tree-shaking:** +``` +import { twoFactor } from "better-auth/plugins/two-factor" +``` +NOT `from "better-auth/plugins"`. + +**Popular plugins:** `twoFactor`, `organization`, `passkey`, `magicLink`, `emailOtp`, `username`, `phoneNumber`, `admin`, `apiKey`, `bearer`, `jwt`, `multiSession`, `sso`, `oauthProvider`, `oidcProvider`, `openAPI`, `genericOAuth`. + +Client plugins go in `createAuthClient({ plugins: [...] })`. + +--- + +## Client + +Import from: `better-auth/client` (vanilla), `better-auth/react`, `better-auth/vue`, `better-auth/svelte`, `better-auth/solid`. + +Key methods: `signUp.email()`, `signIn.email()`, `signIn.social()`, `signOut()`, `useSession()`, `getSession()`, `revokeSession()`, `revokeSessions()`. + +--- + +## Type Safety + +Infer types: `typeof auth.$Infer.Session`, `typeof auth.$Infer.Session.user`. + +For separate client/server projects: `createAuthClient()`. + +--- + +## Common Gotchas + +1. **Model vs table name** - Config uses ORM model name, not DB table name +2. **Plugin schema** - Re-run CLI after adding plugins +3. **Secondary storage** - Sessions go there by default, not DB +4. **Cookie cache** - Custom session fields NOT cached, always re-fetched +5. **Stateless mode** - No DB = session in cookie only, logout on cache expiry +6. **Change email flow** - Sends to current email first, then new email +7. **Drizzle: db not initialized** - `drizzleAdapter(db, ...)` requires a `db` instance from `drizzle()`. See `create-auth` skill for setup examples (node-postgres, postgres.js, Neon). +8. **Drizzle: missing drizzle.config.ts** - `drizzle-kit` commands require a `drizzle.config.ts` pointing to the generated schema file and DB credentials. + +--- + +## Resources + +- [Docs](https://better-auth.com/docs) +- [Options Reference](https://better-auth.com/docs/reference/options) +- [LLMs.txt](https://better-auth.com/llms.txt) +- [GitHub](https://github.com/better-auth/better-auth) +- [Init Options Source](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts) diff --git a/.agents/skills/better-auth-security/SKILL.md b/.agents/skills/better-auth-security/SKILL.md new file mode 100644 index 0000000..5abc5a8 --- /dev/null +++ b/.agents/skills/better-auth-security/SKILL.md @@ -0,0 +1,432 @@ +--- +name: better-auth-security-best-practices +description: Configure rate limiting, manage auth secrets, set up CSRF protection, define trusted origins, secure sessions and cookies, encrypt OAuth tokens, track IP addresses, and implement audit logging for Better Auth. Use when users need to secure their auth setup, prevent brute force attacks, or harden a Better Auth deployment. +--- + +## Secret Management + +### Configuring the Secret + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + secret: process.env.BETTER_AUTH_SECRET, // or via `BETTER_AUTH_SECRET` env +}); +``` + +Better Auth looks for secrets in this order: +1. `options.secret` in your config +2. `BETTER_AUTH_SECRET` environment variable +3. `AUTH_SECRET` environment variable + +### Secret Requirements + +- Rejects default/placeholder secrets in production +- Warns if shorter than 32 characters or entropy below 120 bits +- Generate: `openssl rand -base64 32` +- Never commit secrets to version control + +## Rate Limiting + +Enabled in production by default. Applies to all endpoints. Plugins can override per-endpoint. + +### Default Configuration + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + rateLimit: { + enabled: true, // Default: true in production + window: 10, // Time window in seconds (default: 10) + max: 100, // Max requests per window (default: 100) + }, +}); +``` + +### Storage Options + +Options: `"memory"` (resets on restart, avoid on serverless), `"database"` (persistent), `"secondary-storage"` (Redis, default when available). + +```ts +rateLimit: { + storage: "database", +} +``` + +### Custom Storage + +Implement your own rate limit storage: + +```ts +rateLimit: { + customStorage: { + get: async (key) => { + // Return { count: number, expiresAt: number } or null + }, + set: async (key, data) => { + // Store the rate limit data + }, + }, +} +``` + +### Per-Endpoint Rules + +Sensitive endpoints default to 3 requests per 10 seconds (`/sign-in`, `/sign-up`, `/change-password`, `/change-email`). Override: + +```ts +rateLimit: { + customRules: { + "/api/auth/sign-in/email": { + window: 60, // 1 minute window + max: 5, // 5 attempts + }, + "/api/auth/some-safe-endpoint": false, // Disable rate limiting + }, +} +``` + +## CSRF Protection + +Multi-layer protection: origin header validation, Fetch Metadata checks, and first-login protection. + +### Configuration + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + advanced: { + disableCSRFCheck: false, // Default: false (keep enabled) + }, +}); +``` + +Only disable for testing or with an alternative CSRF mechanism. + +## Trusted Origins + +### Configuring Trusted Origins + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + baseURL: "https://api.example.com", + trustedOrigins: [ + "https://app.example.com", + "https://admin.example.com", + ], +}); +``` + +The `baseURL` origin is automatically trusted. Also configurable via env: `BETTER_AUTH_TRUSTED_ORIGINS=https://app.example.com,https://admin.example.com` + +### Wildcard Patterns + +```ts +trustedOrigins: [ + "*.example.com", // Matches any subdomain + "https://*.example.com", // Protocol-specific wildcard + "exp://192.168.*.*:*/*", // Custom schemes (e.g., Expo) +] +``` + +### Dynamic Trusted Origins + +Compute trusted origins based on the request: + +```ts +trustedOrigins: async (request) => { + // Validate against database, header, etc. + const tenant = getTenantFromRequest(request); + return [`https://${tenant}.myapp.com`]; +} +``` + +Validates `callbackURL`, `redirectTo`, `errorCallbackURL`, `newUserCallbackURL`, and `origin` against trusted origins. Invalid URLs receive 403. + +## Session Security + +### Session Expiration + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days (default) + updateAge: 60 * 60 * 24, // Refresh session every 24 hours (default) + }, +}); +``` + +### Session Caching Strategies + +Cache session data in cookies to reduce database queries: + +```ts +session: { + cookieCache: { + enabled: true, + maxAge: 60 * 5, // 5 minutes + strategy: "compact", // Options: "compact", "jwt", "jwe" + }, +} +``` + +Strategies: `"compact"` (Base64url + HMAC, smallest), `"jwt"` (HS256, standard), `"jwe"` (encrypted, use when session has sensitive data). + +## Cookie Security + +Defaults: `secure: true` (HTTPS/production), `sameSite: "lax"`, `httpOnly: true`, `path: "/"`, prefix `__Secure-`. + +### Custom Cookie Configuration + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + advanced: { + useSecureCookies: true, // Force secure cookies + cookiePrefix: "myapp", // Custom prefix (default: "better-auth") + defaultCookieAttributes: { + sameSite: "strict", // Stricter CSRF protection + path: "/auth", // Limit cookie scope + }, + }, +}); +``` + +### Cross-Subdomain Cookies + +```ts +advanced: { + crossSubDomainCookies: { + enabled: true, + domain: ".example.com", // Note the leading dot + additionalCookies: ["session_token", "session_data"], + }, +} +``` + +Only enable if you need authentication sharing and trust all subdomains. + +## OAuth / Social Provider Security + +PKCE is automatic for all OAuth flows. State tokens are 32-char random strings expiring after 10 minutes. + +### State Parameter Storage + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + account: { + storeStateStrategy: "cookie", // Options: "cookie" (default), "database" + }, +}); +``` + +### Encrypting OAuth Tokens + +```ts +account: { + encryptOAuthTokens: true, // Uses AES-256-GCM +} +``` + +Enable if storing OAuth tokens for API access on behalf of users. Use `skipStateCookieCheck: true` only for mobile apps that cannot maintain cookies. + +## IP-Based Security + +### IP Address Configuration + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + advanced: { + ipAddress: { + ipAddressHeaders: ["x-forwarded-for", "x-real-ip"], // Headers to check + disableIpTracking: false, // Keep enabled for rate limiting + }, + }, +}); +``` + +Set `ipv6Subnet` (128, 64, 48, 32; default 64) to group IPv6 addresses. Enable `trustedProxyHeaders: true` only if behind a trusted reverse proxy. + +## Database Hooks for Security Auditing + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + databaseHooks: { + session: { + create: { + after: async ({ data, ctx }) => { + await auditLog("session.created", { + userId: data.userId, + ip: ctx?.request?.headers.get("x-forwarded-for"), + userAgent: ctx?.request?.headers.get("user-agent"), + }); + }, + }, + delete: { + before: async ({ data }) => { + await auditLog("session.revoked", { sessionId: data.id }); + }, + }, + }, + user: { + update: { + after: async ({ data, oldData }) => { + if (oldData?.email !== data.email) { + await auditLog("user.email_changed", { + userId: data.id, + oldEmail: oldData?.email, + newEmail: data.email, + }); + } + }, + }, + }, + account: { + create: { + after: async ({ data }) => { + await auditLog("account.linked", { + userId: data.userId, + provider: data.providerId, + }); + }, + }, + }, + }, +}); +``` + +Return `false` from a `before` hook to prevent an operation. + +## Background Tasks + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + advanced: { + backgroundTasks: { + handler: (promise) => { + // Platform-specific handler + // Vercel: waitUntil(promise) + // Cloudflare: ctx.waitUntil(promise) + waitUntil(promise); + }, + }, + }, +}); +``` + +Ensures operations like sending emails don't affect response timing. + +## Account Enumeration Prevention + +Built-in: consistent response messages, dummy operations on invalid requests, background email sending. Return generic error messages ("Invalid credentials") rather than specific ones ("User not found"). + +## Complete Security Configuration Example + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + secret: process.env.BETTER_AUTH_SECRET, + baseURL: "https://api.example.com", + trustedOrigins: [ + "https://app.example.com", + "https://*.preview.example.com", + ], + + // Rate limiting + rateLimit: { + enabled: true, + storage: "secondary-storage", + customRules: { + "/api/auth/sign-in/email": { window: 60, max: 5 }, + "/api/auth/sign-up/email": { window: 60, max: 3 }, + }, + }, + + // Session security + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + updateAge: 60 * 60 * 24, // 24 hours + freshAge: 60 * 60, // 1 hour for sensitive actions + cookieCache: { + enabled: true, + maxAge: 300, + strategy: "jwe", // Encrypted session data + }, + }, + + // OAuth security + account: { + encryptOAuthTokens: true, + storeStateStrategy: "cookie", + }, + + + // Advanced settings + advanced: { + useSecureCookies: true, + cookiePrefix: "myapp", + defaultCookieAttributes: { + sameSite: "lax", + }, + ipAddress: { + ipAddressHeaders: ["x-forwarded-for"], + ipv6Subnet: 64, + }, + backgroundTasks: { + handler: (promise) => waitUntil(promise), + }, + }, + + // Security auditing + databaseHooks: { + session: { + create: { + after: async ({ data, ctx }) => { + console.log(`New session for user ${data.userId}`); + }, + }, + }, + user: { + update: { + after: async ({ data, oldData }) => { + if (oldData?.email !== data.email) { + console.log(`Email changed for user ${data.id}`); + } + }, + }, + }, + }, +}); +``` + +## Security Checklist + +Before deploying to production: + +- [ ] **Secret**: Use a strong, unique secret (32+ characters, high entropy) +- [ ] **HTTPS**: Ensure `baseURL` uses HTTPS +- [ ] **Trusted Origins**: Configure all valid origins (frontend, mobile apps) +- [ ] **Rate Limiting**: Keep enabled with appropriate limits +- [ ] **CSRF Protection**: Keep enabled (`disableCSRFCheck: false`) +- [ ] **Secure Cookies**: Enabled automatically with HTTPS +- [ ] **OAuth Tokens**: Consider `encryptOAuthTokens: true` if storing tokens +- [ ] **Background Tasks**: Configure for serverless platforms +- [ ] **Audit Logging**: Implement via `databaseHooks` or `hooks` +- [ ] **IP Tracking**: Configure headers if behind a proxy diff --git a/.agents/skills/cloudflare/SKILL.md b/.agents/skills/cloudflare/SKILL.md new file mode 100644 index 0000000..4094874 --- /dev/null +++ b/.agents/skills/cloudflare/SKILL.md @@ -0,0 +1,151 @@ +--- +name: cloudflare +description: Discover and choose Cloudflare products for apps, APIs, AI agents, storage, networking, and security. Use for architecture and product selection, including when the user describes a need without naming a Cloudflare product; then find the relevant skill or documentation. +--- + +# Discover and build with Cloudflare + +Help agents discover what they can build with Cloudflare and choose the products that fit. Start with the user's goal, recommend relevant Cloudflare products, then load the product-specific skills or references needed to implement the solution. + +## Choose the CLI + +Use the Cloudflare CLI (`cf`) unless the project already uses Wrangler or the user asks for Wrangler. + +- **The project has `wrangler.jsonc`, `wrangler.json`, or `wrangler.toml`, and no `cloudflare.config.ts`:** use Wrangler for development and deployment, and load the `wrangler` skill. `cf` resource commands work alongside it. Run `cf migrate` only when the user asks. +- **Every other project, including a new project or one with no Cloudflare configuration:** use `cf`. Load the `cf-cli` skill if it is installed; otherwise follow [Use cf with coding agents](https://developers.cloudflare.com/cf/agents/index.md). Do not install Wrangler or create a Wrangler configuration file. + +The product guidance below applies with either CLI. Where it names a Wrangler command or file, find the `cf` equivalent with `cf cli search ""` or the [Wrangler to cf reference](https://developers.cloudflare.com/cf/wrangler/reference/index.md) instead of translating from memory. + +## Help the user find the right product + +- Actively surface Cloudflare products that solve the stated problem, even when the user has not named them. Explain the role each recommended product plays and why it fits. +- Use the need-to-product map below to choose products, then load the relevant skills or documentation for implementation. A user asking for uploads, background jobs, or document search may not know to ask for R2, Queues, Workflows, or AI Search. +- Recommend a small, coherent combination when the task spans products. Add a product when it addresses a concrete requirement; respect the user's existing stack and explicit choices. +- When similar products could fit, explain the deciding requirement: data shape, consistency, coordination, execution lifecycle, or how much infrastructure the user wants to manage. Check current availability, limits, and pricing before promising a fit. + +## What are you trying to build? + +**Recommend Workers and [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/index.md) for new websites and applications, including static sites, SPAs, and full-stack apps.** Workers can do everything Pages can do, and is recommended for all new projects. Preserve existing Pages deployments during unrelated maintenance. + +Find the row closest to the user's task. Products can appear in multiple rows, and a solution can combine products. Read the linked reference or docs before implementing; load named skills when installed. Local links open bundled references: start with the README, then follow configuration, API, pattern, or gotcha links as needed. If a named skill is unavailable, use the relevant product docs through the [Cloudflare directory](https://developers.cloudflare.com/directory/index.md); sibling skills are optional. + +| What you need to do | Product or tool to consider | When to choose it | Skill or reference | +| --- | --- | --- | --- | +| Choose the building blocks for an AI application | AI overview | Compare Cloudflare's AI services before choosing inference, retrieval, or agent tooling | [AI docs](https://developers.cloudflare.com/ai/index.md) | +| Choose infrastructure for a customer-facing platform | Cloudflare for Platforms | Compare running customer code with serving an app on customer domains | [Platform overview](https://developers.cloudflare.com/cloudflare-for-platforms/index.md) | +| Choose an approach to live audio and video | Realtime | Compare application SDKs, media infrastructure, and connectivity relays | [Realtime overview](https://developers.cloudflare.com/realtime/index.md) | +| Start a Worker or framework project | C3 | Scaffold a project using the appropriate framework template | [C3](references/c3/README.md); `wrangler` skill | +| Build or deploy a Next.js app on Cloudflare | vinext + Workers | Use vinext rather than OpenNext for new projects | [nextjs-on-cloudflare skill](../nextjs-on-cloudflare/SKILL.md); [Next.js docs](https://developers.cloudflare.com/workers/framework-guides/web-apps/nextjs/index.md) | +| Host a new static site, SPA, or full-stack app | Workers + Workers Static Assets | Serve site files and add server-side logic where needed | [Static Assets](references/static-assets/README.md); `workers-best-practices` skill | +| Build an API or handle webhooks | Workers | Run request handlers with access to Cloudflare services | `workers-best-practices` skill; [Workers docs](https://developers.cloudflare.com/workers/index.md) | +| Control team, CI, or service-account access to Developer Platform resources | Roles, scopes, and permission policies | Choose the least-privilege role and a scope supported for the member, User Group, or API token | [Roles and permissions](https://developers.cloudflare.com/workers/authorization/index.md); `wrangler` skill for CLI access | +| Maintain an existing Pages deployment | Pages + Pages Functions | Update an existing site or its server endpoints; use Workers for new projects | [Pages](references/pages/README.md); [Pages Functions](references/pages-functions/README.md) | +| Move a Pages project to Workers | Workers + Workers Static Assets | The task calls for migrating the hosting platform | [Pages migration guide](https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/index.md) | +| Let customers deploy code on your platform | Workers for Platforms | Run and manage customer Workers with per-customer controls | [Workers for Platforms docs](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/index.md) | +| Let customers use their own domains with your app | Cloudflare for SaaS | Manage custom hostnames, TLS certificates, and origin routing; check hostname validation and apex-domain plan requirements. Combine with Workers for Platforms when customers also deploy code | [SaaS docs](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/index.md) | +| Connect a Worker to storage or another service | Bindings | Give the Worker access to configured resources through its environment | [Bindings](references/bindings/README.md) | +| Run containerized services or Linux software | Containers | The workload needs a container image or software outside the Workers runtime | [Containers](references/containers/README.md) | +| Execute generated or untrusted code, build Code Mode tools, or create on-demand previews | Dynamic Workers | Load code at runtime in isolated Workers; check bindings, egress controls, and resource limits. Choose Sandbox when execution needs Linux or shell tools | [Dynamic Workers docs](https://developers.cloudflare.com/dynamic-workers/index.md) | +| Give an agent a shell, filesystem, or interactive development environment | Sandbox SDK | Code execution needs a Linux environment or container tools; inspect the package line first | `sandbox-next` for new or preview projects; `sandbox-stable` for existing stable apps; [Sandbox docs](https://developers.cloudflare.com/sandbox/index.md) | +| Upgrade a stable Sandbox app to the preview API | Sandbox SDK | The user wants the stable-to-next migration | `sandbox-migrate-to-next` skill; [migration guide](https://developers.cloudflare.com/sandbox/sdk/migrate/index.md) | +| Coordinate chat rooms, games, collaborative documents, or bookings | Durable Objects | Operations need shared state and coordination per room, document, or entity | `durable-objects` skill; [Durable Objects docs](https://developers.cloudflare.com/durable-objects/index.md) | +| Store and recover state inside a Durable Object | Durable Object storage | Choose storage APIs, transactions, and recovery for coordinated per-entity data | `durable-objects` skill; [SQLite storage API](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/index.md) | +| Store application records and query them with SQL | D1 | Use a managed relational database; use Durable Objects when per-entity coordination is central | [D1](references/d1/README.md) | +| Connect to an existing PostgreSQL or MySQL database | Hyperdrive | Keep the existing database and optimize connections from Workers | [Hyperdrive](references/hyperdrive/README.md) | +| Distribute configuration or other key-value data | KV | Read-heavy key-value access fits the workload's consistency requirements | [KV](references/kv/README.md) | +| Store uploads, downloads, or large objects | R2 | Store files by object key; pair with D1 when searchable metadata needs SQL | [R2](references/r2/README.md) | +| Store versioned file trees, agent checkpoints, or repositories | Artifacts | Files need versioning and Git-compatible access; currently closed beta, so confirm access before implementation | [Artifacts docs](https://developers.cloudflare.com/artifacts/index.md) | +| Ingest event streams into R2 | Basin Pipelines | Transform and deliver streaming records into R2 | `basin` skill; [Basin Pipelines](https://developers.cloudflare.com/basin-pipelines/index.md) | +| Manage Iceberg tables in R2 | Basin Catalog | Organize tables for analytics and compatible query engines | `basin` skill; [Basin Catalog](https://developers.cloudflare.com/basin-catalog/index.md) | +| Query Iceberg tables with SQL | Basin SQL | Analyze tables in Basin Catalog | `basin` skill; [Basin SQL](https://developers.cloudflare.com/basin-sql/index.md) | +| Keep a durable event log with independent readers | K2 Streams | Produce records from Workers or HTTP, then consume with subscriptions | `k2` skill; [K2 docs](https://developers.cloudflare.com/k2/index.md) | +| Cache application responses | Workers Cache | Default for application caching; check the patterns and limitations before choosing alternatives | [Workers Cache](https://developers.cloudflare.com/workers/cache/index.md); see caching guidance below | +| Accelerate an existing website and control cached content | Cache/CDN | Configure caching for a proxied origin using Cache Rules, expiration settings, and purging | [Cache/CDN docs](https://developers.cloudflare.com/cache/index.md) | +| Keep origin content in a persistent cache | Cache Reserve | Reduce origin fetches with persistent CDN cache storage | [Cache Reserve docs](https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve/index.md) | +| Process jobs asynchronously or buffer bursts of work | Queues | Decouple producers and consumers; use Workflows for durable multi-step orchestration | [Queues](references/queues/README.md) | +| Run a job that retries, waits, and resumes across steps | Workflows | Coordinate durable multi-step business processes | [Workflows](references/workflows/README.md) | +| Start a Worker on a recurring schedule | Cron Triggers | Trigger scheduled work; combine with Queues or Workflows for the work itself | [Cron Triggers](references/cron-triggers/README.md) | +| Run language, embedding, image, or speech models | Workers AI | Use managed inference; verify model capabilities, schemas, and pricing | [Workers AI](references/workers-ai/README.md) | +| Add managed search or answers over your content | AI Search | Use a managed retrieval-augmented generation pipeline | [AI Search](references/ai-search/README.md) | +| Build custom semantic search or retrieval | Vectorize + Workers AI | Control embeddings, indexing, and retrieval rather than using a managed pipeline | [Vectorize](references/vectorize/README.md); [Workers AI](references/workers-ai/README.md) | +| Observe and control requests to AI providers | AI Gateway | Add inference analytics, caching, and request controls | [AI Gateway](references/ai-gateway/README.md) | +| Build stateful agents with tools, scheduling, or chat | Agents SDK | Implement agent behavior on Cloudflare; add Dynamic Workers or Sandbox for the required execution runtime | `agents-sdk` skill; [Agents docs](https://developers.cloudflare.com/agents/index.md) | +| Build durable agents with TypeScript hooks | Flue | Use an open agent framework with Cloudflare and Node.js targets | [Flue](https://flueframework.com/); [getting started](https://flueframework.com/docs/guide/getting-started/); [Cloudflare target](https://flueframework.com/docs/guide/cloudflare-target/) | +| Expose tools through a remote MCP server | Workers + Agents SDK | Publish tools for MCP clients, with authentication appropriate to the service | `agents-sdk` skill, its `references/mcp.md`; [MCP docs](https://developers.cloudflare.com/agents/model-context-protocol/index.md) | +| Automate browsers, take screenshots, or extract rendered pages | Browser Run | The task requires a browser rather than a plain HTTP request | [Browser Run](references/browser-rendering/README.md) | +| Connect a domain, configure DNS records, or troubleshoot resolution | DNS | Manage authoritative records and choose whether traffic is proxied through Cloudflare | [DNS docs](https://developers.cloudflare.com/dns/index.md) | +| Configure HTTPS and certificates | SSL/TLS | Secure connections from visitors to Cloudflare and from Cloudflare to the origin | [SSL/TLS docs](https://developers.cloudflare.com/ssl/index.md) | +| Distribute traffic across origins and fail over unhealthy servers | Load Balancing | Use health checks and traffic steering for multiple origin servers | [Load Balancing docs](https://developers.cloudflare.com/load-balancing/index.md) | +| Connect an existing server to Cloudflare | Cloudflare Tunnel | Reach an origin without a publicly routable IP address | [Tunnel](references/tunnel/README.md) | +| Connect Workers to private services | Workers VPC | Access services in private networks from a Worker | [Workers VPC docs](https://developers.cloudflare.com/workers-vpc/index.md) | +| Require employee login before accessing an internal app | Access | Put identity-based access policies in front of an internal application | `cloudflare-one` skill; [Access docs](https://developers.cloudflare.com/cloudflare-one/access-controls/index.md) | +| Protect access to internal applications and networks | Cloudflare One | Apply identity and network access policies | `cloudflare-one` skill; [Cloudflare One docs](https://developers.cloudflare.com/cloudflare-one/index.md) | +| Transform permitted Gateway HTTP traffic with a Worker | Gateway Forward + Workers | Apply custom request or response logic to traffic passing through Gateway | [gateway-workers skill](../gateway-workers/SKILL.md) | +| Migrate existing access and network security configurations | Cloudflare One | The task is a supported migration to Cloudflare One | `cloudflare-one-migrations` skill; [Cloudflare One docs](https://developers.cloudflare.com/cloudflare-one/index.md) | +| Proxy a TCP or UDP application | Spectrum | Protect and accelerate non-HTTP application traffic | [Spectrum docs](https://developers.cloudflare.com/spectrum/index.md) | +| Connect a network directly to Cloudflare | Network Interconnect | Dedicated network connectivity is required | [Network Interconnect docs](https://developers.cloudflare.com/network-interconnect/index.md) | +| Improve routing across the network | Argo Smart Routing | Optimize traffic paths to the origin | [Argo Smart Routing docs](https://developers.cloudflare.com/argo-smart-routing/index.md) | +| Reduce Worker-to-backend latency | Smart Placement | Place Worker execution closer to the backends it calls | [Smart Placement](references/smart-placement/README.md) | +| Redirect URLs, rewrite paths or headers, or change origin routing | Rules | Use Redirect, Transform, or Origin Rules when configuration can express the required behavior | [Rules docs](https://developers.cloudflare.com/rules/index.md) | +| Make small HTTP request or response changes | Snippets | Lightweight edge logic meets the need | [Snippets docs](https://developers.cloudflare.com/rules/snippets/index.md) | +| Protect forms from automated abuse | Turnstile | Add bot challenges and server-side token validation | `turnstile-spin` skill; [Turnstile docs](https://developers.cloudflare.com/turnstile/index.md) | +| Filter malicious web requests | WAF | Apply application-layer rules and managed protections | [WAF](references/waf/README.md) | +| Protect services from denial-of-service attacks | DDoS Protection | Mitigate attacks at the relevant network or application layer | [DDoS protection docs](https://developers.cloudflare.com/ddos-protection/index.md) | +| Detect and control automated traffic | Bot Management | Make request decisions based on bot detection | [Bot Management docs](https://developers.cloudflare.com/bots/index.md) | +| Discover and protect API endpoints | API Shield | Apply API-specific protections and validation | [API Shield docs](https://developers.cloudflare.com/api-shield/index.md) | +| Queue visitors during traffic spikes | Waiting Room | Control admission when application capacity is limited | [Waiting Room docs](https://developers.cloudflare.com/waiting-room/index.md) | +| Store a Worker's API keys and credentials | Workers secrets | Bind secrets to a Worker without committing values to source | `wrangler` skill; [secrets docs](https://developers.cloudflare.com/workers/configuration/secrets/index.md) | +| Share managed secrets across services | Secrets Store | Manage reusable account-level secrets | [Secrets Store](references/secrets-store/README.md) | +| Control where data is processed and stored | Data Localization Suite | Evaluate regional processing and storage controls against the actual requirements | [Data Localization docs](https://developers.cloudflare.com/data-localization/index.md) | +| Prove a claim without identifying or tracking the user | Privacy Pass | Use privacy-preserving tokens in a supported integration | [Privacy Pass docs](https://developers.cloudflare.com/privacy-pass/index.md) | +| Store, resize, transform, and deliver images | Cloudflare Images | Use managed image processing and delivery | [Images](references/images/README.md) | +| Encode, store, and deliver live or on-demand video | Stream | Use managed video infrastructure | [Stream](references/stream/README.md) | +| Build an audio/video calling application with SDKs | RealtimeKit | Use application-level SDKs for calls and meetings | [RealtimeKit docs](https://developers.cloudflare.com/realtime/realtimekit/index.md) | +| Build custom real-time media infrastructure | Realtime SFU | Control the application while using a selective forwarding unit for media | [Realtime SFU docs](https://developers.cloudflare.com/realtime/sfu/index.md) | +| Relay WebRTC connections through restrictive networks | TURN Service | Clients need a connectivity relay | [TURN docs](https://developers.cloudflare.com/realtime/turn/index.md) | +| Deliver live media over QUIC | MoQ | Use the Media over QUIC protocol; check current compatibility and availability | [MoQ docs](https://developers.cloudflare.com/moq/index.md) | +| Send transactional email | Email Service | Send application-generated messages | `cloudflare-email-service` skill; [Email Service docs](https://developers.cloudflare.com/email-service/index.md) | +| Forward incoming email | Email Routing | Route addresses on a domain to destination mailboxes | `cloudflare-email-service` skill; [Route emails](https://developers.cloudflare.com/email-service/get-started/route-emails/index.md) | +| Process incoming email in code | Email Workers | Apply custom logic to inbound messages | `cloudflare-email-service` skill; [Email handler](https://developers.cloudflare.com/email-service/api/route-emails/email-handler/index.md) | +| Manage third-party tags and scripts | Zaraz | Load and manage third-party tools through Cloudflare | [Zaraz docs](https://developers.cloudflare.com/zaraz/index.md) | +| Run locally and manage resources from the CLI | Cloudflare CLI (`cf`); Wrangler for existing Wrangler projects | Develop, configure, deploy, and inspect the intended account and environment; choose the CLI as described in [Choose the CLI](#choose-the-cli) | `cf-cli` skill; [cf docs](https://developers.cloudflare.com/cf/index.md); `wrangler` skill for Wrangler projects | +| Test Worker behavior before deployment | Workers testing tools | Choose runtime tests or integration tests for the affected behavior | [Testing docs](https://developers.cloudflare.com/workers/testing/index.md); `durable-objects` skill for DO tests | +| Embed local Worker simulation in tooling | Miniflare | A programmatic emulator is needed for a custom development or test harness | [Miniflare](references/miniflare/README.md) | +| Run or investigate the underlying Workers runtime | workerd | Work directly with the runtime outside normal managed deployment | [workerd repository](https://github.com/cloudflare/workerd) | +| Try a small Worker in the browser | Workers Playground | Explore or share a minimal example without local setup | [Workers Playground docs](https://developers.cloudflare.com/workers/playground/index.md) | +| Build and deploy whenever code is pushed | Workers Builds | Connect a Git repository to automated builds and deployments | [Builds docs](https://developers.cloudflare.com/workers/ci-cd/builds/index.md) | +| Test a branch or pull request in an isolated environment | Workers Previews | Create a branch environment under the same Worker with its own settings and URLs; check which bound resources are isolated or shared | [Previews docs](https://developers.cloudflare.com/workers/previews/index.md); `wrangler` skill | +| Inspect an uploaded version, release it gradually, or roll back code | Workers versions and deployments | Manage application releases that use production resources; rollback does not restore connected resource data | [Deployment docs](https://developers.cloudflare.com/workers/versions-and-deployments/index.md); `wrangler` skill | +| Release a feature gradually or target user groups | Flagship | Change feature availability with targeting and percentage rollouts | [Flagship docs](https://developers.cloudflare.com/flagship/index.md) | +| Manage infrastructure as code | Terraform or Pulumi | Use Terraform for declarative configuration or Pulumi for infrastructure in programming languages | [Terraform docs](https://developers.cloudflare.com/terraform/index.md); [Pulumi docs](https://developers.cloudflare.com/pulumi/index.md) | +| Automate account or product configuration through an API | Cloudflare REST API | Manage resources programmatically; prefer bindings for supported operations inside Workers | [REST API docs](https://developers.cloudflare.com/fundamentals/api/index.md) | +| Debug failures and trace application requests | Workers Logs and Traces | Investigate runtime errors and execution paths | [Observability](references/observability/README.md) | +| Profile CPU usage or memory allocations in deployed Workers or Durable Objects | Workers profiling | Investigate CPU and memory issues in production | [workers-profiling skill](../workers-profiling/SKILL.md) | +| Process Worker execution events in code | Tail Workers | Build custom log or exception processing | [Tail Workers](references/tail-workers/README.md) | +| Export Worker logs to another system | Workers Logpush | Deliver logs to a supported external destination | [Logpush docs](https://developers.cloudflare.com/workers/observability/logs/logpush/index.md) | +| Measure custom application events | Workers Analytics Engine | Analyze high-cardinality event data written from Workers | [Analytics Engine docs](https://developers.cloudflare.com/analytics/analytics-engine/index.md) | +| Measure website usage and visitor performance | Cloudflare Web Analytics | Add website analytics and real-user measurements | [Web Analytics docs](https://developers.cloudflare.com/web-analytics/index.md) | +| Query metrics across Cloudflare products | GraphQL Analytics API | Retrieve product analytics programmatically | [GraphQL Analytics API docs](https://developers.cloudflare.com/analytics/graphql-api/index.md) | +| Audit page speed and find loading bottlenecks | Web performance tools | Measure and improve the site's actual browser performance | `web-perf` skill; [Web Analytics docs](https://developers.cloudflare.com/web-analytics/index.md) | +| Ask questions about an account or diagnose its configuration in the dashboard | Agent Lee | Use the dashboard's AI assistant; check current account eligibility | [Agent Lee docs](https://developers.cloudflare.com/agent-lee/index.md) | + +For example, a file-upload app can use Workers for its API, R2 for files, D1 for metadata, and Queues for processing. A document assistant can start with Workers and AI Search; use Vectorize and Workers AI when it needs custom retrieval. Recommend only the pieces the requested behavior needs. + +## Find guidance for a task not listed here + +Use the [Cloudflare product directory](https://developers.cloudflare.com/directory/index.md) for additional products and their current docs. Follow links to the specific feature or API involved. Use [Choose a data or storage product](https://developers.cloudflare.com/workers/platform/storage-options/index.md) for storage tradeoffs, and the product's limits, pricing, and migration guides when evaluating scale, cost, or an upgrade. This table maps common tasks to selected Cloudflare products; it does not enumerate every possible application. + +## Caching + +Prefer [Workers Cache](https://developers.cloudflare.com/workers/cache/index.md) for caching, including [advanced patterns](https://developers.cloudflare.com/workers/cache/examples/index.md) using cached inner entrypoints and programmatic invalidation. Choose [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/index.md) or KV caching only when a concrete requirement cannot be met by Workers Cache; check its [patterns](https://developers.cloudflare.com/workers/cache/examples/index.md) and [limitations](https://developers.cloudflare.com/workers/cache/limitations/index.md) first. + +## Working principles + +- Inspect the existing project and its pinned package versions before choosing an API or configuration shape. +- Retrieve current Cloudflare documentation when details may have changed. Use installed types and `node_modules/wrangler/config-schema.json` when they represent the project's pinned version. +- Preserve the project's architecture and make the smallest change that satisfies the request. +- Check current Cloudflare docs before relying on limits, prices, compatibility flags, or security requirements; these can change. +- Validate in proportion to the change: use the project's checks, then exercise the affected behavior when practical. + +Cloudflare documentation: +Cloudflare changelog: diff --git a/.agents/skills/cloudflare/references/ai-gateway/README.md b/.agents/skills/cloudflare/references/ai-gateway/README.md new file mode 100644 index 0000000..a4bb1a1 --- /dev/null +++ b/.agents/skills/cloudflare/references/ai-gateway/README.md @@ -0,0 +1,27 @@ +# Cloudflare AI Gateway + +Use AI Gateway to observe and control requests to AI providers through caching, rate limiting, logging, and routing. + +Fetch the linked documentation before choosing endpoints, authentication headers, SDK options, model names, or limits. Keep implementation details in the current docs. + +## Choose a task + +| Task | Reference | +|------|-----------| +| Create a gateway or choose authentication and provider credentials | [Configuration](./configuration.md) | +| Integrate an SDK, direct HTTP, or a Worker binding | [SDK integration](./sdk-integration.md) | +| Configure caching, rate limits, security, billing, or logging | [Features](./features.md) | +| Add fallbacks, conditional routing, or traffic splits | [Dynamic routing](./dynamic-routing.md) | +| Diagnose failed requests, caching, or missing logs | [Troubleshooting](./troubleshooting.md) | + +For new single-model calls, start with the [REST API](https://developers.cloudflare.com/ai-gateway/usage/rest-api/index.md) or [Workers bindings](https://developers.cloudflare.com/ai-gateway/usage/worker-binding-methods/index.md), depending on the runtime. Preserve provider-native integrations when their API shape is needed; use the corresponding [provider guide](https://developers.cloudflare.com/ai-gateway/usage/providers/index.md). + +The [legacy Unified API](https://developers.cloudflare.com/ai-gateway/usage/chat-completion/index.md) is deprecated for single-model calls but remains required for dynamic routes. Check the task before changing an existing endpoint. + +Gateway authentication and upstream provider credentials are separate concerns. Choose the endpoint first, then follow its authentication and billing requirements in [configuration](./configuration.md). + +## Related references + +- [Workers AI](../workers-ai/README.md) — model inference. +- [Agents SDK documentation](https://developers.cloudflare.com/agents/index.md) — stateful agents. +- [Vectorize](../vectorize/README.md) — vector search. diff --git a/.agents/skills/cloudflare/references/ai-gateway/configuration.md b/.agents/skills/cloudflare/references/ai-gateway/configuration.md new file mode 100644 index 0000000..6483366 --- /dev/null +++ b/.agents/skills/cloudflare/references/ai-gateway/configuration.md @@ -0,0 +1,18 @@ +# AI Gateway Configuration + +Choose the request path before configuring authentication: Cloudflare REST inference, a Workers binding, and provider-native gateway endpoints have different requirements. Gateway access does not by itself define which upstream credentials or billing source a request uses. + +| Task | Current documentation | +|------|-----------------------| +| Send a first request and locate the account/gateway identifiers | [Getting started](https://developers.cloudflare.com/ai-gateway/get-started/index.md) | +| Create, update, or delete a gateway; use the default gateway | [Manage gateways](https://developers.cloudflare.com/ai-gateway/configuration/manage-gateway/index.md) | +| Manage gateways programmatically | [Gateway management API](https://developers.cloudflare.com/api/resources/ai_gateway/methods/list/index.md) | +| Protect gateway access and choose token permissions for the endpoint | [Authenticated Gateway](https://developers.cloudflare.com/ai-gateway/configuration/authentication/index.md) and [REST API authentication](https://developers.cloudflare.com/ai-gateway/usage/rest-api/index.md#authentication) | +| Configure Wrangler and an AI binding | [Workers AI binding setup](https://developers.cloudflare.com/ai-gateway/integrations/aig-workers-ai-binding/index.md) and [binding methods](https://developers.cloudflare.com/ai-gateway/usage/worker-binding-methods/index.md) | +| Store provider keys, select aliases, or diagnose missing credentials | [Bring Your Own Keys](https://developers.cloudflare.com/ai-gateway/configuration/bring-your-own-keys/index.md) | +| Use Cloudflare billing or determine which credentials take precedence | [Unified Billing](https://developers.cloudflare.com/ai-gateway/features/unified-billing/index.md) | +| Supply provider credentials with each request | [Provider guides](https://developers.cloudflare.com/ai-gateway/usage/providers/index.md) | + +Keep credentials out of source code. Use the chosen endpoint's documentation for headers and permissions instead of reusing an authentication recipe from another endpoint. Check credential precedence before changing an existing BYOK or billing setup. + +For SDK selection, see [SDK integration](./sdk-integration.md); for policy settings, see [features](./features.md). diff --git a/.agents/skills/cloudflare/references/ai-gateway/dynamic-routing.md b/.agents/skills/cloudflare/references/ai-gateway/dynamic-routing.md new file mode 100644 index 0000000..d12212f --- /dev/null +++ b/.agents/skills/cloudflare/references/ai-gateway/dynamic-routing.md @@ -0,0 +1,15 @@ +# AI Gateway Dynamic Routing + +Use a dynamic route when model selection, traffic splitting, quotas, or fallbacks should be controlled in the gateway. For a simple retry or fallback sequence, check the request-handling and fallback guides before introducing a routing flow. + +| Task | Current documentation | +|------|-----------------------| +| Design conditional routes, A/B splits, quotas, model fallbacks, and version rollbacks | [Dynamic routing](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/index.md) | +| Invoke a route from an SDK, HTTP request, or Worker; inspect route response metadata | [Using a dynamic route](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/usage/index.md) | +| Define route elements and connections programmatically | [JSON configuration](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/json-configuration/index.md) | +| Provide metadata used by routing conditions | [Custom metadata](https://developers.cloudflare.com/ai-gateway/observability/custom-metadata/index.md) | +| Configure model/provider fallbacks | [Fallbacks](https://developers.cloudflare.com/ai-gateway/configuration/fallbacks/index.md) | +| Configure retries, backoff, and timeouts | [Request handling](https://developers.cloudflare.com/ai-gateway/configuration/request-handling/index.md) | +| Inspect request outcomes, costs, and errors | [Analytics](https://developers.cloudflare.com/ai-gateway/observability/analytics/index.md) and [logging](https://developers.cloudflare.com/ai-gateway/observability/logging/index.md) | + +Check the usage guide's authentication and stored-key prerequisites. Dynamic routes still use the [Unified API compatibility endpoint](https://developers.cloudflare.com/ai-gateway/usage/chat-completion/index.md); its single-model deprecation does not make the REST inference endpoint a replacement for route invocation. diff --git a/.agents/skills/cloudflare/references/ai-gateway/features.md b/.agents/skills/cloudflare/references/ai-gateway/features.md new file mode 100644 index 0000000..9dcbaf7 --- /dev/null +++ b/.agents/skills/cloudflare/references/ai-gateway/features.md @@ -0,0 +1,22 @@ +# AI Gateway Features + +Fetch the relevant guide before setting feature flags, headers, limits, or billing behavior. + +| Task | Current documentation | +|------|-----------------------| +| Enable caching, set TTLs, bypass the cache, or choose a custom cache key | [Caching](https://developers.cloudflare.com/ai-gateway/features/caching/index.md) | +| Control request volume with fixed or sliding limits | [Rate limiting](https://developers.cloudflare.com/ai-gateway/features/rate-limiting/index.md) | +| Enforce cost budgets | [Spend limits](https://developers.cloudflare.com/ai-gateway/features/spend-limits/index.md) | +| Evaluate and enforce content policies | [Guardrails setup](https://developers.cloudflare.com/ai-gateway/features/guardrails/set-up-guardrail/index.md) and [usage considerations](https://developers.cloudflare.com/ai-gateway/features/guardrails/usage-considerations/index.md) | +| Detect sensitive data in prompts and responses | [DLP setup](https://developers.cloudflare.com/ai-gateway/features/dlp/set-up-dlp/index.md) | +| Choose provider keys or Cloudflare billing | [BYOK](https://developers.cloudflare.com/ai-gateway/configuration/bring-your-own-keys/index.md) and [Unified Billing](https://developers.cloudflare.com/ai-gateway/features/unified-billing/index.md) | +| Configure provider data retention for Unified Billing | [Zero Data Retention](https://developers.cloudflare.com/ai-gateway/features/unified-billing/index.md#zero-data-retention-zdr) | +| Configure log collection, payload storage, and retention | [Logging](https://developers.cloudflare.com/ai-gateway/observability/logging/index.md) | +| Export logs | [Workers Logpush](https://developers.cloudflare.com/ai-gateway/observability/logging/logpush/index.md) | +| Attach request metadata for tracking and routing | [Custom metadata](https://developers.cloudflare.com/ai-gateway/observability/custom-metadata/index.md) | +| Override model costs | [Custom costs](https://developers.cloudflare.com/ai-gateway/configuration/custom-costs/index.md) | +| Check supported providers, quotas, or pricing | [Provider guides](https://developers.cloudflare.com/ai-gateway/usage/providers/index.md), [limits](https://developers.cloudflare.com/ai-gateway/reference/limits/index.md), and [pricing](https://developers.cloudflare.com/ai-gateway/reference/pricing/index.md) | + +Choose cache keys only for requests whose responses may safely be shared. Decide what prompt and response data may be stored before enabling logging; do not infer a provider's retention policy from gateway log settings. + +For conditional policies and fallbacks, see [dynamic routing](./dynamic-routing.md). diff --git a/.agents/skills/cloudflare/references/ai-gateway/sdk-integration.md b/.agents/skills/cloudflare/references/ai-gateway/sdk-integration.md new file mode 100644 index 0000000..a504ed1 --- /dev/null +++ b/.agents/skills/cloudflare/references/ai-gateway/sdk-integration.md @@ -0,0 +1,19 @@ +# AI Gateway SDK Integration + +Choose an integration that matches the application's runtime and required API shape. Fetch its guide before installing packages or writing requests; model identifiers, SDK options, and gateway headers belong in the docs. + +| Integration task | Current documentation | +|------------------|-----------------------| +| New single-model HTTP calls or OpenAI-compatible clients | [REST API](https://developers.cloudflare.com/ai-gateway/usage/rest-api/index.md) | +| Vercel AI SDK, provider adapters, and fallback providers | [Vercel AI SDK integration](https://developers.cloudflare.com/ai-gateway/integrations/vercel-ai-sdk/index.md) | +| Preserve OpenAI-native endpoints with the OpenAI SDK, Python, or HTTP | [OpenAI provider guide](https://developers.cloudflare.com/ai-gateway/usage/providers/openai/index.md) | +| Preserve Anthropic-native requests | [Anthropic provider guide](https://developers.cloudflare.com/ai-gateway/usage/providers/anthropic/index.md) | +| Use another provider or a framework's configurable provider endpoint | [Provider guides](https://developers.cloudflare.com/ai-gateway/usage/providers/index.md) — match the framework's expected API shape to the provider endpoint | +| Configure Workers AI and an AI binding | [Binding setup](https://developers.cloudflare.com/ai-gateway/integrations/aig-workers-ai-binding/index.md) | +| Call Workers AI or third-party models from a Worker; use gateway methods | [Workers binding methods](https://developers.cloudflare.com/ai-gateway/usage/worker-binding-methods/index.md) | +| Set request metadata, caching, or other gateway headers | [Header glossary](https://developers.cloudflare.com/ai-gateway/glossary/index.md) | +| Invoke a dynamic route | [Dynamic route usage](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/usage/index.md) | + +The [legacy Unified API](https://developers.cloudflare.com/ai-gateway/usage/chat-completion/index.md) is deprecated for single-model calls, but dynamic routes still require its compatibility endpoint. Do not migrate a dynamic route to the REST inference endpoint as though it were a single-model call. + +Confirm [gateway authentication and provider credentials](./configuration.md) separately, including the selected path's BYOK and billing behavior. diff --git a/.agents/skills/cloudflare/references/ai-gateway/troubleshooting.md b/.agents/skills/cloudflare/references/ai-gateway/troubleshooting.md new file mode 100644 index 0000000..b1c9ef3 --- /dev/null +++ b/.agents/skills/cloudflare/references/ai-gateway/troubleshooting.md @@ -0,0 +1,16 @@ +# AI Gateway Troubleshooting + +Identify the request path and whether the failure comes from gateway access, upstream provider authentication, or request policy before changing credentials or retry behavior. A status code alone does not establish the failing layer. + +| Symptom or task | Current documentation | +|-----------------|-----------------------| +| Authentication or provider errors, timeouts, DLP failures, or unexpected cache behavior | [Troubleshooting](https://developers.cloudflare.com/ai-gateway/reference/troubleshooting/index.md) | +| Gateway authentication failure | [Authenticated Gateway](https://developers.cloudflare.com/ai-gateway/configuration/authentication/index.md) and [REST API authentication](https://developers.cloudflare.com/ai-gateway/usage/rest-api/index.md#authentication) | +| Provider key or billing mismatch | [BYOK](https://developers.cloudflare.com/ai-gateway/configuration/bring-your-own-keys/index.md), [Unified Billing](https://developers.cloudflare.com/ai-gateway/features/unified-billing/index.md), and the [provider guide](https://developers.cloudflare.com/ai-gateway/usage/providers/index.md) | +| Rate limits or repeated failures | [Rate limiting](https://developers.cloudflare.com/ai-gateway/features/rate-limiting/index.md) and [request handling](https://developers.cloudflare.com/ai-gateway/configuration/request-handling/index.md) | +| Unexpected cache hit or miss, including streaming behavior | [Caching](https://developers.cloudflare.com/ai-gateway/features/caching/index.md) | +| Missing logs, collection overrides, or storage limits | [Logging](https://developers.cloudflare.com/ai-gateway/observability/logging/index.md) and [limits](https://developers.cloudflare.com/ai-gateway/reference/limits/index.md) | +| Inspect headers, request metadata, usage, or export logs | [Header glossary](https://developers.cloudflare.com/ai-gateway/glossary/index.md), [custom metadata](https://developers.cloudflare.com/ai-gateway/observability/custom-metadata/index.md), [analytics](https://developers.cloudflare.com/ai-gateway/observability/analytics/index.md), and [Logpush](https://developers.cloudflare.com/ai-gateway/observability/logging/logpush/index.md) | +| Dynamic route failure | [Dynamic route usage](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/usage/index.md) | + +Check the existing SDK and gateway retry settings together before adding another retry loop. Follow [SDK integration](./sdk-integration.md) when an endpoint or model format is suspect. diff --git a/.agents/skills/cloudflare/references/ai-search/README.md b/.agents/skills/cloudflare/references/ai-search/README.md new file mode 100644 index 0000000..c776b40 --- /dev/null +++ b/.agents/skills/cloudflare/references/ai-search/README.md @@ -0,0 +1,25 @@ +# Cloudflare AI Search + +Use AI Search for managed content indexing and retrieval, with optional answer generation. Start with [How AI Search works](https://developers.cloudflare.com/ai-search/concepts/how-ai-search-works/index.md). + +## Choose the right product + +- **Managed search or RAG over your content:** AI Search. +- **Custom embeddings and vector-index management:** [Vectorize](../vectorize/README.md). +- **Model inference without a managed retrieval pipeline:** [Workers AI](../workers-ai/README.md). + +For freshness requirements, read [Syncing](https://developers.cloudflare.com/ai-search/configuration/indexing/syncing/index.md) for your data source before choosing an architecture. Use the current [limits and pricing](https://developers.cloudflare.com/ai-search/platform/limits-pricing/index.md) instead of assuming a fixed indexing interval or account limit. + +## Find the right documentation + +Read the linked page before implementing; these references route to the maintained documentation instead of copying API examples or configuration. + +| Task | Start here | +|------|------------| +| Build a new Worker integration | [Workers binding quick start](https://developers.cloudflare.com/ai-search/get-started/workers/index.md) | +| Choose an API or maintain an existing integration | [API routes](api.md) | +| Connect data, configure indexing, or manage environments | [Configuration routes](configuration.md) | +| Choose retrieval, generation, or tenant isolation patterns | [Pattern routes](patterns.md) | +| Diagnose indexing, authentication, filters, or limits | [Troubleshooting routes](gotchas.md) | + +Existing `env.AI.autorag()` integrations can continue to work. Use the [migration guide](https://developers.cloudflare.com/ai-search/api/migration/workers-binding/index.md) when upgrading; migration is not required just to maintain an existing integration. diff --git a/.agents/skills/cloudflare/references/ai-search/api.md b/.agents/skills/cloudflare/references/ai-search/api.md new file mode 100644 index 0000000..a42a8a2 --- /dev/null +++ b/.agents/skills/cloudflare/references/ai-search/api.md @@ -0,0 +1,13 @@ +# AI Search API Routes + +Choose documentation that matches the integration you are working on. Fetch the reference before writing binding configuration, request types, response parsing, or streaming code. + +| Task | Documentation | +|------|---------------| +| New Worker integration: bindings, search, chat completions, and streaming | [Search Workers binding](https://developers.cloudflare.com/ai-search/api/search/workers-binding/index.md) | +| Create, list, configure, and inspect instances | [Instances Workers binding](https://developers.cloudflare.com/ai-search/api/instances/workers-binding/index.md) | +| Query over HTTP and configure request authentication | [Search REST API](https://developers.cloudflare.com/ai-search/api/search/rest-api/index.md) | +| Maintain an existing `env.AI.autorag()` integration | [Legacy Workers binding](https://developers.cloudflare.com/ai-search/api/migration/workers-binding-legacy/index.md) | +| Upgrade a legacy binding, including responses, streaming, and filters | [Workers binding migration](https://developers.cloudflare.com/ai-search/api/migration/workers-binding/index.md) | + +The legacy binding remains supported; use current bindings for new integrations. Keep legacy request and response handling together until deliberately migrating them. diff --git a/.agents/skills/cloudflare/references/ai-search/configuration.md b/.agents/skills/cloudflare/references/ai-search/configuration.md new file mode 100644 index 0000000..307097a --- /dev/null +++ b/.agents/skills/cloudflare/references/ai-search/configuration.md @@ -0,0 +1,16 @@ +# AI Search Configuration Routes + +| Task | Documentation | +|------|---------------| +| Set up a Worker and local development | [Workers binding quick start](https://developers.cloudflare.com/ai-search/get-started/workers/index.md) | +| Select built-in uploads, R2, or a website; check supported formats | [Data sources](https://developers.cloudflare.com/ai-search/configuration/data-source/index.md) | +| Connect existing R2 content | [R2 data source](https://developers.cloudflare.com/ai-search/configuration/data-source/r2/index.md) | +| Configure crawling and diagnose bot-protection requirements | [Website data source](https://developers.cloudflare.com/ai-search/configuration/data-source/website/index.md) | +| Include or exclude files and URL paths | [Path filtering](https://developers.cloudflare.com/ai-search/configuration/indexing/path-filtering/index.md) | +| Configure source syncs, trigger indexing, or pause and resume | [Syncing](https://developers.cloudflare.com/ai-search/configuration/indexing/syncing/index.md) | +| Grant AI Search access to R2 for indexing | [Service API token](https://developers.cloudflare.com/ai-search/configuration/indexing/service-api-token/index.md) | +| Organize instances by application, tenant, or environment | [Namespaces](https://developers.cloudflare.com/ai-search/concepts/namespaces/index.md) | +| Configure models | [Models](https://developers.cloudflare.com/ai-search/configuration/models/index.md) | +| Inspect instance configuration and indexing progress | [Instances Workers binding](https://developers.cloudflare.com/ai-search/api/instances/workers-binding/index.md) | + +Indexing credentials and request authentication serve different purposes. For authenticating search requests over HTTP, use the [Search REST API](https://developers.cloudflare.com/ai-search/api/search/rest-api/index.md) documentation. diff --git a/.agents/skills/cloudflare/references/ai-search/gotchas.md b/.agents/skills/cloudflare/references/ai-search/gotchas.md new file mode 100644 index 0000000..8273611 --- /dev/null +++ b/.agents/skills/cloudflare/references/ai-search/gotchas.md @@ -0,0 +1,13 @@ +# AI Search Troubleshooting Routes + +| Symptom or question | Documentation | +|---------------------|---------------| +| API request fails, including authentication or missing instances | [API error codes](https://developers.cloudflare.com/ai-search/troubleshooting/api-error-codes/index.md) | +| Upload or sync succeeds but content fails during processing | [Indexing error codes](https://developers.cloudflare.com/ai-search/troubleshooting/indexing-error-codes/index.md) | +| Content is missing or stale | [Syncing](https://developers.cloudflare.com/ai-search/configuration/indexing/syncing/index.md) and [supported data sources and formats](https://developers.cloudflare.com/ai-search/configuration/data-source/index.md) | +| Filters return unexpected documents or no matches | [Filtering](https://developers.cloudflare.com/ai-search/configuration/retrieval/filtering/index.md) and [metadata attributes](https://developers.cloudflare.com/ai-search/configuration/indexing/metadata/index.md) | +| Thresholds exclude results or responses need tuning | [Result controls](https://developers.cloudflare.com/ai-search/configuration/retrieval/result-controls/index.md) | +| Binding types, response parsing, or streaming fail after an upgrade | [Workers binding migration](https://developers.cloudflare.com/ai-search/api/migration/workers-binding/index.md) | +| Capacity, file-size, or billing questions | [Limits and pricing](https://developers.cloudflare.com/ai-search/platform/limits-pricing/index.md) | + +For legacy binding behavior, start with [API routes](api.md). Do not apply current filter syntax or response shapes to legacy calls without following the migration guide. diff --git a/.agents/skills/cloudflare/references/ai-search/patterns.md b/.agents/skills/cloudflare/references/ai-search/patterns.md new file mode 100644 index 0000000..a81ee54 --- /dev/null +++ b/.agents/skills/cloudflare/references/ai-search/patterns.md @@ -0,0 +1,15 @@ +# AI Search Pattern Routes + +Choose retrieval-only search when your application displays chunks or handles generation itself; choose chat completions when AI Search should also generate the answer. Read [Search Workers binding](https://developers.cloudflare.com/ai-search/api/search/workers-binding/index.md) for both paths and streaming behavior. + +| Task | Documentation | +|------|---------------| +| Isolate tenants using separate instances or a shared filtered instance | [Multitenancy](https://developers.cloudflare.com/ai-search/how-to/per-tenant-search/index.md) | +| Define built-in or custom metadata | [Metadata attributes](https://developers.cloudflare.com/ai-search/configuration/indexing/metadata/index.md) | +| Filter by metadata, combine conditions, or match a folder and subfolders | [Filtering](https://developers.cloudflare.com/ai-search/configuration/retrieval/filtering/index.md) | +| Tune result count and relevance thresholds | [Result controls](https://developers.cloudflare.com/ai-search/configuration/retrieval/result-controls/index.md) | +| Resolve follow-up queries using conversation context | [Query rewriting](https://developers.cloudflare.com/ai-search/configuration/retrieval/query-rewriting/index.md) | +| Improve result ordering with a second model | [Reranking](https://developers.cloudflare.com/ai-search/configuration/retrieval/reranking/index.md) | +| Customize generation and query-rewriting instructions | [System prompt](https://developers.cloudflare.com/ai-search/configuration/retrieval/system-prompt/index.md) | + +For tenant isolation, read the full multitenancy guide before choosing an approach. A lower-bound folder comparison alone does not establish a tenant boundary; use the documented filtering semantics. diff --git a/.agents/skills/cloudflare/references/bindings/README.md b/.agents/skills/cloudflare/references/bindings/README.md new file mode 100644 index 0000000..cf7a92c --- /dev/null +++ b/.agents/skills/cloudflare/references/bindings/README.md @@ -0,0 +1,16 @@ +# Cloudflare Workers Bindings + +Bindings grant a Worker access to configured resources through its environment. Prefer a product's binding for supported operations inside Workers; use the REST API when the caller or operation requires it. + +Read the relevant current documentation before implementing. These references route to maintained APIs and configuration rather than copying binding catalogs, type tables, or limits. + +## Start here + +- [Bindings overview and catalog](https://developers.cloudflare.com/workers/runtime-apis/bindings/index.md): capability model, available products, environment access, and binding lifecycle. +- [Storage options](https://developers.cloudflare.com/workers/platform/storage-options/index.md): choose storage from consistency, query, and coordination requirements. +- [api.md](./api.md): environment access, generated types, and product APIs. +- [configuration.md](./configuration.md): binding configuration, environments, secrets, and local development. +- [patterns.md](./patterns.md): Worker-to-Worker calls, testing, and resource selection. +- [gotchas.md](./gotchas.md): missing bindings, stale clients, development differences, and limits. + +Treat each binding as a capability granted to code. Select only the resources the Worker needs, and confirm which environment and resource each binding targets before using it. diff --git a/.agents/skills/cloudflare/references/bindings/api.md b/.agents/skills/cloudflare/references/bindings/api.md new file mode 100644 index 0000000..4032849 --- /dev/null +++ b/.agents/skills/cloudflare/references/bindings/api.md @@ -0,0 +1,19 @@ +# Bindings API Reference + +Fetch the relevant documentation before choosing method signatures, binding types, or type-generation settings. + +| Task | Current documentation | +| --- | --- | +| Access bindings in handlers, entrypoint classes, or imported `env` | [Bindings and environment access](https://developers.cloudflare.com/workers/runtime-apis/bindings/index.md) | +| Generate environment and runtime types; configure TypeScript or migrate from `@cloudflare/workers-types` | [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/index.md) | +| Use framework-specific environment access | [Hono on Workers](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/hono/index.md) (follow the framework's linked documentation) | +| Read, write, delete, and list KV keys | [KV Workers API](https://developers.cloudflare.com/kv/api/index.md) | +| Read, write, delete, and list R2 objects | [R2 Workers API](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/index.md) | +| Prepare and bind SQL statements or execute batches | [D1 Workers Binding API](https://developers.cloudflare.com/d1/worker-api/index.md) | +| Address Durable Objects and call their methods | [Durable Objects API](https://developers.cloudflare.com/durable-objects/api/index.md) | +| Send queue messages | [Queues JavaScript APIs](https://developers.cloudflare.com/queues/configuration/javascript-apis/index.md) | +| Run model inference | [Workers AI bindings](https://developers.cloudflare.com/workers-ai/configuration/bindings/index.md) | +| Call another Worker using HTTP or typed RPC | [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/index.md) and [RPC TypeScript](https://developers.cloudflare.com/workers/runtime-apis/rpc/typescript/index.md) | +| Find other product binding APIs, including Browser, mTLS, rate limiting, and Workflows | [Current binding catalog](https://developers.cloudflare.com/workers/runtime-apis/bindings/index.md) | + +Regenerate types after configuration changes and use the selected environment's configuration. Follow the current TypeScript setup for the project's toolchain instead of hardcoding a generated declaration path or maintaining a handwritten binding interface. Types describe the expected bindings; they do not provision resources or prove the deployed environment is configured correctly. diff --git a/.agents/skills/cloudflare/references/bindings/configuration.md b/.agents/skills/cloudflare/references/bindings/configuration.md new file mode 100644 index 0000000..e60a29f --- /dev/null +++ b/.agents/skills/cloudflare/references/bindings/configuration.md @@ -0,0 +1,20 @@ +# Binding Configuration + +Confirm the target account, environment, and resource before adding or changing a binding. Keep staging and production resources separate where their data or permissions must be isolated. + +| Task | Current documentation | +| --- | --- | +| Configure storage, compute, platform, and service bindings | [Wrangler binding configuration](https://developers.cloudflare.com/workers/wrangler/configuration/index.md#bindings) | +| Create or inspect resources and manage deployments | [Wrangler commands](https://developers.cloudflare.com/workers/wrangler/commands/index.md) (select the product's commands) | +| Configure named environments and non-inheritable bindings | [Wrangler environments](https://developers.cloudflare.com/workers/wrangler/environments/index.md) | +| Set non-sensitive configuration | [Environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/index.md) | +| Set or rotate credentials in a chosen environment | [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/index.md) | +| Configure text, data, and Wasm modules in existing projects | [Wrangler bundling](https://developers.cloudflare.com/workers/wrangler/bundling/index.md) and [configuration](https://developers.cloudflare.com/workers/wrangler/configuration/index.md) | +| Choose locally simulated resources or remote bindings | [Local development](https://developers.cloudflare.com/workers/local-development/index.md) and [supported bindings per development mode](https://developers.cloudflare.com/workers/local-development/bindings-per-env/index.md) | +| Supply local variables and secrets | [Local environment variables and secrets](https://developers.cloudflare.com/workers/local-development/environment-variables/index.md) | +| Seed or persist local resource data | [Adding local data](https://developers.cloudflare.com/workers/local-development/local-data/index.md) | +| Generate types after configuring bindings | [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/index.md) | + +Bindings and variables are not inherited automatically by named environments. Configure the required values for the environment being used, including its secrets. Binding names used in code must match configuration; the target resource's ID or name is a separate value. + +Keep credentials out of committed variables and local secret files out of version control. Remote development can access real resources: verify the target rather than assuming that running locally isolates writes. diff --git a/.agents/skills/cloudflare/references/bindings/gotchas.md b/.agents/skills/cloudflare/references/bindings/gotchas.md new file mode 100644 index 0000000..bca53dc --- /dev/null +++ b/.agents/skills/cloudflare/references/bindings/gotchas.md @@ -0,0 +1,18 @@ +# Binding Gotchas and Troubleshooting + +Start by checking the binding name, selected environment, and actual target resource. Regenerating types alone does not fix a missing runtime binding. + +| Symptom or question | What to check | +| --- | --- | +| A binding is undefined or points to unexpected data | [Binding configuration](https://developers.cloudflare.com/workers/wrangler/configuration/index.md#bindings) and [environment inheritance](https://developers.cloudflare.com/workers/wrangler/environments/index.md) | +| TypeScript cannot find a binding or runtime type | [Current type generation and TypeScript setup](https://developers.cloudflare.com/workers/languages/typescript/index.md) | +| Updated credentials are not reflected in a client | [Binding changes and retained global state](https://developers.cloudflare.com/workers/runtime-apis/bindings/index.md#making-changes-to-bindings); verify the [secret's environment and deployment](https://developers.cloudflare.com/workers/configuration/secrets/index.md) | +| Binding calls fail outside a handler | [Environment access and global-scope restrictions](https://developers.cloudflare.com/workers/runtime-apis/bindings/index.md#how-to-access-env) | +| A service target is unavailable or incorrect | [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/index.md) and [multi-Worker development](https://developers.cloudflare.com/workers/local-development/multi-workers/index.md) | +| Local data, secrets, or available bindings differ from deployment | [Development mode support](https://developers.cloudflare.com/workers/local-development/bindings-per-env/index.md), [local data](https://developers.cloudflare.com/workers/local-development/local-data/index.md), and [local secrets](https://developers.cloudflare.com/workers/local-development/environment-variables/index.md) | +| KV reads appear stale or return no value | [How KV works](https://developers.cloudflare.com/kv/concepts/how-kv-works/index.md); verify the namespace and handle missing values | +| Resource-specific API errors or limits | Follow the product from the [binding catalog](https://developers.cloudflare.com/workers/runtime-apis/bindings/index.md) to its troubleshooting and limits documentation | +| Worker resource limits or unexpected charges | [Workers limits](https://developers.cloudflare.com/workers/platform/limits/index.md) and [pricing](https://developers.cloudflare.com/workers/platform/pricing/index.md); check each bound product's limits and pricing separately | +| Need to inspect configuration, resources, deployments, or logs | [Wrangler commands](https://developers.cloudflare.com/workers/wrangler/commands/index.md) and [real-time logs](https://developers.cloudflare.com/workers/observability/logs/real-time-logs/index.md) | + +Never log secret values or return the environment object in a response. Inspect names and configuration without exposing credentials. Check the selected resource and development mode before issuing debugging commands that could mutate remote data. diff --git a/.agents/skills/cloudflare/references/bindings/patterns.md b/.agents/skills/cloudflare/references/bindings/patterns.md new file mode 100644 index 0000000..b6a0ebd --- /dev/null +++ b/.agents/skills/cloudflare/references/bindings/patterns.md @@ -0,0 +1,20 @@ +# Binding Patterns + +Choose the interaction and lifecycle first, then retrieve the implementation guide. + +| Task | Current documentation | +| --- | --- | +| Choose HTTP forwarding or RPC between Workers | [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/index.md) | +| Forward Requests and Responses through a service binding | [Service bindings over HTTP](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/http/index.md) | +| Expose callable methods with `WorkerEntrypoint` | [Service bindings over RPC](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/index.md) and [RPC TypeScript](https://developers.cloudflare.com/workers/runtime-apis/rpc/typescript/index.md) | +| Run connected Workers during development | [Developing with multiple Workers](https://developers.cloudflare.com/workers/local-development/multi-workers/index.md) | +| Test handlers against configured bindings and mock dependencies | [Workers Vitest configuration](https://developers.cloudflare.com/workers/testing/vitest-integration/configuration/index.md) and [test APIs](https://developers.cloudflare.com/workers/testing/vitest-integration/test-apis/index.md) | +| Select KV, D1, R2, or Durable Objects | [Storage options](https://developers.cloudflare.com/workers/platform/storage-options/index.md) | +| Keep clients current when bindings change | [Binding lifecycle](https://developers.cloudflare.com/workers/runtime-apis/bindings/index.md#making-changes-to-bindings) | +| Manage credentials used by external API clients | [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/index.md) | + +Use service bindings for internal Worker calls when appropriate, and choose HTTP or RPC based on the interface being exposed. A service binding does not replace application-level authorization for the caller's requested operation. + +Choose storage based on access patterns and consistency requirements, not copied size or latency thresholds. Parallelize independent binding operations when useful; preserve ordering where one operation depends on another's result. + +Avoid retaining clients derived from mutable bindings across requests without accounting for binding updates. Importing `env` is supported, but binding I/O still requires an appropriate execution context; follow the lifecycle guide rather than assuming all global access is forbidden. diff --git a/.agents/skills/cloudflare/references/browser-rendering/README.md b/.agents/skills/cloudflare/references/browser-rendering/README.md new file mode 100644 index 0000000..bb17a1a --- /dev/null +++ b/.agents/skills/cloudflare/references/browser-rendering/README.md @@ -0,0 +1,18 @@ +# Browser Run (formerly Browser Rendering) + +Use Browser Run for screenshots, PDFs, rendered content extraction, and browser automation. Read the relevant current documentation before implementing; use the [documentation index](https://developers.cloudflare.com/browser-run/llms.txt) to discover additional guides. + +Choose the integration by the work and runtime: + +- For a self-contained screenshot, PDF, or extraction, start with Quick Actions. They are available through REST and Workers bindings; check the chosen action's supported interface. +- For multi-step interactions or persistent state, use browser sessions. In Workers, use Cloudflare's Puppeteer or Playwright package; from external scripts or CI, use the CDP integration. +- When adapting existing automation, preserve its library where supported and check installed versions against the corresponding guide. + +Read only the reference needed for the task: + +| Task | Reference | +|------|-----------| +| Set up bindings, dependencies, or development | [configuration.md](configuration.md) | +| Select an endpoint or browser client API | [api.md](api.md) | +| Implement a workflow or manage reusable sessions | [patterns.md](patterns.md) | +| Diagnose failures or plan capacity and cost | [gotchas.md](gotchas.md) | diff --git a/.agents/skills/cloudflare/references/browser-rendering/api.md b/.agents/skills/cloudflare/references/browser-rendering/api.md new file mode 100644 index 0000000..7c78d33 --- /dev/null +++ b/.agents/skills/cloudflare/references/browser-rendering/api.md @@ -0,0 +1,12 @@ +# Browser Run APIs + +Read the guide for the chosen interface for request schemas, return types, authentication, and supported options. Keep Quick Actions and browser session APIs distinct when adapting examples. + +| Task | Documentation | +|------|---------------| +| Screenshots, PDFs, HTML, scraping, or structured extraction | [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/index.md) — links to each action's request options and examples for REST or Workers bindings | +| Automate a browser in Workers with Puppeteer | [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/index.md) — Cloudflare package, browser operations, and session APIs | +| Automate a browser in Workers with Playwright | [Playwright](https://developers.cloudflare.com/browser-run/playwright/index.md) — Cloudflare package, locators, storage state, and tracing | +| Control a remote browser from an external runtime | [CDP](https://developers.cloudflare.com/browser-run/cdp/index.md) — session endpoints and links to Puppeteer, Playwright, and other clients | + +The product rename does not imply a rename of API paths or token permissions. Use the identifiers shown in the selected guide. diff --git a/.agents/skills/cloudflare/references/browser-rendering/configuration.md b/.agents/skills/cloudflare/references/browser-rendering/configuration.md new file mode 100644 index 0000000..71559a4 --- /dev/null +++ b/.agents/skills/cloudflare/references/browser-rendering/configuration.md @@ -0,0 +1,12 @@ +# Browser Run Configuration + +Check the project's runtime, installed client and Wrangler versions, and compatibility date before adapting setup instructions. Cloudflare's packages for Workers and standard clients connecting over CDP have different setup requirements. + +| Task | Documentation | +|------|---------------| +| Start a project or configure REST authentication | [Get started](https://developers.cloudflare.com/browser-run/get-started/index.md) — Quick Actions and browser session setup | +| Configure a Worker or choose a development mode | [Wrangler reference](https://developers.cloudflare.com/browser-run/reference/wrangler/index.md) — browser bindings, compatibility requirements, and local/remote development | +| Install or update a Workers browser client | [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/index.md) or [Playwright](https://developers.cloudflare.com/browser-run/playwright/index.md) — package-specific setup and supported versions | +| Connect from a script, server, or CI outside Workers | [CDP](https://developers.cloudflare.com/browser-run/cdp/index.md) — authentication and client integration guides | + +Development support depends on the selected interface. Follow its current guidance rather than applying one remote-mode requirement to all Browser Run workflows. diff --git a/.agents/skills/cloudflare/references/browser-rendering/gotchas.md b/.agents/skills/cloudflare/references/browser-rendering/gotchas.md new file mode 100644 index 0000000..d1d37d7 --- /dev/null +++ b/.agents/skills/cloudflare/references/browser-rendering/gotchas.md @@ -0,0 +1,15 @@ +# Browser Run Troubleshooting + +Identify the integration and observed failure before changing timeouts or concurrency. A request-rate limit, exhausted browser time, and a closed session require different responses. + +| Concern | Documentation | +|---------|---------------| +| Quotas, launch rates, concurrency, and session timeouts | [Limits](https://developers.cloudflare.com/browser-run/limits/index.md) — check the current plan and integration-specific limits | +| Browser hours and concurrent-browser charges | [Pricing](https://developers.cloudflare.com/browser-run/pricing/index.md) — distinguish Quick Actions from browser sessions | +| Missing bindings, action failures, or unsupported behavior | [FAQ](https://developers.cloudflare.com/browser-run/faq/index.md) — diagnose the reported error and runtime constraints | +| Puppeteer page evaluation cannot access outer variables | [JavaScript execution](https://pptr.dev/guides/javascript-execution) — browser execution context, passing arguments, and returned values | +| Block resources or handle intercepted Puppeteer requests | [Request interception](https://pptr.dev/guides/network-interception) — continue, respond, or abort requests and avoid duplicate handling | +| Unexpected disconnects or session loss | [Browser close reasons](https://developers.cloudflare.com/browser-run/reference/browser-close-reasons/index.md) — inspect the recorded close reason before choosing recovery | +| Development or compatibility failures | [Wrangler reference](https://developers.cloudflare.com/browser-run/reference/wrangler/index.md) — verify binding configuration and interface-specific development support | + +Before increasing concurrency, check session cleanup and whether the workload can reuse browsers with appropriate isolation; see [patterns.md](patterns.md). Retrieve current limits and pricing when sizing a workload rather than relying on fixed tier tables. diff --git a/.agents/skills/cloudflare/references/browser-rendering/patterns.md b/.agents/skills/cloudflare/references/browser-rendering/patterns.md new file mode 100644 index 0000000..561e546 --- /dev/null +++ b/.agents/skills/cloudflare/references/browser-rendering/patterns.md @@ -0,0 +1,12 @@ +# Browser Run Patterns + +Use the current examples for the selected integration instead of translating between Puppeteer, Playwright, and Quick Actions by changing method names. + +| Task | Documentation | +|------|---------------| +| Implement screenshots, PDFs, or extraction | [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/index.md) — choose the action and follow its example | +| Build custom interactions | [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/index.md) or [Playwright](https://developers.cloudflare.com/browser-run/playwright/index.md) — browser automation examples | +| Reconnect across requests | [Reuse sessions](https://developers.cloudflare.com/browser-run/features/reuse-sessions/index.md) — disconnect/reconnect lifecycle and when to use Durable Objects for stateful ownership | +| Share browser capacity while isolating users | [Concurrency and session isolation](https://developers.cloudflare.com/browser-run/limits/index.md#how-can-i-manage-concurrency-and-session-isolation-with-browser-run) — tabs, browser contexts, and capacity tradeoffs | + +Quick Actions manage their own session lifecycle. For sessions managed by the application, close pages and browsers on completion or failure. If reuse is intentional, follow the client's disconnect/reconnect semantics and handle expired sessions; closing the browser ends it. Keep cookies and storage isolated between users, and coordinate ownership when several requests can reconnect to the same session. diff --git a/.agents/skills/cloudflare/references/c3/README.md b/.agents/skills/cloudflare/references/c3/README.md new file mode 100644 index 0000000..5c002f0 --- /dev/null +++ b/.agents/skills/cloudflare/references/c3/README.md @@ -0,0 +1,112 @@ +# C3 (create-cloudflare) + +Official CLI for scaffolding Cloudflare Workers and Pages projects with templates, TypeScript, and instant deployment. + +## Quick Start + +```bash +# Interactive (recommended for first-time) +npm create cloudflare@latest my-app + +# Worker (API/WebSocket/Cron) +npm create cloudflare@latest my-api -- --type=hello-world --ts + +# Pages (static/SSG) +npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages +``` + +## Platform Decision Tree + +``` +What are you building? + +├─ API / WebSocket / Cron / Email handler +│ └─ Workers (default) - no --platform flag needed +│ npm create cloudflare@latest my-api -- --type=hello-world + +├─ Static site / SSG / Documentation +│ └─ Pages - requires --platform=pages +│ npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages + +├─ Full-stack app (Next.js/Remix/SvelteKit) +│ └─ Follow the current framework guide below + +└─ Convert existing project + └─ npm create cloudflare@latest . -- --type=pre-existing --existing-script=./src/worker.ts +``` + +**Critical:** Pages projects require `--platform=pages` flag. Without it, C3 defaults to Workers. + +## Framework Setup + +Fetch the [Workers framework guide](https://developers.cloudflare.com/workers/framework-guides/index.md) for the chosen framework before scaffolding or adapting an existing app. For Next.js, follow [Next.js on Workers](https://developers.cloudflare.com/workers/framework-guides/web-apps/nextjs/index.md); use the [Pages static export guide](https://developers.cloudflare.com/pages/framework-guides/nextjs/deploy-a-static-nextjs-site/index.md) only when targeting a Next.js static export on Pages. + +## Interactive Flow + +When run without flags, C3 prompts in this order: + +1. **Project name** - Directory to create (defaults to current dir with `.`) +2. **Application type** - `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template` +3. **Platform** - `workers` (default) or `pages` (for web apps only) +4. **Framework** - If web-app: `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, etc. +5. **TypeScript** - `yes` (recommended) or `no` +6. **Git** - Initialize repository? `yes` or `no` +7. **Deploy** - Deploy now? `yes` or `no` (requires `wrangler login`) + +## Installation Methods + +```bash +# NPM +npm create cloudflare@latest + +# Yarn +yarn create cloudflare + +# PNPM +pnpm create cloudflare@latest +``` + +## In This Reference + +| File | Purpose | Use When | +|------|---------|----------| +| **api.md** | Complete CLI flag reference | Scripting, CI/CD, advanced usage | +| **configuration.md** | Generated files, bindings, types | Understanding output, customization | +| **patterns.md** | Workflows, CI/CD, monorepos | Real-world integration | +| **gotchas.md** | Troubleshooting failures | Deployment blocked, errors | + +## Reading Order + +| Task | Read | +|------|------| +| Create first project | README only | +| Set up CI/CD | README → api → patterns | +| Debug failed deploy | gotchas | +| Understand generated files | configuration | +| Full CLI reference | api | +| Create custom template | patterns → configuration | +| Convert existing project | README → patterns | + +## Post-Creation + +```bash +cd my-app + +# Local dev with hot reload +npm run dev + +# Generate TypeScript types for bindings +npm run cf-typegen + +# Deploy to Cloudflare +npm run deploy +``` + +## See Also + +- **workers/README.md** - Workers runtime, bindings, APIs +- **workers-ai/README.md** - AI/ML models +- **pages/README.md** - Pages-specific features +- **wrangler/README.md** - Wrangler CLI beyond initial setup +- **d1/README.md** - SQLite database +- **r2/README.md** - Object storage diff --git a/.agents/skills/cloudflare/references/c3/api.md b/.agents/skills/cloudflare/references/c3/api.md new file mode 100644 index 0000000..d28d3c4 --- /dev/null +++ b/.agents/skills/cloudflare/references/c3/api.md @@ -0,0 +1,70 @@ +# C3 CLI Reference + +## Invocation + +```bash +npm create cloudflare@latest [name] [-- flags] # NPM requires -- +yarn create cloudflare [name] [flags] +pnpm create cloudflare@latest [name] [-- flags] +``` + +## Core Flags + +| Flag | Values | Description | +|------|--------|-------------| +| `--type` | `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template` | Application type | +| `--platform` | `workers` (default), `pages` | Target platform | +| `--framework` | `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, `qwik`, `vue`, `angular`, `hono` | Web framework (requires `--type=web-app`) | +| `--lang` | `ts`, `js`, `python` | Language (for `--type=hello-world`) | +| `--ts` / `--no-ts` | - | TypeScript for web apps | + +## Deployment Flags + +| Flag | Description | +|------|-------------| +| `--deploy` / `--no-deploy` | Deploy immediately (prompts interactive, skips in CI) | +| `--git` / `--no-git` | Initialize git (default: yes) | +| `--open` | Open browser after deploy | + +## Advanced Flags + +| Flag | Description | +|------|-------------| +| `--template=user/repo` | GitHub template or local path | +| `--existing-script=./src/worker.ts` | Existing script (requires `--type=pre-existing`) | +| `--category=ai\|database\|realtime` | Demo filter (requires `--type=demo`) | +| `--experimental` | Enable experimental features | +| `--wrangler-defaults` | Skip wrangler prompts | + +## Environment Variables + +```bash +CLOUDFLARE_API_TOKEN=xxx # For deployment +CLOUDFLARE_ACCOUNT_ID=xxx # Account ID +CF_TELEMETRY_DISABLED=1 # Disable telemetry +``` + +## Exit Codes + +`0` success, `1` user abort, `2` error + +## Examples + +For framework apps, follow [Framework Setup](README.md#framework-setup). + +```bash +# TypeScript Worker +npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --no-deploy + +# Astro blog +npm create cloudflare@latest my-blog -- --type=web-app --framework=astro --ts --deploy + +# CI: non-interactive +npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --no-git --no-deploy + +# GitHub template +npm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi + +# Convert existing project +npm create cloudflare@latest . -- --type=pre-existing --existing-script=./build/worker.js +``` diff --git a/.agents/skills/cloudflare/references/c3/configuration.md b/.agents/skills/cloudflare/references/c3/configuration.md new file mode 100644 index 0000000..37f9f82 --- /dev/null +++ b/.agents/skills/cloudflare/references/c3/configuration.md @@ -0,0 +1,81 @@ +# C3 Generated Configuration + +## Output Structure + +``` +my-app/ +├── src/index.ts # Worker entry point +├── wrangler.jsonc # Cloudflare config +├── package.json # Scripts +├── tsconfig.json +└── .gitignore +``` + +## wrangler.jsonc + +```jsonc +{ + "$schema": "https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/config-schema.json", + "name": "my-app", + "main": "src/index.ts", + "compatibility_date": "2026-01-27" +} +``` + +## Binding Placeholders + +C3 generates **placeholder IDs** that must be replaced before deploy: + +```jsonc +{ + "kv_namespaces": [{ "binding": "MY_KV", "id": "placeholder_kv_id" }], + "d1_databases": [{ "binding": "DB", "database_id": "00000000-..." }] +} +``` + +**Replace with real IDs:** +```bash +npx wrangler kv namespace create MY_KV # Returns real ID +npx wrangler d1 create my-database # Returns real database_id +``` + +**Deployment error if not replaced:** +``` +Error: Invalid KV namespace ID "placeholder_kv_id" +``` + +## Scripts + +```json +{ + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "cf-typegen": "wrangler types" + } +} +``` + +## Type Generation + +Run after adding bindings: +```bash +npm run cf-typegen +``` + +Generates `.wrangler/types/runtime.d.ts`: +```typescript +interface Env { + MY_KV: KVNamespace; + DB: D1Database; +} +``` + +## Post-Creation Checklist + +1. Review `wrangler.jsonc` - check name, compatibility_date +2. Replace placeholder binding IDs with real resource IDs +3. Run `npm run cf-typegen` +4. Test: `npm run dev` +5. Deploy: `npm run deploy` +6. Add secrets: `npx wrangler secret put SECRET_NAME` diff --git a/.agents/skills/cloudflare/references/c3/gotchas.md b/.agents/skills/cloudflare/references/c3/gotchas.md new file mode 100644 index 0000000..ecd664d --- /dev/null +++ b/.agents/skills/cloudflare/references/c3/gotchas.md @@ -0,0 +1,92 @@ +# C3 Troubleshooting + +## Deployment Issues + +### Placeholder IDs + +**Error:** "Invalid namespace ID" +**Fix:** Replace placeholders in wrangler.jsonc with real IDs: +```bash +npx wrangler kv namespace create MY_KV # Get real ID +``` + +### Authentication + +**Error:** "Not authenticated" +**Fix:** `npx wrangler login` or set `CLOUDFLARE_API_TOKEN` + +### Name Conflict + +**Error:** "Worker already exists" +**Fix:** Change `name` in wrangler.jsonc + +## Platform Selection + +| Need | Platform | +|------|----------| +| Git integration, branch previews | `--platform=pages` | +| Durable Objects, D1, Queues | Workers (default) | + +Wrong platform? Recreate with correct `--platform` flag. + +## TypeScript Issues + +**"Cannot find name 'KVNamespace'"** +```bash +npm run cf-typegen # Regenerate types +# Restart TS server in editor +``` + +**Missing types after config change:** Re-run `npm run cf-typegen` + +## Package Manager + +**Multiple lockfiles causing issues:** +```bash +rm pnpm-lock.yaml # If using npm +rm package-lock.json # If using pnpm +``` + +## CI/CD + +**CI hangs on prompts:** +```bash +npm create cloudflare@latest my-app -- \ + --type=hello-world --lang=ts --no-git --no-deploy +``` + +**Auth in CI:** +```yaml +env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} +``` + +## Framework-Specific + +| Framework | Issue | Fix | +|-----------|-------|-----| +| Next.js | create-next-app failed | `npm cache clean --force`, retry | +| Astro | Adapter missing | Install `@astrojs/cloudflare` | +| Remix | Module errors | Update `@remix-run/cloudflare*` | + +## Compatibility Date + +**"Feature X requires compatibility_date >= ..."** +**Fix:** Update `compatibility_date` in wrangler.jsonc to today's date + +## Node.js Version + +**"Node.js version not supported"** +**Fix:** Install Node.js 18+ (`nvm install 20`) + +## Quick Reference + +| Error | Cause | Fix | +|-------|-------|-----| +| Invalid namespace ID | Placeholder binding | Create resource, update config | +| Not authenticated | No login | `npx wrangler login` | +| Cannot find KVNamespace | Missing types | `npm run cf-typegen` | +| Worker already exists | Name conflict | Change `name` | +| CI hangs | Missing flags | Add --type, --lang, --no-deploy | +| Template not found | Bad name | Check cloudflare/templates | diff --git a/.agents/skills/cloudflare/references/c3/patterns.md b/.agents/skills/cloudflare/references/c3/patterns.md new file mode 100644 index 0000000..c9e9f68 --- /dev/null +++ b/.agents/skills/cloudflare/references/c3/patterns.md @@ -0,0 +1,80 @@ +# C3 Usage Patterns + +## Quick Workflows + +For framework apps, follow [Framework Setup](README.md#framework-setup). + +```bash +# TypeScript API Worker +npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --deploy + +# Astro static site +npm create cloudflare@latest my-blog -- --type=web-app --framework=astro --platform=pages --ts +``` + +## CI/CD (GitHub Actions) + +```yaml +- name: Deploy + run: npm run deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} +``` + +**Non-interactive requires:** +```bash +--type= # Required +--no-git # Recommended (CI already in git) +--no-deploy # Deploy separately with secrets +--framework= # For web-app +--ts / --no-ts # Required +``` + +## Monorepo + +C3 detects workspace config (`package.json` workspaces or `pnpm-workspace.yaml`). + +```bash +cd packages/ +npm create cloudflare@latest my-worker -- --type=hello-world --lang=ts --no-deploy +``` + +## Custom Templates + +```bash +# GitHub repo +npm create cloudflare@latest -- --template=username/repo +npm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi + +# Local path +npm create cloudflare@latest my-app -- --template=../my-template +``` + +**Template requires `c3.config.json`:** +```json +{ + "name": "my-template", + "category": "hello-world", + "copies": [{ "path": "src/" }, { "path": "wrangler.jsonc" }], + "transforms": [{ "path": "package.json", "jsonc": { "name": "{{projectName}}" }}] +} +``` + +## Existing Projects + +```bash +# Add Cloudflare to existing Worker +npm create cloudflare@latest . -- --type=pre-existing --existing-script=./dist/index.js +``` + +For existing framework apps, follow [Framework Setup](README.md#framework-setup). + +## Post-Creation Checklist + +1. Review `wrangler.jsonc` - set `compatibility_date`, verify `name` +2. Create bindings: `wrangler kv namespace create`, `wrangler d1 create`, `wrangler r2 bucket create` +3. Generate types: `npm run cf-typegen` +4. Test: `npm run dev` +5. Deploy: `npm run deploy` +6. Set secrets: `wrangler secret put SECRET_NAME` diff --git a/.agents/skills/cloudflare/references/containers/README.md b/.agents/skills/cloudflare/references/containers/README.md new file mode 100644 index 0000000..f6982ed --- /dev/null +++ b/.agents/skills/cloudflare/references/containers/README.md @@ -0,0 +1,23 @@ +# Cloudflare Containers + +Use this reference for containerized applications on the Workers platform, including container-enabled Durable Objects, lifecycle management, and request routing. + +## Choose the runtime + +Use [Containers](https://developers.cloudflare.com/containers/index.md) for existing container images, custom runtimes, system dependencies, full filesystem access, or workloads needing additional CPU and memory. Use [Workers](https://developers.cloudflare.com/workers/index.md) when the application fits the Workers runtime without those requirements. + +Containers are controlled through [Durable Objects](https://developers.cloudflare.com/durable-objects/index.md). An instance's identity does not make its filesystem persistent: design for restarts and store durable data outside the container disk. Read [Container lifecycle](https://developers.cloudflare.com/containers/concepts/architecture/index.md) and [Container interface](https://developers.cloudflare.com/containers/api/container-class/index.md) for the relationship between the process, its Durable Object, and persistent storage. + +## Find the documentation for the task + +Read the linked page before writing code or configuration; use its current API, examples, and constraints rather than reconstructing them from memory. + +| Task | Start here | +| --- | --- | +| Create a project and deploy the first container | [Get started](https://developers.cloudflare.com/containers/get-started/index.md) | +| Configure images, bindings, instance sizes, and deployments | [Configuration](configuration.md) | +| Control startup, requests, lifecycle, and scheduling | [API](api.md) | +| Choose routing or connect other services | [Patterns](patterns.md) | +| Diagnose startup, persistence, capacity, or rollout issues | [Gotchas](gotchas.md) | + +For additional topics, consult the [Containers documentation index](https://developers.cloudflare.com/containers/llms.txt). diff --git a/.agents/skills/cloudflare/references/containers/api.md b/.agents/skills/cloudflare/references/containers/api.md new file mode 100644 index 0000000..af316e9 --- /dev/null +++ b/.agents/skills/cloudflare/references/containers/api.md @@ -0,0 +1,13 @@ +# Containers API + +Use the [Container interface](https://developers.cloudflare.com/containers/api/container-class/index.md) for the current SDK methods, signatures, properties, and examples. Use the [Durable Object Container API](https://developers.cloudflare.com/containers/api/durable-object-container/index.md) when working directly with the runtime rather than the SDK class. + +| Task | Documentation | +| --- | --- | +| Forward HTTP or WebSocket requests | [Request methods](https://developers.cloudflare.com/containers/api/container-class/index.md#request-methods) | +| Start a process, wait for ports, stop, or destroy it | [Start and stop](https://developers.cloudflare.com/containers/api/container-class/index.md#start-and-stop) | +| React to startup, exit, errors, or idle expiry | [Lifecycle hooks](https://developers.cloudflare.com/containers/api/container-class/index.md#lifecycle-hooks) | +| Inspect state or keep background work active | [State and monitoring](https://developers.cloudflare.com/containers/api/container-class/index.md#state-and-monitoring) | +| Schedule callbacks without replacing the SDK's alarm handler | [Scheduling](https://developers.cloudflare.com/containers/api/container-class/index.md#scheduling) | +| Address named instances, select stateless instances, or switch request ports | [Utility functions](https://developers.cloudflare.com/containers/api/container-class/index.md#utility-functions) | +| Communicate over TCP from a Durable Object | [TCP port API](https://developers.cloudflare.com/containers/api/durable-object-container/index.md#gettcpport) | diff --git a/.agents/skills/cloudflare/references/containers/configuration.md b/.agents/skills/cloudflare/references/containers/configuration.md new file mode 100644 index 0000000..eafdd95 --- /dev/null +++ b/.agents/skills/cloudflare/references/containers/configuration.md @@ -0,0 +1,15 @@ +# Containers configuration + +Read the relevant documentation before choosing configuration fields or resource sizes. + +| Task | Documentation | +| --- | --- | +| Configure the container image, Durable Object binding, class, migrations, and instance count | [Wrangler Containers configuration](https://developers.cloudflare.com/workers/wrangler/configuration/index.md#containers) | +| Select a predefined size or configure custom CPU, memory, and disk | [Limits and instance types](https://developers.cloudflare.com/containers/platform/limits/index.md) and [custom instance configuration](https://developers.cloudflare.com/workers/wrangler/configuration/index.md#custom-instance-types) | +| Set ports, readiness checks, idle timeout, entrypoint, or internet access | [Container properties](https://developers.cloudflare.com/containers/api/container-class/index.md#properties) | +| Set runtime variables or pass secrets per instance | [Environment variables](https://developers.cloudflare.com/containers/configuration/environment-variables/index.md) and [environment variables and secrets example](https://developers.cloudflare.com/containers/examples/env-vars-and-secrets/index.md) | +| Build images or use existing registry images | [Image management](https://developers.cloudflare.com/containers/guides/image-management/index.md) | +| Run and iterate locally | [Local development](https://developers.cloudflare.com/containers/guides/local-dev/index.md) | +| Deploy from a workstation or Workers Builds | [Deploy Containers](https://developers.cloudflare.com/containers/guides/deploy/index.md) | +| Control image updates and replacement of running instances | [Rollouts](https://developers.cloudflare.com/containers/configuration/rollouts/index.md) | +| Estimate resource and network costs | [Pricing](https://developers.cloudflare.com/containers/platform/pricing/index.md) | diff --git a/.agents/skills/cloudflare/references/containers/gotchas.md b/.agents/skills/cloudflare/references/containers/gotchas.md new file mode 100644 index 0000000..54f3714 --- /dev/null +++ b/.agents/skills/cloudflare/references/containers/gotchas.md @@ -0,0 +1,16 @@ +# Containers troubleshooting + +Use the current documentation to diagnose behavior instead of relying on copied timeout values, resource limits, or lifecycle recipes. + +| Symptom or concern | Documentation to read | +| --- | --- | +| Startup timeout or unavailable port | [Start and stop](https://developers.cloudflare.com/containers/api/container-class/index.md#start-and-stop), [Container properties](https://developers.cloudflare.com/containers/api/container-class/index.md#properties), and [first-deploy provisioning](https://developers.cloudflare.com/containers/get-started/index.md) | +| WebSocket forwarding fails | [WebSocket example](https://developers.cloudflare.com/containers/examples/websocket/index.md) and [request methods](https://developers.cloudflare.com/containers/api/container-class/index.md#request-methods) | +| Background work stops on idle expiry | [Activity renewal](https://developers.cloudflare.com/containers/api/container-class/index.md#renewactivitytimeout) and [idle expiry hook](https://developers.cloudflare.com/containers/api/container-class/index.md#onactivityexpired) | +| Scheduled callbacks do not run | [Scheduling and alarm ownership](https://developers.cloudflare.com/containers/api/container-class/index.md#scheduling) | +| Shutdown cleanup or filesystem data loss | [Container shutdown and disk lifecycle](https://developers.cloudflare.com/containers/concepts/architecture/index.md#container-shutdown) | +| Out-of-memory errors or resource exhaustion | [FAQ](https://developers.cloudflare.com/containers/faq/index.md) and [limits and instance types](https://developers.cloudflare.com/containers/platform/limits/index.md) | +| Instance count exceeded or unexpected request distribution | [Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/index.md#containers) and [scaling and routing](https://developers.cloudflare.com/containers/configuration/scaling-and-routing/index.md) | +| Worker and container image versions differ after deployment | [Deployment behavior](https://developers.cloudflare.com/containers/guides/deploy/index.md) and [rollouts](https://developers.cloudflare.com/containers/configuration/rollouts/index.md) | +| Local behavior differs from deployed behavior | [Local development](https://developers.cloudflare.com/containers/guides/local-dev/index.md) | +| Logs, cold starts, or runtime availability questions | [FAQ](https://developers.cloudflare.com/containers/faq/index.md) | diff --git a/.agents/skills/cloudflare/references/containers/patterns.md b/.agents/skills/cloudflare/references/containers/patterns.md new file mode 100644 index 0000000..2d7ffc9 --- /dev/null +++ b/.agents/skills/cloudflare/references/containers/patterns.md @@ -0,0 +1,18 @@ +# Containers patterns + +Choose instance identity based on the workload: per-user/session or per-job identities for affinity, one shared identity for a singleton, and interchangeable instances for stateless requests. Read [scaling and routing](https://developers.cloudflare.com/containers/configuration/scaling-and-routing/index.md) for current helpers and scaling behavior before implementing that choice. + +| Task | Documentation | +| --- | --- | +| Distribute requests across stateless instances | [Stateless instances example](https://developers.cloudflare.com/containers/examples/stateless/index.md) | +| Forward WebSocket connections | [WebSocket example](https://developers.cloudflare.com/containers/examples/websocket/index.md) | +| React to lifecycle changes | [Status hooks example](https://developers.cloudflare.com/containers/examples/status-hooks/index.md) | +| Handle shutdown and persist data across restarts | [Container lifecycle](https://developers.cloudflare.com/containers/concepts/architecture/index.md) and [Container interface](https://developers.cloudflare.com/containers/api/container-class/index.md) | +| Keep long operations active or schedule callbacks | [Activity renewal](https://developers.cloudflare.com/containers/api/container-class/index.md#renewactivitytimeout) and [scheduling](https://developers.cloudflare.com/containers/api/container-class/index.md#scheduling) | +| Start containers on a cron schedule | [Cron container example](https://developers.cloudflare.com/containers/examples/cron/index.md) | +| Route requests to multiple ports | [Request methods](https://developers.cloudflare.com/containers/api/container-class/index.md#request-methods) and [utility functions](https://developers.cloudflare.com/containers/api/container-class/index.md#utility-functions) | +| Access Workers bindings from the container | [Connect to Workers and bindings](https://developers.cloudflare.com/containers/configuration/workers-connections/index.md) | + +## Workflows and Queues + +For multi-step orchestration, combine the [Workflows Workers API](https://developers.cloudflare.com/workflows/build/workers-api/index.md) with the [Container API](api.md). For queue-driven jobs, read the [Queues consumer API](https://developers.cloudflare.com/queues/configuration/javascript-apis/index.md#consumer) and [acknowledgement and retry behavior](https://developers.cloudflare.com/queues/configuration/batching-retries/index.md#explicit-acknowledgement-and-retries) alongside the Container API. These pages document the component APIs; they are not end-to-end Container integration examples. diff --git a/.agents/skills/cloudflare/references/cron-triggers/README.md b/.agents/skills/cloudflare/references/cron-triggers/README.md new file mode 100644 index 0000000..b7104e4 --- /dev/null +++ b/.agents/skills/cloudflare/references/cron-triggers/README.md @@ -0,0 +1,20 @@ +# Cloudflare Cron Triggers + +Use Cron Triggers to start periodic Worker jobs. Fetch the relevant current documentation before implementing; configuration, API signatures, examples, and limits belong in the docs. + +- **Set up a recurring job:** [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md) covers scheduling, deployment, and execution history. +- **Implement the job:** [Scheduled handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/index.md) covers controller properties, asynchronous work, and multiple schedules. +- **Schedule durable work:** [Trigger Workflows](https://developers.cloudflare.com/workflows/build/trigger-workflows/index.md) covers direct Workflow schedules and starting instances from a Worker. Check this before introducing a Worker whose only job is to start a Workflow. +- **Check capacity:** fetch [Workers limits](https://developers.cloudflare.com/workers/platform/limits/index.md) for the target plan and invocation type. + +## In This Reference + +- [configuration.md](./configuration.md) — schedule setup, environments, removal, and Green Compute +- [api.md](./api.md) — handler implementation, asynchronous completion, and tests +- [patterns.md](./patterns.md) — choosing execution boundaries and integrations +- [gotchas.md](./gotchas.md) — investigating timing, failures, and repeated work + +## See Also + +- [Workflows](../workflows/README.md) — durable multi-step jobs +- [Queues](../queues/README.md) — asynchronous message processing diff --git a/.agents/skills/cloudflare/references/cron-triggers/api.md b/.agents/skills/cloudflare/references/cron-triggers/api.md new file mode 100644 index 0000000..c363c67 --- /dev/null +++ b/.agents/skills/cloudflare/references/cron-triggers/api.md @@ -0,0 +1,16 @@ +# Cron Triggers API + +Fetch the handler documentation before writing code; use its current language examples and completion semantics. + +| Task | Documentation | +| --- | --- | +| Implement the handler and access the cron expression, scheduled time, bindings, and context | [Scheduled handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/index.md) | +| Route different schedules to different operations | [Handle multiple cron triggers](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/index.md#handle-multiple-cron-triggers) | +| Await work and understand how asynchronous failures affect invocation status | [Handler methods](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/index.md#methods) | +| Invoke a scheduled handler locally with a chosen expression or time | [Test Cron Triggers locally](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#test-cron-triggers-locally) | +| Build tests using runtime-backed controllers and execution contexts | [Workers test APIs](https://developers.cloudflare.com/workers/testing/vitest-integration/test-apis/index.md) | +| Start and inspect a Workflow instance | [Trigger Workflows](https://developers.cloudflare.com/workflows/build/trigger-workflows/index.md) | + +Decide which operation establishes successful completion and make its failures observable. Test each configured schedule and partial-failure recovery. Read the local-testing documentation for the current endpoint and query parameters instead of adding a production HTTP route to imitate the development helper. + +See [patterns.md](./patterns.md) for execution design and [gotchas.md](./gotchas.md) for failures. diff --git a/.agents/skills/cloudflare/references/cron-triggers/configuration.md b/.agents/skills/cloudflare/references/cron-triggers/configuration.md new file mode 100644 index 0000000..a690183 --- /dev/null +++ b/.agents/skills/cloudflare/references/cron-triggers/configuration.md @@ -0,0 +1,16 @@ +# Cron Triggers Configuration + +Fetch the documentation for the configuration operation before changing schedules. + +| Task | Documentation | +| --- | --- | +| Add a handler and configure triggers, including per-environment schedules and deployment propagation | [Add a Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#add-a-cron-trigger) | +| Choose an expression, interpret weekday numbering, or check supported extensions | [Supported cron expressions](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#supported-cron-expressions) | +| Remove schedules or distinguish omission from an empty configuration | [Remove a Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#remove-a-cron-trigger) | +| Configure renewable-energy execution locations | [Green Compute](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#green-compute) | +| Check trigger counts and execution budgets | [Workers limits](https://developers.cloudflare.com/workers/platform/limits/index.md) | +| Schedule Workflow instances directly | [Schedule a Workflow directly](https://developers.cloudflare.com/workflows/build/trigger-workflows/index.md#schedule-a-workflow-directly) | + +Identify the target environment and the intended business timezone before choosing an expression. Review which schedules a deployment will replace, and use the documented propagation behavior when planning a rollout. For Green Compute, follow its account-level configuration rather than inferring settings from Worker placement. + +See [api.md](./api.md) for implementation and [gotchas.md](./gotchas.md) for verification. diff --git a/.agents/skills/cloudflare/references/cron-triggers/gotchas.md b/.agents/skills/cloudflare/references/cron-triggers/gotchas.md new file mode 100644 index 0000000..fc2a054 --- /dev/null +++ b/.agents/skills/cloudflare/references/cron-triggers/gotchas.md @@ -0,0 +1,17 @@ +# Cron Triggers Troubleshooting + +Investigate using the current documentation rather than copied limits or assumed delivery guarantees. + +| Symptom or question | Documentation and check | +| --- | --- | +| Job runs at an unexpected time | Check [UTC execution](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#background) and [expression syntax](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#supported-cron-expressions); compare with the intended business timezone. | +| Schedule is missing after deployment | Check the handler, target environment, and propagation guidance in [Add a Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#add-a-cron-trigger). | +| Removing or preserving schedules has an unexpected result | Review [Remove a Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#remove-a-cron-trigger) before changing empty or omitted configuration. | +| Local invocation fails | Follow [Test Cron Triggers locally](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#test-cron-triggers-locally) for the supported endpoint, port, and query parameters. | +| Async work fails or completion status is surprising | Read [handler methods](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/index.md#methods) and inspect [past events](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#view-past-events). | +| Job exceeds its execution budget | Check [Workers limits](https://developers.cloudflare.com/workers/platform/limits/index.md) before choosing a smaller unit of work or [Workflows](../workflows/README.md). | +| Green Compute behavior differs from expectations | Read [Green Compute](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#green-compute) for its execution-location policy and account configuration. | + +For repeated or partially completed business operations, decide how to identify work and recover safely before selecting storage. Test recovery after each side effect; a marker alone does not establish that the operation completed. Do not assume a particular automatic retry schedule or delivery guarantee without a documented contract. + +See [patterns.md](./patterns.md) for coordination choices and [api.md](./api.md) for tests. diff --git a/.agents/skills/cloudflare/references/cron-triggers/patterns.md b/.agents/skills/cloudflare/references/cron-triggers/patterns.md new file mode 100644 index 0000000..8312ebc --- /dev/null +++ b/.agents/skills/cloudflare/references/cron-triggers/patterns.md @@ -0,0 +1,18 @@ +# Cron Triggers Patterns + +Choose the execution boundary before writing a scheduled job; fetch the relevant integration docs for implementation. + +| Need | Documentation | +| --- | --- | +| Periodic API sync, cleanup, reports, or health checks in a Worker | [Cron Triggers background](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#background) and [scheduled handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/index.md) | +| Different jobs on different schedules | [Handle multiple cron triggers](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/index.md#handle-multiple-cron-triggers) | +| Durable multi-step work started on a schedule | [Trigger Workflows](https://developers.cloudflare.com/workflows/build/trigger-workflows/index.md) | +| Send work to a queue and implement its consumer | [Queues JavaScript APIs](https://developers.cloudflare.com/queues/configuration/javascript-apis/index.md) | +| Coordinate state across invocations | [What are Durable Objects?](https://developers.cloudflare.com/durable-objects/concepts/what-are-durable-objects/index.md) | +| Inspect whether scheduled work ran | [View past events](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md#view-past-events) | + +Keep the trigger separate from the business operation so manual recovery and scheduled execution can share it. Decide how partial progress is recorded, how repeated attempts affect side effects, and who owns completion reporting. When distributing a batch, distinguish successful enqueueing from successful processing. + +Use [Workers limits](https://developers.cloudflare.com/workers/platform/limits/index.md) to evaluate whether the job fits one invocation. If it needs durable steps, waiting, or explicit retry boundaries, inspect [Workflows](../workflows/README.md) before building those mechanisms in the handler. + +See [api.md](./api.md) for tests and [gotchas.md](./gotchas.md) for operational checks. diff --git a/.agents/skills/cloudflare/references/d1/README.md b/.agents/skills/cloudflare/references/d1/README.md new file mode 100644 index 0000000..734d93f --- /dev/null +++ b/.agents/skills/cloudflare/references/d1/README.md @@ -0,0 +1,15 @@ +# Cloudflare D1 Database + +Use D1 for managed relational application data with SQLite semantics. For an existing external database, consider [Hyperdrive](../hyperdrive/); for per-entity coordination, consider [Durable Objects](https://developers.cloudflare.com/workers/platform/storage-options/index.md#sql-in-durable-objects-vs-d1). See [storage options](https://developers.cloudflare.com/workers/platform/storage-options/index.md) before choosing a product. + +Read the relevant current documentation before implementing. These references route tasks to the source of truth rather than maintaining copies of APIs, configuration, or plan tables. + +## Start here + +- [Get started](https://developers.cloudflare.com/d1/get-started/index.md): create a database, bind it to a Worker, and run a first query. +- [configuration.md](./configuration.md): bindings, environments, migrations, local development, and ORM integration. +- [api.md](./api.md): parameterized queries, batches, sessions, HTTP access, and testing. +- [patterns.md](./patterns.md): query design, caching, tenant isolation, replication, and recovery. +- [gotchas.md](./gotchas.md): errors, types, constraints, performance, and limits. + +Check [limits](https://developers.cloudflare.com/d1/platform/limits/index.md) and [pricing](https://developers.cloudflare.com/d1/platform/pricing/index.md) for capacity, allowances, and plan availability; do not infer them from old examples. diff --git a/.agents/skills/cloudflare/references/d1/api.md b/.agents/skills/cloudflare/references/d1/api.md new file mode 100644 index 0000000..c16c50e --- /dev/null +++ b/.agents/skills/cloudflare/references/d1/api.md @@ -0,0 +1,18 @@ +# D1 API Reference + +Fetch the relevant API page before writing queries or assuming method signatures and return types. + +| Task | Current documentation | +| --- | --- | +| Bind values and choose a query execution method | [Prepared statement methods](https://developers.cloudflare.com/d1/worker-api/prepared-statements/index.md) | +| Execute batches and understand transaction rollback; use database sessions | [D1 Database API](https://developers.cloudflare.com/d1/worker-api/d1-database/index.md) | +| Interpret results and query metadata | [Return objects](https://developers.cloudflare.com/d1/worker-api/return-object/index.md) | +| Choose supported JavaScript values and TypeScript result types | [Workers Binding API](https://developers.cloudflare.com/d1/worker-api/index.md) | +| Choose consistency constraints and carry bookmarks between requests | [Read replication and Sessions API](https://developers.cloudflare.com/d1/best-practices/read-replication/index.md) | +| Query from a server-side script outside Workers | [REST query API](https://developers.cloudflare.com/api/resources/d1/subresources/database/methods/query/index.md) | +| Handle query failures and transient errors | [Debug D1](https://developers.cloudflare.com/d1/observability/debug-d1/index.md) and [retry queries](https://developers.cloudflare.com/d1/best-practices/retry-queries/index.md) | +| Test database queries and apply migrations in tests | [Workers Vitest APIs: D1](https://developers.cloudflare.com/workers/testing/vitest-integration/test-apis/index.md#d1) | + +Bind untrusted values with prepared statements; do not interpolate them into SQL. Parameters do not replace identifiers: choose dynamic table, column, or sort names from an application-controlled allowlist. + +D1 sessions provide sequential consistency for replicated queries. They are not a way to extend query execution limits. Choose the starting constraint or bookmark from the application's consistency requirements using the replication guide. diff --git a/.agents/skills/cloudflare/references/d1/configuration.md b/.agents/skills/cloudflare/references/d1/configuration.md new file mode 100644 index 0000000..c53f409 --- /dev/null +++ b/.agents/skills/cloudflare/references/d1/configuration.md @@ -0,0 +1,18 @@ +# D1 Configuration + +Read the task's documentation before adding bindings or running database commands. Confirm the database and environment being targeted, particularly when applying migrations or importing data. + +| Task | Current documentation | +| --- | --- | +| Create a database and attach a Worker binding | [Getting started](https://developers.cloudflare.com/d1/get-started/index.md) | +| Configure binding fields and multiple databases | [Wrangler D1 configuration](https://developers.cloudflare.com/workers/wrangler/configuration/index.md#d1-databases) | +| Separate staging and production databases | [D1 environments](https://developers.cloudflare.com/d1/configuration/environments/index.md) | +| Create, track, and apply schema migrations | [Migrations](https://developers.cloudflare.com/d1/reference/migrations/index.md) | +| Look up CLI flags for management, execution, and exports | [D1 Wrangler commands](https://developers.cloudflare.com/d1/wrangler-commands/index.md) | +| Develop against local database state | [Local development](https://developers.cloudflare.com/d1/best-practices/local-development/index.md) | +| Generate binding types | [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/index.md) | +| Choose an ORM or query builder, including Drizzle | [D1 community projects](https://developers.cloudflare.com/d1/reference/community-projects/index.md) (follow the integration's current setup guide) | +| Import or export SQL data | [Import and export data](https://developers.cloudflare.com/d1/best-practices/import-export-data/index.md) | +| Enable replicas and use them through sessions | [Read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/index.md) | + +Local migrations and data do not automatically update a remote database. Test against a separate staging database before a production migration. Naming another binding `DB_REPLICA` does not configure replica routing; follow the replication guide. diff --git a/.agents/skills/cloudflare/references/d1/gotchas.md b/.agents/skills/cloudflare/references/d1/gotchas.md new file mode 100644 index 0000000..0be9c2d --- /dev/null +++ b/.agents/skills/cloudflare/references/d1/gotchas.md @@ -0,0 +1,18 @@ +# D1 Gotchas & Troubleshooting + +Use current documentation to diagnose the failure before changing query or database configuration. + +| Symptom or question | What to check | +| --- | --- | +| Missing table, query exception, or constraint error | [Debug D1](https://developers.cloudflare.com/d1/observability/debug-d1/index.md); verify the target binding, environment, and applied [migrations](https://developers.cloudflare.com/d1/reference/migrations/index.md) | +| Boolean, date, or other binding type mismatch | [Workers Binding API type conversion](https://developers.cloudflare.com/d1/worker-api/index.md) and [SQL support](https://developers.cloudflare.com/d1/sql-api/sql-statements/index.md) | +| Foreign key failure during writes or migrations | [Foreign key enforcement and deferral](https://developers.cloudflare.com/d1/sql-api/foreign-keys/index.md) | +| Slow queries, scans, or excessive rows read | [Indexes and query plans](https://developers.cloudflare.com/d1/best-practices/use-indexes/index.md) and [metrics](https://developers.cloudflare.com/d1/observability/metrics-analytics/index.md) | +| Query duration, statement, storage, or account limits | [Current limits](https://developers.cloudflare.com/d1/platform/limits/index.md) | +| Unexpected usage charges or plan assumptions | [Pricing](https://developers.cloudflare.com/d1/platform/pricing/index.md) | +| Stale reads after a write | [Sessions, bookmarks, and read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/index.md) | +| Transient query failures | [Retry guidance](https://developers.cloudflare.com/d1/best-practices/retry-queries/index.md); check idempotency before retrying writes | +| Import/export failure or unsupported data | [Import/export behavior and limitations](https://developers.cloudflare.com/d1/best-practices/import-export-data/index.md) | +| Local and deployed databases differ | [Local development](https://developers.cloudflare.com/d1/best-practices/local-development/index.md) and [environment configuration](https://developers.cloudflare.com/d1/configuration/environments/index.md) | + +Continue to bind untrusted SQL values as described in [api.md](./api.md). Do not treat SQL injection as a recoverable database error or assume retries correct invalid queries. diff --git a/.agents/skills/cloudflare/references/d1/patterns.md b/.agents/skills/cloudflare/references/d1/patterns.md new file mode 100644 index 0000000..2ad5c77 --- /dev/null +++ b/.agents/skills/cloudflare/references/d1/patterns.md @@ -0,0 +1,20 @@ +# D1 Patterns & Best Practices + +Use these guides to design the operation, then fetch [api.md](./api.md) for implementation references. + +| Task | Current documentation | +| --- | --- | +| Design pagination, filters, joins, and aggregations | [Query a database](https://developers.cloudflare.com/d1/best-practices/query-d1/index.md) and [supported SQL](https://developers.cloudflare.com/d1/sql-api/sql-statements/index.md) | +| Reduce scans and inspect query plans | [Use indexes](https://developers.cloudflare.com/d1/best-practices/use-indexes/index.md) | +| Batch writes or transform data | [Database API](https://developers.cloudflare.com/d1/worker-api/d1-database/index.md) and [limits](https://developers.cloudflare.com/d1/platform/limits/index.md) | +| Store and query event metadata | [Query JSON](https://developers.cloudflare.com/d1/sql-api/query-json/index.md) | +| Evaluate a cache in front of D1 | [How KV works](https://developers.cloudflare.com/kv/concepts/how-kv-works/index.md) | +| Choose shared or per-tenant databases | [D1 FAQs](https://developers.cloudflare.com/d1/reference/faq/index.md) and [limits](https://developers.cloudflare.com/d1/platform/limits/index.md) | +| Reduce read latency while preserving required consistency | [Read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/index.md) | +| Plan point-in-time recovery or portable backups | [Time Travel](https://developers.cloudflare.com/d1/reference/time-travel/index.md) and [import/export](https://developers.cloudflare.com/d1/best-practices/import-export-data/index.md) | + +Keep result sets bounded and pagination ordering deterministic. Choose indexes from actual query plans. When splitting a large operation into batches, account for the loss of whole-operation atomicity across batches. + +Authorize a tenant before selecting its database or rows; a request header alone is not proof of tenant membership. Application login sessions stored in tables are separate from D1's Sessions API. + +Before caching reads, decide how stale data may be and how writes invalidate cached results. For replicated reads, choose session constraints and bookmark propagation based on read-after-write requirements rather than assuming every read sees the latest primary state. diff --git a/.agents/skills/cloudflare/references/hyperdrive/README.md b/.agents/skills/cloudflare/references/hyperdrive/README.md new file mode 100644 index 0000000..24da871 --- /dev/null +++ b/.agents/skills/cloudflare/references/hyperdrive/README.md @@ -0,0 +1,27 @@ +# Hyperdrive + +Use Hyperdrive to connect Workers to an existing PostgreSQL or MySQL database with connection pooling and optional query caching. It does not replace the origin database or replicate its data. Start with [how Hyperdrive works](https://developers.cloudflare.com/hyperdrive/concepts/how-hyperdrive-works/index.md) and [supported databases and features](https://developers.cloudflare.com/hyperdrive/reference/supported-databases-and-features/index.md) to assess fit. + +## Retrieve current documentation + +Fetch the relevant official page before implementing. Driver versions, compatibility settings, API shapes, CLI flags, cache settings, and limits belong in the docs rather than this reference. Use the [Hyperdrive documentation index](https://developers.cloudflare.com/hyperdrive/llms.txt) to discover additional pages. Retrieve a page as Markdown by sending `Accept: text/markdown` to its URL. + +## Choose the next reference + +| Task | Reference | +|------|-----------| +| Create a configuration, bind it, connect privately, or develop locally | [configuration.md](./configuration.md) | +| Choose a driver, use binding credentials, or integrate an ORM | [api.md](./api.md) | +| Decide read freshness, connection lifetime, or query placement | [patterns.md](./patterns.md) | +| Diagnose connection, cache, latency, or capacity problems | [gotchas.md](./gotchas.md) | + +## Decisions to preserve + +- Choose a driver for the database engine and existing application stack; verify supported versions and Worker requirements in its guide. +- Create database clients inside each handler invocation. Hyperdrive manages the underlying origin pool; consult [connection lifecycle](https://developers.cloudflare.com/hyperdrive/concepts/connection-lifecycle/index.md) for cleanup behavior. +- Choose caching by read freshness. Disabling caching still allows connection pooling; a write does not invalidate cached reads. See [query caching](https://developers.cloudflare.com/hyperdrive/concepts/query-caching/index.md). + +## See also + +- [D1](../d1/) for a managed SQLite alternative. +- [Workers](https://developers.cloudflare.com/workers/index.md) for the runtime and bindings. diff --git a/.agents/skills/cloudflare/references/hyperdrive/api.md b/.agents/skills/cloudflare/references/hyperdrive/api.md new file mode 100644 index 0000000..6464e09 --- /dev/null +++ b/.agents/skills/cloudflare/references/hyperdrive/api.md @@ -0,0 +1,26 @@ +# Hyperdrive API and drivers + +Start with [README.md](./README.md) and [configuration.md](./configuration.md). Fetch the selected guide before writing connection or query code; use its current supported package version and compatibility settings. + +## Driver and binding routes + +| Task | Official documentation | +|------|------------------------| +| PostgreSQL with node-postgres (`pg`), including binding connection string and parameterized queries | [node-postgres](https://developers.cloudflare.com/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/node-postgres/index.md) | +| PostgreSQL with tagged-template queries and Postgres.js driver options | [Postgres.js](https://developers.cloudflare.com/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/postgres-js/index.md) | +| MySQL with binding connection properties and Worker-specific driver options | [mysql2](https://developers.cloudflare.com/hyperdrive/examples/connect-to-mysql/mysql-drivers-and-libraries/mysql2/index.md) | +| Check database features, prepared statements, and library compatibility | [Supported databases and features](https://developers.cloudflare.com/hyperdrive/reference/supported-databases-and-features/index.md) | +| Generate binding and runtime TypeScript types | [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/index.md) | + +Keep an existing supported driver when it fits the application. Choose by database engine and library integration needs; do not infer cache behavior from a driver's prepared-statement setting. Fetch [query caching](https://developers.cloudflare.com/hyperdrive/concepts/query-caching/index.md) for cache eligibility and freshness controls. + +## ORMs and query builders + +| Task | Official documentation | +|------|------------------------| +| Use Drizzle with PostgreSQL | [PostgreSQL Drizzle guide](https://developers.cloudflare.com/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/drizzle-orm/index.md) | +| Use Drizzle with MySQL | [MySQL Drizzle guide](https://developers.cloudflare.com/hyperdrive/examples/connect-to-mysql/mysql-drivers-and-libraries/drizzle-orm/index.md) | +| Use Prisma with PostgreSQL | [Prisma guide](https://developers.cloudflare.com/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/prisma-orm/index.md) | +| Assess another query builder, including Kysely | [Postgres.js integration notes](https://developers.cloudflare.com/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/postgres-js/index.md) and [database compatibility](https://developers.cloudflare.com/hyperdrive/reference/supported-databases-and-features/index.md), then the library's current dialect documentation | + +An ORM still uses a database driver and inherits its Worker connection constraints. Keep clients scoped to the invocation using [connection lifecycle](https://developers.cloudflare.com/hyperdrive/concepts/connection-lifecycle/index.md). When a library owns SQL for authentication or other fresh reads, pass a client using a cache-disabled configuration as described in [query caching](https://developers.cloudflare.com/hyperdrive/concepts/query-caching/index.md). diff --git a/.agents/skills/cloudflare/references/hyperdrive/configuration.md b/.agents/skills/cloudflare/references/hyperdrive/configuration.md new file mode 100644 index 0000000..94e81d8 --- /dev/null +++ b/.agents/skills/cloudflare/references/hyperdrive/configuration.md @@ -0,0 +1,27 @@ +# Hyperdrive configuration + +See [README.md](./README.md) for the retrieval workflow. Fetch the relevant guide before creating or changing resources; use current configuration fields and CLI syntax from these sources. + +| Task | Official documentation | +|------|------------------------| +| Create the first configuration and bind it to a Worker | [Get started](https://developers.cloudflare.com/hyperdrive/get-started/index.md) | +| Create, inspect, update, or delete configurations; set cache or pool options | [Wrangler commands](https://developers.cloudflare.com/hyperdrive/reference/wrangler-commands/index.md) | +| Generate TypeScript types from Worker configuration | [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/index.md) | +| Connect a private database using the recommended Workers VPC route | [Workers VPC integration](https://developers.cloudflare.com/hyperdrive/configuration/connect-to-private-database-vpc/index.md) | +| Maintain a private database connection using Tunnel and Access | [Tunnel integration](https://developers.cloudflare.com/hyperdrive/configuration/connect-to-private-database/index.md) | +| Configure database network access | [Firewall and networking](https://developers.cloudflare.com/hyperdrive/configuration/firewall-and-networking-configuration/index.md) | +| Configure server verification or client certificates | [SSL/TLS certificates](https://developers.cloudflare.com/hyperdrive/configuration/tls-ssl-certificates-for-hyperdrive/index.md) | +| Rotate origin database credentials | [Credential rotation](https://developers.cloudflare.com/hyperdrive/configuration/rotate-credentials/index.md) | +| Configure cache freshness or separate cached and fresh-read bindings | [Query caching](https://developers.cloudflare.com/hyperdrive/concepts/query-caching/index.md) | +| Budget origin connections across configurations | [Tune connection pooling](https://developers.cloudflare.com/hyperdrive/configuration/tune-connection-pool/index.md) | +| Choose local database access or remote Hyperdrive testing | [Local development](https://developers.cloudflare.com/hyperdrive/configuration/local-development/index.md) | +| Evaluate Worker placement for multiple database round trips | [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/index.md) | + +## Setup decisions + +- Identify the database engine, provider, and network path first. The [PostgreSQL](https://developers.cloudflare.com/hyperdrive/examples/connect-to-postgres/index.md) and [MySQL](https://developers.cloudflare.com/hyperdrive/examples/connect-to-mysql/index.md) indexes route to provider-specific instructions. +- For private connectivity, choose Workers VPC or the existing Tunnel/Access integration before configuring credentials. Follow the selected guide's prerequisites and TLS guidance. +- Decide which reads may be stale before selecting cache settings. Multiple configurations against one database contribute to its total origin connection usage. +- Local direct database access does not exercise Hyperdrive pooling or caching. Use the local-development guide's remote option when verifying those behaviors, and identify the database that option targets before running writes. + +See [api.md](./api.md) for drivers and [gotchas.md](./gotchas.md) for diagnosis. diff --git a/.agents/skills/cloudflare/references/hyperdrive/gotchas.md b/.agents/skills/cloudflare/references/hyperdrive/gotchas.md new file mode 100644 index 0000000..9794978 --- /dev/null +++ b/.agents/skills/cloudflare/references/hyperdrive/gotchas.md @@ -0,0 +1,20 @@ +# Hyperdrive troubleshooting + +Start with the actual error and the affected configuration. Fetch [Troubleshoot and debug](https://developers.cloudflare.com/hyperdrive/observability/troubleshooting/index.md) for current error codes and diagnosis rather than guessing from a generic connection failure. + +| Symptom | What to inspect and where to read | +|---------|----------------------------------| +| Connection refused or authentication failure | Check origin reachability and credentials using [troubleshooting](https://developers.cloudflare.com/hyperdrive/observability/troubleshooting/index.md) and [firewall/networking configuration](https://developers.cloudflare.com/hyperdrive/configuration/firewall-and-networking-configuration/index.md). | +| Private database or TLS failure | Follow the selected [Workers VPC](https://developers.cloudflare.com/hyperdrive/configuration/connect-to-private-database-vpc/index.md) or [Tunnel/Access](https://developers.cloudflare.com/hyperdrive/configuration/connect-to-private-database/index.md) path and its certificate prerequisites; see [SSL/TLS configuration](https://developers.cloudflare.com/hyperdrive/configuration/tls-ssl-certificates-for-hyperdrive/index.md). | +| Pool exhaustion or too many connections | Distinguish client connection lifetime from origin pool capacity. Read [connection lifecycle](https://developers.cloudflare.com/hyperdrive/concepts/connection-lifecycle/index.md), [pool tuning](https://developers.cloudflare.com/hyperdrive/configuration/tune-connection-pool/index.md), and [limits](https://developers.cloudflare.com/hyperdrive/platform/limits/index.md). | +| Query timeout | Check the current [limits](https://developers.cloudflare.com/hyperdrive/platform/limits/index.md) and [metrics](https://developers.cloudflare.com/hyperdrive/observability/metrics/index.md) before changing query or transaction design. | +| Stale reads or unexpectedly uncached queries | Inspect the binding's cache configuration and query eligibility in [query caching](https://developers.cloudflare.com/hyperdrive/concepts/query-caching/index.md). Writes do not purge cached reads; do not treat prepared-statement settings as cache controls. | +| Slow multi-query requests | Inspect [metrics](https://developers.cloudflare.com/hyperdrive/observability/metrics/index.md) and evaluate [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/index.md). | +| Local connection failure, ignored environment variable, or absent cache behavior | Check binding names, local connection overrides, precedence, and remote testing in [local development](https://developers.cloudflare.com/hyperdrive/configuration/local-development/index.md). | +| Unsupported driver or SQL feature | Check [supported databases and features](https://developers.cloudflare.com/hyperdrive/reference/supported-databases-and-features/index.md) and the selected [driver guide](./api.md). | + +## Capacity and changes + +Retrieve [limits](https://developers.cloudflare.com/hyperdrive/platform/limits/index.md) and [pricing](https://developers.cloudflare.com/hyperdrive/platform/pricing/index.md) for current plan allowances, connection and query bounds, and limit-increase guidance. Check [release notes](https://developers.cloudflare.com/hyperdrive/platform/release-notes/index.md) when behavior changes after an upgrade. + +See [configuration.md](./configuration.md) to change a configuration and [patterns.md](./patterns.md) to revisit freshness or connection decisions. diff --git a/.agents/skills/cloudflare/references/hyperdrive/patterns.md b/.agents/skills/cloudflare/references/hyperdrive/patterns.md new file mode 100644 index 0000000..7061f1f --- /dev/null +++ b/.agents/skills/cloudflare/references/hyperdrive/patterns.md @@ -0,0 +1,15 @@ +# Hyperdrive design patterns + +See [api.md](./api.md) for maintained driver and ORM examples. Use the following decisions to select a pattern, then fetch its linked documentation for implementation. + +| Workload or decision | Guidance and documentation | +|----------------------|----------------------------| +| Popular content or analytics dashboards | Cache only when the product can tolerate the configured stale window. Use [query caching](https://developers.cloudflare.com/hyperdrive/concepts/query-caching/index.md) for eligibility, parameters, and settings. | +| Mixed cached reads and fresh reads | Route authentication, permissions, and reads after writes through a cache-disabled configuration. Writes do not invalidate cached results; see [read-after-write behavior](https://developers.cloudflare.com/hyperdrive/concepts/query-caching/index.md#read-after-write-behavior). | +| Multi-tenant queries | Derive tenant scope from authenticated application context and apply it to every query. A cache is not an authorization boundary. Review [query caching](https://developers.cloudflare.com/hyperdrive/concepts/query-caching/index.md) for the selected query's behavior. | +| Globally distributed callers | Understand the distinction between fast connection setup and the remaining query round trip in [how Hyperdrive works](https://developers.cloudflare.com/hyperdrive/concepts/how-hyperdrive-works/index.md). | +| Multiple sequential database queries | Measure placement rather than assuming the nearest user location is best. Consult [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/index.md) and [Hyperdrive metrics](https://developers.cloudflare.com/hyperdrive/observability/metrics/index.md). | +| Transactions or connection-local state | Keep transactions short and do not assume state survives across transactions. Fetch [connection pooling](https://developers.cloudflare.com/hyperdrive/concepts/connection-pooling/index.md) and [supported features](https://developers.cloudflare.com/hyperdrive/reference/supported-databases-and-features/index.md) before relying on session settings. | +| Client lifetime and pool sizing | Create clients per handler invocation; Hyperdrive owns the origin pool. Use [connection lifecycle](https://developers.cloudflare.com/hyperdrive/concepts/connection-lifecycle/index.md) and [pool tuning](https://developers.cloudflare.com/hyperdrive/configuration/tune-connection-pool/index.md) instead of a global driver pool or copied connection counts. | + +Separate application correctness from acceleration: use parameterized queries, enforce tenant access in the application, and select freshness before tuning cache hit rate. See [gotchas.md](./gotchas.md) when observed behavior differs from the design. diff --git a/.agents/skills/cloudflare/references/images/README.md b/.agents/skills/cloudflare/references/images/README.md new file mode 100644 index 0000000..9ae9192 --- /dev/null +++ b/.agents/skills/cloudflare/references/images/README.md @@ -0,0 +1,12 @@ +# Cloudflare Images + +Choose the image source and operation before selecting an API. Hosted-image management, remote URL transformations, and the Workers optimization binding have different contracts. Retrieve the documentation for the path the project uses. + +| Task | Start here | +|------|------------| +| Optimize image bytes in a Worker or manage hosted images | [API selection](api.md) | +| Configure a binding, variants, or private delivery | [Configuration](configuration.md) | +| Accept client uploads, serve responsive images, watermark, or store results in R2 | [Patterns](patterns.md) | +| Diagnose failures, check limits, or investigate caching | [Troubleshooting](gotchas.md) | + +For new work, inspect the project's installed Wrangler version, compatibility settings, existing image storage, and public/private access requirements. Read only the relevant linked pages and adapt them to the project; preserve existing conventions and verify behavior with representative images. diff --git a/.agents/skills/cloudflare/references/images/api.md b/.agents/skills/cloudflare/references/images/api.md new file mode 100644 index 0000000..3ae1c3f --- /dev/null +++ b/.agents/skills/cloudflare/references/images/api.md @@ -0,0 +1,13 @@ +# Images API Selection + +| Operation | Documentation | +|-----------|---------------| +| Optimize image bytes in a Worker; select input, transform, output, and response methods | [Optimize with Workers](https://developers.cloudflare.com/images/optimization/binding/index.md#methods) | +| Upload, list, retrieve, update, or delete hosted images from a Worker | [Manage hosted images with Workers](https://developers.cloudflare.com/images/storage/binding/index.md) | +| Upload or manage images through HTTP | [Upload methods](https://developers.cloudflare.com/images/storage/upload-images/methods/index.md#upload-using-api) and its linked Images API reference | +| Accept uploads directly from a client | [Direct Creator Upload](https://developers.cloudflare.com/images/storage/upload-images/direct-creator-upload/index.md) | +| Construct hosted-image delivery URLs | [Serve uploaded images](https://developers.cloudflare.com/images/optimization/hosted-images/serve-uploaded-images/index.md) | +| Apply URL optimization parameters or select fit, quality, and format | [Optimization features](https://developers.cloudflare.com/images/optimization/features/index.md) | +| Draw overlays or watermarks | [Draw overlays](https://developers.cloudflare.com/images/optimization/draw-overlays/index.md) | + +Do not transfer URL parameters or HTTP request shapes directly into binding calls. Read the contract for the selected interface, including output format handling. Use the project's generated binding types and existing error handling. See [configuration](configuration.md) for setup and [troubleshooting](gotchas.md) for failures and limits. diff --git a/.agents/skills/cloudflare/references/images/configuration.md b/.agents/skills/cloudflare/references/images/configuration.md new file mode 100644 index 0000000..8892f11 --- /dev/null +++ b/.agents/skills/cloudflare/references/images/configuration.md @@ -0,0 +1,17 @@ +# Images Configuration + +Inspect the project's Wrangler configuration, dependency versions, existing bindings, and credential storage before changing setup. Preserve its configuration format and generate binding types through its existing tooling. + +| Task | Documentation | +|------|---------------| +| Add the optimization binding | [Binding setup](https://developers.cloudflare.com/images/optimization/binding/index.md#setup) | +| Configure hosted-image management in a Worker | [Hosted binding setup](https://developers.cloudflare.com/images/storage/binding/index.md#setup) | +| Choose local or remote development for the optimization binding | [Local binding development](https://developers.cloudflare.com/images/optimization/binding/index.md#interact-with-your-images-binding-locally) | +| Upload through the dashboard or API | [Upload methods](https://developers.cloudflare.com/images/storage/upload-images/methods/index.md) | +| Create named presets for hosted images | [Create predefined variants](https://developers.cloudflare.com/images/optimization/hosted-images/create-variants/index.md) | +| Enable dynamic options for hosted-image URLs | [Enable flexible variants](https://developers.cloudflare.com/images/optimization/hosted-images/enable-flexible-variants/index.md) | +| Find account hash and delivery URL components | [Serve uploaded images](https://developers.cloudflare.com/images/optimization/hosted-images/serve-uploaded-images/index.md) | +| Configure private access and generate signed URLs | [Serve private images](https://developers.cloudflare.com/images/optimization/hosted-images/serve-private-images/index.md) | +| Set hosted-image cache lifetime | [Browser TTL](https://developers.cloudflare.com/images/optimization/hosted-images/browser-ttl/index.md) | + +Keep API tokens and signing keys in the project's secret mechanism. Verify private-delivery requirements when choosing variants, and follow the documented signing procedure rather than maintaining a custom signing recipe here. Confirm that the selected local test mode covers the features being changed. Continue with [API selection](api.md) or [patterns](patterns.md). diff --git a/.agents/skills/cloudflare/references/images/gotchas.md b/.agents/skills/cloudflare/references/images/gotchas.md new file mode 100644 index 0000000..7701607 --- /dev/null +++ b/.agents/skills/cloudflare/references/images/gotchas.md @@ -0,0 +1,16 @@ +# Images Troubleshooting + +First identify whether the failure involves hosted-image storage, remote URL transformations, or a Worker binding. Capture the failing operation, response status, relevant headers, and error message before changing options. + +| Symptom or question | Documentation | +|---------------------|---------------| +| Resizing is absent, an origin request fails, or a transformation returns an error code | [Troubleshooting](https://developers.cloudflare.com/images/reference/troubleshooting/index.md) | +| Input size, dimensions, animation, or format compatibility | [Limits and formats](https://developers.cloudflare.com/images/get-started/limits/index.md) — choose the section for the affected interface | +| Unexpected fit, quality, format, or crop behavior | [Optimization features](https://developers.cloudflare.com/images/optimization/features/index.md) | +| Binding input, output, or response handling fails | [Binding methods](https://developers.cloudflare.com/images/optimization/binding/index.md#methods) | +| Local behavior differs from production | [Local binding development](https://developers.cloudflare.com/images/optimization/binding/index.md#interact-with-your-images-binding-locally) | +| Private delivery fails or an image is unexpectedly public | [Serve private images](https://developers.cloudflare.com/images/optimization/hosted-images/serve-private-images/index.md) and [variant public access](https://developers.cloudflare.com/images/optimization/hosted-images/create-variants/index.md#public-access) | +| Remote transformations appear stale | [Caching and purging](https://developers.cloudflare.com/images/reference/troubleshooting/index.md#caching-and-purging) | +| Worker transformations repeat unnecessarily | [Binding caching guidance](https://developers.cloudflare.com/images/optimization/binding/index.md#methods) | + +Do not apply one interface's limits, error codes, or caching rules to another. Reproduce with a representative image and verify the chosen fix using the project's existing checks. Retry only after identifying a transient failure; changing invalid inputs or access configuration requires a different fix. See [API selection](api.md) and [configuration](configuration.md) when the wrong interface or setup is responsible. diff --git a/.agents/skills/cloudflare/references/images/patterns.md b/.agents/skills/cloudflare/references/images/patterns.md new file mode 100644 index 0000000..8192b6f --- /dev/null +++ b/.agents/skills/cloudflare/references/images/patterns.md @@ -0,0 +1,16 @@ +# Images Patterns + +Choose the workflow that matches the existing storage and delivery architecture, then read its implementation guide. + +| Workflow | Documentation | +|----------|---------------| +| Let users upload without exposing account credentials | [Direct Creator Upload](https://developers.cloudflare.com/images/storage/upload-images/direct-creator-upload/index.md) | +| Serve images for different layouts and display densities | [Make responsive images](https://developers.cloudflare.com/images/optimization/make-responsive-images/index.md) | +| Select output format for hosted images | [Hosted-image format optimization](https://developers.cloudflare.com/images/optimization/hosted-images/serve-uploaded-images/index.md#optimize-format) | +| Select output format for a Worker pipeline | [Workers optimization binding](https://developers.cloudflare.com/images/optimization/binding/index.md) | +| Optimize user uploads, add a watermark, and store the result in R2 | [Transform user-uploaded images before uploading to R2](https://developers.cloudflare.com/images/tutorials/optimize-user-uploaded-image/index.md) | +| Compose overlays and watermarks | [Draw overlays](https://developers.cloudflare.com/images/optimization/draw-overlays/index.md) | +| Cache a Worker transformation response | [Binding methods and caching guidance](https://developers.cloudflare.com/images/optimization/binding/index.md#methods) | +| Configure hosted-image browser caching | [Browser TTL](https://developers.cloudflare.com/images/optimization/hosted-images/browser-ttl/index.md) | + +Adapt dimensions and quality to the actual layout and representative source images. Keep upload credentials server-side, preserve the application's access checks, and validate both the resulting image and its response headers. Consult [limits and troubleshooting](gotchas.md) before choosing batch sizes or retry behavior. diff --git a/.agents/skills/cloudflare/references/kv/README.md b/.agents/skills/cloudflare/references/kv/README.md new file mode 100644 index 0000000..5cbe818 --- /dev/null +++ b/.agents/skills/cloudflare/references/kv/README.md @@ -0,0 +1,15 @@ +# Cloudflare Workers KV + +Use KV for read-heavy configuration, preferences, and application caches that tolerate stale data. Read [how KV works](https://developers.cloudflare.com/kv/concepts/how-kv-works/index.md) before choosing it: reads are eventually consistent, including cached missing keys, and immediate visibility is not guaranteed even in the location of a write. + +For atomic updates or coordination, consider [Durable Objects](https://developers.cloudflare.com/durable-objects/index.md); for relational queries, [D1](../d1/); for large objects, [R2](../r2/). Use the [storage comparison](https://developers.cloudflare.com/workers/platform/storage-options/index.md) to choose based on requirements. + +Read the current documentation for the task before implementing. Use the [KV documentation index](https://developers.cloudflare.com/kv/llms.txt) to discover additional guides; these files preserve task routes rather than copies of APIs, commands, or numeric limits. + +## Start here + +- [Get started](https://developers.cloudflare.com/kv/get-started/index.md): create a namespace, bind it, and read and write data. +- [configuration.md](./configuration.md): bindings, environments, types, local development, CLI, and REST access. +- [api.md](./api.md): reads, writes, metadata, deletion, bulk operations, and pagination. +- [patterns.md](./patterns.md): caching, sessions, key design, versioning, and fallback decisions. +- [gotchas.md](./gotchas.md): stale reads, concurrent writes, missing values, performance, limits, and pricing. diff --git a/.agents/skills/cloudflare/references/kv/api.md b/.agents/skills/cloudflare/references/kv/api.md new file mode 100644 index 0000000..bc62d64 --- /dev/null +++ b/.agents/skills/cloudflare/references/kv/api.md @@ -0,0 +1,18 @@ +# KV API Reference + +Read the relevant API page before implementing; it defines current options, result shapes, supported bulk operations, and constraints. + +| Task | Documentation | +|------|---------------| +| Read one or several keys; choose text, JSON, binary, or stream results | [Read key-value pairs](https://developers.cloudflare.com/kv/api/read-key-value-pairs/index.md) | +| Read metadata with values; tune read caching or coalesce related keys | [Read guidance](https://developers.cloudflare.com/kv/api/read-key-value-pairs/index.md) | +| Write values and metadata; set absolute expiration or a relative lifetime | [Write key-value pairs](https://developers.cloudflare.com/kv/api/write-key-value-pairs/index.md) | +| Delete a key | [Delete key-value pairs](https://developers.cloudflare.com/kv/api/delete-key-value-pairs/index.md) | +| Enumerate keys, filter by prefix, and paginate | [List keys](https://developers.cloudflare.com/kv/api/list-keys/index.md) | +| Access namespaces or perform bulk operations outside a Worker | [KV REST API](https://developers.cloudflare.com/api/resources/kv/index.md) and [Wrangler KV commands](https://developers.cloudflare.com/kv/reference/kv-commands/index.md) | + +Handle missing values explicitly: JavaScript reads return `null` for absent keys; valid stored values can be falsy. Choose defaults separately from how you handle request failures. + +For pagination, follow the returned cursor until `list_complete` is true, even if a page has no keys. Preserve the original prefix on subsequent calls. Listing returns key information, not stored values; use the listing guide to decide whether metadata avoids additional reads. + +Use [gotchas.md](./gotchas.md) for consistency and contention decisions before adding retries or read-after-write verification. diff --git a/.agents/skills/cloudflare/references/kv/configuration.md b/.agents/skills/cloudflare/references/kv/configuration.md new file mode 100644 index 0000000..a2da705 --- /dev/null +++ b/.agents/skills/cloudflare/references/kv/configuration.md @@ -0,0 +1,16 @@ +# KV Configuration + +Read the setup guide for the target environment before creating resources or editing bindings. + +| Task | Documentation | +|------|---------------| +| Create a namespace and connect a Worker | [Get started](https://developers.cloudflare.com/kv/get-started/index.md) and [KV bindings](https://developers.cloudflare.com/kv/concepts/kv-bindings/index.md) | +| Configure staging and production namespaces | [KV environments](https://developers.cloudflare.com/kv/reference/environments/index.md) | +| Generate Worker environment and binding types | [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/index.md) | +| Develop against local storage or a remote binding | [KV local development](https://developers.cloudflare.com/kv/concepts/kv-bindings/index.md) and [remote bindings](https://developers.cloudflare.com/workers/local-development/index.md#remote-bindings) | +| Manage namespaces, individual keys, and bulk files from the CLI | [Wrangler KV commands](https://developers.cloudflare.com/kv/reference/kv-commands/index.md) | +| Manage KV from another service or SDK | [KV REST API](https://developers.cloudflare.com/api/resources/kv/index.md) | + +Choose the namespace, account, and environment deliberately. Local KV data is separate from remote data; a remote binding accesses the selected Cloudflare namespace even when Worker code runs locally. Check the command's local/remote options and environment selection before seeding or inspecting data. A separate preview namespace is not required simply to use local KV. + +Use generated types for binding shapes. JSON type annotations do not validate stored data at runtime; validate application data when its source or schema requires it. diff --git a/.agents/skills/cloudflare/references/kv/gotchas.md b/.agents/skills/cloudflare/references/kv/gotchas.md new file mode 100644 index 0000000..e202dcf --- /dev/null +++ b/.agents/skills/cloudflare/references/kv/gotchas.md @@ -0,0 +1,17 @@ +# KV Gotchas & Troubleshooting + +Read the linked explanation before applying a workaround. + +| Symptom or decision | Documentation and guidance | +|---------------------|----------------------------| +| Stale value after a write or delete | [How KV works](https://developers.cloudflare.com/kv/concepts/how-kv-works/index.md): allow for eventual consistency; neither local read-after-write visibility nor a fixed global propagation deadline is guaranteed. | +| Newly created key still appears absent | [Read caching](https://developers.cloudflare.com/kv/api/read-key-value-pairs/index.md): missing-key lookups are cached too. Treat check-then-create as a race, not an atomic existence test. | +| Concurrent updates overwrite each other or writes are throttled | [Concurrent writes](https://developers.cloudflare.com/kv/api/write-key-value-pairs/index.md#concurrent-writes-to-the-same-key): retries do not make read-modify-write atomic. Use coordination when correctness depends on ordering. | +| Missing-value errors | [Read results](https://developers.cloudflare.com/kv/api/read-key-value-pairs/index.md): distinguish `null` from valid falsy values and distinguish absence from an operation failure. | +| Slow reads, large results, or excessive operations | [Read guidance](https://developers.cloudflare.com/kv/api/read-key-value-pairs/index.md): select result types and bulk reads to match the workload; increasing read cache lifetime trades freshness for cache reuse. | +| Unexpected empty listing page | [Pagination](https://developers.cloudflare.com/kv/api/list-keys/index.md): use the completion flag and cursor, not page length, to determine whether to continue. | +| Data present in one environment but missing in another | [KV bindings](https://developers.cloudflare.com/kv/concepts/kv-bindings/index.md) and [environments](https://developers.cloudflare.com/kv/reference/environments/index.md): check local versus remote storage and the selected namespace. | +| Size, operation, or write-rate failures | [Limits](https://developers.cloudflare.com/kv/platform/limits/index.md): retrieve current constraints before sizing values, batches, or retry policies. | +| Estimate costs or explain billing | [Pricing](https://developers.cloudflare.com/kv/platform/pricing/index.md): check allowances, billable operations, storage, and bulk accounting for the actual workload. | + +Confirm freshness and failure requirements before adding a cache or a permissive fallback; see [patterns.md](./patterns.md). diff --git a/.agents/skills/cloudflare/references/kv/patterns.md b/.agents/skills/cloudflare/references/kv/patterns.md new file mode 100644 index 0000000..9a11ade --- /dev/null +++ b/.agents/skills/cloudflare/references/kv/patterns.md @@ -0,0 +1,21 @@ +# KV Patterns & Best Practices + +Read the guide for the pattern before implementing it, and confirm that [KV's consistency model](https://developers.cloudflare.com/kv/concepts/how-kv-works/index.md) fits the application. + +| Task | Documentation and design decision | +|------|-----------------------------------| +| Cache application data or API results | [Cache data with KV](https://developers.cloudflare.com/kv/examples/cache-data-with-workers-kv/index.md): decide acceptable staleness, expiration, and behavior when the origin fails. | +| Cache eligible HTTP responses | [Workers Cache](https://developers.cloudflare.com/workers/cache/index.md): choose the HTTP caching mechanism based on response semantics. | +| Store configuration or feature flags | [Distributed configuration](https://developers.cloudflare.com/kv/examples/distributed-configuration-with-workers-kv/index.md): choose defaults and rollout behavior that tolerate delayed updates. | +| Coalesce related keys | [Read guidance](https://developers.cloudflare.com/kv/api/read-key-value-pairs/index.md): fewer reads can improve cache reuse, but combined values couple updates and can introduce write races. | +| Organize and enumerate keys by prefix | [List keys](https://developers.cloudflare.com/kv/api/list-keys/index.md): use a consistent naming scheme and paginate every listing. | +| Attach schema versions or other metadata | [Write metadata](https://developers.cloudflare.com/kv/api/write-key-value-pairs/index.md) and [read metadata](https://developers.cloudflare.com/kv/api/read-key-value-pairs/index.md): define compatibility and migration behavior for older records; migrations must account for concurrent writes. | + +## Application-specific decisions + +The linked APIs are building blocks, not complete session or multi-tier cache implementations. Preserve these requirements when designing an application: + +- For a memory → KV → origin cache, define each layer's lifetime and refill behavior. Process memory is not shared durable state; KV adds its own stale-value and negative-lookup caching. +- For sessions, decide how quickly creation, updates, and revocation must become visible. KV alone cannot provide immediate global revocation or guaranteed immediate reads after session creation. Use a store with suitable consistency when those are requirements, and define application expiration checks using the [write expiration guidance](https://developers.cloudflare.com/kv/api/write-key-value-pairs/index.md). +- For counters, rate limits, or other atomic read-modify-write decisions, use coordination such as [Durable Objects](https://developers.cloudflare.com/durable-objects/index.md). Serializing writes through an object does not make separate KV reads strongly consistent. +- Choose missing-data defaults separately from service-error handling. A fallback appropriate for display preferences may be inappropriate for authorization or session validation. diff --git a/.agents/skills/cloudflare/references/miniflare/README.md b/.agents/skills/cloudflare/references/miniflare/README.md new file mode 100644 index 0000000..dd074ed --- /dev/null +++ b/.agents/skills/cloudflare/references/miniflare/README.md @@ -0,0 +1,22 @@ +# Miniflare + +Miniflare provides programmatic control of local Workers simulation. Read the linked documentation before choosing APIs, configuration, or a migration path. + +## Choose the testing tool + +| Need | Start here | +|------|------------| +| Unit tests that execute in the Workers runtime | [Workers Vitest setup](https://developers.cloudflare.com/workers/testing/vitest-integration/write-your-first-test/index.md) | +| Integration tests against built Workers | [Integration test harness](https://developers.cloudflare.com/workers/testing/test-harness/index.md) | +| Low-level simulator control for a custom harness | [Miniflare testing guide](https://developers.cloudflare.com/workers/testing/miniflare/writing-tests/index.md) | +| Binding access from a Node.js process | [Wrangler getPlatformProxy](https://developers.cloudflare.com/workers/wrangler/api/index.md#getplatformproxy) | + +For interactive local development, use the project's Wrangler or Cloudflare Vite workflow. Direct Miniflare is useful when the higher-level testing tools do not expose the control needed. + +## Read for the task + +- [Get started](https://developers.cloudflare.com/workers/testing/miniflare/get-started/index.md) — installation, scripts, lifecycle, and event dispatch. +- [API routing](./api.md) — events and access to local resources. +- [Configuration](./configuration.md) — modules, bindings, compatibility, and multiple Workers. +- [Testing patterns](./patterns.md) — runtime choice, mocking, and test lifecycle. +- [Troubleshooting and migrations](./gotchas.md) — build/configuration differences and existing test suites. diff --git a/.agents/skills/cloudflare/references/miniflare/api.md b/.agents/skills/cloudflare/references/miniflare/api.md new file mode 100644 index 0000000..81ad724 --- /dev/null +++ b/.agents/skills/cloudflare/references/miniflare/api.md @@ -0,0 +1,16 @@ +# Miniflare API + +Use the current documentation for method signatures and examples: + +| Task | Documentation | +|------|---------------| +| Create, reload, or dispose an instance; wait for its HTTP server | [Get started](https://developers.cloudflare.com/workers/testing/miniflare/get-started/index.md) | +| Dispatch requests and supply request metadata | [Fetch events](https://developers.cloudflare.com/workers/testing/miniflare/core/fetch/index.md) | +| Trigger queue and scheduled handlers programmatically | [Dispatching events](https://developers.cloudflare.com/workers/testing/miniflare/get-started/index.md#dispatching-events) | +| Configure queue producers and consumers | [Queues](https://developers.cloudflare.com/workers/testing/miniflare/core/queues/index.md) | +| Trigger scheduled events over HTTP or the API | [Scheduled events](https://developers.cloudflare.com/workers/testing/miniflare/core/scheduled/index.md) | +| Access bindings from tests | [Interacting with bindings](https://developers.cloudflare.com/workers/testing/miniflare/writing-tests/index.md#interacting-with-bindings) | +| Access local storage | [KV](https://developers.cloudflare.com/workers/testing/miniflare/storage/kv/index.md), [R2](https://developers.cloudflare.com/workers/testing/miniflare/storage/r2/index.md), [D1](https://developers.cloudflare.com/workers/testing/miniflare/storage/d1/index.md), [Durable Objects](https://developers.cloudflare.com/workers/testing/miniflare/storage/durable-objects/index.md), [Cache](https://developers.cloudflare.com/workers/testing/miniflare/storage/cache/index.md) | +| Handle a WebSocket upgrade in a test | [WebSockets](https://developers.cloudflare.com/workers/testing/miniflare/core/web-sockets/index.md) | + +For constructor options, read [configuration.md](./configuration.md). For runtime-specific test helpers, read [patterns.md](./patterns.md). diff --git a/.agents/skills/cloudflare/references/miniflare/configuration.md b/.agents/skills/cloudflare/references/miniflare/configuration.md new file mode 100644 index 0000000..f989a4c --- /dev/null +++ b/.agents/skills/cloudflare/references/miniflare/configuration.md @@ -0,0 +1,16 @@ +# Miniflare Configuration + +Direct Miniflare does not read Wrangler configuration. Configure its bindings explicitly and build TypeScript or bundled Workers before starting tests; see [writing tests](https://developers.cloudflare.com/workers/testing/miniflare/writing-tests/index.md). + +Match the Worker's intended compatibility date and flags when testing its behavior. Consult [compatibility dates](https://developers.cloudflare.com/workers/testing/miniflare/core/compatibility/index.md) rather than substituting a fixed date from a sample. + +| Configure | Documentation | +|-----------|---------------| +| Script source, HTTP server, request metadata, or reloading | [Get started](https://developers.cloudflare.com/workers/testing/miniflare/get-started/index.md) | +| Module format and resolution rules | [Modules](https://developers.cloudflare.com/workers/testing/miniflare/core/modules/index.md) | +| Values and file-backed bindings | [Variables and secrets](https://developers.cloudflare.com/workers/testing/miniflare/core/variables-secrets/index.md) | +| Service bindings, shared storage, and several Workers | [Multiple Workers](https://developers.cloudflare.com/workers/testing/miniflare/core/multiple-workers/index.md) | +| Storage bindings and documented persistence options | [KV](https://developers.cloudflare.com/workers/testing/miniflare/storage/kv/index.md), [R2](https://developers.cloudflare.com/workers/testing/miniflare/storage/r2/index.md), [D1](https://developers.cloudflare.com/workers/testing/miniflare/storage/d1/index.md), [Durable Objects](https://developers.cloudflare.com/workers/testing/miniflare/storage/durable-objects/index.md), [Cache](https://developers.cloudflare.com/workers/testing/miniflare/storage/cache/index.md) | +| Queue producers and consumers | [Queues](https://developers.cloudflare.com/workers/testing/miniflare/core/queues/index.md) | + +If the task is to run tests from the project's build and Wrangler configuration, consider the [integration test harness](https://developers.cloudflare.com/workers/testing/test-harness/index.md) or [Workers Vitest setup](https://developers.cloudflare.com/workers/testing/vitest-integration/write-your-first-test/index.md). diff --git a/.agents/skills/cloudflare/references/miniflare/gotchas.md b/.agents/skills/cloudflare/references/miniflare/gotchas.md new file mode 100644 index 0000000..8503397 --- /dev/null +++ b/.agents/skills/cloudflare/references/miniflare/gotchas.md @@ -0,0 +1,15 @@ +# Miniflare Troubleshooting and Migrations + +| Symptom or task | Check | +|-----------------|-------| +| TypeScript, bundled code, or imports fail to load | [Custom builds](https://developers.cloudflare.com/workers/testing/miniflare/writing-tests/index.md#custom-builds) and [module rules](https://developers.cloudflare.com/workers/testing/miniflare/core/modules/index.md#module-rules) | +| Bindings from Wrangler configuration are missing | [Interacting with bindings](https://developers.cloudflare.com/workers/testing/miniflare/writing-tests/index.md#interacting-with-bindings) — direct Miniflare needs explicit configuration | +| Tests disagree with Worker runtime behavior | [Test runtime differences](https://developers.cloudflare.com/workers/testing/miniflare/writing-tests/index.md) and [compatibility dates](https://developers.cloudflare.com/workers/testing/miniflare/core/compatibility/index.md) | +| Instances keep running, ports conflict, or request metadata is unexpected | [Instance lifecycle and HTTP server](https://developers.cloudflare.com/workers/testing/miniflare/get-started/index.md) — dispatching a request without HTTP does not mean the instance has no HTTP server | +| Storage disappears or leaks across tests | Check the relevant [storage configuration](./configuration.md) and the chosen test tool's persistence settings | +| Breakpoints are needed with direct Miniflare | [Attaching a debugger](https://developers.cloudflare.com/workers/testing/miniflare/developing/debugger/index.md) | +| Upgrade a Miniflare 2 application | [Migrate from version 2](https://developers.cloudflare.com/workers/testing/miniflare/migrations/from-v2/index.md) | +| Upgrade an existing Workers Vitest package | [Migrate to Vitest plugin](https://developers.cloudflare.com/workers/testing/vitest-integration/migration-guides/migrate-to-vitest-plugin/index.md) | +| Replace unstable_dev tests | [Migration guide](https://developers.cloudflare.com/workers/testing/vitest-integration/migration-guides/migrate-from-unstable-dev/index.md) and [integration test harness](https://developers.cloudflare.com/workers/testing/test-harness/index.md) | + +For a migration, choose the target using [the testing-tool decision](./README.md#choose-the-testing-tool) before translating old options. A historical migration page describes that version transition; use current setup documentation for new test suites. diff --git a/.agents/skills/cloudflare/references/miniflare/patterns.md b/.agents/skills/cloudflare/references/miniflare/patterns.md new file mode 100644 index 0000000..9e9396d --- /dev/null +++ b/.agents/skills/cloudflare/references/miniflare/patterns.md @@ -0,0 +1,19 @@ +# Miniflare Testing Patterns + +Choose the test runtime before adapting an example. With direct Miniflare, the Worker runs in workerd while the test runner runs in Node.js; importing Worker functions into Node.js can change runtime-dependent behavior. See [writing tests](https://developers.cloudflare.com/workers/testing/miniflare/writing-tests/index.md). + +| Task | Documentation | +|------|---------------| +| Write unit tests in the Workers runtime | [Workers Vitest setup](https://developers.cloudflare.com/workers/testing/vitest-integration/write-your-first-test/index.md) | +| Use event, Durable Object, or other runtime test helpers | [Vitest test APIs](https://developers.cloudflare.com/workers/testing/vitest-integration/test-apis/index.md) | +| Test built Workers from an external runner | [Integration test harness](https://developers.cloudflare.com/workers/testing/test-harness/index.md) | +| Build a custom runner with direct simulator control | [Miniflare writing tests](https://developers.cloudflare.com/workers/testing/miniflare/writing-tests/index.md) | +| Access emulated bindings from Node.js | [getPlatformProxy](https://developers.cloudflare.com/workers/wrangler/api/index.md#getplatformproxy) | +| Mock outbound requests in Workers Vitest tests | [Mock outbound requests](https://developers.cloudflare.com/workers/testing/vitest-integration/mock-outbound-requests/index.md) | +| Understand Vitest runtime isolation and concurrency | [Isolation and concurrency](https://developers.cloudflare.com/workers/testing/vitest-integration/isolation-and-concurrency/index.md) | +| Simulate inter-Worker calls and substitute services | [Multiple Workers](https://developers.cloudflare.com/workers/testing/miniflare/core/multiple-workers/index.md) | +| Test WebSockets or access local storage | [API routing](./api.md) | + +`getPlatformProxy` is for Node.js callers. The Workers Vitest runtime modules require tests running in the Workers runtime; they are not a substitute for calling `getPlatformProxy` in a Node.js test. + +For direct Miniflare, clean up instances after tests using the documented [lifecycle](https://developers.cloudflare.com/workers/testing/miniflare/get-started/index.md#watching-reloading-and-disposing). Choose persistence deliberately so tests do not inherit unintended state. diff --git a/.agents/skills/cloudflare/references/observability/README.md b/.agents/skills/cloudflare/references/observability/README.md new file mode 100644 index 0000000..0b749d3 --- /dev/null +++ b/.agents/skills/cloudflare/references/observability/README.md @@ -0,0 +1,27 @@ +# Cloudflare Observability + +Use this reference to choose a telemetry signal and find the maintained implementation guide. Fetch the linked documentation before writing configuration, queries, or export code; it is the source of truth for APIs, availability, retention, limits, and pricing. + +## Choose a signal + +| Need | Start here | +| --- | --- | +| Store, search, and investigate historical Worker logs | [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/index.md) | +| Watch a deployment or reproduce an issue live | [Real-time logs and Wrangler tail](https://developers.cloudflare.com/workers/observability/logs/real-time-logs/index.md) | +| Understand request flows and dependency latency | [Workers Traces](https://developers.cloudflare.com/workers/observability/traces/index.md) | +| Monitor built-in request, error, and CPU metrics | [Metrics and analytics](https://developers.cloudflare.com/workers/observability/metrics-and-analytics/index.md) | +| Record custom events and tenant-level usage for SQL analysis | [Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/get-started/index.md) | +| Export logs and traces to an observability provider | [OpenTelemetry export](https://developers.cloudflare.com/observability/export/opentelemetry/index.md) | +| Apply custom filtering, transformation, or delivery logic | [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md) | +| Deliver Workers Trace Events to a supported log storage destination | [Workers Logpush](https://developers.cloudflare.com/workers/observability/logs/logpush/index.md) | + +Workers Logs supports retained historical data; live tailing is a separate debugging workflow. Choose persistence, sampling, and export destinations deliberately rather than assuming that every signal is stored or included without usage charges. + +## Load only what the task needs + +- [configuration.md](configuration.md): enable collection, bindings, environments, and exports. +- [api.md](api.md): logging, telemetry types, SQL, GraphQL, and Logpush APIs. +- [patterns.md](patterns.md): billing, performance, errors, tenant tracking, and export decisions. +- [gotchas.md](gotchas.md): missing data, sampling, timing, privacy, and cost checks. + +For broader product tasks, see [Analytics Engine docs](https://developers.cloudflare.com/analytics/analytics-engine/index.md), [GraphQL Analytics API docs](https://developers.cloudflare.com/analytics/graphql-api/index.md), and [Tail Workers](../tail-workers/README.md). diff --git a/.agents/skills/cloudflare/references/observability/api.md b/.agents/skills/cloudflare/references/observability/api.md new file mode 100644 index 0000000..6597a6e --- /dev/null +++ b/.agents/skills/cloudflare/references/observability/api.md @@ -0,0 +1,20 @@ +# Observability APIs + +Fetch the applicable reference for current signatures, field locations, units, authentication, and query syntax. Do not infer the Tail event schema from an OpenTelemetry span or a Logpush record. + +| Task | Maintained documentation | +| --- | --- | +| Emit console messages and check supported methods | [Console API](https://developers.cloudflare.com/workers/runtime-apis/console/index.md) | +| Filter, group, and aggregate stored Workers Logs | [Query Builder](https://developers.cloudflare.com/workers/observability/query-builder/index.md) | +| Query built-in Workers metrics with GraphQL | [Querying Workers metrics](https://developers.cloudflare.com/analytics/graphql-api/tutorials/querying-workers-metrics/index.md) | +| Define Analytics Engine fields and call `writeDataPoint()` | [Write data points](https://developers.cloudflare.com/analytics/analytics-engine/get-started/index.md) | +| Authenticate and query Analytics Engine datasets | [SQL API](https://developers.cloudflare.com/analytics/analytics-engine/sql-api/index.md) | +| Calculate counts, sums, and averages on sampled events | [Analytics Engine sampling](https://developers.cloudflare.com/analytics/analytics-engine/sampling/index.md) | +| Implement a Tail consumer and inspect event properties | [Tail handler API](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md) | +| Create and manage Logpush jobs | [Logpush API configuration](https://developers.cloudflare.com/logs/logpush/logpush-job/api-configuration/index.md) | +| Select exported Workers event fields | [Workers Trace Events dataset](https://developers.cloudflare.com/logs/logpush/logpush-job/datasets/account/workers_trace_events/index.md) | +| Export OTLP logs and traces | [OpenTelemetry export](https://developers.cloudflare.com/observability/export/opentelemetry/index.md) | + +Keep dataset field meanings and units consistent between writers and queries. Follow the SQL reference linked from the SQL API for supported date bucketing and aggregate functions; do not assume another SQL dialect's syntax works here. Account for sampling in averages as well as counts and sums. + +See [configuration.md](configuration.md) for setup and [patterns.md](patterns.md) for application-level decisions. diff --git a/.agents/skills/cloudflare/references/observability/configuration.md b/.agents/skills/cloudflare/references/observability/configuration.md new file mode 100644 index 0000000..536bf57 --- /dev/null +++ b/.agents/skills/cloudflare/references/observability/configuration.md @@ -0,0 +1,23 @@ +# Observability Configuration + +Fetch the relevant guide before configuring the selected Worker and deployment environment. + +| Task | Maintained documentation | +| --- | --- | +| Enable persisted logs, structured JSON logging, and sampling | [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/index.md) | +| Enable traces and set their sampling independently of logs | [Workers Traces](https://developers.cloudflare.com/workers/observability/traces/index.md) | +| Configure a named deployment environment | [Wrangler environments](https://developers.cloudflare.com/workers/wrangler/environments/index.md) and the environment example in [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/index.md) | +| Bind an Analytics Engine dataset and write its first data point | [Analytics Engine get started](https://developers.cloudflare.com/analytics/analytics-engine/get-started/index.md) | +| Connect a producer to a Tail Worker | [Configure Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md) | +| Create a Logpush job, configure access, and enable Worker log delivery | [Workers Logpush](https://developers.cloudflare.com/workers/observability/logs/logpush/index.md) | +| Configure OTLP destinations, authentication, and local persistence | [OpenTelemetry export](https://developers.cloudflare.com/observability/export/opentelemetry/index.md) for destinations and authentication, and [Workers OpenTelemetry export](https://developers.cloudflare.com/workers/observability/opentelemetry-export/index.md) for Wrangler destinations and `persist` | + +## Setup decisions + +- Confirm which account, Worker, and environment will emit telemetry, then deploy that configuration and generate representative traffic. +- Decide log and trace sampling separately. Increasing sampling during an investigation changes volume and cost; restore the intended operational settings afterwards. +- For Tail Workers, configure the consumer relationship on the producer Worker; use the guide for deployment order and the handler contract. +- Choose whether to persist data in Cloudflare as well as exporting it. Verify destination names, supported signal types, and credentials using the export guides above. +- Use stable structured fields and redact secrets and unnecessary personal data before emission. Configure development and production collection intentionally. + +See [gotchas.md](gotchas.md) when configured telemetry is missing. diff --git a/.agents/skills/cloudflare/references/observability/gotchas.md b/.agents/skills/cloudflare/references/observability/gotchas.md new file mode 100644 index 0000000..bc8d879 --- /dev/null +++ b/.agents/skills/cloudflare/references/observability/gotchas.md @@ -0,0 +1,25 @@ +# Observability Troubleshooting and Constraints + +## Missing or incomplete data + +| Symptom | Check and authoritative guide | +| --- | --- | +| Logs missing from the dashboard | Confirm the deployed Worker/environment, collection and persistence settings, recent traffic, query time range, and sampling. Follow [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/index.md) and [Query Builder](https://developers.cloudflare.com/workers/observability/query-builder/index.md). | +| Live logs differ from stored logs | Confirm which workflow is being inspected; live streams can sample under load. See [real-time logs](https://developers.cloudflare.com/workers/observability/logs/real-time-logs/index.md). | +| Traces missing or incomplete | Check trace enablement and sampling separately from logs, then consult [tracing setup](https://developers.cloudflare.com/workers/observability/traces/index.md) and [known limitations](https://developers.cloudflare.com/workers/observability/traces/known-limitations/index.md). | +| Export destination has no data | Check signal type, destination name, credentials, endpoint compatibility, and provider status using [OpenTelemetry export](https://developers.cloudflare.com/observability/export/opentelemetry/index.md) and, for a Worker, [Workers OpenTelemetry export](https://developers.cloudflare.com/workers/observability/opentelemetry-export/index.md). For a Logpush job, use [Workers Logpush](https://developers.cloudflare.com/workers/observability/logs/logpush/index.md). | +| Tail consumer receives no events | Check the producer's consumer configuration and deployment using [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md). | +| Analytics Engine totals or averages look wrong | Account for sample weights and consistent field meanings using [sampling guidance](https://developers.cloudflare.com/analytics/analytics-engine/sampling/index.md). Check [limits](https://developers.cloudflare.com/analytics/analytics-engine/limits/index.md) for missing writes or expired data. | +| Very short operations appear to take no time | Read [performance and timers](https://developers.cloudflare.com/workers/runtime-apis/performance/index.md) and [trace limitations](https://developers.cloudflare.com/workers/observability/traces/known-limitations/index.md). Tracing does not eliminate the runtime's timing restrictions. | + +## Limits, retention, and cost + +Fetch these pages when estimating cost or diagnosing truncation and missing data: + +- [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/index.md): log size, retention, sampling, and pricing. +- [Workers Traces](https://developers.cloudflare.com/workers/observability/traces/index.md) and [known limitations](https://developers.cloudflare.com/workers/observability/traces/known-limitations/index.md): availability, propagation, and instrumentation constraints. +- [Workers pricing](https://developers.cloudflare.com/workers/platform/pricing/index.md): current observability and Tail Worker billing terms. +- [Analytics Engine limits](https://developers.cloudflare.com/analytics/analytics-engine/limits/index.md) and [pricing](https://developers.cloudflare.com/analytics/analytics-engine/pricing/index.md): field/write limits, retention, query costs, and billing availability. +- [Workers Logpush](https://developers.cloudflare.com/workers/observability/logs/logpush/index.md): Workers-specific eligibility, permissions, and pricing. + +Sampling reduces coverage as well as volume. Do not interpret the absence of a sampled event as proof that an error did not happen. Keep required diagnostic context while avoiding credentials, full sensitive URLs, and unnecessary personal data in logs, custom dimensions, and exported records. diff --git a/.agents/skills/cloudflare/references/observability/patterns.md b/.agents/skills/cloudflare/references/observability/patterns.md new file mode 100644 index 0000000..354978c --- /dev/null +++ b/.agents/skills/cloudflare/references/observability/patterns.md @@ -0,0 +1,14 @@ +# Observability Patterns + +Use these decisions alongside the linked implementation guides. Application event schemas, billing policies, alert thresholds, and delivery behavior still need to be designed for the application. + +| Task | Design decision and documentation | +| --- | --- | +| Usage-based billing | Define the billable event, tenant identity, time window, and accuracy requirements. Start with the maintained [Analytics Engine billing recipe](https://developers.cloudflare.com/analytics/analytics-engine/recipes/usage-based-billing-for-your-saas-product/index.md) and [sampling guidance](https://developers.cloudflare.com/analytics/analytics-engine/sampling/index.md); assess whether sampled estimates satisfy the billing contract. | +| Performance monitoring | Use [built-in metrics](https://developers.cloudflare.com/workers/observability/metrics-and-analytics/index.md) for aggregate health and [traces](https://developers.cloudflare.com/workers/observability/traces/index.md) to investigate dependency latency. Custom measurements need consistent units and aggregation semantics; check [runtime timers](https://developers.cloudflare.com/workers/runtime-apis/performance/index.md) before measuring CPU-only work. | +| Error tracking | Emit structured context without secrets, investigate with [Query Builder](https://developers.cloudflare.com/workers/observability/query-builder/index.md), and choose [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md) if custom alert processing is needed. Define thresholds and duplicate handling for your alert destination. | +| Multi-tenant tracking | Choose the tenant dimension and consistent field positions using [Analytics Engine get started](https://developers.cloudflare.com/analytics/analytics-engine/get-started/index.md) and [sampling guidance](https://developers.cloudflare.com/analytics/analytics-engine/sampling/index.md). Enforce tenant authorization in the application that exposes analytics; a dataset index is not an access-control boundary. | +| Tail Worker filtering | Use the current [Tail handler schema](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md) for outcomes, exceptions, and timing fields, with [Tail configuration](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md) for producer wiring. Define filtering, redaction, and downstream failure handling for the destination. | +| OpenTelemetry export | Prefer the maintained [OTLP export integration](https://developers.cloudflare.com/observability/export/opentelemetry/index.md) for supported destinations. For Honeycomb, follow [Export to Honeycomb](https://developers.cloudflare.com/observability/export/opentelemetry/honeycomb/index.md) instead of synthesizing spans from Tail events. | + +Use [Workers Logpush](https://developers.cloudflare.com/workers/observability/logs/logpush/index.md) when the requirement is Workers Trace Event delivery to a supported log destination. Use a Tail Worker when custom processing is required beyond the configured export integration. diff --git a/.agents/skills/cloudflare/references/pages-functions/README.md b/.agents/skills/cloudflare/references/pages-functions/README.md new file mode 100644 index 0000000..50bc928 --- /dev/null +++ b/.agents/skills/cloudflare/references/pages-functions/README.md @@ -0,0 +1,20 @@ +# Cloudflare Pages Functions + +Use this reference for server-side behavior in an existing Pages project. For new applications, follow the Workers recommendation in the [Pages framework guidance](https://developers.cloudflare.com/pages/framework-guides/index.md). + +| Task | Documentation | +| --- | --- | +| Identify filesystem routes and invocation boundaries | [Routing](https://developers.cloudflare.com/pages/functions/routing/index.md) | +| Implement request handlers | [API reference](https://developers.cloudflare.com/pages/functions/api-reference/index.md) | +| Understand generated Worker output | [Advanced mode](https://developers.cloudflare.com/pages/functions/advanced-mode/index.md) | + +Inspect whether the project uses a Functions directory or framework-generated advanced mode before selecting a routing approach. Fetch current documentation for signatures, supported bindings, configuration, and examples. + +## In This Reference + +- [api.md](./api.md) — handlers, context, middleware, and assets +- [configuration.md](./configuration.md) — bindings, environments, types, and local development +- [patterns.md](./patterns.md) — request ownership and shared logic +- [gotchas.md](./gotchas.md) — route, binding, and runtime investigation + +See [Pages](../pages/README.md) for builds and deployment decisions. diff --git a/.agents/skills/cloudflare/references/pages-functions/api.md b/.agents/skills/cloudflare/references/pages-functions/api.md new file mode 100644 index 0000000..2e05e99 --- /dev/null +++ b/.agents/skills/cloudflare/references/pages-functions/api.md @@ -0,0 +1,13 @@ +# Pages Functions APIs + +Fetch the API reference before writing a handler; keep runtime types and examples in their authoritative documentation. + +| Task | Documentation | +| --- | --- | +| Choose method handlers and access EventContext | [API reference](https://developers.cloudflare.com/pages/functions/api-reference/index.md) | +| Read parameters and resolve dynamic routes | [Routing](https://developers.cloudflare.com/pages/functions/routing/index.md) | +| Compose middleware and continue a request | [Middleware](https://developers.cloudflare.com/pages/functions/middleware/index.md) | +| Use a supported resource binding | [Bindings](https://developers.cloudflare.com/pages/functions/bindings/index.md) | +| Handle requests through generated Worker output | [Advanced mode](https://developers.cloudflare.com/pages/functions/advanced-mode/index.md) | + +Decide which handler owns the response, where shared state is established, and which paths should fall through to assets. The API reference also covers asynchronous work and asset fetching. See [configuration.md](./configuration.md) for binding setup and [patterns.md](./patterns.md) for request design. diff --git a/.agents/skills/cloudflare/references/pages-functions/configuration.md b/.agents/skills/cloudflare/references/pages-functions/configuration.md new file mode 100644 index 0000000..ec23c49 --- /dev/null +++ b/.agents/skills/cloudflare/references/pages-functions/configuration.md @@ -0,0 +1,14 @@ +# Pages Functions Configuration + +Read Pages-specific configuration before reusing settings from a Worker project. + +| Task | Documentation | +| --- | --- | +| Manage Wrangler settings and environment overrides | [Functions configuration](https://developers.cloudflare.com/pages/functions/wrangler-configuration/index.md) | +| Configure supported bindings, variables, and secrets | [Bindings](https://developers.cloudflare.com/pages/functions/bindings/index.md) | +| Generate and configure runtime and environment types | [TypeScript](https://developers.cloudflare.com/pages/functions/typescript/index.md) | +| Run assets and Functions locally | [Local development](https://developers.cloudflare.com/pages/functions/local-development/index.md) | +| Set Function invocation routes | [Routing](https://developers.cloudflare.com/pages/functions/routing/index.md) | +| Evaluate and enable placement | [Smart Placement](https://developers.cloudflare.com/pages/functions/smart-placement/index.md) | + +Identify the target deployment environment and which configuration source controls it. Verify Pages support for each binding and the documented local-development behavior before accessing remote resources. See [Pages configuration](../pages/configuration.md) for build output, headers, and redirects. diff --git a/.agents/skills/cloudflare/references/pages-functions/gotchas.md b/.agents/skills/cloudflare/references/pages-functions/gotchas.md new file mode 100644 index 0000000..e0ba806 --- /dev/null +++ b/.agents/skills/cloudflare/references/pages-functions/gotchas.md @@ -0,0 +1,17 @@ +# Pages Functions Troubleshooting + +Start with the request path, deployment environment, and generated output that actually handled the request. + +| Task | Documentation | +| --- | --- | +| A Function does not run or receives unexpected parameters | [Routing](https://developers.cloudflare.com/pages/functions/routing/index.md) | +| Middleware is skipped or static fallback fails | [Advanced mode](https://developers.cloudflare.com/pages/functions/advanced-mode/index.md) | +| Middleware order or scope is incorrect | [Middleware](https://developers.cloudflare.com/pages/functions/middleware/index.md) | +| Bindings or secrets differ between environments | [Bindings](https://developers.cloudflare.com/pages/functions/bindings/index.md) | +| Runtime or environment types do not match | [TypeScript](https://developers.cloudflare.com/pages/functions/typescript/index.md) | +| A local request behaves differently | [Local development](https://developers.cloudflare.com/pages/functions/local-development/index.md) | +| Inspect exceptions and deployment logs | [Debugging and logging](https://developers.cloudflare.com/pages/functions/debugging-and-logging/index.md) | +| Check runtime quotas | [Workers limits](https://developers.cloudflare.com/workers/platform/limits/index.md) | +| Check request costs | [Functions pricing](https://developers.cloudflare.com/pages/functions/pricing/index.md) | + +Reproduce a failing path through the actual application rather than only calling a handler with a hand-built context. Check the deployed configuration and generated output before changing application code. See [Pages troubleshooting](../pages/gotchas.md) for build, asset, and framework issues. diff --git a/.agents/skills/cloudflare/references/pages-functions/patterns.md b/.agents/skills/cloudflare/references/pages-functions/patterns.md new file mode 100644 index 0000000..8dc4d8f --- /dev/null +++ b/.agents/skills/cloudflare/references/pages-functions/patterns.md @@ -0,0 +1,16 @@ +# Pages Functions Request Design + +Choose where behavior belongs before adapting an example. + +| Task | Documentation | +| --- | --- | +| Share authentication, logging, and error handling | [Middleware](https://developers.cloudflare.com/pages/functions/middleware/index.md) | +| Choose request handlers and asynchronous completion behavior | [API reference](https://developers.cloudflare.com/pages/functions/api-reference/index.md) | +| Keep asset requests outside Function invocation where appropriate | [Routing](https://developers.cloudflare.com/pages/functions/routing/index.md) | +| Use custom or framework-generated routing | [Advanced mode](https://developers.cloudflare.com/pages/functions/advanced-mode/index.md) | +| Integrate storage or another service | [Bindings](https://developers.cloudflare.com/pages/functions/bindings/index.md) | +| Exercise the assembled application locally | [Local development](https://developers.cloudflare.com/pages/functions/local-development/index.md) | + +Define the response owner and middleware scope before adding authentication or response transformations. Test protected routes, rejected requests, and static fallbacks together. Choose consistency and concurrency requirements before using storage for session state or rate limiting; a generic read-modify-write example is not a complete policy. + +See [Pages project decisions](../pages/patterns.md) for framework and migration work. diff --git a/.agents/skills/cloudflare/references/pages/README.md b/.agents/skills/cloudflare/references/pages/README.md new file mode 100644 index 0000000..4bea4c7 --- /dev/null +++ b/.agents/skills/cloudflare/references/pages/README.md @@ -0,0 +1,20 @@ +# Cloudflare Pages + +Use this reference when maintaining an existing Pages project. For new applications, start with Workers as recommended in the [Pages framework guidance](https://developers.cloudflare.com/pages/framework-guides/index.md). Fetch current documentation before implementing. + +| Task | Documentation | +| --- | --- | +| Configure the existing build | [Build configuration](https://developers.cloudflare.com/pages/configuration/build-configuration/index.md) | +| Manage automatic deployments from a repository | [Git integration](https://developers.cloudflare.com/pages/configuration/git-integration/index.md) | +| Deploy prebuilt output | [Direct Upload](https://developers.cloudflare.com/pages/get-started/direct-upload/index.md) | +| Implement server-side requests | [Functions API reference](https://developers.cloudflare.com/pages/functions/api-reference/index.md) | +| Plan a move to Workers | [Migrate from Pages to Workers](https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/index.md) | + +## In This Reference + +- [configuration.md](./configuration.md) — build output, environments, and static rules +- [api.md](./api.md) — request handling and framework integration +- [patterns.md](./patterns.md) — project decisions and migration +- [gotchas.md](./gotchas.md) — build, routing, and deployment investigation + +See [Pages Functions](../pages-functions/README.md) for handler-focused navigation. Identify the existing deployment method and framework before proposing changes. diff --git a/.agents/skills/cloudflare/references/pages/api.md b/.agents/skills/cloudflare/references/pages/api.md new file mode 100644 index 0000000..3081ded --- /dev/null +++ b/.agents/skills/cloudflare/references/pages/api.md @@ -0,0 +1,13 @@ +# Pages Request Handling + +Use the current Pages API and the project framework guide rather than translating a generic Worker example into Pages. + +| Task | Documentation | +| --- | --- | +| Implement handlers and use context or asset fallback | [Functions API reference](https://developers.cloudflare.com/pages/functions/api-reference/index.md) | +| Resolve dynamic paths and invocation routes | [Routing](https://developers.cloudflare.com/pages/functions/routing/index.md) | +| Apply shared request logic | [Middleware](https://developers.cloudflare.com/pages/functions/middleware/index.md) | +| Understand framework-generated Worker output | [Advanced mode](https://developers.cloudflare.com/pages/functions/advanced-mode/index.md) | +| Find the guide for the existing framework | [Framework guides](https://developers.cloudflare.com/pages/framework-guides/index.md) | + +First identify whether routing comes from the Functions directory or generated advanced-mode output. See [Pages Functions APIs](../pages-functions/api.md) for focused handler tasks and [patterns.md](./patterns.md) for migration decisions. diff --git a/.agents/skills/cloudflare/references/pages/configuration.md b/.agents/skills/cloudflare/references/pages/configuration.md new file mode 100644 index 0000000..4092c29 --- /dev/null +++ b/.agents/skills/cloudflare/references/pages/configuration.md @@ -0,0 +1,16 @@ +# Pages Configuration + +Inspect the existing project configuration and build output before changing deployment settings. + +| Task | Documentation | +| --- | --- | +| Set build commands, root/output directories, and build variables | [Build configuration](https://developers.cloudflare.com/pages/configuration/build-configuration/index.md) | +| Manage Wrangler configuration, environments, and dashboard migration | [Functions configuration](https://developers.cloudflare.com/pages/functions/wrangler-configuration/index.md) | +| Configure resource bindings, variables, and secrets | [Bindings](https://developers.cloudflare.com/pages/functions/bindings/index.md) | +| Set static response headers | [Headers](https://developers.cloudflare.com/pages/configuration/headers/index.md) | +| Configure static redirects and rewrites | [Redirects](https://developers.cloudflare.com/pages/configuration/redirects/index.md) | +| Choose which requests invoke Functions | [Routing](https://developers.cloudflare.com/pages/functions/routing/index.md) | +| Configure monorepo project boundaries | [Monorepos](https://developers.cloudflare.com/pages/configuration/monorepos/index.md) | +| Run the project locally | [Local development](https://developers.cloudflare.com/pages/functions/local-development/index.md) | + +Distinguish build-time variables from runtime bindings, and check preview and production separately. Determine whether the framework owns generated routing files before editing them. See [Pages Functions configuration](../pages-functions/configuration.md) for types and placement. diff --git a/.agents/skills/cloudflare/references/pages/gotchas.md b/.agents/skills/cloudflare/references/pages/gotchas.md new file mode 100644 index 0000000..bbac096 --- /dev/null +++ b/.agents/skills/cloudflare/references/pages/gotchas.md @@ -0,0 +1,19 @@ +# Pages Troubleshooting + +Identify whether the failure occurs during the build, asset serving, or Function execution before changing configuration. + +| Task | Documentation | +| --- | --- | +| Build output is missing or incorrect | [Build configuration](https://developers.cloudflare.com/pages/configuration/build-configuration/index.md) | +| A static URL redirects or returns an unexpected 404 | [Serving Pages](https://developers.cloudflare.com/pages/configuration/serving-pages/index.md) | +| Static response headers are not applied | [Headers](https://developers.cloudflare.com/pages/configuration/headers/index.md) | +| A redirect rule does not match | [Redirects](https://developers.cloudflare.com/pages/configuration/redirects/index.md) | +| Investigate a failed Function request | [Debugging and logging](https://developers.cloudflare.com/pages/functions/debugging-and-logging/index.md) | +| Check deployment and file capacity | [Pages limits](https://developers.cloudflare.com/pages/platform/limits/index.md) | +| Understand Function versus static request billing | [Functions pricing](https://developers.cloudflare.com/pages/functions/pricing/index.md) | + +Compare the same route in local, preview, and production environments; record the build output and configuration used by each. See [Pages Functions troubleshooting](../pages-functions/gotchas.md) for handler and binding issues. + +## Framework-Specific + +Fetch the relevant [framework guide](https://developers.cloudflare.com/pages/framework-guides/index.md) before changing adapters or recommending another host. For a move to Workers, follow the [migration guide](https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/index.md). diff --git a/.agents/skills/cloudflare/references/pages/patterns.md b/.agents/skills/cloudflare/references/pages/patterns.md new file mode 100644 index 0000000..810fe88 --- /dev/null +++ b/.agents/skills/cloudflare/references/pages/patterns.md @@ -0,0 +1,15 @@ +# Pages Project Decisions + +Keep project-specific choices here; fetch framework adapters, configuration, and code examples from the docs. + +| Task | Documentation | +| --- | --- | +| Maintain a framework deployment | [Framework guides](https://developers.cloudflare.com/pages/framework-guides/index.md) | +| Deploy from an external build pipeline | [Direct Upload](https://developers.cloudflare.com/pages/get-started/direct-upload/index.md) | +| Manage multiple apps in one repository | [Monorepos](https://developers.cloudflare.com/pages/configuration/monorepos/index.md) | +| Evaluate backend locality | [Smart Placement](https://developers.cloudflare.com/pages/functions/smart-placement/index.md) | +| Plan a move to Workers | [Migrate from Pages to Workers](https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/index.md) | + +Check the existing build command, adapter, and output ownership together. Evaluate placement using the application’s backend dependencies and measured latency. For a migration, inventory routes, middleware, bindings, static rules, and deployment settings before following the migration guide. + +See [Pages Functions patterns](../pages-functions/patterns.md) for request-level decisions. diff --git a/.agents/skills/cloudflare/references/queues/README.md b/.agents/skills/cloudflare/references/queues/README.md new file mode 100644 index 0000000..fefa7bc --- /dev/null +++ b/.agents/skills/cloudflare/references/queues/README.md @@ -0,0 +1,24 @@ +# Cloudflare Queues + +Use Queues to decouple producers from asynchronous consumers and buffer bursts of work. Design consumers for duplicate delivery; use Workflows when the task needs durable multi-step orchestration. + +Fetch the relevant documentation below before implementing. Treat current Cloudflare docs as the source of truth for API signatures, acknowledgement semantics, configuration, limits, and pricing. + +## Choose a consumer + +- Use a Worker push consumer when processing runs on Workers. +- Use an HTTP pull consumer when processing runs in another environment; plan for polling, visibility timeouts, and acknowledgement leases. +- Choose a message encoding the consumer can decode. Check serialization and compatibility-date behavior before sending existing application objects. + +See [How Queues works](https://developers.cloudflare.com/queues/reference/how-queues-works/index.md) and [delivery guarantees](https://developers.cloudflare.com/queues/reference/delivery-guarantees/index.md) before choosing ordering or deduplication strategies. + +## Read by task + +| Task | Reference | +|------|-----------| +| Create queues, bind producers, and configure consumers | [configuration.md](./configuration.md) | +| Send messages and implement acknowledgement or retries | [api.md](./api.md) | +| Buffer APIs, defer jobs, or integrate with storage and orchestration | [patterns.md](./patterns.md) | +| Diagnose delivery failures, duplicates, or capacity issues | [gotchas.md](./gotchas.md) | + +For a first application, fetch [Getting started](https://developers.cloudflare.com/queues/get-started/index.md). Retrieve [limits](https://developers.cloudflare.com/queues/platform/limits/index.md) and [pricing](https://developers.cloudflare.com/queues/platform/pricing/index.md) before sizing throughput, retention, or cost; plan-specific values are not maintained here. diff --git a/.agents/skills/cloudflare/references/queues/api.md b/.agents/skills/cloudflare/references/queues/api.md new file mode 100644 index 0000000..9e6faa4 --- /dev/null +++ b/.agents/skills/cloudflare/references/queues/api.md @@ -0,0 +1,16 @@ +# Queues API Reference + +Fetch the current API documentation for the operation being implemented; do not infer signatures, payloads, or acknowledgement rules from old examples. + +| Task | Documentation | +|------|---------------| +| Send individual messages or batches; choose encoding; implement a typed Worker queue handler; dispatch by queue name | [JavaScript APIs](https://developers.cloudflare.com/queues/configuration/javascript-apis/index.md) | +| Understand automatic acknowledgement, explicit per-message and batch actions, precedence, delivery failures, delays, and backoff | [Batching, retries, and delays](https://developers.cloudflare.com/queues/configuration/batching-retries/index.md) | +| Pull over HTTP and acknowledge or retry using leases | [Pull consumers](https://developers.cloudflare.com/queues/configuration/pull-consumers/index.md) | +| Publish from outside Workers | [Publish to a Queue via HTTP](https://developers.cloudflare.com/queues/examples/publish-to-a-queue-via-http/index.md) | + +Acknowledge only after the intended work succeeds. For independently processed messages, use per-message outcomes to avoid replaying successful work when another message fails. If catching an error and continuing, explicitly request a retry for work that still needs processing; a successful handler return can acknowledge messages automatically. Fetch the linked acknowledgement rules before mixing message-level and batch-level actions. + +Await required work, including downstream writes or sends, before acknowledging it. Check the JavaScript API's handler lifecycle rules before using `waitUntil()`; background work is not independent of delivery success. + +See [configuration.md](./configuration.md) for bindings and consumer setup, and [gotchas.md](./gotchas.md) for delivery diagnostics. diff --git a/.agents/skills/cloudflare/references/queues/configuration.md b/.agents/skills/cloudflare/references/queues/configuration.md new file mode 100644 index 0000000..c08779b --- /dev/null +++ b/.agents/skills/cloudflare/references/queues/configuration.md @@ -0,0 +1,20 @@ +# Queues Configuration + +Fetch the relevant guide before writing configuration or running CLI commands. Check the project's Wrangler version and compatibility date when adapting examples. + +| Task | Documentation | +|------|---------------| +| Create a queue and connect producer and consumer Workers | [Getting started](https://developers.cloudflare.com/queues/get-started/index.md) | +| Configure producer bindings, Worker consumers, retention, and concurrency settings | [Configure Queues](https://developers.cloudflare.com/queues/configuration/configure-queues/index.md) | +| Configure an external HTTP consumer and its visibility timeout | [Pull consumers](https://developers.cloudflare.com/queues/configuration/pull-consumers/index.md) | +| Choose batching, retry policy, or delivery delays | [Batching, retries, and delays](https://developers.cloudflare.com/queues/configuration/batching-retries/index.md) | +| Preserve messages that exhaust retries | [Dead Letter Queues](https://developers.cloudflare.com/queues/configuration/dead-letter-queues/index.md) | +| Set consumer scaling for downstream capacity | [Consumer concurrency](https://developers.cloudflare.com/queues/configuration/consumer-concurrency/index.md) | +| Choose content types and type Worker messages | [JavaScript APIs](https://developers.cloudflare.com/queues/configuration/javascript-apis/index.md) | +| Create, update, attach, remove, or delete queues and consumers | [Wrangler commands](https://developers.cloudflare.com/queues/reference/wrangler-commands/index.md) | +| Pause delivery, resume it, or purge messages | [Pause and purge](https://developers.cloudflare.com/queues/configuration/pause-purge/index.md) | +| Develop and test producers and consumers locally | [Local development](https://developers.cloudflare.com/queues/configuration/local-development/index.md) | + +Choose push or pull based on where processing runs, then select an encoding supported by that consumer. Tune batching for acceptable latency and downstream write capacity. Decide how failed messages will be inspected and replayed before configuring a dead-letter queue. + +Fetch [limits](https://developers.cloudflare.com/queues/platform/limits/index.md) and [pricing](https://developers.cloudflare.com/queues/platform/pricing/index.md) for the account's plan before selecting retention, delays, or capacity. Do not reuse numeric settings from unrelated examples. diff --git a/.agents/skills/cloudflare/references/queues/gotchas.md b/.agents/skills/cloudflare/references/queues/gotchas.md new file mode 100644 index 0000000..16187d0 --- /dev/null +++ b/.agents/skills/cloudflare/references/queues/gotchas.md @@ -0,0 +1,18 @@ +# Queues Gotchas & Troubleshooting + +Fetch the linked documentation before changing retry policy or interpreting delivery behavior. + +| Symptom or question | Documentation and decision | +|---------------------|----------------------------| +| Successful work repeats after another message fails | Read [acknowledgement and retry rules](https://developers.cloudflare.com/queues/configuration/batching-retries/index.md); use per-message outcomes for independent work. | +| A caught failure disappears instead of retrying | Read [handler lifecycle and APIs](https://developers.cloudflare.com/queues/configuration/javascript-apis/index.md); returning successfully can acknowledge messages. Explicitly retry failed work when continuing. | +| Duplicate processing | Read [delivery guarantees](https://developers.cloudflare.com/queues/reference/delivery-guarantees/index.md); enforce idempotency at the side-effect destination. | +| Pull consumers cannot decode payloads | Check [pull consumer encoding](https://developers.cloudflare.com/queues/configuration/pull-consumers/index.md) and [content types](https://developers.cloudflare.com/queues/configuration/javascript-apis/index.md) against the producer. | +| Messages stop arriving or backlog grows | Check [consumer configuration](https://developers.cloudflare.com/queues/configuration/configure-queues/index.md), [pause state](https://developers.cloudflare.com/queues/configuration/pause-purge/index.md), and [queue metrics](https://developers.cloudflare.com/queues/observability/metrics/index.md). | +| Dead-letter volume rises or messages disappear after retries | Read [Dead Letter Queues](https://developers.cloudflare.com/queues/configuration/dead-letter-queues/index.md); inspect failures and plan recovery before increasing retries. | +| API errors, resource exhaustion, or CPU failures | Read [error codes](https://developers.cloudflare.com/queues/reference/error-codes/index.md), [limits](https://developers.cloudflare.com/queues/platform/limits/index.md), and [consumer concurrency](https://developers.cloudflare.com/queues/configuration/consumer-concurrency/index.md). | +| Retention, delay, throughput, or cost assumptions no longer hold | Retrieve current [limits](https://developers.cloudflare.com/queues/platform/limits/index.md) and [pricing](https://developers.cloudflare.com/queues/platform/pricing/index.md) for the account's plan. | + +Distinguish transient dependency failures from invalid payloads before choosing retry or recovery behavior. Acknowledging a failed message does not send it to a dead-letter queue. If handling a permanent failure separately, persist the intended recovery record successfully before acknowledging; use the documented dead-letter policy when relying on retry exhaustion. + +See [patterns.md](./patterns.md) for idempotency and downstream integration decisions. diff --git a/.agents/skills/cloudflare/references/queues/patterns.md b/.agents/skills/cloudflare/references/queues/patterns.md new file mode 100644 index 0000000..d199582 --- /dev/null +++ b/.agents/skills/cloudflare/references/queues/patterns.md @@ -0,0 +1,21 @@ +# Queues Patterns & Best Practices + +Fetch the guide matching the task and adapt its example to the application's delivery and failure requirements. + +| Task | Documentation | +|------|---------------| +| Accept requests and enqueue asynchronous tasks; publish to multiple queues | [Publish to a Queue via Workers](https://developers.cloudflare.com/queues/examples/publish-to-a-queue-via-workers/index.md) | +| Buffer writes to an external API or defer a job | [Batching, retries, and delays](https://developers.cloudflare.com/queues/configuration/batching-retries/index.md) | +| Handle upstream rate limits and backpressure | [Handle rate limits of external APIs](https://developers.cloudflare.com/queues/tutorials/handle-rate-limits/index.md) and [consumer concurrency](https://developers.cloudflare.com/queues/configuration/consumer-concurrency/index.md) | +| Isolate workloads with different latency or capacity needs | [Configure Queues](https://developers.cloudflare.com/queues/configuration/configure-queues/index.md) | +| Retain exhausted retries for inspection and recovery | [Dead Letter Queues](https://developers.cloudflare.com/queues/configuration/dead-letter-queues/index.md) | +| Process R2 object events | [R2 event notifications](https://developers.cloudflare.com/r2/buckets/event-notifications/index.md) | +| Batch output into R2 | [Use Queues to store data in R2](https://developers.cloudflare.com/queues/examples/send-errors-to-r2/index.md) | +| Batch writes into D1 | [D1 database API](https://developers.cloudflare.com/d1/worker-api/d1-database/index.md) | +| Start durable multi-step jobs | [Trigger Workflows](https://developers.cloudflare.com/workflows/build/trigger-workflows/index.md) | +| Publish from a Durable Object | [Use Queues from Durable Objects](https://developers.cloudflare.com/queues/examples/use-queues-with-durable-objects/index.md) | +| Route consumer work to a Durable Object | [Invoke Durable Object methods](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/index.md) | + +Design side effects for [at-least-once delivery](https://developers.cloudflare.com/queues/reference/delivery-guarantees/index.md). A separate check-then-write deduplication flag is not an atomic guarantee: concurrent delivery or a crash between the side effect and recording completion can repeat work. Prefer idempotency keys or transactional enforcement at the destination. + +Acknowledge after the destination confirms success. For fan-out, plan for some sends succeeding before another fails; retries must not duplicate downstream effects. Separate queues can isolate workloads, but do not imply a global priority or ordering guarantee. diff --git a/.agents/skills/cloudflare/references/r2/README.md b/.agents/skills/cloudflare/references/r2/README.md new file mode 100644 index 0000000..8556511 --- /dev/null +++ b/.agents/skills/cloudflare/references/r2/README.md @@ -0,0 +1,26 @@ +# Cloudflare R2 Object Storage + +Use R2 for objects such as uploads, media, backups, and static assets. Fetch the linked documentation before implementing; API signatures, configuration, limits, and pricing belong in the current docs. + +## Choose an access path + +- Use a Workers binding for object access inside a Worker: [Workers API setup](https://developers.cloudflare.com/r2/get-started/workers-api/index.md). +- Use the S3-compatible API for existing S3 clients or direct client access through presigned URLs: [S3 setup](https://developers.cloudflare.com/r2/get-started/s3/index.md). Check supported operations rather than assuming full S3 parity. +- Decide whether objects need application authorization, temporary access, or public delivery before exposing the bucket. See [patterns.md](./patterns.md). + +## Find the task + +| Task | Reference | +|------|-----------| +| Bindings, credentials, local development, bucket settings | [configuration.md](./configuration.md) | +| Object operations, metadata, conditions, multipart, CLI | [api.md](./api.md) | +| Uploads, streaming, caching, public delivery, event processing | [patterns.md](./patterns.md) | +| Pagination, conditional responses, failed uploads, limits | [gotchas.md](./gotchas.md) | + +For other topics, discover pages through the [R2 documentation index](https://developers.cloudflare.com/r2/llms.txt). Check [pricing](https://developers.cloudflare.com/r2/pricing/index.md) before estimating costs. + +## See also + +- [Workers](https://developers.cloudflare.com/workers/index.md) for request handling. +- [KV](../kv/) or [D1](../d1/) for application metadata associated with objects. +- [Queues](../queues/) for asynchronous processing of object events. diff --git a/.agents/skills/cloudflare/references/r2/api.md b/.agents/skills/cloudflare/references/r2/api.md new file mode 100644 index 0000000..63e4a5d --- /dev/null +++ b/.agents/skills/cloudflare/references/r2/api.md @@ -0,0 +1,16 @@ +# R2 API Reference + +Fetch the relevant page before writing code. Use the Workers API for bucket bindings and the S3 API for S3 clients; their types and semantics differ. + +| Task | Current documentation | +|------|-----------------------| +| Read, write, inspect, delete, or list objects; metadata, checksums, ranges, and return types | [Workers API reference](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/index.md) | +| Implement a Worker that serves or writes objects | [Use R2 from Workers](https://developers.cloudflare.com/r2/api/workers/workers-api-usage/index.md) | +| Create, resume, complete, or abort multipart uploads | [Multipart Worker and client example](https://developers.cloudflare.com/r2/api/workers/workers-multipart-usage/index.md) | +| Check supported S3 operations and headers | [S3 compatibility](https://developers.cloudflare.com/r2/api/s3/api/index.md) | +| Configure an S3 JavaScript client | [AWS SDK for JavaScript v3](https://developers.cloudflare.com/r2/examples/aws/aws-sdk-js-v3/index.md) | +| Sign temporary upload or download access | [Presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/index.md) | +| Encrypt with customer-provided keys | [SSE-C usage](https://developers.cloudflare.com/r2/examples/ssec/index.md) | +| Manage buckets and objects from the command line | [Wrangler R2 commands](https://developers.cloudflare.com/r2/reference/wrangler-commands/index.md) | + +Use generated project types rather than maintaining local copies of R2 interfaces; see [Workers TypeScript guidance](https://developers.cloudflare.com/workers/languages/typescript/index.md). For pagination and conditional response handling, read [gotchas.md](./gotchas.md) alongside the API reference. diff --git a/.agents/skills/cloudflare/references/r2/configuration.md b/.agents/skills/cloudflare/references/r2/configuration.md new file mode 100644 index 0000000..d64079f --- /dev/null +++ b/.agents/skills/cloudflare/references/r2/configuration.md @@ -0,0 +1,19 @@ +# R2 Configuration + +Fetch the task's documentation before editing Wrangler configuration or bucket settings. + +| Task | Current documentation | +|------|-----------------------| +| Create a bucket and bind it to a Worker | [Workers API setup](https://developers.cloudflare.com/r2/get-started/workers-api/index.md) | +| Choose local simulation or a remote bucket during development | [Supported bindings per development mode](https://developers.cloudflare.com/workers/local-development/bindings-per-env/index.md) and [local development](https://developers.cloudflare.com/workers/local-development/index.md) | +| Create S3 credentials and scope permissions | [R2 authentication](https://developers.cloudflare.com/r2/api/tokens/index.md) | +| Set the S3 endpoint and SDK region | [AWS SDK for JavaScript v3](https://developers.cloudflare.com/r2/examples/aws/aws-sdk-js-v3/index.md) | +| Choose placement hints or a jurisdiction | [Data location](https://developers.cloudflare.com/r2/reference/data-location/index.md) | +| Configure browser origins, methods, and headers | [CORS](https://developers.cloudflare.com/r2/buckets/cors/index.md) | +| Set expiration, storage transitions, or incomplete-upload cleanup | [Object lifecycles](https://developers.cloudflare.com/r2/buckets/object-lifecycles/index.md) | +| Choose or change storage classes | [Storage classes](https://developers.cloudflare.com/r2/buckets/storage-classes/index.md) and [pricing](https://developers.cloudflare.com/r2/pricing/index.md) | +| Send object events to a queue | [Event notifications](https://developers.cloudflare.com/r2/buckets/event-notifications/index.md) | +| Configure public access or a custom domain | [Public buckets](https://developers.cloudflare.com/r2/buckets/public-buckets/index.md) | +| Manage bucket settings with Wrangler | [R2 commands](https://developers.cloudflare.com/r2/reference/wrangler-commands/index.md) | + +Choose the development bucket deliberately: a remote binding accesses real data. Scope S3 credentials to the required buckets and operations; Workers bindings use their own access mechanism. Review lifecycle prefixes and retention needs before applying deletion rules, and evaluate retrieval and minimum-storage charges before choosing a storage class. diff --git a/.agents/skills/cloudflare/references/r2/gotchas.md b/.agents/skills/cloudflare/references/r2/gotchas.md new file mode 100644 index 0000000..4a9a581 --- /dev/null +++ b/.agents/skills/cloudflare/references/r2/gotchas.md @@ -0,0 +1,17 @@ +# R2 Gotchas & Troubleshooting + +Use the current references to diagnose the actual response or error instead of copying a workaround. + +| Symptom or decision | What to check | +|---------------------|---------------| +| Listing stops early or escapes the intended prefix | Follow `truncated` and the returned cursor, retaining the original prefix, delimiter, and metadata options on subsequent requests. See the listing section of the [Workers API reference](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/index.md). | +| Conditional read has no body, or conditional write returns null | Distinguish a missing object from a failed condition; choose the HTTP response for the actual request condition. See conditional operations in the [Workers API reference](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/index.md). | +| ETag, metadata, checksum, or stream upload behaves unexpectedly | Check supported values and return types in the [Workers API reference](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/index.md), the [Worker upload example](https://developers.cloudflare.com/r2/api/workers/workers-api-usage/index.md), and [Workers streams](https://developers.cloudflare.com/workers/runtime-apis/streams/index.md). | +| Multipart upload fails or cannot be resumed | Check part constraints and handle an upload that has already completed or aborted: [multipart guide](https://developers.cloudflare.com/r2/api/workers/workers-multipart-usage/index.md) and [API reference](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/index.md). | +| S3 authentication or signed URL fails | Verify credentials, endpoint, region, operation, signed headers, and expiry using [SDK setup](https://developers.cloudflare.com/r2/examples/aws/aws-sdk-js-v3/index.md), [authentication](https://developers.cloudflare.com/r2/api/tokens/index.md), and [presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/index.md). | +| Browser fails but an HTTP client succeeds | Check [CORS](https://developers.cloudflare.com/r2/buckets/cors/index.md) and [troubleshooting](https://developers.cloudflare.com/r2/platform/troubleshooting/index.md). | +| Local and deployed data or behavior differ | Check [local development](https://developers.cloudflare.com/workers/local-development/index.md), [supported bindings](https://developers.cloudflare.com/workers/local-development/bindings-per-env/index.md), and the local persistence options in [Wrangler R2 commands](https://developers.cloudflare.com/r2/reference/wrangler-commands/index.md). | +| Reads serve old or missing content after an update | Check the [consistency model and cache interactions](https://developers.cloudflare.com/r2/reference/consistency/index.md). | +| Upload size, metadata size, storage cost, or lifecycle behavior is unexpected | Fetch [limits](https://developers.cloudflare.com/r2/platform/limits/index.md), [pricing](https://developers.cloudflare.com/r2/pricing/index.md), [storage classes](https://developers.cloudflare.com/r2/buckets/storage-classes/index.md), and [object lifecycles](https://developers.cloudflare.com/r2/buckets/object-lifecycles/index.md). | + +For other failures, start with [R2 troubleshooting](https://developers.cloudflare.com/r2/platform/troubleshooting/index.md) and [error codes](https://developers.cloudflare.com/r2/api/error-codes/index.md). diff --git a/.agents/skills/cloudflare/references/r2/patterns.md b/.agents/skills/cloudflare/references/r2/patterns.md new file mode 100644 index 0000000..ebbded6 --- /dev/null +++ b/.agents/skills/cloudflare/references/r2/patterns.md @@ -0,0 +1,18 @@ +# R2 Patterns & Best Practices + +Choose the access and delivery model, then fetch the implementation guide. + +| Task | Current documentation | +|------|-----------------------| +| Stream object downloads or accept uploads through a Worker | [Use R2 from Workers](https://developers.cloudflare.com/r2/api/workers/workers-api-usage/index.md) | +| Add conditional reads/writes, range handling, checksums, or batch deletion | [Workers API reference](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/index.md) | +| Upload large files with multipart state tracked by the client | [Multipart Worker and client example](https://developers.cloudflare.com/r2/api/workers/workers-multipart-usage/index.md) | +| Upload directly from a browser or share a temporary download | [Presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/index.md) and [CORS](https://developers.cloudflare.com/r2/buckets/cors/index.md) | +| Cache responses served by a Worker | [Cache API example](https://developers.cloudflare.com/r2/examples/cache-api/index.md) | +| Deliver public objects through a custom domain or evaluate r2.dev | [Public buckets](https://developers.cloudflare.com/r2/buckets/public-buckets/index.md) | +| Process object changes asynchronously | [Event notifications](https://developers.cloudflare.com/r2/buckets/event-notifications/index.md) and [Queues](../queues/) | +| Expire objects or transition storage classes | [Object lifecycles](https://developers.cloudflare.com/r2/buckets/object-lifecycles/index.md) and [storage classes](https://developers.cloudflare.com/r2/buckets/storage-classes/index.md) | + +Authorize the caller for the selected object key and operation before exposing a Worker endpoint or issuing a presigned URL. A key-format check alone does not establish access rights. Set the intended expiry and signed request constraints for temporary access; configure browser CORS separately. + +Keep private responses out of shared public caches. Choose cache keys and invalidation around the application's access model, and check [R2 consistency and caching behavior](https://developers.cloudflare.com/r2/reference/consistency/index.md) when objects can change. For multipart uploads, plan for failed parts, completion, and cleanup using the linked guide. diff --git a/.agents/skills/cloudflare/references/secrets-store/README.md b/.agents/skills/cloudflare/references/secrets-store/README.md new file mode 100644 index 0000000..9d463c1 --- /dev/null +++ b/.agents/skills/cloudflare/references/secrets-store/README.md @@ -0,0 +1,22 @@ +# Cloudflare Secrets Store + +Use Secrets Store for account-level credentials shared across Workers or supported integrations. Use [Worker secrets](https://developers.cloudflare.com/workers/configuration/secrets/index.md) when credentials belong to one Worker and do not need centralized sharing. + +Fetch the relevant documentation before implementing. Current Cloudflare docs are the source of truth for binding APIs, management commands, permissions, availability, and quotas. Use the [Secrets Store documentation index](https://developers.cloudflare.com/secrets-store/llms.txt) to discover additional guidance. + +## Choose the scope + +- Share a secret only among services that should use the same credential and rotate together. +- Separate development, staging, and production credentials; select the intended account and environment before managing or binding a secret. +- Grant only the management permissions and consuming-service scopes needed. Permission to view metadata does not imply permission to bind or retrieve a value; fetch [access control](https://developers.cloudflare.com/secrets-store/access-control/index.md) for the current rules. + +## Read by task + +| Task | Reference | +|------|-----------| +| Create secrets, configure bindings, or prepare local development | [configuration.md](./configuration.md) | +| Read a secret in a Worker or automate management | [api.md](./api.md) | +| Plan rotation, migration, encryption, or auditing | [patterns.md](./patterns.md) | +| Diagnose access, deployment, or quota failures | [gotchas.md](./gotchas.md) | + +Fetch the [product overview](https://developers.cloudflare.com/secrets-store/index.md) for current availability and supported integrations. For AI Gateway provider credentials, use [Bring your own keys](https://developers.cloudflare.com/ai-gateway/configuration/bring-your-own-keys/index.md). diff --git a/.agents/skills/cloudflare/references/secrets-store/api.md b/.agents/skills/cloudflare/references/secrets-store/api.md new file mode 100644 index 0000000..3905bfa --- /dev/null +++ b/.agents/skills/cloudflare/references/secrets-store/api.md @@ -0,0 +1,21 @@ +# Secrets Store APIs + +Fetch the current API documentation before implementing calls or copying types. + +| Task | Documentation | +|------|---------------| +| Read a bound account secret asynchronously in a Worker | [Workers integration: access the secret](https://developers.cloudflare.com/secrets-store/integrations/workers/index.md#3-access-the-secret-on-the-env-object) | +| Generate binding and runtime types for the Worker configuration | [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/index.md) | +| Manage stores and secrets, inspect metadata, or query quota | [Secrets Store REST API](https://developers.cloudflare.com/api/resources/secrets_store/index.md) | +| Choose authorization and consuming-service scope | [Access control](https://developers.cloudflare.com/secrets-store/access-control/index.md) | +| Manage secrets through the CLI instead of REST | [Wrangler Secrets Store commands](https://developers.cloudflare.com/workers/wrangler/commands/secrets-store/index.md) | + +## Runtime decisions + +Account-secret bindings require asynchronous retrieval; they are not the direct string values exposed by [Worker secrets](https://developers.cloudflare.com/workers/configuration/secrets/index.md). Account management APIs and metadata reads are separate from consuming a bound secret in a Worker. + +Handle retrieval failures at the application's error boundary without exposing credentials. Reuse a retrieved value within the request when useful; avoid long-lived application caches that could keep revoked credentials in use. Validate structured secret values against the application's schema before using them. + +Never return a credential to a client or include it in logs, error messages, or telemetry. Record only the non-sensitive context needed to diagnose a failure. + +See [configuration.md](./configuration.md) for setup and [gotchas.md](./gotchas.md) for access failures. diff --git a/.agents/skills/cloudflare/references/secrets-store/configuration.md b/.agents/skills/cloudflare/references/secrets-store/configuration.md new file mode 100644 index 0000000..dd4405e --- /dev/null +++ b/.agents/skills/cloudflare/references/secrets-store/configuration.md @@ -0,0 +1,25 @@ +# Secrets Store configuration + +Fetch the guide for the operation you are performing before writing configuration or running management commands. + +## Setup and management + +| Task | Documentation | +|------|---------------| +| Create a store and secret, then bind it through Wrangler or the dashboard | [Workers integration](https://developers.cloudflare.com/secrets-store/integrations/workers/index.md) | +| Create, edit, duplicate, or delete account secrets | [Manage secrets](https://developers.cloudflare.com/secrets-store/manage-secrets/how-to/index.md) | +| Look up current store/secret command syntax and local versus remote flags | [Wrangler Secrets Store commands](https://developers.cloudflare.com/workers/wrangler/commands/secrets-store/index.md) | +| Configure bindings for each deployment environment | [Wrangler environments](https://developers.cloudflare.com/workers/wrangler/environments/index.md) | +| Choose user roles, CI token permissions, and secret scopes | [Secrets Store access control](https://developers.cloudflare.com/secrets-store/access-control/index.md) | + +Treat the store ID, secret ID, secret name, and Worker binding name as different identifiers. Use the identifier required by the documented operation; do not infer update or delete flags from the create command. + +## Local development and deployment + +Secrets Store management commands default to local state; production operations use the documented remote option. Local development needs separately provisioned local secrets. Follow the local-development notes in [Workers integration](https://developers.cloudflare.com/secrets-store/integrations/workers/index.md) and the [command reference](https://developers.cloudflare.com/workers/wrangler/commands/secrets-store/index.md). + +Check the selected account, deployment environment, secret scope, and binding configuration before deploying. In CI, distinguish permission to read metadata from permission to attach a secret to a Worker; use the [CI/CD access-control guidance](https://developers.cloudflare.com/secrets-store/access-control/index.md#api-token-permissions). + +Use protected secret input rather than putting credential values in command arguments, source files, or CI logs. For interactive CLI use, follow the command reference's secret-value prompt guidance. Keep local credentials out of version control. + +See [api.md](./api.md) for runtime access and [patterns.md](./patterns.md) before replacing a shared credential. diff --git a/.agents/skills/cloudflare/references/secrets-store/gotchas.md b/.agents/skills/cloudflare/references/secrets-store/gotchas.md new file mode 100644 index 0000000..e6fbdeb --- /dev/null +++ b/.agents/skills/cloudflare/references/secrets-store/gotchas.md @@ -0,0 +1,19 @@ +# Secrets Store troubleshooting + +Start with the failing operation and fetch its documentation before changing credentials or bindings. + +| Symptom or decision | What to check | Documentation | +|---------------------|---------------|---------------| +| Deployment cannot attach a secret | Selected account, caller's binding permission, and the secret's consuming-service scope; metadata read permission alone is insufficient | [Access control](https://developers.cloudflare.com/secrets-store/access-control/index.md) | +| Secret is missing or the wrong value is used | Store, secret name, binding name, and selected deployment environment | [Workers integration](https://developers.cloudflare.com/secrets-store/integrations/workers/index.md), [Wrangler environments](https://developers.cloudflare.com/workers/wrangler/environments/index.md) | +| Secret works in production but fails locally | Local secret provisioning and the management command's local/remote target | [Workers integration](https://developers.cloudflare.com/secrets-store/integrations/workers/index.md), [Wrangler commands](https://developers.cloudflare.com/workers/wrangler/commands/secrets-store/index.md) | +| CLI update, retrieval, or deletion fails | The operation's required identifier and flags; a secret name is not interchangeable with its ID | [Wrangler commands](https://developers.cloudflare.com/workers/wrangler/commands/secrets-store/index.md) | +| Binding is treated as a string or has incorrect types | Asynchronous account-secret access and generated configuration types | [Runtime access](https://developers.cloudflare.com/secrets-store/integrations/workers/index.md#3-access-the-secret-on-the-env-object), [TypeScript](https://developers.cloudflare.com/workers/languages/typescript/index.md) | +| Quota or value-size validation fails | Current account quota and the requested operation's schema | [Secrets Store REST API](https://developers.cloudflare.com/api/resources/secrets_store/index.md) | +| Regional or integration support is unclear | Current product availability and supported consumers | [Product overview](https://developers.cloudflare.com/secrets-store/index.md) | + +Do not diagnose failures by printing secret values or returning raw errors to clients. Check metadata and sanitized operation context. Validate JSON or other structured values before consuming them, and handle retrieval or parsing failures at the application's error boundary. + +Before deleting a secret to fix a binding conflict or quota problem, identify all consumers. Follow [secret management](https://developers.cloudflare.com/secrets-store/manage-secrets/how-to/index.md) and the rotation decisions in [patterns.md](./patterns.md); removing a shared credential can affect multiple services. + +See [configuration.md](./configuration.md) for setup and [api.md](./api.md) for API selection. diff --git a/.agents/skills/cloudflare/references/secrets-store/patterns.md b/.agents/skills/cloudflare/references/secrets-store/patterns.md new file mode 100644 index 0000000..538f4e9 --- /dev/null +++ b/.agents/skills/cloudflare/references/secrets-store/patterns.md @@ -0,0 +1,27 @@ +# Secrets Store patterns + +Use these decisions to choose the relevant guides; fetch the linked documentation before implementing. + +## Rotation and sharing + +Fetch [edit, duplicate, and delete operations](https://developers.cloudflare.com/secrets-store/manage-secrets/how-to/index.md) and [Workers binding setup](https://developers.cloudflare.com/secrets-store/integrations/workers/index.md) before changing a credential. Editing a shared secret affects every service using it. Inventory consumers and coordinate the change with the credential's issuer. + +An application rotation plan must account for old and new credential validity, consumer rollout, verification, rollback, and eventual revocation. The management guide describes secret operations, not an end-to-end zero-downtime rotation protocol. Do not retry arbitrary failed requests with an old key: retries must respect upstream authentication semantics and the operation's idempotency. + +## Migrate from Worker secrets + +Read [Worker secrets](https://developers.cloudflare.com/workers/configuration/secrets/index.md) alongside [Secrets Store Workers integration](https://developers.cloudflare.com/secrets-store/integrations/workers/index.md). Migration changes both the binding configuration and access from a direct value to asynchronous retrieval. Verify the new binding in staging, resolve naming conflicts during rollout, and remove the old secret only after consumers have switched successfully. + +## Encryption and signing + +For cryptographic operations, fetch [Workers Web Crypto](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/index.md) and the [request-signing example](https://developers.cloudflare.com/workers/examples/signing-requests/index.md). Use the [Secrets Store integration](https://developers.cloudflare.com/secrets-store/integrations/workers/index.md) to retrieve key material. + +These pages cover the runtime primitives; they do not define an application's encrypted KV format, key lifecycle, or signing protocol. Choose those explicitly, including key encoding, nonce handling, verification, and rotation of data encrypted under old keys. Validate JSON secrets at runtime rather than relying on a TypeScript assertion. + +## Audit and integrations + +Fetch [Secrets Store audit logs](https://developers.cloudflare.com/secrets-store/audit-logs/index.md) for the recorded actions and how to inspect them. Keep application telemetry free of credential values; do not assume account audit events replace application-level success and failure monitoring. + +For AI Gateway credentials, use [Bring your own keys](https://developers.cloudflare.com/ai-gateway/configuration/bring-your-own-keys/index.md). For an internal authentication service, consult [Worker service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/index.md) and define the application's authorization boundary before exposing signing or secret-backed operations. + +See [configuration.md](./configuration.md) for permissions and environments, and [gotchas.md](./gotchas.md) for troubleshooting. diff --git a/.agents/skills/cloudflare/references/smart-placement/README.md b/.agents/skills/cloudflare/references/smart-placement/README.md new file mode 100644 index 0000000..d213a87 --- /dev/null +++ b/.agents/skills/cloudflare/references/smart-placement/README.md @@ -0,0 +1,138 @@ +# Cloudflare Workers Smart Placement + +Automatic workload placement optimization to minimize latency by running Workers closer to backend infrastructure rather than end users. + +## Core Concept + +Smart Placement automatically analyzes Worker request duration across Cloudflare's global network and intelligently routes requests to optimal data center locations. Instead of defaulting to the location closest to the end user, Smart Placement can forward requests to locations closer to backend infrastructure when this reduces overall request duration. + +### When to Use + +**Enable Smart Placement when:** +- Worker makes multiple round trips to backend services/databases +- Backend infrastructure is geographically concentrated +- Request duration dominated by backend latency rather than network latency from user +- Running backend logic in Workers (APIs, data aggregation, SSR with DB calls) +- Worker uses `fetch` handler (not RPC methods) + +**Do NOT enable for:** +- Workers serving only static content or cached responses +- Workers without significant backend communication +- Pure edge logic (auth checks, redirects, simple transformations) +- Workers without fetch event handlers +- Workers with RPC methods or named entrypoints (only `fetch` handlers are affected) +- Pages/Assets Workers with `run_worker_first = true` (degrades asset serving) + +### Decision Tree + +``` +Does your Worker have a fetch handler? +├─ No → Smart Placement won't work (skip) +└─ Yes + │ + Does it make multiple backend calls (DB/API)? + ├─ No → Don't enable (won't help) + └─ Yes + │ + Is backend geographically concentrated? + ├─ No (globally distributed) → Probably won't help + └─ Yes or uncertain + │ + Does it serve static assets with run_worker_first=true? + ├─ Yes → Don't enable (will hurt performance) + └─ No → Enable Smart Placement + │ + After 15min, check placement_status + ├─ SUCCESS → Monitor metrics + ├─ INSUFFICIENT_INVOCATIONS → Need more traffic + └─ UNSUPPORTED_APPLICATION → Disable (hurting performance) +``` + +### Key Architecture Pattern + +**Recommended:** Split full-stack applications into separate Workers: +``` +User → Frontend Worker (at edge, close to user) + ↓ Service Binding + Backend Worker (Smart Placement enabled, close to DB/API) + ↓ + Database/Backend Service +``` + +This maintains fast, reactive frontends while optimizing backend latency. + +## Quick Start + +```jsonc +// wrangler.jsonc +{ + "placement": { + "mode": "smart" // or "off" to explicitly disable + } +} +``` + +Deploy and wait 15 minutes for analysis. Check status via API or dashboard metrics. + +**To disable:** Set `"mode": "off"` or remove `placement` field entirely (both equivalent). + +## Requirements + +- Wrangler 2.20.0+ +- Analysis time: Up to 15 minutes after enabling +- Traffic requirements: Consistent traffic from multiple global locations +- Available on all Workers plans (Free, Paid, Enterprise) + +## Placement Status Values + +```typescript +type PlacementStatus = + | undefined // Not yet analyzed + | 'SUCCESS' // Successfully optimized + | 'INSUFFICIENT_INVOCATIONS' // Not enough traffic + | 'UNSUPPORTED_APPLICATION'; // Made Worker slower (reverted) +``` + +## CLI Commands + +```bash +# Deploy with Smart Placement +wrangler deploy + +# Check placement status +curl -H "Authorization: Bearer $TOKEN" \ + https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/services/$WORKER_NAME \ + | jq .result.placement_status + +# Monitor +wrangler tail your-worker-name --header cf-placement +``` + +## Reading Order + +**First time?** Start here: +1. This README - understand core concepts and when to use Smart Placement +2. [configuration.md](./configuration.md) - set up wrangler.jsonc and understand limitations +3. [patterns.md](./patterns.md) - see practical examples for your use case +4. [api.md](./api.md) - monitor and verify Smart Placement is working +5. [gotchas.md](./gotchas.md) - troubleshoot common issues + +**Quick lookup:** +- "Should I enable Smart Placement?" → See "When to Use" above +- "How do I configure it?" → [configuration.md](./configuration.md) +- "How do I split frontend/backend?" → [patterns.md](./patterns.md) +- "Why isn't it working?" → [gotchas.md](./gotchas.md) + +## In This Reference + +- [configuration.md](./configuration.md) - wrangler.jsonc setup, mode values, validation rules +- [api.md](./api.md) - Placement Status API, cf-placement header, monitoring +- [patterns.md](./patterns.md) - Frontend/backend split, database workers, SSR patterns +- [gotchas.md](./gotchas.md) - Troubleshooting INSUFFICIENT_INVOCATIONS, performance issues + +## See Also + +- [workers](https://developers.cloudflare.com/workers/index.md) - Worker runtime and fetch handlers +- [d1](../d1/) - D1 database that benefits from Smart Placement +- [durable-objects](https://developers.cloudflare.com/durable-objects/index.md) - Durable Objects with backend logic +- [bindings](../bindings/) - Service bindings for frontend/backend split diff --git a/.agents/skills/cloudflare/references/smart-placement/api.md b/.agents/skills/cloudflare/references/smart-placement/api.md new file mode 100644 index 0000000..6608985 --- /dev/null +++ b/.agents/skills/cloudflare/references/smart-placement/api.md @@ -0,0 +1,183 @@ +# Smart Placement API + +## Placement Status API + +Query Worker placement status via Cloudflare API: + +```bash +curl -X GET "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/workers/services/{WORKER_NAME}" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" +``` + +Response includes `placement_status` field: + +```typescript +type PlacementStatus = + | undefined // Not yet analyzed + | 'SUCCESS' // Successfully optimized + | 'INSUFFICIENT_INVOCATIONS' // Not enough traffic + | 'UNSUPPORTED_APPLICATION'; // Made Worker slower (reverted) +``` + +## Status Meanings + +**`undefined` (not present)** +- Worker not yet analyzed +- Always runs at default edge location closest to user + +**`SUCCESS`** +- Analysis complete, Smart Placement active +- Worker runs in optimal location (may be edge or remote) + +**`INSUFFICIENT_INVOCATIONS`** +- Not enough requests to make placement decision +- Requires consistent multi-region traffic +- Always runs at default edge location + +**`UNSUPPORTED_APPLICATION`** (rare, <1% of Workers) +- Smart Placement made Worker slower +- Placement decision reverted +- Always runs at edge location +- Won't be re-analyzed until redeployed + +## cf-placement Header (Beta) + +Smart Placement adds response header indicating routing decision: + +```typescript +// Remote placement (Smart Placement routed request) +"cf-placement: remote-LHR" // Routed to London + +// Local placement (default edge routing) +"cf-placement: local-EWR" // Stayed at Newark edge +``` + +Format: `{placement-type}-{IATA-code}` +- `remote-*` = Smart Placement routed to remote location +- `local-*` = Stayed at default edge location +- IATA code = nearest airport to data center + +**Warning:** Beta feature, may be removed before GA. + +## Detecting Smart Placement in Code + +**Note:** `cf-placement` header is a beta feature and may change or be removed. + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const placementHeader = request.headers.get('cf-placement'); + + if (placementHeader?.startsWith('remote-')) { + const location = placementHeader.split('-')[1]; + console.log(`Smart Placement routed to ${location}`); + } else if (placementHeader?.startsWith('local-')) { + const location = placementHeader.split('-')[1]; + console.log(`Running at edge location ${location}`); + } + + return new Response('OK'); + } +} satisfies ExportedHandler; +``` + +## Request Duration Metrics + +Available in Cloudflare dashboard when Smart Placement enabled: + +**Workers & Pages → [Your Worker] → Metrics → Request Duration** + +Shows histogram comparing: +- Request duration WITH Smart Placement (99% of traffic) +- Request duration WITHOUT Smart Placement (1% baseline) + +**Request Duration vs Execution Duration:** +- **Request duration:** Total time from request arrival to response delivery (includes network latency) +- **Execution duration:** Time Worker code actively executing (excludes network waits) + +Use request duration to measure Smart Placement impact. + +### Interpreting Metrics + +| Metric Comparison | Interpretation | Action | +|-------------------|----------------|--------| +| WITH < WITHOUT | Smart Placement helping | Keep enabled | +| WITH ≈ WITHOUT | Neutral impact | Consider disabling to free resources | +| WITH > WITHOUT | Smart Placement hurting | Disable with `mode: "off"` | + +**Why Smart Placement might hurt performance:** +- Worker primarily serves static assets or cached content +- Backend services are globally distributed (no single optimal location) +- Worker has minimal backend communication +- Using Pages with `assets.run_worker_first = true` + +**Typical improvements when Smart Placement helps:** +- 20-50% reduction in request duration for database-heavy Workers +- 30-60% reduction for Workers making multiple backend API calls +- Larger improvements when backend is geographically concentrated + +## Monitoring Commands + +```bash +# Tail Worker logs +wrangler tail your-worker-name + +# Tail with filters +wrangler tail your-worker-name --status error +wrangler tail your-worker-name --header cf-placement + +# Check placement status via API +curl -H "Authorization: Bearer $TOKEN" \ + https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/services/$WORKER_NAME \ + | jq .result.placement_status +``` + +## TypeScript Types + +```typescript +// Placement status returned by API (field may be absent) +type PlacementStatus = + | 'SUCCESS' + | 'INSUFFICIENT_INVOCATIONS' + | 'UNSUPPORTED_APPLICATION' + | undefined; + +// Placement configuration in wrangler.jsonc +type PlacementMode = 'smart' | 'off'; + +interface PlacementConfig { + mode: PlacementMode; + // Legacy fields (deprecated/removed): + // hint?: string; // REMOVED - no longer supported +} + +// Explicit placement (separate feature from Smart Placement) +interface ExplicitPlacementConfig { + region?: string; + host?: string; + hostname?: string; + // Cannot combine with mode field +} + +// Worker metadata from API response +interface WorkerMetadata { + placement?: PlacementConfig | ExplicitPlacementConfig; + placement_status?: PlacementStatus; +} + +// Service Binding for backend Worker +interface Env { + BACKEND_SERVICE: Fetcher; // Service Binding to backend Worker + DATABASE: D1Database; +} + +// Example Worker with Service Binding +export default { + async fetch(request: Request, env: Env): Promise { + // Forward to backend Worker with Smart Placement enabled + const response = await env.BACKEND_SERVICE.fetch(request); + return response; + } +} satisfies ExportedHandler; +``` diff --git a/.agents/skills/cloudflare/references/smart-placement/configuration.md b/.agents/skills/cloudflare/references/smart-placement/configuration.md new file mode 100644 index 0000000..4f506ac --- /dev/null +++ b/.agents/skills/cloudflare/references/smart-placement/configuration.md @@ -0,0 +1,196 @@ +# Smart Placement Configuration + +## wrangler.jsonc Setup + +```jsonc +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "placement": { + "mode": "smart" + } +} +``` + +## Placement Mode Values + +| Mode | Behavior | +|------|----------| +| `"smart"` | Enable Smart Placement - automatic optimization based on traffic analysis | +| `"off"` | Explicitly disable Smart Placement - always run at edge closest to user | +| Not specified | Default behavior - run at edge closest to user (same as `"off"`) | + +**Note:** Smart Placement vs Explicit Placement are separate features. Smart Placement (`mode: "smart"`) uses automatic analysis. For manual placement control, see explicit placement options (`region`, `host`, `hostname` fields - not covered in this reference). + +## Frontend + Backend Split Configuration + +### Frontend Worker (No Smart Placement) + +```jsonc +// frontend-worker/wrangler.jsonc +{ + "name": "frontend", + "main": "frontend-worker.ts", + // No "placement" - runs at edge + "services": [ + { + "binding": "BACKEND", + "service": "backend-api" + } + ] +} +``` + +### Backend Worker (Smart Placement Enabled) + +```jsonc +// backend-api/wrangler.jsonc +{ + "name": "backend-api", + "main": "backend-worker.ts", + "placement": { + "mode": "smart" + }, + "d1_databases": [ + { + "binding": "DATABASE", + "database_id": "xxx" + } + ] +} +``` + +## Requirements & Limitations + +### Requirements +- **Wrangler version:** 2.20.0+ +- **Analysis time:** Up to 15 minutes +- **Traffic requirements:** Consistent multi-location traffic +- **Workers plan:** All plans (Free, Paid, Enterprise) + +### What Smart Placement Affects + +**CRITICAL LIMITATION - Smart Placement ONLY Affects `fetch` Handlers:** + +Smart Placement is fundamentally limited to Workers with default `fetch` handlers. This is a key architectural constraint. + +- ✅ **Affects:** `fetch` event handlers ONLY (the default export's fetch method) +- ❌ **Does NOT affect:** + - RPC methods (Service Bindings with `WorkerEntrypoint` - see example below) + - Named entrypoints (exports other than `default`) + - Workers without `fetch` handlers + - Queue consumers, scheduled handlers, or other event types + +**Example - Smart Placement ONLY affects `fetch`:** +```typescript +// ✅ Smart Placement affects this: +export default { + async fetch(request: Request, env: Env): Promise { + // This runs close to backend when Smart Placement enabled + const data = await env.DATABASE.prepare('SELECT * FROM users').all(); + return Response.json(data); + } +} + +// ❌ Smart Placement DOES NOT affect these: +export class MyRPC extends WorkerEntrypoint { + async myMethod() { + // This ALWAYS runs at edge, Smart Placement has NO EFFECT + const data = await this.env.DATABASE.prepare('SELECT * FROM users').all(); + return data; + } +} + +export async function scheduled(event: ScheduledEvent, env: Env) { + // NOT affected by Smart Placement +} +``` + +**Consequence:** If your backend logic uses RPC methods (`WorkerEntrypoint`), Smart Placement cannot optimize those calls. You must use fetch-based patterns for Smart Placement to work. + +**Solution:** Convert RPC methods to fetch endpoints, or use a wrapper Worker with `fetch` handler that calls your backend RPC (though this adds latency). + +### Baseline Traffic +Smart Placement automatically routes 1% of requests WITHOUT optimization as baseline for performance comparison. + +### Validation Rules + +**Mutually exclusive fields:** +- `mode` cannot be used with explicit placement fields (`region`, `host`, `hostname`) +- Choose either Smart Placement OR explicit placement, not both + +```jsonc +// ✅ Valid - Smart Placement +{ "placement": { "mode": "smart" } } + +// ✅ Valid - Explicit Placement (different feature) +{ "placement": { "region": "us-east1" } } + +// ❌ Invalid - Cannot combine +{ "placement": { "mode": "smart", "region": "us-east1" } } +``` + +## Dashboard Configuration + +**Workers & Pages** → Select Worker → **Settings** → **General** → **Placement: Smart** → Wait 15min → Check **Metrics** + +## TypeScript Types + +```typescript +interface Env { + BACKEND: Fetcher; + DATABASE: D1Database; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const data = await env.DATABASE.prepare('SELECT * FROM table').all(); + return Response.json(data); + } +} satisfies ExportedHandler; +``` + +## Cloudflare Pages/Assets Warning + +**CRITICAL PERFORMANCE ISSUE:** Enabling Smart Placement with `assets.run_worker_first = true` in Pages projects **severely degrades asset serving performance**. This is one of the most common misconfigurations. + +**Why this is bad:** +- Smart Placement routes ALL requests (including static assets) away from edge to remote locations +- Static assets (HTML, CSS, JS, images) should ALWAYS be served from edge closest to user +- Result: 2-5x slower asset loading times, poor user experience + +**Problem:** Smart Placement routes asset requests away from edge, but static assets should always be served from edge closest to user. + +**Solutions (in order of preference):** +1. **Recommended:** Split into separate Workers (frontend at edge + backend with Smart Placement) +2. Set `"mode": "off"` to explicitly disable Smart Placement for Pages/Assets Workers +3. Use `assets.run_worker_first = false` (serves assets first, bypasses Worker for static content) + +```jsonc +// ❌ BAD - Degrades asset performance by 2-5x +{ + "name": "pages-app", + "placement": { "mode": "smart" }, + "assets": { "run_worker_first": true } +} + +// ✅ GOOD - Frontend at edge, backend optimized +// frontend-worker/wrangler.jsonc +{ + "name": "frontend", + "assets": { "run_worker_first": true } + // No placement - runs at edge +} + +// backend-worker/wrangler.jsonc +{ + "name": "backend-api", + "placement": { "mode": "smart" }, + "d1_databases": [{ "binding": "DB", "database_id": "xxx" }] +} +``` + +**Key takeaway:** Never enable Smart Placement on Workers that serve static assets with `run_worker_first = true`. + +## Local Development + +Smart Placement does NOT work in `wrangler dev` (local only). Test by deploying: `wrangler deploy --env staging` diff --git a/.agents/skills/cloudflare/references/smart-placement/gotchas.md b/.agents/skills/cloudflare/references/smart-placement/gotchas.md new file mode 100644 index 0000000..dc94e9b --- /dev/null +++ b/.agents/skills/cloudflare/references/smart-placement/gotchas.md @@ -0,0 +1,174 @@ +# Smart Placement Gotchas + +## Common Errors + +### "INSUFFICIENT_INVOCATIONS" + +**Cause:** Not enough traffic for Smart Placement to analyze +**Solution:** +- Ensure Worker receives consistent global traffic +- Wait longer (analysis takes up to 15 minutes) +- Send test traffic from multiple global locations +- Check Worker has fetch event handler + +### "UNSUPPORTED_APPLICATION" + +**Cause:** Smart Placement made Worker slower rather than faster +**Reasons:** +- Worker doesn't make backend calls (runs faster at edge) +- Backend calls are cached (network latency to user more important) +- Backend service has good global distribution +- Worker serves static assets or Pages content + +**Solutions:** +- Disable Smart Placement: `{ "placement": { "mode": "off" } }` +- Review whether Worker actually benefits from Smart Placement +- Consider caching strategy to reduce backend calls +- For Pages/Assets Workers, use separate backend Worker with Smart Placement + +### "No request duration metrics" + +**Cause:** Smart Placement not enabled, insufficient time passed, insufficient traffic, or analysis incomplete +**Solution:** +- Ensure Smart Placement enabled in config +- Wait 15+ minutes after deployment +- Verify Worker has sufficient traffic +- Check `placement_status` is `SUCCESS` + +### "cf-placement header missing" + +**Cause:** Smart Placement not enabled, beta feature removed, or Worker not analyzed yet +**Solution:** Verify Smart Placement enabled, wait for analysis (15min), check if beta feature still available + +## Pages/Assets + Smart Placement Performance Degradation + +**Problem:** Static assets load 2-5x slower when Smart Placement enabled with `run_worker_first = true`. + +**Cause:** Smart Placement routes ALL requests (including static assets like HTML, CSS, JS, images) to remote locations. Static content should ALWAYS be served from edge closest to user. + +**Solution:** Split into separate Workers OR disable Smart Placement: +```jsonc +// ❌ BAD - Assets routed away from user +{ + "name": "pages-app", + "placement": { "mode": "smart" }, + "assets": { "run_worker_first": true } +} + +// ✅ GOOD - Assets at edge, API optimized +// frontend/wrangler.jsonc +{ + "name": "frontend", + "assets": { "run_worker_first": true } + // No placement field - stays at edge +} + +// backend/wrangler.jsonc +{ + "name": "backend-api", + "placement": { "mode": "smart" } +} +``` + +This is one of the most common and impactful Smart Placement misconfigurations. + +## Monolithic Full-Stack Worker + +**Problem:** Frontend and backend logic in single Worker with Smart Placement enabled. + +**Cause:** Smart Placement optimizes for backend latency but increases user-facing response time. + +**Solution:** Split into two Workers: +```jsonc +// frontend/wrangler.jsonc +{ + "name": "frontend", + "placement": { "mode": "off" }, // Explicit: stay at edge + "services": [{ "binding": "BACKEND", "service": "backend-api" }] +} + +// backend/wrangler.jsonc +{ + "name": "backend-api", + "placement": { "mode": "smart" }, + "d1_databases": [{ "binding": "DB", "database_id": "xxx" }] +} +``` + +## Local Development Confusion + +**Issue:** Smart Placement doesn't work in `wrangler dev`. + +**Explanation:** Smart Placement only activates in production deployments, not local development. + +**Solution:** Test Smart Placement in staging environment: `wrangler deploy --env staging` + +## Baseline Traffic & Analysis Time + +**Note:** Smart Placement routes 1% of requests WITHOUT optimization for comparison (expected). + +**Analysis time:** Up to 15 minutes. During analysis, Worker runs at edge. Monitor `placement_status`. + +## RPC Methods Not Affected (Critical Limitation) + +**Problem:** Enabled Smart Placement on backend but RPC calls still slow. + +**Cause:** Smart Placement ONLY affects `fetch` handlers. RPC methods (Service Bindings with `WorkerEntrypoint`) are NEVER affected. + +**Why:** RPC bypasses `fetch` handler - Smart Placement can only route `fetch` requests. + +**Solution:** Convert to fetch-based Service Bindings: + +```typescript +// ❌ RPC - Smart Placement has NO EFFECT +export class BackendRPC extends WorkerEntrypoint { + async getData() { + // ALWAYS runs at edge + return await this.env.DATABASE.prepare('SELECT * FROM table').all(); + } +} + +// ✅ Fetch - Smart Placement WORKS +export default { + async fetch(request: Request, env: Env): Promise { + // Runs close to DATABASE when Smart Placement enabled + const data = await env.DATABASE.prepare('SELECT * FROM table').all(); + return Response.json(data); + } +} +``` + +## Requirements + +- **Wrangler 2.20.0+** required +- **Consistent multi-region traffic** needed for analysis +- **Only affects fetch handlers** - RPC methods and named entrypoints not affected + +## Limits + +| Resource/Limit | Value | Notes | +|----------------|-------|-------| +| Analysis time | Up to 15 minutes | After enabling | +| Baseline traffic | 1% | Routed without optimization | +| Min Wrangler version | 2.20.0+ | Required | +| Traffic requirement | Multi-region | Consistent needed | + +## Disabling Smart Placement + +```jsonc +{ "placement": { "mode": "off" } } // Explicit disable +// OR remove "placement" field entirely (same effect) +``` + +Both behaviors identical - Worker runs at edge closest to user. + +## When NOT to Use Smart Placement + +- Workers serving only static content or cached responses +- Workers without significant backend communication +- Pure edge logic (auth checks, redirects, simple transformations) +- Workers without fetch event handlers +- Pages/Assets Workers with `run_worker_first = true` +- Workers using RPC methods instead of fetch handlers + +These scenarios won't benefit and may perform worse with Smart Placement. diff --git a/.agents/skills/cloudflare/references/smart-placement/patterns.md b/.agents/skills/cloudflare/references/smart-placement/patterns.md new file mode 100644 index 0000000..40dc4dd --- /dev/null +++ b/.agents/skills/cloudflare/references/smart-placement/patterns.md @@ -0,0 +1,183 @@ +# Smart Placement Patterns + +## Backend Worker with Database Access + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const user = await env.DATABASE.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(); + const orders = await env.DATABASE.prepare('SELECT * FROM orders WHERE user_id = ?').bind(userId).all(); + return Response.json({ user, orders }); + } +}; +``` + +```jsonc +{ "placement": { "mode": "smart" }, "d1_databases": [{ "binding": "DATABASE", "database_id": "xxx" }] } +``` + +## Frontend + Backend Split (Service Bindings) + +**Frontend:** Runs at edge for fast user response +**Backend:** Smart Placement runs close to database + +```typescript +// Frontend Worker - routes requests to backend +interface Env { + BACKEND: Fetcher; // Service Binding to backend Worker +} + +export default { + async fetch(request: Request, env: Env): Promise { + if (new URL(request.url).pathname.startsWith('/api/')) { + return env.BACKEND.fetch(request); // Forward to backend + } + return new Response('Frontend content'); + } +}; + +// Backend Worker - database operations +interface BackendEnv { + DATABASE: D1Database; +} + +export default { + async fetch(request: Request, env: BackendEnv): Promise { + const data = await env.DATABASE.prepare('SELECT * FROM table').all(); + return Response.json(data); + } +}; +``` + +**CRITICAL:** Use fetch-based Service Bindings (shown above). If using RPC with `WorkerEntrypoint`, Smart Placement will NOT optimize those method calls - only `fetch` handlers are affected. + +**RPC vs Fetch - CRITICAL:** Smart Placement ONLY works with fetch-based bindings, NOT RPC. + +```typescript +// ❌ RPC - Smart Placement has NO EFFECT on backend RPC methods +export class BackendRPC extends WorkerEntrypoint { + async getData() { + // ALWAYS runs at edge, Smart Placement ignored + return await this.env.DATABASE.prepare('SELECT * FROM table').all(); + } +} + +// ✅ Fetch - Smart Placement WORKS +export default { + async fetch(request: Request, env: Env): Promise { + // Runs close to DATABASE when Smart Placement enabled + const data = await env.DATABASE.prepare('SELECT * FROM table').all(); + return Response.json(data); + } +}; +``` + +## External API Integration + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const apiUrl = 'https://api.partner.com'; + const headers = { 'Authorization': `Bearer ${env.API_KEY}` }; + + const [profile, transactions] = await Promise.all([ + fetch(`${apiUrl}/profile`, { headers }), + fetch(`${apiUrl}/transactions`, { headers }) + ]); + + return Response.json({ + profile: await profile.json(), + transactions: await transactions.json() + }); + } +}; +``` + +## SSR / API Gateway Pattern + +```typescript +// Frontend (edge) - auth/routing close to user +export default { + async fetch(request: Request, env: Env) { + if (!request.headers.get('Authorization')) { + return new Response('Unauthorized', { status: 401 }); + } + const data = await env.BACKEND.fetch(request); + return new Response(renderPage(await data.json()), { + headers: { 'Content-Type': 'text/html' } + }); + } +}; + +// Backend (Smart Placement) - DB operations close to data +export default { + async fetch(request: Request, env: Env) { + const data = await env.DATABASE.prepare('SELECT * FROM pages WHERE id = ?').bind(pageId).first(); + return Response.json(data); + } +}; +``` + +## Durable Objects with Smart Placement + +**Key principle:** Smart Placement does NOT control WHERE Durable Objects run. DOs always run in their designated region (based on jurisdiction or smart location hints). + +**What Smart Placement DOES affect:** The location of the coordinator Worker's `fetch` handler that makes calls to multiple DOs. + +**Pattern:** Enable Smart Placement on coordinator Worker that aggregates data from multiple DOs: + +```typescript +// Worker with Smart Placement - aggregates data from multiple DOs +export default { + async fetch(request: Request, env: Env): Promise { + const userId = new URL(request.url).searchParams.get('user'); + + // Get DO stubs + const userDO = env.USER_DO.get(env.USER_DO.idFromName(userId)); + const analyticsID = env.ANALYTICS_DO.idFromName(`analytics-${userId}`); + const analyticsDO = env.ANALYTICS_DO.get(analyticsID); + + // Fetch from multiple DOs + const [userData, analyticsData] = await Promise.all([ + userDO.fetch(new Request('https://do/profile')), + analyticsDO.fetch(new Request('https://do/stats')) + ]); + + return Response.json({ + user: await userData.json(), + analytics: await analyticsData.json() + }); + } +}; +``` + +```jsonc +// wrangler.jsonc +{ + "placement": { "mode": "smart" }, + "durable_objects": { + "bindings": [ + { "name": "USER_DO", "class_name": "UserDO" }, + { "name": "ANALYTICS_DO", "class_name": "AnalyticsDO" } + ] + } +} +``` + +**When this helps:** +- Worker's `fetch` handler runs closer to DO regions, reducing network latency for multiple DO calls +- Most beneficial when DOs are geographically concentrated or in specific jurisdictions +- Helps when coordinator makes many sequential or parallel DO calls + +**When this DOESN'T help:** +- DOs are globally distributed (no single optimal Worker location) +- Worker only calls a single DO +- DO calls are infrequent or cached + +## Best Practices + +- Split full-stack apps: frontend at edge, backend with Smart Placement +- Use fetch-based Service Bindings (not RPC) +- Enable for backend logic: APIs, data aggregation, DB operations +- Don't enable for: static content, edge logic, RPC methods, Pages with `run_worker_first` +- Wait 15+ min for analysis, verify `placement_status = SUCCESS` diff --git a/.agents/skills/cloudflare/references/static-assets/README.md b/.agents/skills/cloudflare/references/static-assets/README.md new file mode 100644 index 0000000..caa2237 --- /dev/null +++ b/.agents/skills/cloudflare/references/static-assets/README.md @@ -0,0 +1,21 @@ +# Workers Static Assets + +Use Workers Static Assets for new static sites, SPAs, generated sites, and applications combining assets with server logic. Inspect the framework, build output, and existing deployment configuration before changing routing. + +| Task | Documentation | +|------|---------------| +| Set up and deploy a static site or application | [Get started](https://developers.cloudflare.com/workers/static-assets/get-started/index.md) | +| Choose configuration and an optional asset binding | [Configuration and bindings](https://developers.cloudflare.com/workers/static-assets/binding/index.md) | +| Serve a client-rendered application | [SPA routing](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/index.md) | +| Serve generated HTML and custom error pages | [SSG routing](https://developers.cloudflare.com/workers/static-assets/routing/static-site-generation/index.md) | +| Use a full-stack framework | [Full-stack application guides](https://developers.cloudflare.com/workers/static-assets/routing/full-stack-application/index.md) | +| Evaluate moving an existing Pages project | [Pages migration guide](https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/index.md) | + +Do not choose a platform solely from the framework name. For an existing Pages project, inspect its current features and migration requirements before proposing a move. + +## Reading Order + +1. [configuration.md](configuration.md) — build output and routing configuration. +2. [api.md](api.md) — fetch assets and handle responses. +3. [patterns.md](patterns.md) — choose a routing design. +4. [gotchas.md](gotchas.md) — diagnose routing, caching, and deployment issues. diff --git a/.agents/skills/cloudflare/references/static-assets/api.md b/.agents/skills/cloudflare/references/static-assets/api.md new file mode 100644 index 0000000..f542630 --- /dev/null +++ b/.agents/skills/cloudflare/references/static-assets/api.md @@ -0,0 +1,16 @@ +# Static Assets Binding API + +Read the binding reference before implementing calls. Check the configured binding name and use the project's existing environment types. + +| Task | Documentation | +|------|---------------| +| Forward a request or fetch a specific asset | [Runtime API reference](https://developers.cloudflare.com/workers/static-assets/binding/index.md#runtime-api-reference) | +| Understand how binding requests apply HTML and fallback settings | [Binding fetch behavior](https://developers.cloudflare.com/workers/static-assets/binding/index.md#runtime-api-reference) and [HTML handling](https://developers.cloudflare.com/workers/static-assets/routing/advanced/html-handling/index.md) | +| Run authorization or transform content before serving | [Run your Worker script first](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/index.md#run-your-worker-script-first) | +| Inspect default MIME, cache, and validation headers | [Default headers](https://developers.cloudflare.com/workers/static-assets/headers/index.md#default-headers) | +| Add or override response headers | [Custom headers and Worker-response caveat](https://developers.cloudflare.com/workers/static-assets/headers/index.md#custom-headers) | +| Fetch assets imported by Vite | [Vite asset features](https://developers.cloudflare.com/workers/vite-plugin/reference/static-assets/index.md#features) | + +When selecting a different asset, construct a full URL using the incoming request as the base. Preserve the returned status and headers when transforming a response; do not collapse every unsuccessful response into an application 404. + +See [configuration.md](configuration.md) for bindings and [patterns.md](patterns.md) for routing choices. diff --git a/.agents/skills/cloudflare/references/static-assets/configuration.md b/.agents/skills/cloudflare/references/static-assets/configuration.md new file mode 100644 index 0000000..e540448 --- /dev/null +++ b/.agents/skills/cloudflare/references/static-assets/configuration.md @@ -0,0 +1,16 @@ +# Static Assets Configuration + +Inspect the build script, output directory, Wrangler configuration, and any framework-generated deployment configuration first. Configure the files actually produced by the build, and identify which paths need Worker logic. + +| Task | Documentation | +|------|---------------| +| Set the asset directory and exclude non-public files | [Directory](https://developers.cloudflare.com/workers/static-assets/binding/index.md#directory) and [ignoring assets](https://developers.cloudflare.com/workers/static-assets/binding/index.md#ignoring-assets) | +| Make assets available to Worker code | [Asset binding configuration](https://developers.cloudflare.com/workers/static-assets/binding/index.md#binding) | +| Select paths that must run Worker logic before asset serving | [Worker-first configuration](https://developers.cloudflare.com/workers/static-assets/binding/index.md#run_worker_first) | +| Configure SPA fallback and navigation behavior | [SPA configuration](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/index.md#configuration) | +| Configure generated HTML and missing-page responses | [SSG configuration](https://developers.cloudflare.com/workers/static-assets/routing/static-site-generation/index.md#configuration) | +| Choose canonical HTML URLs and trailing slash handling | [HTML handling modes](https://developers.cloudflare.com/workers/static-assets/routing/advanced/html-handling/index.md) | +| Configure assets produced by the Cloudflare Vite plugin | [Vite asset configuration](https://developers.cloudflare.com/workers/vite-plugin/reference/static-assets/index.md#configuration) | +| Configure static response headers or redirects | [Headers](https://developers.cloudflare.com/workers/static-assets/headers/index.md) and [redirects](https://developers.cloudflare.com/workers/static-assets/redirects/index.md) | + +For Vite projects, inspect generated output before overriding asset paths. For protected routes, ensure that the selected routing configuration reaches the authorization logic before returning an asset. Use [patterns.md](patterns.md) to decide routing intent and [gotchas.md](gotchas.md) to verify it. diff --git a/.agents/skills/cloudflare/references/static-assets/gotchas.md b/.agents/skills/cloudflare/references/static-assets/gotchas.md new file mode 100644 index 0000000..841f68d --- /dev/null +++ b/.agents/skills/cloudflare/references/static-assets/gotchas.md @@ -0,0 +1,18 @@ +# Static Assets Gotchas + +Compare the deployed build output and effective configuration with the exact request that failed. Test browser navigation separately from client-side fetches when investigating SPA routing. + +| Symptom or decision | Documentation | +|---------------------|---------------| +| Asset missing or unexpected files uploaded | [Asset directory](https://developers.cloudflare.com/workers/static-assets/binding/index.md#directory) and [ignore rules](https://developers.cloudflare.com/workers/static-assets/binding/index.md#ignoring-assets) | +| Worker is bypassed for an asset or protected path | [Worker-first routing](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/index.md#run-your-worker-script-first) | +| Browser navigation returns HTML for an API path | [SPA navigation requests](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/index.md#navigation-requests) and [advanced routing](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/index.md#advanced-routing-control) | +| Unexpected redirects or trailing slashes | [HTML handling](https://developers.cloudflare.com/workers/static-assets/routing/advanced/html-handling/index.md) | +| A missing route serves the wrong fallback | [SPA routing](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/index.md) or [SSG custom 404 pages](https://developers.cloudflare.com/workers/static-assets/routing/static-site-generation/index.md#custom-404-pages) | +| Cache behavior or custom headers differ from expectations | [Default and custom headers](https://developers.cloudflare.com/workers/static-assets/headers/index.md) | +| Vite development and deployment behave differently | [Vite asset configuration and output](https://developers.cloudflare.com/workers/vite-plugin/reference/static-assets/index.md) | +| Worker-first requests return 429 or affect cost | [Billing and limitations](https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/index.md) | +| Asset count or file size exceeds deployment limits | [Static asset platform limits](https://developers.cloudflare.com/workers/platform/limits/index.md#static-assets) | +| Placement adds latency to asset requests | [Worker routing and placement caveat](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/index.md#run-your-worker-script-first) | + +Verify a public asset, API endpoint, protected path, and missing URL against the routing intent. Inspect response status, redirect location, and cache headers before changing fallback or cache configuration. Return to [configuration.md](configuration.md) and [api.md](api.md) for the relevant settings and binding behavior. diff --git a/.agents/skills/cloudflare/references/static-assets/patterns.md b/.agents/skills/cloudflare/references/static-assets/patterns.md new file mode 100644 index 0000000..b83ffd6 --- /dev/null +++ b/.agents/skills/cloudflare/references/static-assets/patterns.md @@ -0,0 +1,17 @@ +# Static Assets Routing Patterns + +Decide which requests need application logic before choosing configuration. Record expected behavior for public files, API paths, protected content, browser navigation, and missing URLs. + +| Routing need | Documentation | +|--------------|---------------| +| Serve public assets with minimal Worker involvement | [Default Worker routing](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/index.md) | +| Combine a SPA with API routes | [Advanced SPA routing control](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/index.md#advanced-routing-control) | +| Require authentication or transform assets before serving | [Run Worker before each request](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/index.md#run-worker-before-each-request) | +| Route an OAuth callback or selected dynamic paths to the Worker | [Run Worker first for selective paths](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/index.md#run-worker-first-for-selective-paths) | +| Select assets for locale or experiment variants | [Asset binding API](https://developers.cloudflare.com/workers/static-assets/binding/index.md#runtime-api-reference) and [Worker-first routing](https://developers.cloudflare.com/workers/static-assets/binding/index.md#run_worker_first) | +| Serve generated HTML with a custom 404 | [Custom 404 pages](https://developers.cloudflare.com/workers/static-assets/routing/static-site-generation/index.md#custom-404-pages) | +| Apply static cache/security headers or redirects | [Custom headers](https://developers.cloudflare.com/workers/static-assets/headers/index.md#custom-headers) and [redirects](https://developers.cloudflare.com/workers/static-assets/redirects/index.md) | + +Keep public asset paths eligible for direct serving when they do not need application logic. Ensure protected paths cannot bypass the authorization handler through an asset match or navigation fallback. For locale or experiment routing, map accepted variants to known build outputs and define a fallback explicitly. + +Use [configuration.md](configuration.md) for setup and [gotchas.md](gotchas.md) for representative request checks. diff --git a/.agents/skills/cloudflare/references/stream/README.md b/.agents/skills/cloudflare/references/stream/README.md new file mode 100644 index 0000000..e3e9d4b --- /dev/null +++ b/.agents/skills/cloudflare/references/stream/README.md @@ -0,0 +1,22 @@ +# Cloudflare Stream + +Use Stream for on-demand video upload and playback or live broadcasting. Start with the application's upload source, player, and access requirements, then read the corresponding documentation before writing code. + +## Choose a Workflow + +| Task | Read | +|---|---| +| Let users upload without exposing an API token | [Direct creator uploads](https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/index.md) | +| Choose server upload, resumable upload, or import from a URL | [Upload methods and supported formats](https://developers.cloudflare.com/stream/uploading-videos/index.md) | +| Embed playback or integrate an existing player | [Stream Player](https://developers.cloudflare.com/stream/viewing-videos/using-the-stream-player/index.md) or [HLS/DASH players](https://developers.cloudflare.com/stream/viewing-videos/using-own-player/index.md) | +| Restrict viewing to authorized users or embedding origins | [Secure your Stream](https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/index.md) | +| Broadcast live, replay recordings, or simulcast | [Live workflow routing](./api-live.md) | +| Check usage, costs, or upload constraints | [Analytics](https://developers.cloudflare.com/stream/getting-analytics/index.md), [pricing](https://developers.cloudflare.com/stream/pricing/index.md), and [upload requirements](https://developers.cloudflare.com/stream/uploading-videos/index.md) | + +## In This Reference + +- [configuration.md](./configuration.md): project setup and access decisions. +- [api.md](./api.md): upload, playback, editing, and library operations. +- [api-live.md](./api-live.md): live inputs, outputs, recording, and WebRTC. +- [patterns.md](./patterns.md): application workflow decisions. +- [gotchas.md](./gotchas.md): troubleshooting routes. diff --git a/.agents/skills/cloudflare/references/stream/api-live.md b/.agents/skills/cloudflare/references/stream/api-live.md new file mode 100644 index 0000000..7af2e96 --- /dev/null +++ b/.agents/skills/cloudflare/references/stream/api-live.md @@ -0,0 +1,18 @@ +# Stream Live Streaming API + +Choose the ingest protocol and viewer experience before creating live inputs. Read the current examples for returned endpoints and credentials instead of constructing publish URLs from an input ID. + +| Task | Read | +|---|---| +| Create an RTMPS/SRT input and connect an encoder | [Start a live stream](https://developers.cloudflare.com/stream/stream-live/start-stream-live/index.md) | +| Update inputs, recording, retention, or stream keys | [Manage live inputs](https://developers.cloudflare.com/stream/stream-live/start-stream-live/index.md#manage-live-inputs) | +| Choose persistent-channel playback versus a particular video | [View by live input ID or video ID](https://developers.cloudflare.com/stream/stream-live/watch-live-stream/index.md#view-by-live-input-id-or-video-id) | +| Find and replay recorded broadcasts | [Replay recordings](https://developers.cloudflare.com/stream/stream-live/replay-recordings/index.md) | +| Forward broadcasts to external platforms | [Simulcasting configuration and limits](https://developers.cloudflare.com/stream/stream-live/simulcasting/index.md) | +| Receive connection and disconnection notifications | [Live webhooks](https://developers.cloudflare.com/stream/stream-live/webhooks/index.md) | +| Publish and play using WHIP/WHEP | [WebRTC requirements and endpoints](https://developers.cloudflare.com/stream/webrtc-beta/index.md) and [browser integration](https://developers.cloudflare.com/stream/examples/browser-based-webrtc/index.md) | +| Diagnose encoder, buffering, or latency problems | [Live troubleshooting](https://developers.cloudflare.com/stream/stream-live/troubleshooting/index.md) | + +Keep publishing credentials separate from viewer playback data. Decide whether the application stores a reusable live input, individual recording IDs, or both. Route recording processing events through [video webhooks](https://developers.cloudflare.com/stream/manage-video-library/using-webhooks/index.md); live connection events have their own notification setup. + +See [configuration.md](./configuration.md) for access decisions and [patterns.md](./patterns.md) for application state handling. diff --git a/.agents/skills/cloudflare/references/stream/api.md b/.agents/skills/cloudflare/references/stream/api.md new file mode 100644 index 0000000..9a87fd4 --- /dev/null +++ b/.agents/skills/cloudflare/references/stream/api.md @@ -0,0 +1,21 @@ +# Stream API Reference + +Read the task-specific documentation for request schemas, SDK examples, and response fields. For live inputs and outputs, use [api-live.md](./api-live.md). + +| Task | Read | +|---|---| +| Issue a one-time upload URL to an end user | [Direct creator uploads](https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/index.md) | +| Resume uploads or handle unreliable connections | [Resumable uploads and requirements](https://developers.cloudflare.com/stream/uploading-videos/resumable-uploads/index.md) | +| Import a video already hosted elsewhere | [Upload via link](https://developers.cloudflare.com/stream/uploading-videos/upload-via-link/index.md) | +| Embed an iframe or React player | [Use the Stream Player](https://developers.cloudflare.com/stream/viewing-videos/using-the-stream-player/index.md) | +| Use HLS/DASH with an existing player | [Use your own player](https://developers.cloudflare.com/stream/viewing-videos/using-own-player/index.md) | +| Issue playback tokens | [Signed URLs and token-generation choices](https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/index.md#three-ways-to-generate-signed-tokens) | +| Generate previews or downloadable files | [Thumbnails](https://developers.cloudflare.com/stream/viewing-videos/displaying-thumbnails/index.md) and [downloads](https://developers.cloudflare.com/stream/viewing-videos/download-videos/index.md) | +| Upload captions or generate them with Stream | [Add captions](https://developers.cloudflare.com/stream/edit-videos/adding-captions/index.md) | +| Apply branding during upload | [Watermarks](https://developers.cloudflare.com/stream/edit-videos/applying-watermarks/index.md) | +| Trim an on-demand video | [Video clipping](https://developers.cloudflare.com/stream/edit-videos/video-clipping/index.md) | +| List and filter videos through the REST API | [List videos](https://developers.cloudflare.com/api/resources/stream/methods/list/index.md) | +| Manage videos from a Worker | [Stream binding methods](https://developers.cloudflare.com/stream/manage-video-library/bindings/index.md#methods) | +| React to encoding success or failure | [Video webhooks](https://developers.cloudflare.com/stream/manage-video-library/using-webhooks/index.md) | + +Keep the returned video UID associated with the application's owning user or record. Treat upload completion and playback readiness as separate application states; see [workflow decisions](./patterns.md). diff --git a/.agents/skills/cloudflare/references/stream/configuration.md b/.agents/skills/cloudflare/references/stream/configuration.md new file mode 100644 index 0000000..363d5ac --- /dev/null +++ b/.agents/skills/cloudflare/references/stream/configuration.md @@ -0,0 +1,18 @@ +# Stream Configuration + +Inspect the existing runtime, API client or binding, authentication layer, player, and secret storage before adding Stream. Choose upload and viewing permissions from the application's requirements. + +| Task | Read | +|---|---| +| Configure Stream in a Worker and use its binding | [Stream binding setup](https://developers.cloudflare.com/stream/manage-video-library/bindings/index.md#setup) | +| Set creator upload constraints and metadata | [Direct creator uploads](https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/index.md) | +| Choose a token endpoint, Worker binding, or signing key | [Token-generation choices](https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/index.md#three-ways-to-generate-signed-tokens) | +| Require private playback or apply token restrictions | [Secure your Stream](https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/index.md) | +| Restrict embedding origins | [Allowed origins](https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/index.md#allowed-origins) | +| Configure processing notifications and their secret | [Video webhooks](https://developers.cloudflare.com/stream/manage-video-library/using-webhooks/index.md) | +| Configure live recording or external destinations | [Live inputs](https://developers.cloudflare.com/stream/stream-live/start-stream-live/index.md) and [simulcasting](https://developers.cloudflare.com/stream/stream-live/simulcasting/index.md) | +| Choose player configuration or framework integration | [Stream Player](https://developers.cloudflare.com/stream/viewing-videos/using-the-stream-player/index.md) | + +Use the project's server-side secret handling for API tokens, signing keys, and webhook secrets. Identify the authorization check that permits issuing an upload URL or playback token; Stream configuration alone does not define the application's user entitlements. + +See [api.md](./api.md), [api-live.md](./api-live.md), and [gotchas.md](./gotchas.md). diff --git a/.agents/skills/cloudflare/references/stream/gotchas.md b/.agents/skills/cloudflare/references/stream/gotchas.md new file mode 100644 index 0000000..0c02ec8 --- /dev/null +++ b/.agents/skills/cloudflare/references/stream/gotchas.md @@ -0,0 +1,18 @@ +# Stream Gotchas + +Identify whether the failure occurs during upload, encoding, authorization, playback, or live ingestion before changing configuration. Read the linked requirements and error guidance for that stage. + +| Symptom or check | Read | +|---|---| +| Unsupported file or upload constraint failure | [Supported formats and upload requirements](https://developers.cloudflare.com/stream/uploading-videos/index.md) | +| Large upload fails or restarts after interruption | [Resumable upload requirements](https://developers.cloudflare.com/stream/uploading-videos/resumable-uploads/index.md#requirements) | +| Stream cannot fetch an imported video | [Upload via link requirements](https://developers.cloudflare.com/stream/uploading-videos/upload-via-link/index.md) | +| Upload finished but video is not playable | [Upload progress tracking](https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/index.md#track-upload-progress) and [processing error codes](https://developers.cloudflare.com/stream/manage-video-library/using-webhooks/index.md#error-codes) | +| Private playback or embedding fails | [Signed tokens, restrictions, and allowed origins](https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/index.md) | +| Webhook signature verification fails | [Verify the raw body and signature](https://developers.cloudflare.com/stream/manage-video-library/using-webhooks/index.md#verify-webhook-authenticity) | +| A custom HLS/DASH player behaves incorrectly | [Own-player integration and manifest handling](https://developers.cloudflare.com/stream/viewing-videos/using-own-player/index.md) | +| Live stream will not connect or playback buffers | [Live troubleshooting](https://developers.cloudflare.com/stream/stream-live/troubleshooting/index.md) | +| Simulcast output does not behave as expected | [Output configuration and limits](https://developers.cloudflare.com/stream/stream-live/simulcasting/index.md) | +| Estimate storage or delivery costs | [Stream pricing](https://developers.cloudflare.com/stream/pricing/index.md) | + +Check the actual response and current docs before adopting a retry policy or treating a video as ready. Keep credentials out of browser code and logs; use [configuration.md](./configuration.md) for access setup and [patterns.md](./patterns.md) for state decisions. diff --git a/.agents/skills/cloudflare/references/stream/patterns.md b/.agents/skills/cloudflare/references/stream/patterns.md new file mode 100644 index 0000000..91de4ec --- /dev/null +++ b/.agents/skills/cloudflare/references/stream/patterns.md @@ -0,0 +1,22 @@ +# Stream Patterns + +Use the official workflow examples after identifying where the existing application handles authorization, video ownership, and processing state. + +| Workflow | Read | +|---|---| +| Browser uploads through a server-issued URL | [Direct creator uploads](https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/index.md) | +| Large or interruption-prone uploads | [Resumable uploads](https://developers.cloudflare.com/stream/uploading-videos/resumable-uploads/index.md) | +| Update application state when processing completes | [Video webhooks](https://developers.cloudflare.com/stream/manage-video-library/using-webhooks/index.md) and [upload progress tracking](https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/index.md#track-upload-progress) | +| Verify an incoming processing notification | [Webhook authenticity](https://developers.cloudflare.com/stream/manage-video-library/using-webhooks/index.md#verify-webhook-authenticity) | +| Embed playback in a React application | [Stream Player and framework integrations](https://developers.cloudflare.com/stream/viewing-videos/using-the-stream-player/index.md) | +| Serve private videos | [Signed playback and signing examples](https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/index.md) | +| Broadcast from a browser | [Browser-based WebRTC](https://developers.cloudflare.com/stream/examples/browser-based-webrtc/index.md) | + +## Application Decisions + +- Check the user's permission before issuing upload URLs or playback tokens. Decide how uploaded video IDs map to application records and who may later view or delete them. +- Model uploading, processing, ready, and failed states in the UI. Use documented status and notification data; select retry and timeout policies to suit the application instead of assuming a fixed encoding deadline. +- Preserve the raw webhook request body for verification before applying state changes. Decide how the application's existing event handling reconciles notifications with stored video records. +- For live playback, decide whether viewers follow a channel across broadcasts or open a specific recording; see [live workflows](./api-live.md). + +See [configuration.md](./configuration.md) for setup and [gotchas.md](./gotchas.md) for diagnosis. diff --git a/.agents/skills/cloudflare/references/tail-workers/README.md b/.agents/skills/cloudflare/references/tail-workers/README.md new file mode 100644 index 0000000..56aaa70 --- /dev/null +++ b/.agents/skills/cloudflare/references/tail-workers/README.md @@ -0,0 +1,21 @@ +# Cloudflare Tail Workers + +Use Tail Workers when execution events need custom processing. Fetch the current documentation before implementing handlers, configuration, or integrations. + +| Task | Documentation | +| --- | --- | +| Decide whether custom processing is needed | [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md) | +| Export logs and traces to an observability destination | [OpenTelemetry export](https://developers.cloudflare.com/observability/export/opentelemetry/index.md) | +| Inspect a deployment interactively | [Real-time logs](https://developers.cloudflare.com/workers/observability/logs/real-time-logs/index.md) | +| Implement the consumer | [Tail handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md) | + +Before adding a Tail Worker, check whether built-in OpenTelemetry export meets the destination’s needs. Use the Tail Workers guide for the tradeoff, then identify the custom filtering or transformation that remains necessary. + +## In This Reference + +- [configuration.md](./configuration.md) — producer, consumer, destination, and environment setup +- [api.md](./api.md) — event fields, execution outcomes, and redaction +- [patterns.md](./patterns.md) — destination and filtering decisions +- [gotchas.md](./gotchas.md) — connection, data, and delivery investigation + +See [observability](../observability/README.md) for broader logging and tracing choices. diff --git a/.agents/skills/cloudflare/references/tail-workers/api.md b/.agents/skills/cloudflare/references/tail-workers/api.md new file mode 100644 index 0000000..44df5e9 --- /dev/null +++ b/.agents/skills/cloudflare/references/tail-workers/api.md @@ -0,0 +1,13 @@ +# Tail Workers APIs + +Fetch the handler reference for current event shapes and language examples instead of maintaining local interface definitions. + +| Task | Documentation | +| --- | --- | +| Implement the handler and understand asynchronous processing | [Tail handler syntax](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md#syntax) | +| Interpret execution outcomes, logs, exceptions, and timestamps | [Tail handler event reference](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md) | +| Inspect request fields and redaction behavior | [TailRequest](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md#tailrequest) | +| Interpret events from dynamic dispatch and user Workers | [Handler parameters](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md#parameters) | +| Write aggregated metrics | [Analytics Engine from Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md#use-analytics-engine-for-aggregated-metrics) | + +Choose the event fields required by the destination and preserve the distinction between execution outcome and HTTP response status. Review what data may be retained before bypassing redaction; the documented heuristics are not a complete application privacy policy. See [patterns.md](./patterns.md) for filter design. diff --git a/.agents/skills/cloudflare/references/tail-workers/configuration.md b/.agents/skills/cloudflare/references/tail-workers/configuration.md new file mode 100644 index 0000000..de151b1 --- /dev/null +++ b/.agents/skills/cloudflare/references/tail-workers/configuration.md @@ -0,0 +1,15 @@ +# Tail Workers Configuration + +Identify the producer Worker and consumer Worker as separate deployment resources before changing their configuration. + +| Task | Documentation | +| --- | --- | +| Connect a producer to a Tail Worker | [Configure Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md#configure-tail-workers) | +| Configure environments and resource bindings | [Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/index.md) | +| Configure destination credentials | [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/index.md) | +| Use built-in telemetry destinations instead | [OpenTelemetry export](https://developers.cloudflare.com/observability/export/opentelemetry/index.md) | +| Check availability and billing model | [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md) | +| Check runtime capacity | [Workers limits](https://developers.cloudflare.com/workers/platform/limits/index.md) | +| Check the handler required by the consumer | [Tail handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md) | + +Confirm which project owns each deployment and which environment the destination belongs to. Verify a known producer request reaches the intended destination before expanding coverage. See [api.md](./api.md) for payload handling and [gotchas.md](./gotchas.md) for investigation. diff --git a/.agents/skills/cloudflare/references/tail-workers/gotchas.md b/.agents/skills/cloudflare/references/tail-workers/gotchas.md new file mode 100644 index 0000000..bcdbcd3 --- /dev/null +++ b/.agents/skills/cloudflare/references/tail-workers/gotchas.md @@ -0,0 +1,16 @@ +# Tail Workers Troubleshooting + +Trace one known producer invocation through consumer execution and destination receipt to locate the failure. + +| Task | Documentation | +| --- | --- | +| Consumer receives no events or producer setup fails | [Configure Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md#configure-tail-workers) | +| Async processing or event interpretation is incorrect | [Tail handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md) | +| Filters confuse HTTP responses with execution failures | [TailItems](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md#tailitems) | +| Request data is missing or unexpectedly retained | [TailRequest](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md#tailrequest) | +| Inspect producer and consumer execution | [Real-time logs](https://developers.cloudflare.com/workers/observability/logs/real-time-logs/index.md) | +| Reconsider a custom exporter at higher volume | [OpenTelemetry export](https://developers.cloudflare.com/observability/export/opentelemetry/index.md) | + +Verify producer configuration, consumer identity, and destination credentials independently. Include both handled error responses and thrown exceptions when checking filters. Make destination failures observable without exposing the payload or credentials in diagnostic logs. Do not infer retention, retries, or batch guarantees from an example. + +See [api.md](./api.md) for data contracts and [patterns.md](./patterns.md) for destination decisions. diff --git a/.agents/skills/cloudflare/references/tail-workers/patterns.md b/.agents/skills/cloudflare/references/tail-workers/patterns.md new file mode 100644 index 0000000..acd853d --- /dev/null +++ b/.agents/skills/cloudflare/references/tail-workers/patterns.md @@ -0,0 +1,15 @@ +# Tail Workers Processing Decisions + +Define the destination contract and required event coverage before writing transformations. + +| Task | Documentation | +| --- | --- | +| Export supported telemetry without a custom consumer | [OpenTelemetry export](https://developers.cloudflare.com/observability/export/opentelemetry/index.md) | +| Process and forward custom execution events | [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md) | +| Aggregate metrics rather than retain individual events | [Analytics Engine from Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/index.md#use-analytics-engine-for-aggregated-metrics) | +| Choose filters based on execution and request fields | [Tail handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md) | +| Review sensitive request data handling | [TailRequest](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md#tailrequest) | + +Decide which producers, routes, failures, and successful requests each destination needs. If sampling is appropriate, specify how it affects the questions the data must answer. Minimize retained fields, test transformation and serialization against representative events, and check destination rejection behavior. + +Keep producer identity when combining events from multiple Workers. Define how delivery failures become visible before adding fallback storage or batching infrastructure. See [configuration.md](./configuration.md) for deployment ownership. diff --git a/.agents/skills/cloudflare/references/tunnel/README.md b/.agents/skills/cloudflare/references/tunnel/README.md new file mode 100644 index 0000000..20427d3 --- /dev/null +++ b/.agents/skills/cloudflare/references/tunnel/README.md @@ -0,0 +1,20 @@ +# Cloudflare Tunnel + +Use Tunnel to connect origin services to Cloudflare. Inspect the existing tunnel, management mode, and intended audience before choosing a setup. Fetch current docs for commands, configuration, and limits. + +| Task | Documentation | +| --- | --- | +| Create a remotely-managed tunnel or a temporary development tunnel | [Setup](https://developers.cloudflare.com/tunnel/get-started/index.md) | +| Maintain a tunnel managed through local files | [Create a locally-managed tunnel](https://developers.cloudflare.com/tunnel/features/locally-managed-tunnels/create-local-tunnel/index.md) | +| Publish an application and check protocol requirements | [Routing](https://developers.cloudflare.com/tunnel/concepts/routing/index.md) | +| Choose private networking, Workers VPC, or Access integration | [Integrations](https://developers.cloudflare.com/tunnel/integrations/index.md) | + +Decide whether the goal is a public application, authenticated private access, or connectivity from a Worker. Then identify who owns configuration and how it will be deployed; multiple environments alone do not require local management. + +## In This Reference + +- [configuration.md](./configuration.md) — management mode, ingress, and origin settings +- [networking.md](./networking.md) — firewall, connectivity, and private-network investigation +- [api.md](./api.md) — programmatic setup and tunnel operations +- [patterns.md](./patterns.md) — deployment and availability decisions +- [gotchas.md](./gotchas.md) — troubleshooting and operational checks diff --git a/.agents/skills/cloudflare/references/tunnel/api.md b/.agents/skills/cloudflare/references/tunnel/api.md new file mode 100644 index 0000000..6a1e48a --- /dev/null +++ b/.agents/skills/cloudflare/references/tunnel/api.md @@ -0,0 +1,14 @@ +# Tunnel APIs and Commands + +Fetch current operation schemas, permissions, and examples before automating tunnel changes. + +| Task | Documentation | +| --- | --- | +| Create a tunnel, configure ingress, and create application DNS through the API | [Setup](https://developers.cloudflare.com/tunnel/get-started/index.md) | +| List existing tunnels and inspect response fields | [List Cloudflare Tunnels API](https://developers.cloudflare.com/api/resources/zero_trust/subresources/tunnels/subresources/cloudflared/methods/list/index.md) | +| Retrieve and rotate tunnel tokens | [Tunnel tokens](https://developers.cloudflare.com/tunnel/reference/tunnel-tokens/index.md) | +| Manage local tunnels using the CLI | [Useful commands](https://developers.cloudflare.com/tunnel/features/locally-managed-tunnels/tunnel-useful-commands/index.md) | +| Configure public DNS and routing behavior | [Routing](https://developers.cloudflare.com/tunnel/concepts/routing/index.md) | +| Select private-network integration and its setup guide | [Integrations](https://developers.cloudflare.com/tunnel/integrations/index.md) | + +Identify the account, tunnel ID, and management mode before a write. Review existing routes before replacing configuration; distinguish tunnel lifecycle operations from DNS changes. Use [patterns.md](./patterns.md) when infrastructure as code owns these resources. diff --git a/.agents/skills/cloudflare/references/tunnel/configuration.md b/.agents/skills/cloudflare/references/tunnel/configuration.md new file mode 100644 index 0000000..5caf7ee --- /dev/null +++ b/.agents/skills/cloudflare/references/tunnel/configuration.md @@ -0,0 +1,14 @@ +# Tunnel Configuration + +Read the documentation for the existing management mode before changing routes or credentials. + +| Task | Documentation | +| --- | --- | +| Configure remotely-managed tunnels | [Setup](https://developers.cloudflare.com/tunnel/get-started/index.md) | +| Edit local ingress rules, service mappings, and validate matching | [Configuration file](https://developers.cloudflare.com/tunnel/features/locally-managed-tunnels/configuration-file/index.md) | +| Configure origin TLS, HTTP, and connection behavior | [Origin parameters](https://developers.cloudflare.com/tunnel/reference/origin-parameters/index.md) | +| Configure runtime flags and service arguments | [Run parameters](https://developers.cloudflare.com/tunnel/reference/run-parameters/index.md) | +| Manage remote tunnel tokens and rotation | [Tunnel tokens](https://developers.cloudflare.com/tunnel/reference/tunnel-tokens/index.md) | +| Choose service protocols and DNS routing | [Routing](https://developers.cloudflare.com/tunnel/concepts/routing/index.md) | + +Confirm which configuration source the running process uses, then review the routes affected by the change. Match origin settings to the actual service and certificate rather than copying settings from a different deployment. See [networking.md](./networking.md) for connectivity and [patterns.md](./patterns.md) for rollout decisions. diff --git a/.agents/skills/cloudflare/references/tunnel/gotchas.md b/.agents/skills/cloudflare/references/tunnel/gotchas.md new file mode 100644 index 0000000..d331932 --- /dev/null +++ b/.agents/skills/cloudflare/references/tunnel/gotchas.md @@ -0,0 +1,15 @@ +# Tunnel Troubleshooting + +Capture the tunnel status, failing route, management mode, and cloudflared logs before changing settings. + +| Task | Documentation | +| --- | --- | +| Tunnel fails to connect or reports an error | [Troubleshooting](https://developers.cloudflare.com/tunnel/troubleshooting/index.md) | +| Tunnel is healthy but an HTTPS application fails or redirects | [HTTPS origins](https://developers.cloudflare.com/tunnel/troubleshooting/https-origins/index.md) | +| Inspect connection health and application diagnostics | [Monitoring](https://developers.cloudflare.com/tunnel/observability/index.md) | +| Check local configuration and rule matching | [Configuration file](https://developers.cloudflare.com/tunnel/features/locally-managed-tunnels/configuration-file/index.md) | +| Connections behave unexpectedly after token rotation | [Tunnel tokens](https://developers.cloudflare.com/tunnel/reference/tunnel-tokens/index.md) | +| Check replica capacity or firewall requirements | [Configuration](https://developers.cloudflare.com/tunnel/configuration/index.md) | +| Update an existing installation | [Update cloudflared](https://developers.cloudflare.com/tunnel/guides/update-cloudflared/index.md) | + +Separate tunnel health from origin availability. Check the service address, protocol, and certificate before relaxing verification. Scope operational changes to the intended tunnel and replicas; follow the documented rotation and upgrade sequence instead of stopping every cloudflared process on a host. See [networking.md](./networking.md) for connectivity checks. diff --git a/.agents/skills/cloudflare/references/tunnel/networking.md b/.agents/skills/cloudflare/references/tunnel/networking.md new file mode 100644 index 0000000..8dcaa46 --- /dev/null +++ b/.agents/skills/cloudflare/references/tunnel/networking.md @@ -0,0 +1,14 @@ +# Tunnel Networking + +Investigate the connection from cloudflared to Cloudflare separately from the connection to the origin and the client access path. + +| Task | Documentation | +| --- | --- | +| Determine required egress ports and destinations | [Firewall rules](https://developers.cloudflare.com/tunnel/configuration/index.md#firewall-rules) | +| Diagnose DNS, QUIC, or TCP connectivity | [Connection errors](https://developers.cloudflare.com/tunnel/troubleshooting/index.md#connection-errors) | +| Configure transport and runtime options | [Run parameters](https://developers.cloudflare.com/tunnel/reference/run-parameters/index.md) | +| Check service protocols and client requirements | [Routing](https://developers.cloudflare.com/tunnel/concepts/routing/index.md) | +| Set up private-network access with Cloudflare One | [Connect private networks with cloudflared](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/private-net/cloudflared/index.md) | +| Inspect tunnel health, logs, and metrics | [Monitoring](https://developers.cloudflare.com/tunnel/observability/index.md) | + +Test from the machine or container running cloudflared. Compare the actual firewall policy with the current documented destinations; do not infer transport ports from the origin protocol. For private access, follow the linked Cloudflare One setup for routes and device-client configuration instead of reusing public-hostname instructions. See [gotchas.md](./gotchas.md) for origin failures. diff --git a/.agents/skills/cloudflare/references/tunnel/patterns.md b/.agents/skills/cloudflare/references/tunnel/patterns.md new file mode 100644 index 0000000..7344c75 --- /dev/null +++ b/.agents/skills/cloudflare/references/tunnel/patterns.md @@ -0,0 +1,15 @@ +# Tunnel Deployment Decisions + +Choose deployment ownership and availability requirements before adapting an example. + +| Task | Documentation | +| --- | --- | +| Install a remotely-managed tunnel on a host or in Docker | [Setup](https://developers.cloudflare.com/tunnel/get-started/index.md) | +| Deploy cloudflared inside a cluster | [Kubernetes](https://developers.cloudflare.com/tunnel/guides/kubernetes/index.md) | +| Manage tunnel infrastructure declaratively | [Terraform](https://developers.cloudflare.com/tunnel/guides/terraform/index.md) | +| Deploy replicas and check current capacity | [Replicas and high availability](https://developers.cloudflare.com/tunnel/configuration/index.md#replicas-and-high-availability) | +| Choose redundancy or explicit traffic steering | [Routing](https://developers.cloudflare.com/tunnel/concepts/routing/index.md) | +| Plan upgrades for the existing installation method | [Update cloudflared](https://developers.cloudflare.com/tunnel/guides/update-cloudflared/index.md) | +| Add authentication or private connectivity | [Integrations](https://developers.cloudflare.com/tunnel/integrations/index.md) | + +Establish which service each replica can reach and how configuration and credentials reach each host. Decide whether simple redundancy meets the requirement or whether health-based routing needs a load balancer. Verify replacement replicas before retiring existing ones, and include application-level checks in the rollout. See [api.md](./api.md) for programmatic operations. diff --git a/.agents/skills/cloudflare/references/vectorize/README.md b/.agents/skills/cloudflare/references/vectorize/README.md new file mode 100644 index 0000000..9ad241b --- /dev/null +++ b/.agents/skills/cloudflare/references/vectorize/README.md @@ -0,0 +1,22 @@ +# Cloudflare Vectorize + +Use Vectorize when you need to control embeddings, vector indexing, and retrieval for semantic search, recommendations, or RAG. For a managed retrieval pipeline, see [AI Search](../ai-search/README.md). + +Fetch current documentation before implementing. Start with the [Vectorize documentation index](https://developers.cloudflare.com/vectorize/llms.txt) to discover pages; load only those relevant to the task. Treat the docs as the source of truth for APIs, configuration, models, limits, and pricing. + +## Task routing + +| Task | Read | +|------|------| +| Create an index and connect a Worker | [Configuration](configuration.md) and [Introduction to Vectorize](https://developers.cloudflare.com/vectorize/get-started/intro/index.md) | +| Insert, update, query, retrieve, or delete vectors | [API routes](api.md) | +| Generate embeddings, build RAG, or partition tenant data | [Patterns](patterns.md) | +| Diagnose missing matches, metadata, or rejected requests | [Gotchas](gotchas.md) | + +## Decisions to make first + +- Use a consistent embedding model and preprocessing for stored vectors and queries. Matching dimensions alone does not make different models' embeddings compatible. +- Choose dimensions from the embedding output and a distance metric appropriate to that model. Changing either requires a new index; check [index configuration and scoring semantics](https://developers.cloudflare.com/vectorize/best-practices/create-indexes/index.md) before choosing thresholds. +- Plan filterable metadata before ingestion. Adding an index later requires re-upserting existing vectors to index that metadata; see [metadata filtering](https://developers.cloudflare.com/vectorize/reference/metadata-filtering/index.md). +- A namespace partitions search; your application must authorize access and derive tenant scope from trusted identity. See [tenant patterns](patterns.md). +- Design for asynchronous mutation visibility rather than assuming a completed write is already searchable. See [mutation semantics](https://developers.cloudflare.com/vectorize/reference/client-api/index.md). diff --git a/.agents/skills/cloudflare/references/vectorize/api.md b/.agents/skills/cloudflare/references/vectorize/api.md new file mode 100644 index 0000000..358a45d --- /dev/null +++ b/.agents/skills/cloudflare/references/vectorize/api.md @@ -0,0 +1,18 @@ +# Vectorize API routes + +Fetch the relevant section of the [Workers binding API](https://developers.cloudflare.com/vectorize/reference/client-api/index.md) before writing calls or types. + +| Task | Current documentation | +|------|-----------------------| +| Vector shape, binding, and generated TypeScript types | [Vectorize API](https://developers.cloudflare.com/vectorize/reference/client-api/index.md) | +| Insert, upsert, retrieve by ID, delete, or inspect an index | [Operations](https://developers.cloudflare.com/vectorize/reference/client-api/index.md#operations) | +| Query by vector or ID; choose returned metadata, values, and scoring precision | [Query vectors](https://developers.cloudflare.com/vectorize/best-practices/query-vectors/index.md) and [query options](https://developers.cloudflare.com/vectorize/reference/client-api/index.md#query-vectors) | +| Filter by metadata, combine conditions, or use nested properties | [Metadata filtering](https://developers.cloudflare.com/vectorize/reference/metadata-filtering/index.md) | +| Batch ingestion and select vector formats | [Insert vectors](https://developers.cloudflare.com/vectorize/best-practices/insert-vectors/index.md) and [current limits](https://developers.cloudflare.com/vectorize/platform/limits/index.md) | +| Manage indexes or vectors outside a Worker | [Wrangler commands](https://developers.cloudflare.com/vectorize/reference/wrangler-commands/index.md) and [REST API](https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/list/index.md) | + +## Operation choices + +- Choose insert when existing IDs should be preserved; choose upsert when they should be replaced. Upsert replaces the whole vector, including metadata, so provide the complete intended record. +- Request only the values and metadata the caller needs. Indexed metadata can omit fields or truncate strings; full metadata and vector values change query limits and latency. Fetch the current query options before choosing a result count. +- Treat accepted mutations and query visibility as separate events. Use current mutation guidance when implementing ingestion verification or read-after-write behavior. diff --git a/.agents/skills/cloudflare/references/vectorize/configuration.md b/.agents/skills/cloudflare/references/vectorize/configuration.md new file mode 100644 index 0000000..6e083ac --- /dev/null +++ b/.agents/skills/cloudflare/references/vectorize/configuration.md @@ -0,0 +1,19 @@ +# Vectorize configuration routes + +| Task | Current documentation | +|------|-----------------------| +| Create an index, choose dimensions and metric | [Create indexes](https://developers.cloudflare.com/vectorize/best-practices/create-indexes/index.md) | +| Bind an index to a Worker, develop, deploy, and verify queries | [Introduction to Vectorize](https://developers.cloudflare.com/vectorize/get-started/intro/index.md) | +| Configure bindings and generate types | [Binding and TypeScript guidance](https://developers.cloudflare.com/vectorize/reference/client-api/index.md#binding-to-a-worker) | +| Create, list, or delete metadata indexes | [Metadata filtering](https://developers.cloudflare.com/vectorize/reference/metadata-filtering/index.md) and [Wrangler commands](https://developers.cloudflare.com/vectorize/reference/wrangler-commands/index.md) | +| Manage indexes and vectors through the CLI | [Wrangler commands](https://developers.cloudflare.com/vectorize/reference/wrangler-commands/index.md) | +| Upload NDJSON and batch ingestion | [Insert vectors](https://developers.cloudflare.com/vectorize/best-practices/insert-vectors/index.md) | +| Check capacity, payload, namespace, or batch constraints | [Limits](https://developers.cloudflare.com/vectorize/platform/limits/index.md) | + +## Configuration decisions + +Confirm the embedding model, output dimensions, and distance metric before provisioning: dimensions and metric cannot be changed in place. Plan a new index and re-embedding where needed when changing models. + +Create metadata indexes before ingesting vectors that must be filterable. If adding one to an existing dataset, plan to re-upsert the affected vectors after index creation. + +Choose metadata granularity around actual queries. For range filters over high-cardinality fields, consider buckets that preserve the application's required precision; do not bucket identifiers used for exact matches. Fetch the [cardinality guidance](https://developers.cloudflare.com/vectorize/best-practices/insert-vectors/index.md#performance-tips-when-filtering-by-metadata) before designing the schema. diff --git a/.agents/skills/cloudflare/references/vectorize/gotchas.md b/.agents/skills/cloudflare/references/vectorize/gotchas.md new file mode 100644 index 0000000..ff48b17 --- /dev/null +++ b/.agents/skills/cloudflare/references/vectorize/gotchas.md @@ -0,0 +1,15 @@ +# Vectorize troubleshooting routes + +Fetch current documentation before diagnosing a numeric limit, API error, or delayed mutation. Do not infer batch sizes or result limits from old snippets. + +| Symptom or decision | What to check | Current documentation | +|---------------------|---------------|-----------------------| +| A write succeeded but search has not changed | Mutations are asynchronous; acceptance does not guarantee query visibility | [Insert, upsert, and delete semantics](https://developers.cloudflare.com/vectorize/reference/client-api/index.md#operations) | +| Ingestion is slow or a batch is rejected | Batch size depends on the interface; inspect throughput and payload constraints | [Write throughput](https://developers.cloudflare.com/vectorize/best-practices/insert-vectors/index.md#improve-write-throughput) and [limits](https://developers.cloudflare.com/vectorize/platform/limits/index.md) | +| Query count is rejected or metadata is incomplete | Returned values and metadata affect query limits; indexed metadata can be truncated | [Query options](https://developers.cloudflare.com/vectorize/reference/client-api/index.md#query-vectors) | +| Metadata filters return no matches | Confirm field type, operators, nesting, and index creation; re-upsert data written before the metadata index existed | [Metadata filtering](https://developers.cloudflare.com/vectorize/reference/metadata-filtering/index.md) | +| Query has no matches or poor relevance | Check embedding model and dimensions, metric, namespace, filters, and mutation visibility | [Query vectors](https://developers.cloudflare.com/vectorize/best-practices/query-vectors/index.md) and [index configuration](https://developers.cloudflare.com/vectorize/best-practices/create-indexes/index.md) | +| Existing IDs or metadata behave unexpectedly on update | Insert preserves existing IDs; upsert replaces the full vector and metadata | [Mutation semantics](https://developers.cloudflare.com/vectorize/reference/client-api/index.md#operations) | +| Capacity or model output no longer fits | Check current limits and model output dimensions; changing dimensions or metric requires another index | [Limits](https://developers.cloudflare.com/vectorize/platform/limits/index.md) and [create indexes](https://developers.cloudflare.com/vectorize/best-practices/create-indexes/index.md) | + +For changes to embedding providers or tenant boundaries, also read [pattern decisions](patterns.md). diff --git a/.agents/skills/cloudflare/references/vectorize/patterns.md b/.agents/skills/cloudflare/references/vectorize/patterns.md new file mode 100644 index 0000000..531268e --- /dev/null +++ b/.agents/skills/cloudflare/references/vectorize/patterns.md @@ -0,0 +1,24 @@ +# Vectorize pattern routes + +| Task | Current documentation | +|------|-----------------------| +| Generate and query Workers AI embeddings | [Vectorize and Workers AI](https://developers.cloudflare.com/vectorize/get-started/embeddings/index.md) | +| Query with embeddings from OpenAI | [OpenAI integration](https://developers.cloudflare.com/vectorize/best-practices/query-vectors/index.md#openai) | +| Choose embedding dimensions and distance metric | [Create indexes](https://developers.cloudflare.com/vectorize/best-practices/create-indexes/index.md) | +| Build a retrieval-augmented generation application | [Workers AI RAG tutorial](https://developers.cloudflare.com/workers-ai/guides/tutorials/build-a-retrieval-augmented-generation-ai/index.md) | +| Link search results to source documents | [Vector metadata](https://developers.cloudflare.com/vectorize/best-practices/insert-vectors/index.md#metadata) | +| Partition vectors by tenant | [Namespaces](https://developers.cloudflare.com/vectorize/best-practices/insert-vectors/index.md#namespaces) and [namespace versus metadata filtering](https://developers.cloudflare.com/vectorize/reference/metadata-filtering/index.md#namespace-versus-metadata-filtering) | +| Combine similarity search with categorical or range filters | [Metadata filtering](https://developers.cloudflare.com/vectorize/reference/metadata-filtering/index.md) | +| Ingest or update vectors in batches | [Insert vectors](https://developers.cloudflare.com/vectorize/best-practices/insert-vectors/index.md) and [limits](https://developers.cloudflare.com/vectorize/platform/limits/index.md) | + +## Embedding and retrieval decisions + +Keep ingestion and query embeddings compatible: use the same model and preprocessing, and extract the individual vector from the provider's documented response shape. Fetch the selected model's current documentation for dimensions and input requirements. + +For RAG, store a reliable reference to the source content and request the metadata needed to resolve it. Handle missing or deleted source documents before passing retrieved context to generation. + +## Tenant scope + +Namespaces and metadata filters narrow searches; they do not authenticate the caller. Derive the permitted tenant scope from trusted identity and enforce it on every relevant read and write, including ID-based retrieval and deletion. Do not assume a namespace query option protects other operations. + +Choose namespace or metadata partitioning based on the required query scope and current limits. Both narrow the search space; avoid assuming metadata filtering happens after vector search. If tenant IDs are stored in metadata, create the corresponding metadata index before ingestion. diff --git a/.agents/skills/cloudflare/references/waf/README.md b/.agents/skills/cloudflare/references/waf/README.md new file mode 100644 index 0000000..5cfb2e8 --- /dev/null +++ b/.agents/skills/cloudflare/references/waf/README.md @@ -0,0 +1,21 @@ +# Cloudflare WAF + +Use this reference for managed protection, custom request policies, rate limiting, and investigation of blocked traffic. Read the relevant developer documentation before implementing; it owns schemas, expressions, ruleset IDs, phase order, and plan availability. + +| Task | Start here | +|------|------------| +| Choose and enable WAF protections | [Get started](https://developers.cloudflare.com/waf/get-started/index.md) | +| Deploy managed protection | [Managed rules deployment](https://developers.cloudflare.com/waf/managed-rules/deploy-api/index.md) | +| Match application-specific requests | [Custom rules](https://developers.cloudflare.com/waf/custom-rules/create-api/index.md) | +| Limit request volume | [Rate limiting](https://developers.cloudflare.com/waf/rate-limiting-rules/create-api/index.md) | +| Understand score-based detection | [Attack score](https://developers.cloudflare.com/waf/detections/attack-score/index.md) | +| Diagnose blocked or unmitigated requests | [Managed rules troubleshooting](https://developers.cloudflare.com/waf/managed-rules/troubleshooting/index.md) | + +Identify the target account or zone and inspect existing rules before planning a change. Keep the requested traffic scope explicit, especially for exceptions and account-wide deployments. + +## Reading Order + +1. [configuration.md](configuration.md) — deployment method and existing configuration. +2. [api.md](api.md) — API workflows and expression references. +3. [patterns.md](patterns.md) — choose a protection or exception workflow. +4. [gotchas.md](gotchas.md) — diagnose ordering, scope, and false positives. diff --git a/.agents/skills/cloudflare/references/waf/api.md b/.agents/skills/cloudflare/references/waf/api.md new file mode 100644 index 0000000..397c42c --- /dev/null +++ b/.agents/skills/cloudflare/references/waf/api.md @@ -0,0 +1,17 @@ +# WAF API Reference + +Read the matching workflow before writing API calls or translating them into the project's installed SDK. Retrieve identifiers from the target account or zone; do not reuse example IDs. + +| Task | Documentation | +|------|---------------| +| Inspect the entry point and add custom rules | [Create a custom rule via API](https://developers.cloudflare.com/waf/custom-rules/create-api/index.md) | +| Discover managed rulesets and deploy them | [Deploy managed rules via API](https://developers.cloudflare.com/waf/managed-rules/deploy-api/index.md) | +| Create rate limits with the current request schema | [Create a rate limiting rule via API](https://developers.cloudflare.com/waf/rate-limiting-rules/create-api/index.md) | +| Select counting characteristics, expressions, periods, and mitigation behavior | [Rate limiting parameters](https://developers.cloudflare.com/waf/rate-limiting-rules/parameters/index.md) | +| Replace a ruleset deliberately or choose an additive operation | [Update or deploy a ruleset](https://developers.cloudflare.com/ruleset-engine/rulesets-api/update/index.md) | +| Construct expressions using supported fields, operators, and functions | [Rules language](https://developers.cloudflare.com/ruleset-engine/rules-language/index.md) | +| Choose actions and understand terminating behavior | [Actions reference](https://developers.cloudflare.com/ruleset-engine/rules-language/actions/index.md) | +| Override managed rules, tags, or a ruleset | [Managed ruleset overrides](https://developers.cloudflare.com/ruleset-engine/managed-rulesets/override-managed-ruleset/index.md) | +| Choose exactly what a skip rule bypasses | [Skip options](https://developers.cloudflare.com/waf/custom-rules/skip/options/index.md) | + +For an addition, prefer the workflow's operation that adds a rule to an existing ruleset. When replacing a ruleset, include every rule that must remain; review the resulting rule list before applying it. See [gotchas.md](gotchas.md) for scope and evaluation checks. diff --git a/.agents/skills/cloudflare/references/waf/configuration.md b/.agents/skills/cloudflare/references/waf/configuration.md new file mode 100644 index 0000000..3b4b46d --- /dev/null +++ b/.agents/skills/cloudflare/references/waf/configuration.md @@ -0,0 +1,18 @@ +# WAF Configuration + +Identify the account or zone, existing rulesets, and the system managing them before choosing a deployment method. Use the permission requirements in the selected workflow rather than a copied token-permission list. + +| Task | Documentation | +|------|---------------| +| Enable protections and configure them in the dashboard | [WAF get started](https://developers.cloudflare.com/waf/get-started/index.md) | +| Configure custom rules through the API or SDK | [Custom rules API workflow](https://developers.cloudflare.com/waf/custom-rules/create-api/index.md) | +| Configure managed rules through the API or SDK | [Managed rules API workflow](https://developers.cloudflare.com/waf/managed-rules/deploy-api/index.md) | +| Configure rate limiting through the API or SDK | [Rate limiting API workflow](https://developers.cloudflare.com/waf/rate-limiting-rules/create-api/index.md) | +| Manage custom rules with Terraform | [Custom rules Terraform guide](https://developers.cloudflare.com/terraform/additional-configurations/waf-custom-rules/index.md) | +| Manage managed rulesets with Terraform | [Managed rules Terraform guide](https://developers.cloudflare.com/terraform/additional-configurations/waf-managed-rulesets/index.md) | +| Manage rate limits with Terraform | [Rate limiting Terraform guide](https://developers.cloudflare.com/terraform/additional-configurations/rate-limiting-rules/index.md) | +| Decide account versus zone placement and phase | [WAF phases](https://developers.cloudflare.com/waf/reference/phases/index.md) | + +When adopting existing rules into Terraform, follow the selected guide's import instructions and inspect the plan for unintended removals. Keep the existing management tool when it fits the task. For SDK or Pulumi projects, verify the installed package's types before translating the documented API workflow. + +Continue with [patterns.md](patterns.md) for protection choices and [gotchas.md](gotchas.md) for diagnostics. diff --git a/.agents/skills/cloudflare/references/waf/gotchas.md b/.agents/skills/cloudflare/references/waf/gotchas.md new file mode 100644 index 0000000..aaa0d66 --- /dev/null +++ b/.agents/skills/cloudflare/references/waf/gotchas.md @@ -0,0 +1,19 @@ +# WAF Gotchas + +Use observed requests and the deployed ruleset definitions to diagnose behavior before changing protection. + +| Symptom or decision | Documentation to read | +|---------------------|-----------------------| +| A rule executes earlier or later than expected | [WAF phases and account/zone order](https://developers.cloudflare.com/waf/reference/phases/index.md) and [terminating actions](https://developers.cloudflare.com/ruleset-engine/rules-language/actions/index.md) | +| A skip rule leaves a protection active | [Skip scope, phases, products, and logging](https://developers.cloudflare.com/waf/custom-rules/skip/options/index.md) | +| Updating a ruleset removes unrelated rules | [Ruleset replacement semantics](https://developers.cloudflare.com/ruleset-engine/rulesets-api/update/index.md) | +| An expression fails to parse | [Rules language elements](https://developers.cloudflare.com/ruleset-engine/rules-language/index.md) | +| Score-based rules match unexpected traffic | [Attack score meaning, special values, and plan availability](https://developers.cloudflare.com/waf/detections/attack-score/index.md) | +| Managed overrides conflict | [Override precedence and scope](https://developers.cloudflare.com/ruleset-engine/managed-rulesets/override-managed-ruleset/index.md) | +| Legitimate traffic is blocked, or attacks reach the origin | [False-positive and false-negative investigation](https://developers.cloudflare.com/waf/managed-rules/troubleshooting/index.md) | +| Rate limits affect shared-IP users or count unexpected requests | [Characteristics, NAT support, and counting expressions](https://developers.cloudflare.com/waf/rate-limiting-rules/parameters/index.md) | +| API creation fails or the request body is unclear | [Custom rule creation](https://developers.cloudflare.com/waf/custom-rules/create-api/index.md), [managed deployment](https://developers.cloudflare.com/waf/managed-rules/deploy-api/index.md), or [rate limit creation](https://developers.cloudflare.com/waf/rate-limiting-rules/create-api/index.md) | + +Check account versus zone scope, rule position, and the action that actually handled the request. For rate limiting, inspect matching and counting criteria separately. Avoid broad exceptions as a shortcut for diagnosing a single false positive. + +Return to [api.md](api.md) for operations and [configuration.md](configuration.md) for deployment ownership. diff --git a/.agents/skills/cloudflare/references/waf/patterns.md b/.agents/skills/cloudflare/references/waf/patterns.md new file mode 100644 index 0000000..a035832 --- /dev/null +++ b/.agents/skills/cloudflare/references/waf/patterns.md @@ -0,0 +1,17 @@ +# WAF Patterns + +Choose the workflow that matches the requested outcome, then retrieve its current examples. Make rule scope explicit and review the affected traffic before enforcement. + +| Outcome | Documentation | +|---------|---------------| +| Apply managed protection to a zone | [Deploy managed rules](https://developers.cloudflare.com/waf/managed-rules/deploy-api/index.md) | +| Change a managed rule's behavior or evaluate it with logging | [Override a managed ruleset](https://developers.cloudflare.com/ruleset-engine/managed-rulesets/override-managed-ruleset/index.md) | +| Enforce an application-specific request policy | [Create a custom rule](https://developers.cloudflare.com/waf/custom-rules/create-api/index.md) | +| Use attack detection in a request policy | [Attack score semantics and availability](https://developers.cloudflare.com/waf/detections/attack-score/index.md) | +| Protect a login or API endpoint from excessive requests | [Rate limiting API examples](https://developers.cloudflare.com/waf/rate-limiting-rules/create-api/index.md) and [counting parameters](https://developers.cloudflare.com/waf/rate-limiting-rules/parameters/index.md) | +| Exempt narrowly identified traffic from selected protections | [Available skip options](https://developers.cloudflare.com/waf/custom-rules/skip/options/index.md) | +| Adjust protection after a false positive | [Managed rules troubleshooting](https://developers.cloudflare.com/waf/managed-rules/troubleshooting/index.md) | + +For a false positive, identify the matching rule and request scope before choosing an exception or override. Keep the adjustment as narrow as the evidence supports. For combined protections, check [WAF phases](https://developers.cloudflare.com/waf/reference/phases/index.md) before deciding where an exception belongs. + +Use [configuration.md](configuration.md) to select the deployment method and [api.md](api.md) to preserve existing rules while changing it. diff --git a/.agents/skills/cloudflare/references/workers-ai/README.md b/.agents/skills/cloudflare/references/workers-ai/README.md new file mode 100644 index 0000000..9e069e6 --- /dev/null +++ b/.agents/skills/cloudflare/references/workers-ai/README.md @@ -0,0 +1,24 @@ +# Cloudflare Workers AI + +Use Workers AI for managed model inference from Workers or an external service. Fetch the relevant documentation before choosing a model or writing integration code; model availability, schemas, capabilities, limits, and prices change independently. + +## Choose a model + +Start with the [model catalog](https://developers.cloudflare.com/workers-ai/models/index.md) and open the selected model's page for its exact identifier, input/output schema, context window, and supported features. Compare candidates on the user's task, language, quality requirements, latency, and [current pricing](https://developers.cloudflare.com/workers-ai/platform/pricing/index.md). Evaluate with representative inputs rather than treating model size as a quality or cost ranking. + +For tool use, streaming, or structured output, confirm support for the selected model and integration. For embeddings, check output dimensions and compatibility with the existing index; changing the model may require re-embedding stored documents, even if dimensions match. + +## Route by task + +- [configuration.md](./configuration.md): choose an integration, configure bindings and types, or set up development. +- [api.md](./api.md): find inference schemas, streaming, tool calling, and structured output. +- [patterns.md](./patterns.md): choose direct generation or RAG, and find integration examples. +- [gotchas.md](./gotchas.md): diagnose binding, schema, limit, pricing, and SDK issues. + +If a topic is missing, use the [Workers AI documentation index](https://developers.cloudflare.com/workers-ai/llms.txt) to find its current page. + +## Related products + +- [Vectorize](../vectorize/): vector storage and retrieval. +- [AI Gateway](../ai-gateway/): inference analytics, caching, and request controls. +- [Workers](https://developers.cloudflare.com/workers/index.md): runtime and application hosting. diff --git a/.agents/skills/cloudflare/references/workers-ai/api.md b/.agents/skills/cloudflare/references/workers-ai/api.md new file mode 100644 index 0000000..64c8f24 --- /dev/null +++ b/.agents/skills/cloudflare/references/workers-ai/api.md @@ -0,0 +1,13 @@ +# Workers AI API + +Fetch the selected model's page from the [model catalog](https://developers.cloudflare.com/workers-ai/models/index.md) for request fields, output format, dimensions, and examples. Text, embeddings, images, audio, and translation do not share one response schema. + +| Task | Documentation | +|------|---------------| +| Invoke inference through a Worker binding | [Workers bindings](https://developers.cloudflare.com/workers-ai/configuration/bindings/index.md) | +| Invoke inference over HTTP | [REST API reference](https://developers.cloudflare.com/api/resources/ai/methods/run/index.md) | +| Stream text or use SDK abstractions | [Vercel AI SDK](https://developers.cloudflare.com/workers-ai/configuration/ai-sdk/index.md), or the selected model's streaming example | +| Define tools and handle tool results | [Function calling](https://developers.cloudflare.com/workers-ai/features/function-calling/index.md) | +| Request structured output | [JSON mode](https://developers.cloudflare.com/workers-ai/features/json-mode/index.md) | + +Use the response and stream format documented for the chosen integration. Do not assume native binding streams are parsed objects or apply OpenAI response parsing to every model. Check the model's batching support and limits before combining inputs in one request. diff --git a/.agents/skills/cloudflare/references/workers-ai/configuration.md b/.agents/skills/cloudflare/references/workers-ai/configuration.md new file mode 100644 index 0000000..c561de4 --- /dev/null +++ b/.agents/skills/cloudflare/references/workers-ai/configuration.md @@ -0,0 +1,16 @@ +# Workers AI Configuration + +Read the setup guide for the application's existing integration and installed SDK/Wrangler versions before adapting configuration. + +| Task | Documentation | +|------|---------------| +| Create and develop a Worker with Workers AI | [Workers and Wrangler setup](https://developers.cloudflare.com/workers-ai/get-started/workers-wrangler/index.md) | +| Add an AI binding to an existing Worker | [Workers bindings](https://developers.cloudflare.com/workers-ai/configuration/bindings/index.md) | +| Generate environment and runtime types | [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/index.md) | +| Call inference from outside Workers | [REST API setup and authentication](https://developers.cloudflare.com/workers-ai/get-started/rest-api/index.md) | +| Use the Vercel AI SDK | [AI SDK integration](https://developers.cloudflare.com/workers-ai/configuration/ai-sdk/index.md) | +| Adapt an existing OpenAI SDK client | [OpenAI compatible endpoints](https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/index.md) | + +Prefer the native binding for a Worker that does not need an SDK abstraction; use REST for external services. Preserve an existing SDK integration when it meets the task, and check its supported endpoints and model features before substituting providers. + +Local Worker execution and local inference are different: Workers AI inference uses the Cloudflare account even during local development and consumes usage. Follow the current setup guide for development configuration; do not assume the entire Worker must run remotely. diff --git a/.agents/skills/cloudflare/references/workers-ai/gotchas.md b/.agents/skills/cloudflare/references/workers-ai/gotchas.md new file mode 100644 index 0000000..1b6b919 --- /dev/null +++ b/.agents/skills/cloudflare/references/workers-ai/gotchas.md @@ -0,0 +1,15 @@ +# Workers AI Troubleshooting + +Use the actual error, model identifier, integration, and installed versions to choose the relevant reference. + +| Symptom or decision | Documentation and checks | +|---------------------|--------------------------| +| Missing binding or types | [Binding configuration](https://developers.cloudflare.com/workers-ai/configuration/bindings/index.md) and [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/index.md); check the environment being run | +| Development inference fails | [Workers and Wrangler setup](https://developers.cloudflare.com/workers-ai/get-started/workers-wrangler/index.md); check account access and binding setup | +| Unknown model, invalid input, or unexpected response | Open the exact model in the [catalog](https://developers.cloudflare.com/workers-ai/models/index.md); check its schema, context window, and feature support | +| Inference error or retry decision | [Error codes and HTTP statuses](https://developers.cloudflare.com/workers-ai/platform/errors/index.md) | +| Throttling or concurrency planning | [Current limits](https://developers.cloudflare.com/workers-ai/platform/limits/index.md) | +| Usage or cost estimate | [Current pricing](https://developers.cloudflare.com/workers-ai/platform/pricing/index.md); use the selected model's billing units and expected workload | +| Old SDK examples fail | [Native binding](https://developers.cloudflare.com/workers-ai/configuration/bindings/index.md), [AI SDK](https://developers.cloudflare.com/workers-ai/configuration/ai-sdk/index.md), or [OpenAI compatibility](https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/index.md), according to the integration | + +Do not copy an error-code mapping, per-request neuron estimate, or context-window range from another model or an older example. Measure latency for the intended workload rather than promising a fixed cold-start or inference time. diff --git a/.agents/skills/cloudflare/references/workers-ai/patterns.md b/.agents/skills/cloudflare/references/workers-ai/patterns.md new file mode 100644 index 0000000..bd946ea --- /dev/null +++ b/.agents/skills/cloudflare/references/workers-ai/patterns.md @@ -0,0 +1,14 @@ +# Workers AI Patterns + +Use direct generation when the supplied context fits the selected model and retrieval is unnecessary. Use RAG when answers need grounding in a document corpus or relevant passages must be selected from larger data; decide from the actual model context budget rather than a fixed token threshold. + +| Task | Documentation | +|------|---------------| +| Build retrieval with Workers AI, Vectorize, and document storage | [RAG tutorial](https://developers.cloudflare.com/workers-ai/guides/tutorials/build-a-retrieval-augmented-generation-ai/index.md) | +| Stream responses or integrate tool calling in an SDK application | [AI SDK integration](https://developers.cloudflare.com/workers-ai/configuration/ai-sdk/index.md) | +| Constrain generated JSON | [JSON mode](https://developers.cloudflare.com/workers-ai/features/json-mode/index.md) | +| Add caching, retries, or model fallbacks | [Caching](https://developers.cloudflare.com/ai-gateway/features/caching/index.md), [request handling](https://developers.cloudflare.com/ai-gateway/configuration/request-handling/index.md), and [dynamic routing](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/index.md) | + +Treat tutorial models as examples; select models using the [model criteria](./README.md#choose-a-model). For RAG, embed queries and documents with compatible models and match the index dimensions to the embeddings. Budget for retrieval and embedding work as well as generation. + +Before adding a fallback model, verify that it can satisfy the same schema, context, and tool requirements. For retry decisions, distinguish transient failures from invalid inputs or configuration using the [error and limit references](./gotchas.md). diff --git a/.agents/skills/cloudflare/references/workflows/README.md b/.agents/skills/cloudflare/references/workflows/README.md new file mode 100644 index 0000000..adf61c2 --- /dev/null +++ b/.agents/skills/cloudflare/references/workflows/README.md @@ -0,0 +1,23 @@ +# Cloudflare Workflows + +Use Workflows for durable, multi-step jobs that must retry, wait, and resume without losing completed work. An instance is one execution; steps define persistence and retry boundaries. + +Fetch the relevant current documentation before implementing. API shapes, configuration, testing helpers, limits, and examples belong in the docs rather than in this reference. + +- **Start a project:** [Build your first Workflow](https://developers.cloudflare.com/workflows/get-started/guide/index.md) covers scaffolding, configuration, deployment, and a first instance. +- **Design durable execution:** [Rules of Workflows](https://developers.cloudflare.com/workflows/build/rules-of-workflows/index.md) covers step boundaries, replay, state, and idempotency. +- **Implement or manage an instance:** [Workers API](https://developers.cloudflare.com/workflows/build/workers-api/index.md) covers steps, instance operations, parameters, and return types. +- **Check capacity and cost:** fetch [limits](https://developers.cloudflare.com/workflows/reference/limits/index.md) and [pricing](https://developers.cloudflare.com/workflows/reference/pricing/index.md) for the target plan. + +## In This Reference + +- [configuration.md](./configuration.md) — setup, bindings, retry configuration, and local development +- [api.md](./api.md) — steps, instance lifecycle, events, CLI, and REST operations +- [patterns.md](./patterns.md) — design decisions, examples, orchestration, and tests +- [gotchas.md](./gotchas.md) — failures, timeouts, replay, and capacity investigation + +## See Also + +- [Durable Objects](https://developers.cloudflare.com/durable-objects/index.md) — stateful coordination +- [Queues](../queues/README.md) — asynchronous message delivery +- [Workers](https://developers.cloudflare.com/workers/index.md) — application entry points that trigger instances diff --git a/.agents/skills/cloudflare/references/workflows/api.md b/.agents/skills/cloudflare/references/workflows/api.md new file mode 100644 index 0000000..fb583aa --- /dev/null +++ b/.agents/skills/cloudflare/references/workflows/api.md @@ -0,0 +1,17 @@ +# Workflow APIs + +Fetch the documentation for the operation before writing code; use its current signatures and serialization rules. + +| Task | Documentation | +| --- | --- | +| Implement steps; create, batch, inspect, pause, resume, restart, or terminate instances; check parameter and return types | [Workers API](https://developers.cloudflare.com/workflows/build/workers-api/index.md) | +| Access a step's name, occurrence, retry attempt, and resolved configuration | [Step context](https://developers.cloudflare.com/workflows/build/step-context/index.md) | +| Configure retries, backoff, timeouts, non-retryable failures, or relative/absolute sleeps | [Sleeping and retrying](https://developers.cloudflare.com/workflows/build/sleeping-and-retrying/index.md) | +| Pass initial parameters, wait for an external event, or send an event to an instance | [Events and parameters](https://developers.cloudflare.com/workflows/build/events-and-parameters/index.md) | +| Start instances from a Worker or another Workflow, or schedule execution | [Trigger Workflows](https://developers.cloudflare.com/workflows/build/trigger-workflows/index.md) | +| Trigger and manage instances from the command line | [Wrangler commands](https://developers.cloudflare.com/workflows/reference/wrangler-commands/index.md) | +| Manage Workflows over HTTP, including authentication and request bodies | [Workflows REST API](https://developers.cloudflare.com/api/resources/workflows/methods/list/index.md) | + +Decide whether input is available at creation or must arrive later as an event. Starting a child instance does not establish that it has completed; choose how the parent will observe completion. Check current instance ID retention and creation semantics before designing duplicate-trigger handling. + +See [configuration.md](./configuration.md) and [patterns.md](./patterns.md). diff --git a/.agents/skills/cloudflare/references/workflows/configuration.md b/.agents/skills/cloudflare/references/workflows/configuration.md new file mode 100644 index 0000000..95786d2 --- /dev/null +++ b/.agents/skills/cloudflare/references/workflows/configuration.md @@ -0,0 +1,18 @@ +# Workflow Configuration + +Use the current guides for configuration fields and setup commands; check the project's installed Wrangler version and generated binding types before adapting an existing project. + +| Task | Documentation | +| --- | --- | +| Scaffold and deploy a Workflow class with its binding | [Build your first Workflow](https://developers.cloudflare.com/workflows/get-started/guide/index.md) | +| Configure one or more Workflows, including a binding to a Workflow in another Worker | [Wrangler Workflows configuration](https://developers.cloudflare.com/workers/wrangler/configuration/index.md#workflows) | +| Configure storage, AI, and other resources used by steps | [Workers bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/index.md) | +| Choose step retry, backoff, timeout, and sleep behavior | [Sleeping and retrying](https://developers.cloudflare.com/workflows/build/sleeping-and-retrying/index.md) | +| Schedule instances or trigger them from another Worker | [Trigger Workflows](https://developers.cloudflare.com/workflows/build/trigger-workflows/index.md) | +| Trigger a Workflow from Pages Functions | [Call Workflows from Pages](https://developers.cloudflare.com/workflows/build/call-workflows-from-pages/index.md) | +| Develop and inspect instances locally | [Local development](https://developers.cloudflare.com/workflows/build/local-development/index.md) | +| Set resource budgets and inspect execution | [Limits](https://developers.cloudflare.com/workflows/reference/limits/index.md) and [metrics and analytics](https://developers.cloudflare.com/workflows/observability/metrics-analytics/index.md) | + +Distinguish the Worker that defines the Workflow from callers that trigger it. For Pages, follow the documented intermediary Worker/service-binding approach. A step's elapsed-time timeout and the Worker's active CPU budget address different failure modes; configure them based on the actual failure. + +See [api.md](./api.md), [patterns.md](./patterns.md), and [gotchas.md](./gotchas.md). diff --git a/.agents/skills/cloudflare/references/workflows/gotchas.md b/.agents/skills/cloudflare/references/workflows/gotchas.md new file mode 100644 index 0000000..f71124b --- /dev/null +++ b/.agents/skills/cloudflare/references/workflows/gotchas.md @@ -0,0 +1,19 @@ +# Gotchas & Debugging + +Start with the failing instance and step, then fetch the relevant guide before changing code or resource limits. + +| Symptom or question | What to check | +| --- | --- | +| Step timeout or repeated failure | [Sleeping and retrying](https://developers.cloudflare.com/workflows/build/sleeping-and-retrying/index.md) for per-attempt timeout, retry policy, and non-retryable failures | +| CPU exhaustion despite a short run | [Limits](https://developers.cloudflare.com/workflows/reference/limits/index.md) for active CPU budgets; increasing an elapsed-time timeout does not increase CPU capacity | +| Missing event or event timeout | [Events and parameters](https://developers.cloudflare.com/workflows/build/events-and-parameters/index.md) for instance targeting, event type/payload requirements, and timeout handling | +| State disappears or branches change after resuming | [Rules of Workflows](https://developers.cloudflare.com/workflows/build/rules-of-workflows/index.md) for persisted step returns, deterministic names and conditionals, and awaited operations | +| Duplicate charge, write, or notification | Review the destination's idempotency guarantees and [step design](./patterns.md#design-decisions); retries can repeat an external operation even when its previous attempt committed | +| Instance ID collision or unexpected batch result | [Workers API](https://developers.cloudflare.com/workflows/build/workers-api/index.md) for creation semantics, plus [limits](https://developers.cloudflare.com/workflows/reference/limits/index.md) for retention | +| Oversized results, queued instances, or missing historical data | [Limits](https://developers.cloudflare.com/workflows/reference/limits/index.md) for return/event sizes, concurrency, creation rates, and retention; export required long-term results before expiry | +| Local-only failure or failing introspection test | [Local development](https://developers.cloudflare.com/workflows/build/local-development/index.md) and [Workflow test APIs](https://developers.cloudflare.com/workers/testing/vitest-integration/test-apis/index.md#workflows) | +| Inspect execution and cost | [Metrics and analytics](https://developers.cloudflare.com/workflows/observability/metrics-analytics/index.md), [Wrangler commands](https://developers.cloudflare.com/workflows/reference/wrangler-commands/index.md), and [pricing](https://developers.cloudflare.com/workflows/reference/pricing/index.md) | + +CPU time measures active computation; waiting for network or storage I/O is elapsed time. Event waits, sleeps, and retry delays also have their own documented behavior. Check the current limits page for how these states affect concurrency and step accounting rather than treating every wait as active execution. + +See [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), and [patterns.md](./patterns.md). diff --git a/.agents/skills/cloudflare/references/workflows/patterns.md b/.agents/skills/cloudflare/references/workflows/patterns.md new file mode 100644 index 0000000..6b6851f --- /dev/null +++ b/.agents/skills/cloudflare/references/workflows/patterns.md @@ -0,0 +1,30 @@ +# Workflow Patterns + +## Design Decisions + +Read [Rules of Workflows](https://developers.cloudflare.com/workflows/build/rules-of-workflows/index.md) before choosing step boundaries or concurrency patterns. + +- Separate work into steps that can be retried independently. Persist results through step returns and keep side effects inside steps. +- Make side effects safe to repeat. A retry can happen after an external write succeeds; use the destination's idempotency mechanism or atomic deduplication. A separate check followed by a write does not itself guarantee idempotency. +- Base step names, loops, and branches on stable input or persisted results. In-memory state and fresh time/random values cannot serve as durable replay state. +- Await step operations, and check the documented replay behavior before combining steps in parallel or racing them. +- Keep large data in external storage when appropriate and pass references between steps; consult current return-type and size constraints. + +## Examples and Orchestration + +| Task | Documentation | +| --- | --- | +| Process images with human approval; handle approval events and timeouts | [Human-in-the-loop image tagging](https://developers.cloudflare.com/workflows/examples/wait-for-event/index.md) | +| Implement a payment and notification sequence | [Pay cart and send invoice](https://developers.cloudflare.com/workflows/examples/send-invoices/index.md) | +| Export data to object storage | [Export and save D1 database](https://developers.cloudflare.com/workflows/examples/backup-d1/index.md) | +| Delay lifecycle follow-ups or retry transient failures | [Sleeping and retrying](https://developers.cloudflare.com/workflows/build/sleeping-and-retrying/index.md) | +| Schedule jobs or start child Workflows | [Trigger Workflows](https://developers.cloudflare.com/workflows/build/trigger-workflows/index.md) | +| Design parallel work, races, conditional steps, and batch creation | [Rules of Workflows](https://developers.cloudflare.com/workflows/build/rules-of-workflows/index.md) | + +## Testing Workflows + +Fetch [Vitest setup](https://developers.cloudflare.com/workers/testing/vitest-integration/write-your-first-test/index.md) for current dependencies and configuration, then use the [Workflow test APIs](https://developers.cloudflare.com/workers/testing/vitest-integration/test-apis/index.md#workflows) for introspection, step/event mocks, sleep controls, and cleanup. + +Test retry behavior, event arrival and timeout paths, and duplicate external effects. Use documented introspection waits to observe completion rather than assuming a newly created instance has finished. + +See [configuration.md](./configuration.md), [api.md](./api.md), and [gotchas.md](./gotchas.md). diff --git a/.agents/skills/dockerfile-validator/SKILL.md b/.agents/skills/dockerfile-validator/SKILL.md new file mode 100644 index 0000000..9b0233b --- /dev/null +++ b/.agents/skills/dockerfile-validator/SKILL.md @@ -0,0 +1,297 @@ +--- +name: dockerfile-validator +description: Validate, lint, audit, or scan a Dockerfile for security and best practices. +--- + +# Dockerfile Validator + +Validate Dockerfiles with deterministic stages, clear severity reporting, and explicit fallbacks when tools or network access are constrained. + +## Trigger Phrases + +Use this skill when the user asks for tasks like: +- "validate this Dockerfile" +- "lint/check my Dockerfile" +- "security scan Dockerfile" +- "optimize Docker image size/build time" +- "review Dockerfile before merge" +- "find issues in Dockerfile.prod/Dockerfile.dev" + +## Use / Do Not Use + +Use this skill for: +- Syntax and lint validation +- Security and secrets checks +- Best-practice and performance review +- Dockerfile hardening before CI/CD or production + +Do not use this skill for: +- Generating a new Dockerfile from scratch (use `dockerfile-generator`) +- Running containers, debugging runtime behavior, or image registry operations + +## Local Files In This Skill + +- Validator script: `scripts/dockerfile-validate.sh` +- References: + - `references/security_checklist.md` + - `references/optimization_guide.md` + - `references/docker_best_practices.md` +- Example Dockerfiles: `examples/*.Dockerfile` + +## Deterministic Execution Flow (Required) + +Run these steps in order. Do not skip steps unless a documented fallback branch applies. + +### 1. Preflight and Path Setup + +Assume repo root as working directory: + +```bash +cd /path/to/repo +SKILL_DIR="devops-skills-plugin/skills/dockerfile-validator" +TARGET_DOCKERFILE="Dockerfile" # replace when user provides a path +``` + +Validate inputs before running tools: + +```bash +test -f "$SKILL_DIR/scripts/dockerfile-validate.sh" +test -f "$TARGET_DOCKERFILE" +``` + +If either check fails, stop and report the exact missing path. + +### 2. Read the Target Dockerfile Explicitly + +Use explicit file-read commands (not abstract "Read tool" wording): + +```bash +sed -n '1,220p' "$TARGET_DOCKERFILE" +``` + +If needed for long files: + +```bash +sed -n '220,440p' "$TARGET_DOCKERFILE" +``` + +### 3. Run Validation Script + +Primary command: + +```bash +bash "$SKILL_DIR/scripts/dockerfile-validate.sh" "$TARGET_DOCKERFILE" +``` + +Optional captured run for structured reporting: + +```bash +bash "$SKILL_DIR/scripts/dockerfile-validate.sh" "$TARGET_DOCKERFILE" | tee /tmp/dockerfile-validator.out +``` + +### 4. Classify Findings by Severity (Standard) + +Use this standard severity model: + +- `Critical` + - Hardcoded secrets/credentials + - Explicit root runtime with high-risk context + - High-impact security policy failures +- `High` + - Checkov failures for container hardening + - hadolint errors likely to cause insecure/unreliable builds + - Missing or unsafe runtime-user posture (`USER`) +- `Medium` + - `:latest` image tags, missing pinning, cache-cleanup misses + - Build cache inefficiency and layered install anti-patterns +- `Low` + - Style/info guidance and non-blocking optimization suggestions + +### 5. No-Issue Fast Path (Required) + +If validation has no actionable findings: +- Return a concise pass summary. +- Do **not** open reference files. +- Do **not** generate fix diffs. + +Use fast path when all are true: +- Script reports overall pass. +- No security failures. +- No error/warning findings requiring user action. + +### 6. Reference Loading Rules (Only When Findings Exist) + +Only read references that match actual findings. Read each required file once. + +Issue-to-reference mapping: + +| Issue category | Trigger examples | Read this file | +|---|---|---| +| Secrets, root user, exposed sensitive ports, hardening gaps | `CKV_DOCKER_*`, hardcoded token/password, root runtime | `references/security_checklist.md` | +| Image size, layer count, multi-stage opportunities, cache efficiency, `.dockerignore` gaps | too many `RUN`, single-stage with build deps, cache misses | `references/optimization_guide.md` | +| Tag pinning, instruction usage, COPY vs ADD, WORKDIR/CMD/ENTRYPOINT conventions | `:latest`, unpinned packages, instruction-level best practices | `references/docker_best_practices.md` | + +Explicit read commands: + +```bash +sed -n '1,220p' "$SKILL_DIR/references/security_checklist.md" +sed -n '1,220p' "$SKILL_DIR/references/optimization_guide.md" +sed -n '1,220p' "$SKILL_DIR/references/docker_best_practices.md" +``` + +For targeted extraction: + +```bash +rg -n "USER|secrets|EXPOSE|HEALTHCHECK" "$SKILL_DIR/references/security_checklist.md" +rg -n "multi-stage|cache|layer|dockerignore" "$SKILL_DIR/references/optimization_guide.md" +rg -n "FROM|COPY|ADD|WORKDIR|CMD|ENTRYPOINT|latest" "$SKILL_DIR/references/docker_best_practices.md" +``` + +### 7. Produce Standard Report Output + +Use this template for every non-fast-path run: + +```markdown +## Dockerfile Validation Report +- Target: +- Command: `bash ` +- Overall result: PASS | FAIL | PARTIAL (fallback) + +### Critical +- + +### High +- + +### Medium +- + +### Low +- + +### Recommended Fixes +- + +### References Used +- + +### Fallbacks Used +- `None` or exact fallback branch + reason +``` + +### 8. Offer Fix Application + +After reporting: +- Ask whether to apply fixes. +- If user approves, patch the Dockerfile and rerun validation. + +## Fallback Behavior (Explicit) + +When the primary script cannot complete, use deterministic fallback branches and report them. + +### Fallback A: Python/Tool Install Constraint + +Condition: +- Script exits with tool-install failure (for example Python missing, package install blocked, or restricted environment). + +Action: +1. Report primary failure and why. +2. Run manual minimum checks: + +```bash +# Basic syntax signal (if Docker is available) +DOCKERFILE_DIR="$(dirname "$TARGET_DOCKERFILE")" +docker build --no-cache -f "$TARGET_DOCKERFILE" "$DOCKERFILE_DIR" + +# High-value static checks +grep -nEi "^[[:space:]]*FROM[[:space:]]+.*:latest" "$TARGET_DOCKERFILE" || true +grep -nEi "^[[:space:]]*(ENV|ARG)[[:space:]].*(password|secret|token|api[_-]?key)[[:space:]]*=" "$TARGET_DOCKERFILE" || true +grep -nEi "^[[:space:]]*USER[[:space:]]+(root|0(:0)?)$" "$TARGET_DOCKERFILE" || true +grep -nEi "^[[:space:]]*HEALTHCHECK[[:space:]]+" "$TARGET_DOCKERFILE" || true +``` + +3. Classify output with `PARTIAL` result and clearly label skipped checks. + +### Fallback B: hadolint Not Available but Docker Available + +Use hadolint container image: + +```bash +docker run --rm -i hadolint/hadolint < "$TARGET_DOCKERFILE" +``` + +### Fallback C: No Docker, No hadolint/checkov + +Run only manual regex-based checks (Fallback A step 2), clearly mark as `PARTIAL`, and state which scanners were skipped. + +## Quick Command Set + +### Validate one Dockerfile + +```bash +cd /path/to/repo +bash devops-skills-plugin/skills/dockerfile-validator/scripts/dockerfile-validate.sh Dockerfile +``` + +### Validate alternate file + +```bash +cd /path/to/repo +bash devops-skills-plugin/skills/dockerfile-validator/scripts/dockerfile-validate.sh Dockerfile.prod +``` + +### Validate skill examples + +```bash +cd /path/to/repo/devops-skills-plugin/skills/dockerfile-validator +bash scripts/dockerfile-validate.sh examples/good-example.Dockerfile +bash scripts/dockerfile-validate.sh examples/security-issues.Dockerfile +``` + +### Run regression checks (CI entrypoint) + +```bash +cd /path/to/repo +bash devops-skills-plugin/skills/dockerfile-validator/scripts/test_validate.sh +``` + +Optional strict mode for CI environments that must enforce ShellCheck: + +```bash +STRICT_SHELLCHECK=true bash devops-skills-plugin/skills/dockerfile-validator/scripts/test_validate.sh +``` + +## Progressive Disclosure Rules + +- Always read the target Dockerfile first. +- Do not read any reference files unless findings require them. +- Read only the matching reference file(s) from the issue-to-reference mapping. +- Do not reread the same reference unless new issue categories appear. + +## Done Criteria + +Consider this skill execution complete only when all conditions below are satisfied: + +- Trigger matched a Dockerfile validation/lint/security/optimization request. +- Target Dockerfile path was explicitly verified. +- Validation command (or explicit fallback) was executed. +- Findings were reported using severity buckets (`Critical`, `High`, `Medium`, `Low`). +- Reference usage matched issue categories and was explicitly listed. +- No-issue fast path skipped unnecessary reference reads. +- If fixes were applied, validation was rerun and final status reported. + +## Resources + +- Script: `scripts/dockerfile-validate.sh` +- CI/regression entrypoint: `scripts/test_validate.sh` +- Security reference: `references/security_checklist.md` +- Optimization reference: `references/optimization_guide.md` +- Best-practices reference: `references/docker_best_practices.md` +- Examples: `examples/good-example.Dockerfile`, `examples/bad-example.Dockerfile`, `examples/security-issues.Dockerfile`, `examples/python-optimized.Dockerfile`, `examples/golang-distroless.Dockerfile` + +## Source Links + +- [Docker Build Best Practices](https://docs.docker.com/build/building/best-practices/) +- [Dockerfile Reference](https://docs.docker.com/reference/dockerfile/) +- [Checkov Dockerfile Scanning](https://www.checkov.io/7.Scan%20Examples/Dockerfile.html) +- [hadolint](https://github.com/hadolint/hadolint) diff --git a/.agents/skills/dockerfile-validator/examples/.dockerignore.example b/.agents/skills/dockerfile-validator/examples/.dockerignore.example new file mode 100644 index 0000000..ef644a3 --- /dev/null +++ b/.agents/skills/dockerfile-validator/examples/.dockerignore.example @@ -0,0 +1,34 @@ +# VCS +.git +.gitignore + +# Local environment and secrets +.env +.env.* +*.pem +*.key + +# Build and dependency artifacts +node_modules/ +dist/ +build/ +target/ +venv/ +.venv/ +__pycache__/ +*.pyc + +# Logs and temp files +*.log +*.tmp +tmp/ + +# Tooling and editor files +.idea/ +.vscode/ +.DS_Store + +# Docker-related files not needed in image +Dockerfile* +docker-compose*.yml +.dockerignore diff --git a/.agents/skills/dockerfile-validator/examples/bad-example.Dockerfile b/.agents/skills/dockerfile-validator/examples/bad-example.Dockerfile new file mode 100644 index 0000000..baae01c --- /dev/null +++ b/.agents/skills/dockerfile-validator/examples/bad-example.Dockerfile @@ -0,0 +1,37 @@ +# Bad Example - Demonstrates common mistakes and anti-patterns +# DO NOT USE THIS IN PRODUCTION + +# Issue 1: Using :latest tag (unpredictable, not reproducible) +FROM ubuntu:latest + +# Issue 2: Running as root (security risk) +# Issue 3: No WORKDIR set (unclear where commands run) + +# Issue 4: Separate RUN commands (creates unnecessary layers) +RUN apt-get update +RUN apt-get install -y curl +RUN apt-get install -y vim +RUN apt-get install -y git +# Issue 5: No cache cleanup (increases image size) + +# Issue 6: Using shell form instead of exec form +WORKDIR app + +# Issue 7: Copying everything before installing dependencies (poor caching) +COPY . . + +# Issue 8: No version pinning for packages +RUN pip install flask + +# Issue 9: Potential secret exposure +ENV API_KEY=secret123 +ENV PASSWORD=admin + +# Issue 10: Exposing SSH port (security risk) +EXPOSE 22 +EXPOSE 80 + +# Issue 11: No HEALTHCHECK defined +# Issue 12: No USER directive (runs as root) +# Issue 13: Shell form instead of exec form (poor signal handling) +CMD python app.py \ No newline at end of file diff --git a/.agents/skills/dockerfile-validator/examples/golang-distroless.Dockerfile b/.agents/skills/dockerfile-validator/examples/golang-distroless.Dockerfile new file mode 100644 index 0000000..d6a11d7 --- /dev/null +++ b/.agents/skills/dockerfile-validator/examples/golang-distroless.Dockerfile @@ -0,0 +1,50 @@ +# syntax=docker/dockerfile:1 + +# Multi-stage Go Dockerfile with distroless runtime +# Demonstrates minimal attack surface and optimal size + +# Build stage +FROM golang:1.21-alpine AS builder + +# Install build dependencies +# hadolint ignore=DL3018 +RUN apk add --no-cache git ca-certificates + +WORKDIR /src + +# Copy go mod files for dependency caching +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build static binary +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags='-w -s -extldflags "-static"' \ + -a \ + -o /app/server \ + ./cmd/server + +# Runtime stage - distroless (minimal, secure) +# checkov:skip=CKV_DOCKER_2:Distroless runtime uses external liveness/readiness probes instead of in-container HEALTHCHECK. +# checkov:skip=CKV_DOCKER_3:distroless:nonroot image already runs as a non-root user. +FROM gcr.io/distroless/static-debian11:nonroot + +# Copy CA certificates for HTTPS +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Copy binary from builder +COPY --from=builder /app/server /server + +# Expose port +EXPOSE 8080 + +# Already running as non-root (distroless:nonroot) +# User is automatically set to uid:gid 65532:65532 + +# Health check (limited in distroless, use external checks) +# HEALTHCHECK not available in distroless + +# Exec form entrypoint +ENTRYPOINT ["/server"] diff --git a/.agents/skills/dockerfile-validator/examples/good-example.Dockerfile b/.agents/skills/dockerfile-validator/examples/good-example.Dockerfile new file mode 100644 index 0000000..ae001a1 --- /dev/null +++ b/.agents/skills/dockerfile-validator/examples/good-example.Dockerfile @@ -0,0 +1,52 @@ +# syntax=docker/dockerfile:1 + +# Example of a well-structured, secure, and optimized Dockerfile +# This demonstrates Docker best practices for a Node.js application + +# Build stage - includes all build dependencies +FROM node:21-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package files first for better layer caching +COPY package.json package-lock.json ./ + +# Install dependencies with cache mount for faster rebuilds +RUN --mount=type=cache,target=/root/.cache/npm \ + npm ci --only=production + +# Copy application source +COPY . . + +# Build the application (if needed) +RUN npm run build + +# Runtime stage - minimal image with only necessary runtime dependencies +FROM node:21-alpine AS runtime + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +# Set working directory +WORKDIR /app + +# Copy built application and dependencies from builder stage +COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist +COPY --from=builder --chown=nodejs:nodejs /app/package.json ./ + +# Switch to non-root user +USER nodejs + +# Expose application port +EXPOSE 3000 + +# Add healthcheck for container monitoring +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); })" + +# Use exec form for better signal handling +ENTRYPOINT ["node"] +CMD ["dist/index.js"] \ No newline at end of file diff --git a/.agents/skills/dockerfile-validator/examples/python-optimized.Dockerfile b/.agents/skills/dockerfile-validator/examples/python-optimized.Dockerfile new file mode 100644 index 0000000..2143f08 --- /dev/null +++ b/.agents/skills/dockerfile-validator/examples/python-optimized.Dockerfile @@ -0,0 +1,53 @@ +# syntax=docker/dockerfile:1 + +# Multi-stage Python Dockerfile with best practices +# Optimized for size and security + +# Build stage +FROM python:3.12-slim AS builder + +WORKDIR /app + +# Install build dependencies +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for layer caching +COPY requirements.txt ./ + +# Install Python dependencies with cache mount +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --user --no-cache-dir -r requirements.txt + +# Runtime stage - minimal +FROM python:3.12-slim AS runtime + +# Create non-root user +RUN groupadd -r appuser && useradd -r -g appuser appuser + +WORKDIR /app + +# Copy Python packages from builder +COPY --from=builder /root/.local /root/.local + +# Copy application code +COPY --chown=appuser:appuser . . + +# Add local bin to PATH +ENV PATH=/root/.local/bin:$PATH + +# Switch to non-root user +USER appuser + +# Expose application port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s \ + CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1 + +# Use exec form +ENTRYPOINT ["python"] +CMD ["-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/.agents/skills/dockerfile-validator/examples/security-issues.Dockerfile b/.agents/skills/dockerfile-validator/examples/security-issues.Dockerfile new file mode 100644 index 0000000..ea162de --- /dev/null +++ b/.agents/skills/dockerfile-validator/examples/security-issues.Dockerfile @@ -0,0 +1,42 @@ +# Dockerfile with Intentional Security Issues +# FOR TESTING VALIDATION ONLY - DO NOT USE IN PRODUCTION + +# Issue: Using :latest tag +FROM python:latest + +# Issue: Running as root user throughout + +WORKDIR /app + +# Issue: Hardcoded secrets +ENV DATABASE_PASSWORD=super_secret_password +ENV API_TOKEN=abc123xyz789 +ARG SECRET_KEY=my_secret_key + +# Issue: Installing unnecessary packages, no version pinning +RUN apt-get update && apt-get install -y \ + openssh-server \ + telnet \ + ftp \ + vim \ + nano + +# Issue: No cache cleanup +# (apt lists remain, increasing image size) + +# Issue: Using ADD instead of COPY +ADD . /app + +# Issue: Installing packages without version pins +RUN pip install flask requests sqlalchemy + +# Issue: Exposing SSH port +EXPOSE 22 +EXPOSE 23 +EXPOSE 5000 + +# Issue: No USER directive - will run as root +# Issue: No HEALTHCHECK + +# Issue: Shell form (doesn't handle signals properly) +CMD python app.py \ No newline at end of file diff --git a/.agents/skills/dockerfile-validator/references/docker_best_practices.md b/.agents/skills/dockerfile-validator/references/docker_best_practices.md new file mode 100644 index 0000000..b756d58 --- /dev/null +++ b/.agents/skills/dockerfile-validator/references/docker_best_practices.md @@ -0,0 +1,348 @@ +# Docker Best Practices Reference + +This document summarizes official Docker best practices based on current recommendations from Docker documentation and industry standards. + +## General Principles + +### 1. Create Ephemeral Containers +- Containers should be as stateless and ephemeral as possible +- Should be able to stop, destroy, and recreate with minimal setup +- Align with Twelve-Factor App methodology + +### 2. Understand Build Context +- Use `.dockerignore` to exclude unnecessary files +- Keep context size minimal for faster builds +- Don't include secrets or sensitive data in context + +### 3. Use Multi-Stage Builds +- Separate build dependencies from runtime +- Dramatically reduce final image size +- Improve security by minimizing attack surface + +### 4. One Concern Per Container +- Each container should address a single concern +- Makes containers more reusable and easier to scale +- Simplifies debugging and updates + +## Dockerfile Instructions Best Practices + +### FROM + +**Use specific tags, not :latest** +```dockerfile +# Bad +FROM node:latest + +# Good +FROM node:21-alpine + +# Better +FROM node:21-alpine@sha256:abc123... +``` + +**Choose minimal base images** +- Alpine Linux: ~5 MB base (vs ~80 MB for Ubuntu) +- Distroless: No shell, package manager (minimal attack surface) +- Scratch: Absolutely minimal (for static binaries) + +**Prefer official images** +- Look for "Official Image" or "Verified Publisher" badges +- Official images are maintained and regularly updated + +### RUN + +**Chain commands to reduce layers** +```dockerfile +# Bad - creates 4 layers +RUN apt-get update +RUN apt-get install -y curl +RUN apt-get install -y vim +RUN curl -sL https://example.com/script.sh | bash + +# Good - creates 1 layer +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + vim \ + && rm -rf /var/lib/apt/lists/* \ + && curl -sL https://example.com/script.sh | bash +``` + +**Clean up in same layer** +```dockerfile +# Package manager cache must be removed in same RUN +RUN apt-get update && apt-get install -y \ + package1 \ + package2 \ + && rm -rf /var/lib/apt/lists/* + +# For Alpine +RUN apk add --no-cache package1 package2 +``` + +**Use --no-install-recommends for apt** +```dockerfile +RUN apt-get install -y --no-install-recommends package +``` + +**Pin package versions** +```dockerfile +# For apt +RUN apt-get install -y package=1.2.3-1 + +# For apk +RUN apk add package=1.2.3-r0 + +# For pip +RUN pip install package==1.2.3 +``` + +**Sort multi-line arguments** +```dockerfile +RUN apt-get update && apt-get install -y \ + curl \ + git \ + vim \ + wget \ + && rm -rf /var/lib/apt/lists/* +``` + +**Use pipefail for pipes** +```dockerfile +RUN set -o pipefail && wget -O - https://example.com | wc -l > /number +``` + +### COPY vs ADD + +**Prefer COPY over ADD** +```dockerfile +# Use COPY for files and directories +COPY app.py /app/ + +# Only use ADD for auto-extraction or remote URLs +ADD https://example.com/file.tar.gz /tmp/ +``` + +**Use COPY --chown to avoid extra layer** +```dockerfile +# Bad - creates extra layer +COPY app.py /app/ +RUN chown user:user /app/app.py + +# Good - single layer +COPY --chown=user:user app.py /app/ +``` + +### WORKDIR + +**Use absolute paths** +```dockerfile +# Bad +WORKDIR app + +# Good +WORKDIR /app +``` + +**Don't use RUN cd** +```dockerfile +# Bad +RUN cd /app && npm install + +# Good +WORKDIR /app +RUN npm install +``` + +### USER + +**Don't run as root** +```dockerfile +# Create user +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# Or for Alpine +RUN addgroup -g 1001 -S appuser && adduser -S appuser -u 1001 + +# Switch to user +USER appuser +``` + +**Use high UID (>10000) for better security** +```dockerfile +RUN useradd -u 10001 -m appuser +USER appuser +``` + +### CMD and ENTRYPOINT + +**Use exec form for proper signal handling** +```dockerfile +# Bad - shell form (doesn't handle signals) +CMD python app.py + +# Good - exec form +CMD ["python", "app.py"] +``` + +**Combine ENTRYPOINT and CMD** +```dockerfile +# ENTRYPOINT defines the executable +ENTRYPOINT ["python"] + +# CMD provides default arguments (can be overridden) +CMD ["app.py"] +``` + +### EXPOSE + +**Document ports even though it doesn't publish** +```dockerfile +EXPOSE 8080 +EXPOSE 443 +``` + +### HEALTHCHECK + +**Add health checks for services** +```dockerfile +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 +``` + +### LABEL + +**Add metadata** +```dockerfile +LABEL org.opencontainers.image.authors="team@example.com" +LABEL org.opencontainers.image.version="1.0.0" +LABEL org.opencontainers.image.description="Application description" +``` + +## Build Optimization + +### Layer Caching + +**Order instructions from least to most frequently changing** +```dockerfile +# 1. Base image (rarely changes) +FROM node:21-alpine + +# 2. System packages (rarely change) +RUN apk add --no-cache curl + +# 3. Dependencies (change occasionally) +COPY package*.json ./ +RUN npm ci + +# 4. Source code (changes frequently) +COPY . . +``` + +### Multi-Stage Builds + +**Separate build and runtime** +```dockerfile +# Build stage +FROM node:21 AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# Runtime stage +FROM node:21-alpine +COPY --from=builder /app/dist /app +CMD ["node", "/app/index.js"] +``` + +### BuildKit Features + +**Enable modern features** +```dockerfile +# syntax=docker/dockerfile:1 + +# Use cache mounts +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install -r requirements.txt + +# Use secret mounts (secrets not in final image) +RUN --mount=type=secret,id=aws,target=/root/.aws/credentials \ + aws s3 cp s3://bucket/file . +``` + +## Security Best Practices + +### 1. Scan Images +```bash +docker scan myimage:tag +# or +trivy image myimage:tag +``` + +### 2. Use Minimal Base Images +- Fewer packages = fewer vulnerabilities +- Alpine, distroless, or scratch + +### 3. Don't Store Secrets in Images +```dockerfile +# Bad +ENV DATABASE_PASSWORD=secret123 + +# Good - use runtime config or secrets +# Pass at runtime: docker run -e DATABASE_PASSWORD=... +``` + +### 4. Run as Non-Root +```dockerfile +USER appuser +``` + +### 5. Use Read-Only Filesystem +```bash +docker run --read-only myimage +``` + +### 6. Limit Capabilities +```bash +docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimage +``` + +## Common Anti-Patterns + +### ❌ Using :latest tag +- Unpredictable +- Not reproducible +- Can break without warning + +### ❌ Not cleaning package cache +```dockerfile +# Missing cleanup increases image by hundreds of MB +RUN apt-get update && apt-get install -y package +# Missing: && rm -rf /var/lib/apt/lists/* +``` + +### ❌ Running as root +- Security risk +- Violates principle of least privilege + +### ❌ Installing unnecessary packages +```dockerfile +# Bloated image +RUN apt-get install -y vim nano emacs curl wget +``` + +### ❌ Using ADD instead of COPY +- ADD has implicit behavior +- Can extract archives unexpectedly + +### ❌ Multiple FROM in non-multi-stage context +- Creates confusion +- Use multi-stage builds properly + +## Resources + +- [Official Docker Best Practices](https://docs.docker.com/build/building/best-practices/) +- [Dockerfile Reference](https://docs.docker.com/reference/dockerfile/) +- [Docker Security Best Practices](https://docs.docker.com/develop/security-best-practices/) +- [Multi-Stage Builds](https://docs.docker.com/build/building/multi-stage/) \ No newline at end of file diff --git a/.agents/skills/dockerfile-validator/references/optimization_guide.md b/.agents/skills/dockerfile-validator/references/optimization_guide.md new file mode 100644 index 0000000..ca6a679 --- /dev/null +++ b/.agents/skills/dockerfile-validator/references/optimization_guide.md @@ -0,0 +1,473 @@ +# Dockerfile Optimization Guide + +Comprehensive guide for optimizing Docker images for size, build time, and runtime performance. + +## Image Size Optimization + +### 1. Choose Minimal Base Images + +**Size Comparison:** +``` +ubuntu:22.04 ~80 MB +alpine:3.21 ~5 MB +distroless/base ~20 MB +scratch ~0 MB (empty) +``` + +**When to use each:** + +**Alpine** - General purpose minimal Linux +```dockerfile +FROM alpine:3.21 +RUN apk add --no-cache python3 +``` +- ✅ Very small (5 MB) +- ✅ Has package manager +- ✅ Good for interpreted languages +- ⚠️ Uses musl libc (compatibility issues with some C libraries) + +**Distroless** - Production containers +```dockerfile +FROM gcr.io/distroless/python3 +COPY --from=builder /app /app +``` +- ✅ No shell, package manager (secure) +- ✅ Minimal attack surface +- ✅ Small size +- ⚠️ Cannot exec into container for debugging +- ⚠️ Must use multi-stage builds + +**Scratch** - Static binaries only +```dockerfile +FROM scratch +COPY --from=builder /app/binary / +``` +- ✅ Absolutely minimal +- ✅ Perfect for Go, Rust static binaries +- ⚠️ No OS utilities +- ⚠️ No debug capabilities + +### 2. Multi-Stage Builds + +**Problem: Build tools bloat production images** + +**Single-stage (bloated):** +```dockerfile +FROM golang:1.21 +WORKDIR /app +COPY . . +RUN go build -o server +CMD ["./server"] + +# Result: ~1 GB (includes Go toolchain) +``` + +**Multi-stage (optimized):** +```dockerfile +# Build stage +FROM golang:1.21 AS builder +WORKDIR /app +COPY . . +RUN go build -o server + +# Production stage +FROM alpine:3.21 +COPY --from=builder /app/server /server +CMD ["/server"] + +# Result: ~10 MB (100x smaller!) +``` + +### 3. Layer Optimization + +**Combine RUN commands:** + +```dockerfile +# Bad - 4 layers, poor caching +RUN apt-get update +RUN apt-get install -y curl +RUN curl -O https://example.com/file +RUN rm -f file + +# Good - 1 layer, cache cleaned +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && curl -O https://example.com/file \ + && rm -rf /var/lib/apt/lists/* +``` + +### 4. Package Manager Cache Cleanup + +**APT (Debian/Ubuntu):** +```dockerfile +RUN apt-get update && apt-get install -y --no-install-recommends \ + package1 \ + package2 \ + && rm -rf /var/lib/apt/lists/* +``` +- Saves ~100-200 MB per layer +- Must be in same RUN command + +**APK (Alpine):** +```dockerfile +RUN apk add --no-cache package1 package2 +``` +- Doesn't create cache at all +- Or: `apk add package && rm -rf /var/cache/apk/*` + +**YUM/DNF (RHEL/Fedora):** +```dockerfile +RUN yum install -y package \ + && yum clean all \ + && rm -rf /var/cache/yum +``` + +**Pip (Python):** +```dockerfile +RUN pip install --no-cache-dir package +``` + +**NPM (Node.js):** +```dockerfile +RUN npm ci --only=production +# Or with cache mount: +RUN --mount=type=cache,target=/root/.npm \ + npm ci --only=production +``` + +### 5. Use .dockerignore + +**Problem: Entire project copied into image** + +``` +.dockerignore contents: +.git/ +node_modules/ +*.log +.env +tests/ +docs/ +README.md +``` + +**Impact:** +- Faster builds (smaller context) +- Smaller images (fewer files) +- Prevents accidental secret leaks + +## Build Time Optimization + +### 1. Leverage Build Cache + +**Order matters - least to most frequently changing:** + +```dockerfile +# 1. Base image (rarely changes) +FROM node:21-alpine + +# 2. System dependencies (rarely change) +RUN apk add --no-cache curl + +# 3. Application dependencies (change occasionally) +COPY package*.json ./ +RUN npm ci + +# 4. Application code (changes frequently) +COPY . . +RUN npm run build +``` + +**Why this works:** +- Docker caches each layer +- Layers rebuild when files change +- Putting frequently-changing files last preserves cache for earlier layers + +### 2. BuildKit Cache Mounts + +**Enable BuildKit:** +```bash +export DOCKER_BUILDKIT=1 +``` + +**Use cache mounts:** +```dockerfile +# syntax=docker/dockerfile:1 + +# Python with pip cache +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install -r requirements.txt + +# Node.js with npm cache +RUN --mount=type=cache,target=/root/.npm \ + npm ci + +# Go with module cache +RUN --mount=type=cache,target=/go/pkg/mod \ + go build -o app +``` + +**Benefits:** +- Persistent cache across builds +- Dramatically faster dependency installation +- Shared cache between projects + +### 3. Parallel Multi-Stage Builds + +```dockerfile +# These stages run in parallel +FROM alpine AS fetch-1 +RUN wget https://example.com/file1 + +FROM alpine AS fetch-2 +RUN wget https://example.com/file2 + +# This stage waits for both +FROM alpine +COPY --from=fetch-1 /file1 . +COPY --from=fetch-2 /file2 . +``` + +## Runtime Performance Optimization + +### 1. Exec Form for CMD/ENTRYPOINT + +```dockerfile +# Bad - shell form (extra shell process) +CMD python app.py + +# Good - exec form (direct execution) +CMD ["python", "app.py"] +``` + +**Benefits:** +- Faster startup (no shell) +- Proper signal handling (SIGTERM) +- Lower memory usage + +### 2. Health Checks + +```dockerfile +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ + CMD curl -f http://localhost:8080/health || exit 1 +``` + +**Benefits:** +- Container orchestrators can detect unhealthy containers +- Automatic restarts +- Better uptime + +### 3. Resource Awareness + +```dockerfile +# Use all available CPUs +ENV GOMAXPROCS=0 + +# Or limit to specific count +ENV GOMAXPROCS=4 +``` + +## Language-Specific Optimizations + +### Node.js + +```dockerfile +FROM node:21-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production + +FROM node:21-alpine +COPY --from=builder /app/node_modules ./node_modules +COPY . . +USER node +CMD ["node", "server.js"] +``` + +**Tips:** +- Use `npm ci` instead of `npm install` +- Install only production dependencies +- Use Alpine variant (node:21-alpine vs node:21 = 150MB vs 900MB) + +### Python + +```dockerfile +FROM python:3.12-slim AS builder +WORKDIR /app +COPY requirements.txt . +RUN pip install --user --no-cache-dir -r requirements.txt + +FROM python:3.12-slim +COPY --from=builder /root/.local /root/.local +ENV PATH=/root/.local/bin:$PATH +COPY . . +USER nobody +CMD ["python", "app.py"] +``` + +**Tips:** +- Use slim variant (python:3.12-slim vs python:3.12 = 50MB vs 1GB) +- Install to --user to copy to final stage +- Use --no-cache-dir to avoid pip cache + +### Go + +```dockerfile +FROM golang:1.21-alpine AS builder +WORKDIR /src +COPY go.* ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app + +FROM scratch +COPY --from=builder /app /app +ENTRYPOINT ["/app"] +``` + +**Tips:** +- Use scratch for static binaries +- Disable CGO for static linking +- Use `-ldflags="-s -w"` to strip debug info (smaller binary) + +### Java + +```dockerfile +FROM eclipse-temurin:21-jdk AS builder +WORKDIR /app +COPY pom.xml . +RUN mvn dependency:go-offline +COPY src ./src +RUN mvn package + +FROM eclipse-temurin:21-jre-alpine +COPY --from=builder /app/target/*.jar /app.jar +CMD ["java", "-jar", "/app.jar"] +``` + +**Tips:** +- Use JRE instead of JDK for runtime (smaller) +- Download dependencies separately for caching +- Consider custom JRE with jlink for minimal image + +## Advanced Techniques + +### 1. Multi-Architecture Builds + +```bash +docker buildx build --platform linux/amd64,linux/arm64 -t myapp . +``` + +### 2. Build Secrets + +```dockerfile +# syntax=docker/dockerfile:1 +RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ + npm ci +``` + +```bash +docker build --secret id=npmrc,src=$HOME/.npmrc . +``` + +**Benefits:** +- Secrets not in final image +- Not in build history +- Secure credential usage + +### 3. SSH Mounts + +```dockerfile +RUN --mount=type=ssh \ + git clone git@github.com:private/repo.git +``` + +```bash +docker build --ssh default . +``` + +### 4. Layer Squashing + +```bash +docker build --squash -t myapp . +``` + +**Benefits:** +- Single layer in final image +- Smaller size if cleanup commands are separate + +**Drawbacks:** +- Loses layer caching benefits +- Slower rebuilds + +## Optimization Checklist + +- [ ] Use minimal base image (Alpine, distroless, scratch) +- [ ] Implement multi-stage builds +- [ ] Combine RUN commands +- [ ] Clean package manager cache +- [ ] Order layers by change frequency +- [ ] Use BuildKit cache mounts +- [ ] Create .dockerignore file +- [ ] Use exec form for CMD/ENTRYPOINT +- [ ] Add HEALTHCHECK for services +- [ ] Pin dependency versions +- [ ] Remove development dependencies +- [ ] Use --no-install-recommends for apt +- [ ] Consider language-specific optimizations +- [ ] Enable BuildKit features + +## Measuring Optimization + +### Before Optimization +```bash +docker images myapp +# REPOSITORY TAG SIZE +# myapp latest 1.2GB +``` + +### After Optimization +```bash +docker images myapp-optimized +# REPOSITORY TAG SIZE +# myapp-optimized latest 50MB +``` + +### Build Time Comparison +```bash +time docker build -t myapp . +# real 5m30s + +time docker build -t myapp-optimized . +# real 0m45s (with cache) +``` + +## Tools for Analysis + +### dive - Layer Analysis +```bash +dive myapp:latest +``` +- Shows layer-by-layer size +- Identifies wasted space +- Suggests optimizations + +### docker history +```bash +docker history myapp:latest +``` +- Shows each layer's size +- Identifies large layers + +### docker scout +```bash +docker scout cves myapp:latest +``` +- Scans for vulnerabilities +- Recommends base image updates + +## Resources + +- [Docker Best Practices](https://docs.docker.com/build/building/best-practices/) +- [BuildKit Documentation](https://docs.docker.com/build/buildkit/) +- [Multi-Stage Builds](https://docs.docker.com/build/building/multi-stage/) +- [dive - Layer Explorer](https://github.com/wagoodman/dive) \ No newline at end of file diff --git a/.agents/skills/dockerfile-validator/references/security_checklist.md b/.agents/skills/dockerfile-validator/references/security_checklist.md new file mode 100644 index 0000000..047a1bc --- /dev/null +++ b/.agents/skills/dockerfile-validator/references/security_checklist.md @@ -0,0 +1,208 @@ +# Container Security Checklist + +A comprehensive security checklist for Dockerfiles and container images. + +## Build-Time Security + +### Base Image Security + +- [ ] Use official or verified base images +- [ ] Pin base image to specific tag (not :latest) +- [ ] Consider digest pinning for critical applications +- [ ] Prefer minimal base images (Alpine, distroless, scratch) +- [ ] Scan base images for known vulnerabilities +- [ ] Keep base images updated regularly + +### Secrets Management + +- [ ] Never hardcode secrets in Dockerfile +- [ ] Don't use ENV or ARG for sensitive data +- [ ] Use Docker build secrets (--secret flag) +- [ ] Use runtime configuration for secrets +- [ ] Scan for accidentally committed secrets +- [ ] Use .dockerignore to exclude secret files + +### Package Management + +- [ ] Pin package versions for reproducibility +- [ ] Only install necessary packages (--no-install-recommends) +- [ ] Clean package manager cache in same layer +- [ ] Verify package signatures when possible +- [ ] Use official package repositories +- [ ] Audit dependencies for known vulnerabilities + +### User and Permissions + +- [ ] Create and use non-root user +- [ ] Set USER directive before CMD/ENTRYPOINT +- [ ] Use high UID (>10000) for better isolation +- [ ] Set proper file ownership with COPY --chown +- [ ] Don't use sudo in containers +- [ ] Avoid privileged operations + +### Layer and File Security + +- [ ] Use .dockerignore to exclude sensitive files +- [ ] Don't copy unnecessary files (use specific COPY) +- [ ] Remove secrets after use in same layer +- [ ] Don't log sensitive information +- [ ] Minimize number of layers +- [ ] Use multi-stage builds to exclude build secrets + +## Common Vulnerabilities + +### SSH/Remote Access + +- [ ] Don't install or expose SSH (port 22) +- [ ] Don't install telnet, FTP, or other insecure protocols +- [ ] Use `docker exec` for debugging instead of SSH +- [ ] Don't run sshd in containers + +### Network Exposure + +- [ ] Only EXPOSE necessary ports +- [ ] Don't bind to 0.0.0.0 in development images +- [ ] Use internal networks for inter-container communication +- [ ] Implement proper firewall rules +- [ ] Use TLS for network communications + +### File System Security + +- [ ] Consider read-only root filesystem +- [ ] Use tmpfs for temporary files +- [ ] Set proper file permissions +- [ ] Don't store secrets in environment variables +- [ ] Use volume mounts for sensitive data + +## Runtime Security + +### Container Configuration + +- [ ] Run with --read-only flag when possible +- [ ] Drop unnecessary capabilities (--cap-drop) +- [ ] Use security profiles (AppArmor, SELinux) +- [ ] Set resource limits (CPU, memory) +- [ ] Use user namespaces +- [ ] Enable content trust (DOCKER_CONTENT_TRUST) + +### Health and Monitoring + +- [ ] Implement HEALTHCHECK in Dockerfile +- [ ] Monitor container logs +- [ ] Set up security scanning in CI/CD +- [ ] Use runtime security tools +- [ ] Monitor for anomalous behavior +- [ ] Implement proper logging without secrets + +### Network Security + +- [ ] Use custom bridge networks +- [ ] Implement network segmentation +- [ ] Use encrypted overlays for swarm +- [ ] Configure DNS properly +- [ ] Use service mesh for microservices +- [ ] Implement network policies + +## Image Registry Security + +### Registry Configuration + +- [ ] Use private registries for internal images +- [ ] Enable image scanning in registry +- [ ] Implement access controls +- [ ] Use image signing (Docker Content Trust) +- [ ] Scan for vulnerabilities before pull +- [ ] Regularly update registry software + +### Image Distribution + +- [ ] Sign images before distribution +- [ ] Verify image signatures on pull +- [ ] Use TLS for registry communication +- [ ] Implement role-based access control +- [ ] Audit image pull/push events +- [ ] Use image provenance metadata + +## Security Scanning Tools + +### Static Analysis +- **hadolint** - Dockerfile linting +- **Checkov** - Policy-as-code scanning +- **dockerfilelint** - Best practices checker + +### Vulnerability Scanning +- **Trivy** - Comprehensive vulnerability scanner +- **Snyk** - Dependency vulnerability scanner +- **Clair** - Container vulnerability analysis +- **Anchore** - Deep image inspection + +### Runtime Security +- **Falco** - Runtime threat detection +- **Aqua Security** - Container security platform +- **Sysdig** - Container monitoring and security + +## Compliance and Standards + +### Industry Standards + +- [ ] Follow CIS Docker Benchmark +- [ ] Comply with NIST guidelines +- [ ] Adhere to OWASP Container Security +- [ ] Meet PCI DSS requirements (if applicable) +- [ ] Follow SOC 2 controls (if applicable) + +### Security Policies + +- [ ] Document security requirements +- [ ] Implement security review process +- [ ] Define incident response procedures +- [ ] Regular security audits +- [ ] Security training for developers +- [ ] Maintain security documentation + +## Quick Security Wins + +### Easy Fixes + +1. **Use specific base image tags** + ```dockerfile + FROM alpine:3.21 # Not alpine:latest + ``` + +2. **Run as non-root** + ```dockerfile + USER appuser + ``` + +3. **Clean package cache** + ```dockerfile + RUN apk add --no-cache package + ``` + +4. **Don't expose unnecessary ports** + ```dockerfile + # Only expose what's needed + EXPOSE 8080 + ``` + +5. **Add health checks** + ```dockerfile + HEALTHCHECK CMD curl -f http://localhost/ || exit 1 + ``` + +## Security Checklist Summary + +| Category | Critical | High | Medium | +|----------|----------|------|--------| +| Base Image | Use official, pin version | Scan for CVEs | Update regularly | +| Secrets | Never in code | Use secrets mgmt | Scan commits | +| Users | Run as non-root | High UID | Proper permissions | +| Network | TLS only | Minimal exposure | Firewall rules | +| Runtime | Drop capabilities | Read-only FS | Resource limits | + +## Resources + +- [CIS Docker Benchmark](https://www.cisecurity.org/benchmark/docker) +- [OWASP Docker Security](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html) +- [NIST Container Security Guide](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-190.pdf) +- [Docker Security Best Practices](https://docs.docker.com/develop/security-best-practices/) \ No newline at end of file diff --git a/.agents/skills/dockerfile-validator/scripts/dockerfile-validate.sh b/.agents/skills/dockerfile-validator/scripts/dockerfile-validate.sh new file mode 100644 index 0000000..67f8ac2 --- /dev/null +++ b/.agents/skills/dockerfile-validator/scripts/dockerfile-validate.sh @@ -0,0 +1,699 @@ +#!/bin/bash + +################################################################################ +# Dockerfile Validator - Complete Lifecycle Management +# +# Single self-contained script that handles: +# - Tool installation (hadolint + Checkov in Python venvs) +# - Syntax validation (hadolint) +# - Security scanning (Checkov) +# - Best practices validation (custom checks) +# - Optimization analysis (custom checks) +# - Automatic cleanup on exit (success or failure) +# +# Usage: ./dockerfile-validate.sh [Dockerfile] +################################################################################ + +set -euo pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +PURPLE='\033[0;35m' +BOLD='\033[1m' +NC='\033[0m' + +# Configuration +DOCKERFILE="${1:-Dockerfile}" +VENV_BASE_DIR="" +HADOLINT_VENV="" +CHECKOV_VENV="" +TEMP_INSTALL=false +HADOLINT_CMD="" +CHECKOV_CMD="" +HADOLINT_MISSING=false +CHECKOV_MISSING=false + +# Environment variable to force temporary installation (for testing cleanup) +# Usage: FORCE_TEMP_INSTALL=true bash scripts/dockerfile-validate.sh Dockerfile +FORCE_TEMP_INSTALL="${FORCE_TEMP_INSTALL:-false}" + +# Exit codes +EXIT_CODE=0 + +# Counters for custom checks +BP_ERRORS=0 +BP_WARNINGS=0 +BP_INFO=0 +BP_HAS_WARNINGS=false # set true when BP has warnings but no errors (drives WARN summary state) +RUN_COUNT=0 + +################################################################################ +# Cleanup Function - Called on EXIT +################################################################################ +cleanup() { + local exit_code=$? + + if [ "$TEMP_INSTALL" = true ] && [ -n "$VENV_BASE_DIR" ]; then + echo "" + echo -e "${YELLOW}Cleaning up temporary installation...${NC}" + + if [ -d "$VENV_BASE_DIR" ]; then + rm -rf "$VENV_BASE_DIR" + echo -e "${GREEN}✓ Removed temporary venvs${NC}" + fi + + echo -e "${GREEN}✓ Cleanup complete${NC}" + fi + + exit $exit_code +} + +# Set trap for cleanup on any exit +trap cleanup EXIT INT TERM + +################################################################################ +# Tool Installation Functions +################################################################################ + +check_python() { + if command -v python3 &> /dev/null; then + PYTHON_CMD="python3" + elif command -v python &> /dev/null; then + PYTHON_CMD="python" + else + echo -e "${RED}ERROR: Python 3 is required but not installed${NC}" >&2 + exit 2 + fi + + # Verify Python version (need 3.8+) + PYTHON_VERSION=$($PYTHON_CMD --version 2>&1 | awk '{print $2}') + MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1) + MINOR=$(echo $PYTHON_VERSION | cut -d. -f2) + + if [ "$MAJOR" -lt 3 ] || ([ "$MAJOR" -eq 3 ] && [ "$MINOR" -lt 8 ]); then + echo -e "${RED}ERROR: Python 3.8+ required (found $PYTHON_VERSION)${NC}" >&2 + exit 2 + fi +} + +check_tools() { + # If FORCE_TEMP_INSTALL is set, skip tool check and force installation + if [ "$FORCE_TEMP_INSTALL" = "true" ]; then + echo -e "${YELLOW}FORCE_TEMP_INSTALL=true: Forcing temporary tool installation for testing${NC}" + HADOLINT_MISSING=true + CHECKOV_MISSING=true + return 1 + fi + + HADOLINT_MISSING=false + CHECKOV_MISSING=false + + # Check for hadolint (system-installed) + if command -v hadolint &> /dev/null; then + HADOLINT_CMD="hadolint" + else + HADOLINT_MISSING=true + fi + + # Check for Checkov (system-installed) + if command -v checkov &> /dev/null; then + CHECKOV_CMD="checkov" + else + CHECKOV_MISSING=true + fi + + # Return 0 if both found, 1 if installation needed + [ "$HADOLINT_MISSING" = false ] && [ "$CHECKOV_MISSING" = false ] +} + +install_hadolint() { + echo -e "${BLUE}Installing hadolint...${NC}" + + mkdir -p "$HADOLINT_VENV" + $PYTHON_CMD -m venv "$HADOLINT_VENV" 2>&1 | grep -v "upgrade pip" || true + + "$HADOLINT_VENV/bin/pip" install --quiet --upgrade pip + "$HADOLINT_VENV/bin/pip" install --quiet hadolint-bin + + if "$HADOLINT_VENV/bin/hadolint" --version &> /dev/null; then + HADOLINT_CMD="$HADOLINT_VENV/bin/hadolint" + VERSION=$("$HADOLINT_VENV/bin/hadolint" --version | head -n1) + echo -e "${GREEN}✓ hadolint installed: $VERSION${NC}" + else + echo -e "${RED}✗ hadolint installation failed${NC}" >&2 + exit 2 + fi +} + +install_checkov() { + echo -e "${BLUE}Installing Checkov...${NC}" + + mkdir -p "$CHECKOV_VENV" + $PYTHON_CMD -m venv "$CHECKOV_VENV" 2>&1 | grep -v "upgrade pip" || true + + "$CHECKOV_VENV/bin/pip" install --quiet --upgrade pip + "$CHECKOV_VENV/bin/pip" install --quiet checkov + + if "$CHECKOV_VENV/bin/checkov" --version &> /dev/null; then + CHECKOV_CMD="$CHECKOV_VENV/bin/checkov" + VERSION=$("$CHECKOV_VENV/bin/checkov" --version 2>&1) + echo -e "${GREEN}✓ Checkov installed: $VERSION${NC}" + else + echo -e "${RED}✗ Checkov installation failed${NC}" >&2 + exit 2 + fi +} + +install_tools() { + if [ "$HADOLINT_MISSING" = false ] && [ "$CHECKOV_MISSING" = false ]; then + return 0 + fi + + echo -e "${YELLOW}${BOLD}Installing validation tools...${NC}" + echo "" + + TEMP_INSTALL=true + VENV_BASE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/dockerfile-validator.XXXXXX") + HADOLINT_VENV="${VENV_BASE_DIR}/hadolint-venv" + CHECKOV_VENV="${VENV_BASE_DIR}/checkov-venv" + + check_python + + if [ "$HADOLINT_MISSING" = true ]; then + install_hadolint + fi + + if [ "$CHECKOV_MISSING" = true ]; then + install_checkov + fi + + echo "" +} + +################################################################################ +# Dockerfile Preprocessing - Handle Multi-line Instructions +################################################################################ + +# Normalize Dockerfile by joining continuation lines (lines ending with \) +# This allows accurate counting of multi-line instructions +normalize_dockerfile() { + local dockerfile="$1" + # Use awk to join lines that end with backslash + awk ' + /\\$/ { + sub(/\\$/, "") + printf "%s", $0 + next + } + { print } + ' "$dockerfile" +} + +count_instruction() { + local content="$1" + local instruction="$2" + + printf '%s\n' "$content" | awk -v instruction="$instruction" ' + BEGIN { IGNORECASE=1 } + $0 ~ "^[[:space:]]*" instruction "[[:space:]]" { count++ } + END { print count + 0 } + ' +} + +from_images() { + local content="$1" + + printf '%s\n' "$content" | awk ' + BEGIN { IGNORECASE=1 } + function parse_from_image(line, n, token, i) { + sub(/^[[:space:]]*FROM[[:space:]]+/, "", line) + sub(/[[:space:]]+#.*/, "", line) + n = split(line, token, /[[:space:]]+/) + + i = 1 + while (i <= n && token[i] ~ /^--/) { + # Handle both --platform= and --platform forms. + if (token[i] == "--platform" && i < n) { + i += 2 + continue + } + i++ + } + + if (i <= n) { + return token[i] + } + + return "" + } + + /^[[:space:]]*FROM[[:space:]]+/ { + image = parse_from_image($0) + if (image != "") { + print image + } + } + ' +} + +final_from_image() { + local content="$1" + + from_images "$content" | tail -n1 +} + +is_nonroot_base_image() { + local image="$1" + + if echo "$image" | grep -qiE 'distroless[^[:space:]]*:nonroot|:nonroot$'; then + return 0 + fi + + return 1 +} + +################################################################################ +# Validation Functions +################################################################################ + +run_hadolint() { + echo -e "${CYAN}${BOLD}[1/4] Syntax Validation (hadolint)${NC}" + echo "=====================================" + echo "" + + if "$HADOLINT_CMD" "$DOCKERFILE" 2>&1; then + echo "" + echo -e "${GREEN}✓ Syntax validation passed${NC}" + return 0 + else + local hadolint_exit=$? + echo "" + echo -e "${YELLOW}⚠ Syntax issues found${NC}" + EXIT_CODE=1 + return $hadolint_exit + fi +} + +run_checkov() { + echo -e "${CYAN}${BOLD}[2/4] Security Scan (Checkov)${NC}" + echo "================================" + echo "" + + if "$CHECKOV_CMD" -f "$DOCKERFILE" --framework dockerfile --compact 2>&1; then + echo "" + echo -e "${GREEN}✓ Security scan passed${NC}" + return 0 + else + local checkov_exit=$? + echo "" + echo -e "${YELLOW}⚠ Security issues found${NC}" + EXIT_CODE=1 + return $checkov_exit + fi +} + +run_best_practices() { + echo -e "${CYAN}${BOLD}[3/4] Best Practices Validation${NC}" + echo "====================================" + echo "" + + # Reset counters + BP_ERRORS=0 + BP_WARNINGS=0 + BP_INFO=0 + + # Create normalized version for accurate multi-line instruction counting + local normalized_content + normalized_content=$(normalize_dockerfile "$DOCKERFILE") + local final_image + final_image=$(final_from_image "$normalized_content") + + # Check for :latest tag + if grep -qiE "^[[:space:]]*FROM[[:space:]]+[^[:space:]]+:latest([[:space:]]|$)" "$DOCKERFILE"; then + echo -e "${YELLOW}[WARNING] Base image using :latest tag${NC}" + echo " → Use specific version tags for reproducibility" + ((BP_WARNINGS++)) + fi + + # Check for USER directive + if ! grep -qiE "^[[:space:]]*USER[[:space:]]+" "$DOCKERFILE"; then + if is_nonroot_base_image "$final_image"; then + echo -e "${PURPLE}[INFO] No USER directive, but final base image is non-root: $final_image${NC}" + echo " → Confirm runtime user requirements for your platform" + ((BP_INFO++)) + else + echo -e "${YELLOW}[WARNING] No USER directive - container will run as root${NC}" + echo " → Add 'USER ' before CMD/ENTRYPOINT" + ((BP_WARNINGS++)) + fi + else + LAST_USER=$(grep -iE "^[[:space:]]*USER[[:space:]]+" "$DOCKERFILE" | tail -n1 | awk '{print $2}') + LAST_USER_LOWER=$(echo "$LAST_USER" | tr '[:upper:]' '[:lower:]') + if [[ "$LAST_USER_LOWER" == "root" ]] || [[ "$LAST_USER" == "0" ]] || [[ "$LAST_USER" == "0:0" ]]; then + echo -e "${RED}[ERROR] Last USER directive sets user to root${NC}" + echo " → Container should not run as root user" + ((BP_ERRORS++)) + EXIT_CODE=1 + fi + fi + + # Check for HEALTHCHECK + if ! grep -qiE "^[[:space:]]*HEALTHCHECK[[:space:]]+" "$DOCKERFILE"; then + if grep -qiE "^[[:space:]]*EXPOSE[[:space:]]+|^[[:space:]]*(CMD|ENTRYPOINT)[[:space:]].*server" "$DOCKERFILE"; then + echo -e "${PURPLE}[INFO] No HEALTHCHECK defined for service container${NC}" + echo " → Consider adding HEALTHCHECK for monitoring" + ((BP_INFO++)) + fi + fi + + # Check RUN command efficiency (using normalized content for accurate counting) + RUN_COUNT=$(count_instruction "$normalized_content" "RUN") + if [ "$RUN_COUNT" -gt "5" ]; then + echo -e "${PURPLE}[INFO] High number of RUN commands ($RUN_COUNT)${NC}" + echo " → Consider combining related commands to reduce layers" + ((BP_INFO++)) + fi + + # Check for apt-get cache cleanup (must happen in same RUN instruction) + APT_INSTALL_WITHOUT_CLEAN=$(printf '%s\n' "$normalized_content" | awk ' + BEGIN { IGNORECASE=1 } + /^[[:space:]]*RUN[[:space:]]+/ && /apt-get[[:space:]]+install/ { + has_clean = ($0 ~ /rm[[:space:]]+-rf[[:space:]]+\/var\/lib\/apt\/lists/ || $0 ~ /apt-get[[:space:]]+clean/) + if (!has_clean) { count++ } + } + END { print count + 0 } + ') + + if [ "$APT_INSTALL_WITHOUT_CLEAN" -gt 0 ]; then + echo -e "${YELLOW}[WARNING] apt-get install found without same-layer cache cleanup${NC}" + echo " → Add '&& rm -rf /var/lib/apt/lists/*' to the same RUN instruction" + ((BP_WARNINGS++)) + fi + + # Check for apk --no-cache (using normalized content) + APK_ADD_WITHOUT_NOCACHE=$(printf '%s\n' "$normalized_content" | awk ' + BEGIN { IGNORECASE=1 } + /^[[:space:]]*RUN[[:space:]]+/ && /apk[[:space:]]+add/ { + has_no_cache = ($0 ~ /apk[[:space:]]+add[^#]*--no-cache/) + has_manual_cleanup = ($0 ~ /rm[[:space:]]+-rf[[:space:]]+\/var\/cache\/apk/) + if (!has_no_cache && !has_manual_cleanup) { count++ } + } + END { print count + 0 } + ') + + if [ "$APK_ADD_WITHOUT_NOCACHE" -gt 0 ]; then + echo -e "${YELLOW}[WARNING] apk add without --no-cache or manual cache cleanup${NC}" + echo " → Use 'apk add --no-cache' to avoid cache in image" + ((BP_WARNINGS++)) + fi + + # Check for hardcoded secrets (ignore comments) + # Use tolower() instead of IGNORECASE=1 for ~ operator: BSD awk (macOS) does not + # honour IGNORECASE when using the dynamic ~ operator, only for literal /patterns/. + # Dockerfile convention is UPPERCASE variable names (ENV PASSWORD=, ENV API_KEY=), + # so without tolower() all secrets are silently missed on macOS. + if awk ' + /^[[:space:]]*#/ { next } + /^[[:space:]]*(ENV|ARG)[[:space:]]/ { + lower = tolower($0) + if (lower ~ /(password|secret|api_key|api-key|apikey|token)[[:space:]]*=/) { + found=1 + } + } + END { exit found ? 0 : 1 } + ' "$DOCKERFILE"; then + echo -e "${RED}[ERROR] Potential hardcoded secrets in ENV/ARG${NC}" + echo " → Never hardcode secrets in Dockerfiles" + ((BP_ERRORS++)) + EXIT_CODE=1 + fi + + # Check for poor COPY ordering (COPY . before dependency installation) + # This hurts build cache efficiency - dependencies should be copied first. + # Stage-aware: resets tracking on each FROM so a COPY . in a builder stage + # does not produce a false positive against installs in a separate runtime stage. + COPY_ORDER_ISSUE=$(printf '%s\n' "$normalized_content" | awk ' + { + lower = tolower($0) + + # New build stage — reset per-stage COPY . tracking + if (lower ~ /^[[:space:]]*from[[:space:]]+/) { + stage_copy_line = 0 + next + } + + # Skip comment lines + if (lower ~ /^[[:space:]]*#/) { next } + + # Record the first COPY . in the current stage (ignore COPY --from=) + if (stage_copy_line == 0 && lower ~ /^[[:space:]]*copy[[:space:]]+/) { + stripped = lower + sub(/^[[:space:]]*copy[[:space:]]+/, "", stripped) + while (stripped ~ /^--[^[:space:]]+[[:space:]]+/) { + sub(/^--[^[:space:]]+[[:space:]]+/, "", stripped) + } + split(stripped, parts, /[[:space:]]+/) + if (parts[1] == "." || parts[1] == "./") { + stage_copy_line = NR + } + } + + # Flag if a package install follows COPY . within the same stage + if (stage_copy_line > 0 && NR > stage_copy_line && lower ~ /^[[:space:]]*run[[:space:]]+/) { + if (lower ~ /pip[[:space:]]+install|npm[[:space:]]+(install|ci)|yarn([[:space:]]|$)|go[[:space:]]+mod[[:space:]]|apt-get[[:space:]]+install|apk[[:space:]]+add/) { + found = 1 + exit + } + } + } + END { print found + 0 } + ') + + if [ "$COPY_ORDER_ISSUE" -gt 0 ]; then + echo -e "${YELLOW}[WARNING] COPY . appears before dependency installation${NC}" + echo " → Copy dependency files (package.json, requirements.txt) first for better cache" + echo " → Then install dependencies, then COPY . for source code" + ((BP_WARNINGS++)) + fi + + echo "" + echo "Best Practices Summary:" + echo -e " Errors: ${RED}$BP_ERRORS${NC}" + echo -e " Warnings: ${YELLOW}$BP_WARNINGS${NC}" + echo -e " Info: ${PURPLE}$BP_INFO${NC}" + echo "" + + if [ $BP_ERRORS -eq 0 ] && [ $BP_WARNINGS -eq 0 ]; then + echo -e "${GREEN}✓ Best practices validation passed${NC}" + return 0 + elif [ $BP_ERRORS -eq 0 ]; then + echo -e "${YELLOW}⚠ Best practices completed with warnings${NC}" + BP_HAS_WARNINGS=true + return 0 + else + echo -e "${RED}✗ Best practices validation failed${NC}" + return 1 + fi +} + +run_optimization() { + echo -e "${CYAN}${BOLD}[4/4] Optimization Analysis${NC}" + echo "==============================" + echo "" + + # Create normalized version for accurate multi-line instruction counting + local normalized_content + normalized_content=$(normalize_dockerfile "$DOCKERFILE") + + # Analyze base images + BASE_IMAGES=$(from_images "$normalized_content") + + echo -e "${BLUE}Base Image Analysis:${NC}" + for image in $BASE_IMAGES; do + if echo "$image" | grep -qi "distroless"; then + continue + fi + + if echo "$image" | grep -qiE "ubuntu|debian|centos|fedora"; then + echo -e " ${PURPLE}[OPTIMIZATION] Consider Alpine alternative for: $image${NC}" + echo " → Alpine images are 10-100x smaller" + fi + done + echo "" + + # Multi-stage analysis (using normalized content) + FROM_COUNT=$(count_instruction "$normalized_content" "FROM") + + echo -e "${BLUE}Build Structure:${NC}" + if [ "$FROM_COUNT" -eq "1" ]; then + if echo "$normalized_content" | grep -qiE "apt-get install.*(gcc|make|build)" || \ + echo "$normalized_content" | grep -qiE "apk add.*(gcc|make|build)"; then + echo -e " ${PURPLE}[OPTIMIZATION] Build tools detected in single-stage build${NC}" + echo " → Consider multi-stage build to exclude build tools from final image" + fi + else + FINAL_FROM=$(final_from_image "$normalized_content") + if echo "$FINAL_FROM" | grep -qiE "distroless|alpine|scratch"; then + echo -e " ${GREEN}✓ Using minimal base for final stage: $FINAL_FROM${NC}" + else + echo -e " ${PURPLE}[OPTIMIZATION] Final stage could use smaller base image${NC}" + echo " → Consider: alpine, distroless, or scratch" + fi + fi + echo "" + + # Layer count (reuse RUN_COUNT from best practices if available, otherwise calculate) + RUN_COUNT=$(count_instruction "$normalized_content" "RUN") + + echo -e "${BLUE}Layer Optimization:${NC}" + echo " RUN commands: $RUN_COUNT" + if [ "$RUN_COUNT" -gt "7" ]; then + echo -e " ${PURPLE}[OPTIMIZATION] Consider combining RUN commands${NC}" + echo " → Reduces layer count and image size" + fi + echo "" + + # .dockerignore check + DOCKERFILE_DIR=$(dirname "$DOCKERFILE") + if [ ! -f "$DOCKERFILE_DIR/.dockerignore" ]; then + echo -e "${YELLOW}[INFO] No .dockerignore file found${NC}" + echo " → Create .dockerignore to optimize build context" + echo "" + fi + + echo -e "${GREEN}✓ Optimization analysis complete${NC}" + return 0 +} + +################################################################################ +# Main Execution +################################################################################ + +show_help() { + # Use echo -e so that BOLD/NC variables (which hold \033[...m escape sequences) + # are interpreted correctly. cat << EOF expands variables but does not process + # \033 escape sequences, causing raw escape codes to appear in the output. + echo -e "${BOLD}Dockerfile Validator - Complete Lifecycle${NC}" + echo "" + echo "Validates Dockerfiles with automatic tool management and cleanup." + echo "" + echo -e "${BOLD}Usage:${NC}" + echo " $(basename "$0") [Dockerfile]" + echo "" + echo -e "${BOLD}Validation Stages:${NC}" + echo " 1. Syntax validation (hadolint)" + echo " 2. Security scanning (Checkov)" + echo " 3. Best practices validation" + echo " 4. Optimization analysis" + echo "" + echo -e "${BOLD}Features:${NC}" + echo " • Auto-installs tools if not found" + echo " • Runs all validation stages" + echo " • Auto-cleanup on exit" + echo "" + echo -e "${BOLD}Examples:${NC}" + echo " $(basename "$0") # Validate ./Dockerfile" + echo " $(basename "$0") Dockerfile.prod # Validate specific file" + echo "" + echo -e "${BOLD}Exit Codes:${NC}" + echo " 0 All validations passed" + echo " 1 One or more validations failed" + echo " 2 Critical error" + echo "" +} + +# Check for help +ARG1="${1:-}" +if [[ "$ARG1" == "-h" ]] || [[ "$ARG1" == "--help" ]]; then + show_help + exit 0 +fi + +# Validate input +if [ ! -f "$DOCKERFILE" ]; then + echo -e "${RED}ERROR: Dockerfile not found: $DOCKERFILE${NC}" >&2 + echo "" + echo "Usage: $(basename "$0") [Dockerfile]" + exit 2 +fi + +# Print header +echo "" +echo -e "${CYAN}${BOLD}========================================${NC}" +echo -e "${CYAN}${BOLD} Dockerfile Validator${NC}" +echo -e "${CYAN}${BOLD}========================================${NC}" +echo "" +echo -e "${BOLD}Target:${NC} $DOCKERFILE" +echo -e "${BOLD}Date:${NC} $(date '+%Y-%m-%d %H:%M:%S')" +echo "" + +# Check and install tools if needed +if ! check_tools; then + install_tools +fi + +echo -e "${CYAN}${BOLD}Running Validations...${NC}" +echo "" + +# Track results +HADOLINT_RESULT="SKIP" +CHECKOV_RESULT="SKIP" +BEST_PRACTICES_RESULT="SKIP" +OPTIMIZATION_RESULT="SKIP" + +# Run all validations +run_hadolint && HADOLINT_RESULT="PASS" || HADOLINT_RESULT="FAIL" +echo "" + +run_checkov && CHECKOV_RESULT="PASS" || CHECKOV_RESULT="FAIL" +echo "" + +run_best_practices && { + [ "$BP_HAS_WARNINGS" = true ] && BEST_PRACTICES_RESULT="WARN" || BEST_PRACTICES_RESULT="PASS" +} || BEST_PRACTICES_RESULT="FAIL" +echo "" + +run_optimization && OPTIMIZATION_RESULT="INFO" +echo "" + +# Print summary +echo -e "${CYAN}${BOLD}========================================${NC}" +echo -e "${CYAN}${BOLD} Validation Summary${NC}" +echo -e "${CYAN}${BOLD}========================================${NC}" +echo "" + +# Print results +[ "$HADOLINT_RESULT" = "PASS" ] && echo -e " Syntax (hadolint): ${GREEN}✓ PASSED${NC}" || echo -e " Syntax (hadolint): ${RED}✗ FAILED${NC}" +[ "$CHECKOV_RESULT" = "PASS" ] && echo -e " Security (Checkov): ${GREEN}✓ PASSED${NC}" || echo -e " Security (Checkov): ${RED}✗ FAILED${NC}" +if [ "$BEST_PRACTICES_RESULT" = "PASS" ]; then + echo -e " Best Practices: ${GREEN}✓ PASSED${NC}" +elif [ "$BEST_PRACTICES_RESULT" = "WARN" ]; then + echo -e " Best Practices: ${YELLOW}⚠ WARNED${NC}" +else + echo -e " Best Practices: ${RED}✗ FAILED${NC}" +fi +echo -e " Optimization: ${BLUE}ℹ INFORMATIONAL${NC}" + +echo "" +echo -e "${CYAN}${BOLD}========================================${NC}" +echo "" + +# Overall result +if [ $EXIT_CODE -eq 0 ]; then + echo -e "${GREEN}${BOLD}✓ Overall Result: PASSED${NC}" + echo "" + echo "Your Dockerfile meets validation requirements." +else + echo -e "${RED}${BOLD}✗ Overall Result: FAILED${NC}" + echo "" + echo "Please address the issues identified above." +fi + +echo "" + +# Exit (cleanup trap will run automatically) +exit $EXIT_CODE diff --git a/.agents/skills/dockerfile-validator/scripts/test_validate.sh b/.agents/skills/dockerfile-validator/scripts/test_validate.sh new file mode 100644 index 0000000..2a63f12 --- /dev/null +++ b/.agents/skills/dockerfile-validator/scripts/test_validate.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# +# CI-friendly regression entrypoint for dockerfile-validator. +# Runs syntax checks, regression tests, and optional ShellCheck linting. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +readonly SKILL_DIR + +VALIDATOR_SCRIPT="$SKILL_DIR/scripts/dockerfile-validate.sh" +REGRESSION_SCRIPT="$SKILL_DIR/tests/test_regression.sh" +STRICT_SHELLCHECK="${STRICT_SHELLCHECK:-false}" + +echo "Running dockerfile-validator CI checks..." + +# Fast syntax guard for scripts used by the regression suite. +bash -n "$VALIDATOR_SCRIPT" "$REGRESSION_SCRIPT" "$0" + +# Regression coverage for parser/best-practice branches. +bash "$REGRESSION_SCRIPT" + +if command -v shellcheck >/dev/null 2>&1; then + shellcheck "$VALIDATOR_SCRIPT" "$REGRESSION_SCRIPT" "$0" + echo "ShellCheck: PASS" +elif [[ "$STRICT_SHELLCHECK" == "true" ]]; then + echo "ShellCheck: required but not installed (STRICT_SHELLCHECK=true)" >&2 + exit 1 +else + echo "ShellCheck: SKIP (not installed; set STRICT_SHELLCHECK=true to require it)" +fi + +echo "PASS: dockerfile-validator CI checks" diff --git a/.agents/skills/dockerfile-validator/tests/fixtures/copy-before-yarn-lock-read.Dockerfile b/.agents/skills/dockerfile-validator/tests/fixtures/copy-before-yarn-lock-read.Dockerfile new file mode 100644 index 0000000..6fef28c --- /dev/null +++ b/.agents/skills/dockerfile-validator/tests/fixtures/copy-before-yarn-lock-read.Dockerfile @@ -0,0 +1,6 @@ +FROM node:20-alpine +WORKDIR /app +COPY . . +RUN cat yarn.lock >/dev/null 2>&1 || true +USER 1000 +CMD ["node", "server.js"] diff --git a/.agents/skills/dockerfile-validator/tests/fixtures/copy-before-yarn.Dockerfile b/.agents/skills/dockerfile-validator/tests/fixtures/copy-before-yarn.Dockerfile new file mode 100644 index 0000000..e63d580 --- /dev/null +++ b/.agents/skills/dockerfile-validator/tests/fixtures/copy-before-yarn.Dockerfile @@ -0,0 +1,6 @@ +FROM node:20-alpine +WORKDIR /app +COPY . . +RUN yarn +USER 1000 +CMD ["node", "server.js"] diff --git a/.agents/skills/dockerfile-validator/tests/fixtures/from-platform-nonroot.Dockerfile b/.agents/skills/dockerfile-validator/tests/fixtures/from-platform-nonroot.Dockerfile new file mode 100644 index 0000000..074bc79 --- /dev/null +++ b/.agents/skills/dockerfile-validator/tests/fixtures/from-platform-nonroot.Dockerfile @@ -0,0 +1,7 @@ +FROM --platform=linux/amd64 golang:1.24-alpine AS build +WORKDIR /src +RUN printf '#!/bin/sh\necho hello\n' > /out && chmod +x /out + +FROM --platform linux/amd64 gcr.io/distroless/static-debian11:nonroot AS runtime +COPY --from=build /out /out +ENTRYPOINT ["/out"] diff --git a/.agents/skills/dockerfile-validator/tests/test_regression.sh b/.agents/skills/dockerfile-validator/tests/test_regression.sh new file mode 100644 index 0000000..b144c0e --- /dev/null +++ b/.agents/skills/dockerfile-validator/tests/test_regression.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +set -euo pipefail + +SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VALIDATOR="$SKILL_DIR/scripts/dockerfile-validate.sh" +FIXTURES_DIR="$SKILL_DIR/tests/fixtures" +TMP_DIR="$(mktemp -d)" + +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +PASS=0 +FAIL=0 + +pass() { + echo " PASS: $1" + PASS=$((PASS + 1)) +} + +fail() { + echo " FAIL: $1" + FAIL=$((FAIL + 1)) +} + +make_stub_tools() { + mkdir -p "$TMP_DIR/bin" + + cat > "$TMP_DIR/bin/hadolint" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +exit 0 +EOF + + cat > "$TMP_DIR/bin/checkov" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +exit 0 +EOF + + chmod +x "$TMP_DIR/bin/hadolint" "$TMP_DIR/bin/checkov" +} + +validator_output() { + local dockerfile="$1" + PATH="$TMP_DIR/bin:$PATH" bash "$VALIDATOR" "$dockerfile" 2>&1 || true +} + +validator_exit_code() { + local dockerfile="$1" + local exit_code=0 + PATH="$TMP_DIR/bin:$PATH" bash "$VALIDATOR" "$dockerfile" >/dev/null 2>&1 || exit_code=$? + echo "$exit_code" +} + +assert_output_contains() { + local label="$1" + local dockerfile="$2" + local pattern="$3" + + local output + output="$(validator_output "$dockerfile")" + + if echo "$output" | grep -qE "$pattern"; then + pass "$label" + else + fail "$label (missing pattern: $pattern)" + echo " --- validator output ---" + echo "$output" | sed 's/^/ /' + echo " --- end output ---" + fi +} + +assert_output_not_contains() { + local label="$1" + local dockerfile="$2" + local pattern="$3" + + local output + output="$(validator_output "$dockerfile")" + + if echo "$output" | grep -qE "$pattern"; then + fail "$label (unexpected pattern: $pattern)" + echo " --- validator output ---" + echo "$output" | sed 's/^/ /' + echo " --- end output ---" + else + pass "$label" + fi +} + +assert_exit_zero() { + local label="$1" + local dockerfile="$2" + + local exit_code + exit_code="$(validator_exit_code "$dockerfile")" + + if [[ "$exit_code" -eq 0 ]]; then + pass "$label" + else + fail "$label (expected 0, got $exit_code)" + fi +} + +echo "Running dockerfile-validator regression tests..." +echo "" + +make_stub_tools + +COPY_BEFORE_YARN="$FIXTURES_DIR/copy-before-yarn.Dockerfile" +COPY_BEFORE_YARN_LOCK="$FIXTURES_DIR/copy-before-yarn-lock-read.Dockerfile" +FROM_PLATFORM_NONROOT="$FIXTURES_DIR/from-platform-nonroot.Dockerfile" + +echo "[copy-before-yarn]" +assert_output_contains \ + "flags COPY . before bare RUN yarn" \ + "$COPY_BEFORE_YARN" \ + "COPY \\. appears before dependency installation" +assert_exit_zero \ + "warn-only result stays non-failing" \ + "$COPY_BEFORE_YARN" + +echo "" +echo "[copy-before-yarn-lock-read]" +assert_output_not_contains \ + "does not misclassify yarn.lock as yarn install" \ + "$COPY_BEFORE_YARN_LOCK" \ + "COPY \\. appears before dependency installation" +assert_exit_zero \ + "non-install RUN after COPY . remains non-failing" \ + "$COPY_BEFORE_YARN_LOCK" + +echo "" +echo "[from-platform-nonroot]" +assert_output_contains \ + "parses --platform final image as non-root base" \ + "$FROM_PLATFORM_NONROOT" \ + "No USER directive, but final base image is non-root: gcr.io/distroless/static-debian11:nonroot" +assert_output_not_contains \ + "does not emit root warning for non-root distroless base" \ + "$FROM_PLATFORM_NONROOT" \ + "container will run as root" +assert_output_contains \ + "optimization stage reports parsed minimal final base" \ + "$FROM_PLATFORM_NONROOT" \ + "Using minimal base for final stage: gcr.io/distroless/static-debian11:nonroot" +assert_output_not_contains \ + "base image analysis never treats --platform as image" \ + "$FROM_PLATFORM_NONROOT" \ + "Consider Alpine alternative for: --platform" +assert_exit_zero \ + "platform flag fixture exits successfully" \ + "$FROM_PLATFORM_NONROOT" + +echo "" +echo "Summary: $PASS passed, $FAIL failed" + +if [[ "$FAIL" -ne 0 ]]; then + exit 1 +fi + +echo "PASS: dockerfile-validator regression tests" diff --git a/.agents/skills/github-actions-generator/SKILL.md b/.agents/skills/github-actions-generator/SKILL.md new file mode 100644 index 0000000..8068c61 --- /dev/null +++ b/.agents/skills/github-actions-generator/SKILL.md @@ -0,0 +1,350 @@ +--- +name: github-actions-generator +description: Create, generate, or scaffold GitHub Actions workflows, action.yml, or .github/workflows CI/CD pipelines. +--- + +# GitHub Actions Generator + +Generate production-ready GitHub Actions workflows and custom actions following current best practices, security standards, and naming conventions. All generated resources are automatically validated using the devops-skills:github-actions-validator skill. + +## Quick Reference + +| Capability | When to Use | Reference | +|------------|-------------|-----------| +| Workflows | CI/CD, automation, testing | `references/best-practices.md` | +| Composite Actions | Reusable step combinations | `references/custom-actions.md` | +| Docker Actions | Custom environments/tools | `references/custom-actions.md` | +| JavaScript Actions | API interactions, complex logic | `references/custom-actions.md` | +| Reusable Workflows | Shared patterns across repos | `references/advanced-triggers.md` | +| Security Scanning | Dependency review, SBOM | `references/best-practices.md` | +| Modern Features | Summaries, environments | `references/modern-features.md` | + +--- + +## Trigger Decision Tree + +Route every request through this decision tree before reading references or generating files: + +1. If the user asks for `.github/workflows/*.yml` CI/CD automation, choose **Workflow Generation**. +2. If the user asks for `action.yml` or a reusable step package, choose **Custom Action Generation**. +3. If the user asks for `workflow_call` or shared pipelines across repositories, choose **Reusable Workflow Generation**. +4. If the request includes security-only scanning (dependency review, SBOM, CodeQL), stay on **Workflow Generation** with the security pattern. +5. If intent is ambiguous, ask one disambiguation question: "Do you want a workflow, a custom action, or a reusable workflow?" + +## Progressive Disclosure Route + +Load only what is needed for the selected route, in this order: + +| Route | Load First (required) | Load Next (only if needed) | Primary Template | +|-------|------------------------|------------------------------|------------------| +| Workflow Generation | `references/best-practices.md` | `references/common-actions.md`, `references/expressions-and-contexts.md`, `references/modern-features.md` | `assets/templates/workflow/basic_workflow.yml` | +| Custom Action Generation | `references/custom-actions.md` | `references/best-practices.md` | `assets/templates/action/composite/action.yml`, `assets/templates/action/docker/`, `assets/templates/action/javascript/` | +| Reusable Workflow Generation | `references/advanced-triggers.md` | `references/best-practices.md`, `references/common-actions.md` | `assets/templates/workflow/reusable_workflow.yml` | + +If a required reference/template is unavailable, continue with the closest available reference and report the fallback explicitly in output. + +--- + +## Core Capabilities + +### 1. Generate Workflows + +**Triggers:** "Create a workflow for...", "Build a CI/CD pipeline..." + +**Process:** +1. Understand requirements (triggers, runners, dependencies) +2. Define trust boundaries (internal branches vs fork PRs vs external triggers) +3. Set default `permissions` to read-only, then elevate only per job when required +4. Reference `references/best-practices.md` for patterns +5. Reference `references/common-actions.md` for action versions +6. Generate workflow with: + - Semantic names, pinned actions (SHA), explicit permissions + - Concurrency controls, caching, matrix strategies + - Fork-safe PR handling (no secrets in untrusted contexts) +7. **Validate** with devops-skills:github-actions-validator skill +8. Fix issues and re-validate if needed + +**Minimal Example:** +```yaml +name: CI Pipeline + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + cache: 'npm' + - run: npm ci + - run: npm test +``` + +**Untrusted PR Guardrail (required for secret-using jobs):** +```yaml +jobs: + deploy: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository +``` + +### 2. Generate Custom Actions + +**Triggers:** "Create a composite action...", "Build a Docker action...", "Create a JavaScript action..." + +**Types:** +- **Composite:** Combine multiple steps → Fast startup +- **Docker:** Custom environment/tools → Isolated +- **JavaScript:** API access, complex logic → Fastest + +**Process:** +1. Use templates from `assets/templates/action/` +2. Follow structure in `references/custom-actions.md` +3. Include branding, inputs/outputs, documentation +4. **Validate** with devops-skills:github-actions-validator skill + +See `references/custom-actions.md` for: +- Action metadata and branding +- Directory structure patterns +- Versioning and release workflows + +### 3. Generate Reusable Workflows + +**Triggers:** "Create a reusable workflow...", "Make this workflow callable..." + +**Key Elements:** +- `workflow_call` trigger with typed inputs +- Explicit secrets (avoid `secrets: inherit`) +- Explicit trusted-caller expectations (document org/repo boundaries) +- Outputs mapped from job outputs +- Minimal permissions + +```yaml +on: + workflow_call: + inputs: + environment: + required: true + type: string + secrets: + deploy-token: + required: false + outputs: + result: + value: ${{ jobs.build.outputs.result }} +``` + +When secrets are required, pass only the exact secret names needed and prefer environment protection rules for deployment stages. + +See `references/advanced-triggers.md` for complete patterns. + +### 4. Generate Security Workflows + +**Triggers:** "Add security scanning...", "Add dependency review...", "Generate SBOM..." + +**Components:** +- **Dependency Review:** `actions/dependency-review-action@v4` +- **SBOM Attestations:** `actions/attest-sbom@v2` +- **CodeQL Analysis:** `github/codeql-action` + +**Permission Model:** +Use a read-only workflow-level baseline, then elevate only in the security job that requires write scopes. +```yaml +permissions: + contents: read + +jobs: + security-scan: + permissions: + contents: read + security-events: write # For CodeQL + id-token: write # For attestations + attestations: write # For attestations +``` + +See `references/best-practices.md` section on security. + +### 5. Modern Features + +**Triggers:** "Add job summaries...", "Use environments...", "Run in container..." + +See `references/modern-features.md` for: +- Job summaries (`$GITHUB_STEP_SUMMARY`) +- Deployment environments with approvals +- Container jobs with services +- Workflow annotations + +### 6. Third-Party Action Documentation and Citation + +When using third-party actions (any `uses:` entry not in the same repository): + +1. **Search for documentation:** + ``` + "[owner/repo] [version] github action documentation" + ``` + +2. **Or use Context7 MCP:** + - `mcp__context7__resolve-library-id` to find action + - `mcp__context7__query-docs` for documentation + +3. **Pin to SHA with version comment:** + ```yaml + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + ``` + +4. **Cite source and version in the response:** + - Action source (repository URL) + - Version source (release/tag/changelog URL) + - Selected commit SHA and human-readable version + - Access date for the source used + +See `references/common-actions.md` for pre-verified action versions. + +--- + +## Validation Workflow + +**CRITICAL:** Every generated resource MUST be validated. + +1. Generate workflow/action file +2. Invoke `devops-skills:github-actions-validator` skill +3. If errors: fix and re-validate +4. If success: present with usage instructions + +**Skip validation only for:** +- Partial code snippets +- Documentation examples +- User explicitly requests skip + +## Fallback Behavior (Tooling and Environment Constraints) + +If required tooling or network access is unavailable, use this deterministic fallback order: + +1. If `devops-skills:github-actions-validator` is unavailable, run local fallback checks: + - `actionlint` (if installed) + - `yamllint` (if installed) + - manual YAML/schema review with a clear "not tool-validated" note +2. If Context7 or internet access is unavailable: + - use `references/common-actions.md` for known action versions + - state that external version verification could not be completed +3. If a template path is missing: + - generate from the closest template pattern in `assets/templates/` + - document which template was substituted + +Fallback usage must always be reported in the final output. + +--- + +## Mandatory Standards + +All generated resources must follow: + +| Standard | Implementation | +|----------|---------------| +| **Security** | Pin to SHA, minimal permissions, mask secrets | +| **Performance** | Caching, concurrency, shallow checkout | +| **Naming** | Descriptive names, lowercase-hyphen files | +| **Error Handling** | Timeouts, cleanup with `if: always()` | + +See `references/best-practices.md` for complete guidelines. + +--- + +## Resources + +### Reference Documents + +| Document | Content | When to Use | +|----------|---------|-------------| +| `references/best-practices.md` | Security, performance, patterns | Every workflow | +| `references/common-actions.md` | Action versions, inputs, outputs | Public action usage | +| `references/expressions-and-contexts.md` | `${{ }}` syntax, contexts, functions | Complex conditionals | +| `references/advanced-triggers.md` | workflow_run, dispatch, ChatOps | Workflow orchestration | +| `references/custom-actions.md` | Metadata, structure, versioning | Custom action creation | +| `references/modern-features.md` | Summaries, environments, containers | Enhanced workflows | + +### Templates + +| Template | Location | +|----------|----------| +| Basic Workflow | `assets/templates/workflow/basic_workflow.yml` | +| Reusable Workflow | `assets/templates/workflow/reusable_workflow.yml` | +| Composite Action | `assets/templates/action/composite/action.yml` | +| Docker Action | `assets/templates/action/docker/` | +| JavaScript Action | `assets/templates/action/javascript/` | + +--- + +## Common Patterns + +### Matrix Testing +```yaml +strategy: + matrix: + os: [ubuntu-latest, windows-latest] + node: [18, 20, 22] + fail-fast: false +``` + +### Conditional Deployment +```yaml +deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' +``` + +### Artifact Sharing +```yaml +# Upload +- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: build-${{ github.sha }} + path: dist/ + +# Download (in dependent job) +- uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: build-${{ github.sha }} +``` + +### Third-Party Action Citation Block +```text +Third-party action citations: +- actions/checkout: https://github.com/actions/checkout (version: v6.0.2, sha: de0fac2e4500dabe0009e67214ff5f5447ce83dd, accessed: 2026-02-28) +``` + +--- + +## Done Criteria + +The task is complete only when all checks below pass: + +1. The request route was selected using the trigger decision tree. +2. Only the minimum required references/templates were loaded first. +3. Every third-party action is pinned to a commit SHA and has source/version citation. +4. Validation was run, or a skip exception/fallback path was explicitly documented. +5. Output includes assumptions, security-sensitive decisions (permissions/secrets), and generated file paths. + +--- + +## Workflow Summary + +1. **Route** the request using the trigger decision tree +2. **Load** the minimum references/templates for that route +3. **Generate** using mandatory security and naming standards +4. **Cite** and pin third-party actions (source, version, SHA) +5. **Validate** with `devops-skills:github-actions-validator` (or documented fallback) +6. **Fix and re-validate** until clean +7. **Present** validated output with citations, assumptions, and file paths diff --git a/.agents/skills/github-actions-generator/assets/templates/action/composite/action.yml b/.agents/skills/github-actions-generator/assets/templates/action/composite/action.yml new file mode 100644 index 0000000..cec3a4e --- /dev/null +++ b/.agents/skills/github-actions-generator/assets/templates/action/composite/action.yml @@ -0,0 +1,82 @@ +name: '[ACTION_NAME]' +description: '[ACTION_DESCRIPTION]' +author: '[AUTHOR_NAME]' + +# Define inputs that users can provide +inputs: + [input-name]: + description: '[INPUT_DESCRIPTION]' + required: [true/false] + default: '[DEFAULT_VALUE]' + + [input-name-2]: + description: '[INPUT_DESCRIPTION]' + required: [true/false] + +# Define outputs that the action produces +outputs: + [output-name]: + description: '[OUTPUT_DESCRIPTION]' + value: ${{ steps.[step-id].outputs.[output-name] }} + + [output-name-2]: + description: '[OUTPUT_DESCRIPTION]' + value: ${{ steps.[step-id].outputs.[output-name-2] }} + +# Branding (optional - for GitHub Marketplace) +branding: + icon: '[ICON_NAME]' # Feather icon name + color: '[COLOR]' # white, yellow, blue, green, orange, red, purple, gray-dark + +runs: + using: 'composite' + steps: + # Step 1: Validation + - name: Validate inputs + shell: bash + run: | + if [ -z "${{ inputs.[input-name] }}" ]; then + echo "::error::Input [input-name] is required" + exit 1 + fi + echo "✅ Inputs validated" + + # Step 2: Setup + - name: [SETUP_STEP_NAME] + uses: "[SETUP_ACTION]@[SHA]" # [VERSION] + with: + [SETUP_INPUT]: ${{ inputs.[input-name] }} + + # Step 3: Main logic + - name: [MAIN_STEP_NAME] + id: [step-id] + shell: bash + env: + INPUT_VALUE: ${{ inputs.[input-name] }} + run: | + echo "::group::Running [MAIN_STEP_NAME]" + + # Main action logic here + [MAIN_COMMAND] + + # Set outputs + echo "[output-name]=[OUTPUT_VALUE]" >> $GITHUB_OUTPUT + echo "[output-name-2]=[OUTPUT_VALUE_2]" >> $GITHUB_OUTPUT + + echo "::endgroup::" + + # Step 4: Post-processing (optional) + - name: Post-processing + if: success() + shell: bash + run: | + echo "✅ Action completed successfully" + echo "Output: ${{ steps.[step-id].outputs.[output-name] }}" + + # Step 5: Cleanup (optional) + - name: Cleanup + if: always() + shell: bash + run: | + # Cleanup logic here + echo "Cleanup completed" diff --git a/.agents/skills/github-actions-generator/assets/templates/action/docker/Dockerfile b/.agents/skills/github-actions-generator/assets/templates/action/docker/Dockerfile new file mode 100644 index 0000000..6897e28 --- /dev/null +++ b/.agents/skills/github-actions-generator/assets/templates/action/docker/Dockerfile @@ -0,0 +1,25 @@ +# Base image +FROM [BASE_IMAGE]:[TAG] + +# Install required tools with minimal packages, then clean package caches +RUN [PACKAGE_MANAGER] update && \ + [PACKAGE_MANAGER] install [PACKAGE_INSTALL_FLAGS] \ + [PACKAGE_1] \ + [PACKAGE_2] \ + && [CLEANUP_COMMAND] + +# Set working directory +WORKDIR /app + +# Copy application files +COPY entrypoint.sh /entrypoint.sh + +# Make entrypoint executable +RUN chmod +x /entrypoint.sh + +# Create and switch to a non-root runtime user (replace placeholders per base image) +RUN [CREATE_NON_ROOT_USER_COMMAND] +USER [RUNTIME_USER] + +# Set entrypoint +ENTRYPOINT ["/entrypoint.sh"] diff --git a/.agents/skills/github-actions-generator/assets/templates/action/docker/action.yml b/.agents/skills/github-actions-generator/assets/templates/action/docker/action.yml new file mode 100644 index 0000000..0ceba58 --- /dev/null +++ b/.agents/skills/github-actions-generator/assets/templates/action/docker/action.yml @@ -0,0 +1,42 @@ +name: '[ACTION_NAME]' +description: '[ACTION_DESCRIPTION]' +author: '[AUTHOR_NAME]' + +# Define inputs that users can provide +inputs: + [input-name]: + description: '[INPUT_DESCRIPTION]' + required: [true/false] + default: '[DEFAULT_VALUE]' + + [input-name-2]: + description: '[INPUT_DESCRIPTION]' + required: [true/false] + +# Define outputs that the action produces +outputs: + [output-name]: + description: '[OUTPUT_DESCRIPTION]' + +# Branding (optional - for GitHub Marketplace) +branding: + icon: '[ICON_NAME]' # Feather icon name + color: '[COLOR]' # white, yellow, blue, green, orange, red, purple, gray-dark + +runs: + using: 'docker' + image: 'Dockerfile' + # Optional: Use pre-built image instead + # image: 'docker://[REGISTRY]/[IMAGE]:[TAG]' + + # Arguments passed to container entrypoint + args: + - ${{ inputs.[input-name] }} + - ${{ inputs.[input-name-2] }} + + # Environment variables + env: + [ENV_VAR]: ${{ inputs.[input-name] }} + + # Optional: Override entrypoint + # entrypoint: '/entrypoint.sh' diff --git a/.agents/skills/github-actions-generator/assets/templates/action/docker/entrypoint.sh b/.agents/skills/github-actions-generator/assets/templates/action/docker/entrypoint.sh new file mode 100644 index 0000000..51d2984 --- /dev/null +++ b/.agents/skills/github-actions-generator/assets/templates/action/docker/entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +# Action entrypoint script +# Arguments are passed from action.yml + +[INPUT_1]="$1" +[INPUT_2]="$2" + +# Validate inputs +if [ -z "$[INPUT_1]" ]; then + echo "::error::[INPUT_1] is required" + exit 1 +fi + +echo "::group::Running [ACTION_NAME]" +echo "Input 1: $[INPUT_1]" +echo "Input 2: $[INPUT_2]" + +# Main action logic +[MAIN_COMMAND] + +# Set outputs +echo "[output-name]=[OUTPUT_VALUE]" >> "$GITHUB_OUTPUT" + +echo "::endgroup::" +echo "✅ Action completed successfully" diff --git a/.agents/skills/github-actions-generator/assets/templates/action/javascript/action.yml b/.agents/skills/github-actions-generator/assets/templates/action/javascript/action.yml new file mode 100644 index 0000000..5f62b47 --- /dev/null +++ b/.agents/skills/github-actions-generator/assets/templates/action/javascript/action.yml @@ -0,0 +1,33 @@ +name: '[ACTION_NAME]' +description: '[ACTION_DESCRIPTION]' +author: '[AUTHOR_NAME]' + +# Define inputs that users can provide +inputs: + github-token: + description: 'GitHub token for API access' + required: false + default: ${{ github.token }} + + [input-name]: + description: '[INPUT_DESCRIPTION]' + required: [true/false] + default: '[DEFAULT_VALUE]' + +# Define outputs that the action produces +outputs: + [output-name]: + description: '[OUTPUT_DESCRIPTION]' + +# Branding (optional - for GitHub Marketplace) +branding: + icon: '[ICON_NAME]' # Feather icon name + color: '[COLOR]' # white, yellow, blue, green, orange, red, purple, gray-dark + +runs: + using: 'node20' + main: 'dist/index.js' + # Optional: Run code before action + # pre: 'dist/pre.js' + # Optional: Run code after action + # post: 'dist/post.js' diff --git a/.agents/skills/github-actions-generator/assets/templates/action/javascript/index.js b/.agents/skills/github-actions-generator/assets/templates/action/javascript/index.js new file mode 100644 index 0000000..f80c5e2 --- /dev/null +++ b/.agents/skills/github-actions-generator/assets/templates/action/javascript/index.js @@ -0,0 +1,50 @@ +const core = require('@actions/core'); +const github = require('@actions/github'); + +async function run() { + try { + // Get inputs + const githubToken = core.getInput('github-token'); + const inputName = core.getInput('[input-name]', { required: true }); + + // Log inputs (mask sensitive data) + core.info(`Input: ${inputName}`); + + // Create GitHub client + const octokit = github.getOctokit(githubToken); + + // Get context information + const context = github.context; + core.info(`Repository: ${context.repo.owner}/${context.repo.repo}`); + core.info(`Event: ${context.eventName}`); + + // Main action logic + core.startGroup('Running [ACTION_NAME]'); + + // Example: Get repository information + const { data: repo } = await octokit.rest.repos.get({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + + core.info(`Repository stars: ${repo.stargazers_count}`); + + // [ADD YOUR LOGIC HERE] + + core.endGroup(); + + // Set outputs + core.setOutput('[output-name]', '[OUTPUT_VALUE]'); + + // Success + core.info('✅ Action completed successfully'); + } catch (error) { + // Handle errors + core.setFailed(`Action failed: ${error.message}`); + if (error.stack) { + core.debug(error.stack); + } + } +} + +run(); diff --git a/.agents/skills/github-actions-generator/assets/templates/action/javascript/package.json b/.agents/skills/github-actions-generator/assets/templates/action/javascript/package.json new file mode 100644 index 0000000..2ce2cb3 --- /dev/null +++ b/.agents/skills/github-actions-generator/assets/templates/action/javascript/package.json @@ -0,0 +1,27 @@ +{ + "name": "[ACTION_NAME]", + "version": "1.0.0", + "description": "[ACTION_DESCRIPTION]", + "main": "dist/index.js", + "scripts": { + "build": "ncc build index.js -o dist --source-map --license licenses.txt", + "test": "jest", + "lint": "eslint ." + }, + "keywords": [ + "github-actions", + "[KEYWORD_1]", + "[KEYWORD_2]" + ], + "author": "[AUTHOR_NAME]", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0" + }, + "devDependencies": { + "@vercel/ncc": "^0.38.1", + "eslint": "^8.57.0", + "jest": "^29.7.0" + } +} diff --git a/.agents/skills/github-actions-generator/assets/templates/workflow/basic_workflow.yml b/.agents/skills/github-actions-generator/assets/templates/workflow/basic_workflow.yml new file mode 100644 index 0000000..11b9638 --- /dev/null +++ b/.agents/skills/github-actions-generator/assets/templates/workflow/basic_workflow.yml @@ -0,0 +1,242 @@ +# [WORKFLOW_NAME] +# +# [WORKFLOW_DESCRIPTION] +# +# Triggers: +# - [TRIGGER_EVENTS] +# +# Required secrets: +# - [SECRET_NAME]: [SECRET_DESCRIPTION] +# +# Required permissions: +# - [PERMISSION_SCOPE]: [read/write] + +name: [WORKFLOW_NAME] + +on: + # Trigger on push to specific branches + push: + branches: [main, develop] + paths: + - '[PATH_PATTERN]/**' + - '!**.md' + + # Trigger on pull requests + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + + # Manual trigger + workflow_dispatch: + inputs: + environment: + description: 'Environment to deploy' + required: true + type: choice + options: + - dev + - staging + - production + default: 'dev' + debug: + description: 'Enable debug mode' + required: false + type: boolean + default: false + + # Scheduled trigger (daily at 2 AM UTC) + schedule: + - cron: '0 2 * * *' + +# Set default permissions to read-only (security best practice) +permissions: + contents: read + +# Prevent duplicate runs +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Environment variables available to all jobs +env: + [ENV_VAR_NAME]: '[ENV_VAR_VALUE]' + +jobs: + # Job 1: Linting and validation + lint: + name: Lint and Validate + runs-on: ubuntu-latest + timeout-minutes: 10 + + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + + - name: [SETUP_STEP_NAME] + uses: [SETUP_ACTION] + with: + [SETUP_INPUT]: [SETUP_VALUE] + cache: '[CACHE_TYPE]' + + - name: Install dependencies + run: [INSTALL_COMMAND] + + - name: Run linter + run: [LINT_COMMAND] + + # Job 2: Testing + test: + name: Test on [PLATFORM] [VERSION] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + permissions: + contents: read + checks: write # For test reporting + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + [VERSION_KEY]: [[VERSION_1], [VERSION_2], [VERSION_3]] + exclude: + # Exclude expensive combinations if needed + - os: macos-latest + [VERSION_KEY]: [VERSION_1] + fail-fast: false + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: "[SETUP_STEP_NAME] ${{ matrix.[VERSION_KEY] }}" + uses: [SETUP_ACTION] + with: + [VERSION_INPUT]: ${{ matrix.[VERSION_KEY] }} + cache: '[CACHE_TYPE]' + + - name: Install dependencies + run: [INSTALL_COMMAND] + + - name: Run tests + run: [TEST_COMMAND] + + - name: Upload test results + if: always() + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: test-results-${{ matrix.os }}-${{ matrix.[VERSION_KEY] }} + path: [TEST_RESULTS_PATH] + retention-days: 7 + + - name: Upload coverage + if: matrix.os == 'ubuntu-latest' && matrix.[VERSION_KEY] == [MAIN_VERSION] + uses: codecov/codecov-action@e0b68c6749509c5f83f984dd99a76a1c1a231044 # v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: [COVERAGE_FILE_PATH] + fail_ci_if_error: true + + # Job 3: Build + build: + name: Build Application + needs: [lint, test] + runs-on: ubuntu-latest + timeout-minutes: 20 + + permissions: + contents: read + + outputs: + build-id: ${{ steps.build.outputs.id }} + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: [SETUP_STEP_NAME] + uses: [SETUP_ACTION] + with: + [SETUP_INPUT]: [SETUP_VALUE] + cache: '[CACHE_TYPE]' + + - name: Install dependencies + run: [INSTALL_COMMAND] + + - name: Build application + id: build + run: | + [BUILD_COMMAND] + echo "id=build-${{ github.sha }}" >> $GITHUB_OUTPUT + + - name: Upload build artifacts + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: build-${{ github.sha }} + path: [BUILD_OUTPUT_PATH] + retention-days: 7 + if-no-files-found: error + + # Job 4: Deploy (conditional) + deploy: + name: Deploy to [ENVIRONMENT] + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 15 + + # Use environment for deployment protection rules + environment: + name: [ENVIRONMENT_NAME] + url: [ENVIRONMENT_URL] + + permissions: + contents: read + deployments: write + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download build artifacts + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: build-${{ github.sha }} + path: [BUILD_OUTPUT_PATH] + + - name: Deploy to [ENVIRONMENT] + env: + [DEPLOY_SECRET]: ${{ secrets.[DEPLOY_SECRET] }} + run: | + [DEPLOY_COMMAND] + + - name: Verify deployment + run: | + [VERIFICATION_COMMAND] + + - name: Notify on failure + if: failure() + run: | + echo "Deployment failed!" + # Add notification logic here + + # Job 5: Cleanup (always runs) + cleanup: + name: Cleanup + needs: [deploy] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 5 + + permissions: + contents: read + + steps: + - name: Cleanup temporary resources + run: | + echo "Performing cleanup..." + # Add cleanup logic here diff --git a/.agents/skills/github-actions-generator/assets/templates/workflow/reusable_workflow.yml b/.agents/skills/github-actions-generator/assets/templates/workflow/reusable_workflow.yml new file mode 100644 index 0000000..98207cf --- /dev/null +++ b/.agents/skills/github-actions-generator/assets/templates/workflow/reusable_workflow.yml @@ -0,0 +1,106 @@ +# [REUSABLE_WORKFLOW_NAME] +# +# [WORKFLOW_DESCRIPTION] +# +# Inputs: +# - [INPUT_NAME]: [INPUT_DESCRIPTION] +# +# Secrets: +# - [SECRET_NAME]: [SECRET_DESCRIPTION] +# +# Outputs: +# - [OUTPUT_NAME]: [OUTPUT_DESCRIPTION] +# +# Usage: +# jobs: +# call-workflow: +# uses: [OWNER]/[REPO]/.github/workflows/[FILENAME]@[REF] +# with: +# [INPUT_NAME]: [INPUT_VALUE] +# secrets: +# [SECRET_NAME]: ${{ secrets.[SECRET_NAME] }} + +name: [REUSABLE_WORKFLOW_NAME] + +on: + workflow_call: + inputs: + [INPUT_NAME]: + description: '[INPUT_DESCRIPTION]' + required: [true/false] + type: [string/boolean/number/choice/environment] + default: '[DEFAULT_VALUE]' # Optional, only for optional inputs + + # Add more inputs as needed + # Example types: + # - string: text values + # - boolean: true/false + # - number: numeric values + # - choice: predefined options (requires 'options' field) + # - environment: GitHub environment name + + secrets: + [SECRET_NAME]: + description: '[SECRET_DESCRIPTION]' + required: [true/false] + + # Add more secrets as needed + # Alternatively, use 'secrets: inherit' to pass all secrets + # (use sparingly for security) + + outputs: + [OUTPUT_NAME]: + description: '[OUTPUT_DESCRIPTION]' + value: ${{ jobs.[JOB_ID].outputs.[OUTPUT_KEY] }} + + # Add more outputs as needed + # Outputs must be mapped from job outputs + +# Set default permissions (security best practice) +permissions: + contents: read + +# Environment variables available to all jobs +env: + [ENV_VAR_NAME]: '[ENV_VAR_VALUE]' + +jobs: + [JOB_ID]: + name: [JOB_NAME] + runs-on: [ubuntu-latest/windows-latest/macos-latest] + timeout-minutes: [TIMEOUT_MINUTES] + + # Job-specific permissions + permissions: + contents: read + [PERMISSION_SCOPE]: [read/write] + + # Define outputs for this job + outputs: + [OUTPUT_KEY]: ${{ steps.[STEP_ID].outputs.[OUTPUT_NAME] }} + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + + - name: [STEP_NAME] + id: [STEP_ID] + run: | + # Use inputs: ${{ inputs.[INPUT_NAME] }} + # Use secrets: ${{ secrets.[SECRET_NAME] }} + echo "[OUTPUT_NAME]=[OUTPUT_VALUE]" >> $GITHUB_OUTPUT + + # Add more steps as needed + + # Add more jobs as needed + [ANOTHER_JOB_ID]: + name: [ANOTHER_JOB_NAME] + needs: [JOB_ID] # Optional: depend on previous job + runs-on: ubuntu-latest + + steps: + - name: Use previous job output + run: | + echo "Output from previous job: ${{ needs.[JOB_ID].outputs.[OUTPUT_KEY] }}" diff --git a/.agents/skills/github-actions-generator/examples/README.md b/.agents/skills/github-actions-generator/examples/README.md new file mode 100644 index 0000000..5b63c9e --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/README.md @@ -0,0 +1,147 @@ +# GitHub Actions Generator Examples + +This directory contains example workflows and actions generated using the github-actions-generator skill. + +## Workflows + +### Language-Specific CI Pipelines + +#### nodejs-ci.yml +Complete CI pipeline for Node.js applications demonstrating: +- Matrix testing across multiple Node.js versions and operating systems +- Dependency caching with `actions/setup-node` +- Parallel linting and testing +- Artifact uploading for test results +- Code coverage reporting with Codecov +- Concurrency controls + +**Use case:** Standard CI/CD for Node.js projects + +#### python-ci.yml +Python CI pipeline demonstrating: +- Matrix testing across Python versions +- Virtual environment management +- Dependency caching with pip +- Testing with pytest +- Code quality checks + +**Use case:** Python application CI/CD + +#### go-ci.yml +Go CI pipeline demonstrating: +- Go module caching +- Cross-platform builds +- Go testing and benchmarking +- Static code analysis + +**Use case:** Go application CI/CD + +### Container & Deployment Workflows + +#### docker-build-push.yml +Docker image build and push workflow demonstrating: +- Multi-platform builds (amd64, arm64) +- GitHub Container Registry integration +- Docker layer caching with GitHub Actions cache +- Automatic tagging based on git events +- Secure authentication with `GITHUB_TOKEN` + +**Use case:** Containerized application deployment + +#### multi-environment-deploy.yml +Multi-environment deployment workflow demonstrating: +- Environment protection rules and approval gates +- Dynamic environment selection (dev, staging, production) +- AWS deployment with OIDC authentication +- Blue-green deployment strategy +- Automatic rollback on failure +- Smoke tests and health checks +- Deployment verification + +**Use case:** Production-grade multi-stage deployments + +### Advanced Workflow Patterns + +#### monorepo-ci.yml +Monorepo CI pipeline demonstrating: +- Path-based change detection +- Conditional job execution based on affected packages +- Cross-package dependency management +- Parallel builds for independent packages +- Artifact sharing between jobs +- Package-specific test strategies + +**Use case:** Monorepo projects with multiple packages + +#### scheduled-tasks.yml +Scheduled maintenance workflow demonstrating: +- Cron schedule configuration +- Dependency security audits +- Stale branch cleanup +- Cache management +- External service health checks +- Automated issue creation +- Weekly metrics reporting + +**Use case:** Repository maintenance and monitoring + +### Security Workflows + +#### security/dependency-review.yml +Dependency review workflow demonstrating: +- Pull request dependency scanning +- Vulnerability severity thresholds +- License policy enforcement +- Automatic build failure on policy violations + +**Use case:** Supply chain security for PRs + +#### security/sbom-attestation.yml +SBOM and attestation workflow demonstrating: +- Software Bill of Materials generation +- SBOM attestation with GitHub's signing infrastructure +- Build provenance attestation +- Container vulnerability scanning with Trivy +- Multi-platform container builds +- Security scan results upload to GitHub Security tab + +**Use case:** Supply chain security compliance + +## Actions + +### setup-node-cached/action.yml +Composite action for Node.js setup demonstrating: +- Smart dependency caching for npm, yarn, and pnpm +- Input validation +- Multiple package manager support +- Cache hit detection +- Grouped output for better logging + +**Use case:** Reusable Node.js setup across multiple workflows + +## Usage + +These examples can be used as: +1. **Templates** - Copy and modify for your own projects +2. **Learning Resources** - Study best practices and patterns +3. **Testing** - Validate with github-actions-validator skill + +## Testing Examples + +To validate any of these examples: + +```bash +cd devops-skills-plugin/skills/github-actions-validator +bash scripts/validate_workflow.sh ../../github-actions-generator/examples/workflows/nodejs-ci.yml +``` + +## Best Practices Demonstrated + +All examples follow these best practices: +- ✅ Actions pinned to SHAs with version comments +- ✅ Minimal permissions with explicit `permissions:` blocks +- ✅ Concurrency controls to prevent duplicate runs +- ✅ Proper timeout settings +- ✅ Semantic naming conventions +- ✅ Comprehensive error handling +- ✅ Security-first approach diff --git a/.agents/skills/github-actions-generator/examples/actions/setup-node-cached/action.yml b/.agents/skills/github-actions-generator/examples/actions/setup-node-cached/action.yml new file mode 100644 index 0000000..3af2916 --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/actions/setup-node-cached/action.yml @@ -0,0 +1,93 @@ +name: 'Setup Node.js with Smart Caching' +description: 'Setup Node.js with intelligent dependency caching and installation' +author: 'Example Author' + +inputs: + node-version: + description: 'Node.js version to use' + required: true + package-manager: + description: 'Package manager to use (npm, yarn, pnpm)' + required: false + default: 'npm' + cache-dependency-path: + description: 'Path to lock file(s)' + required: false + default: '**/package-lock.json' + +outputs: + cache-hit: + description: 'Whether cache was hit' + value: ${{ steps.cache.outputs.cache-hit }} + node-version: + description: 'Actual Node.js version installed' + value: ${{ steps.setup.outputs.node-version }} + +branding: + icon: 'package' + color: 'green' + +runs: + using: 'composite' + steps: + - name: Validate inputs + shell: bash + run: | + if [[ ! "${{ inputs.package-manager }}" =~ ^(npm|yarn|pnpm)$ ]]; then + echo "::error::Invalid package manager: ${{ inputs.package-manager }}" + exit 1 + fi + echo "✅ Inputs validated" + + - name: Setup Node.js ${{ inputs.node-version }} + id: setup + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ inputs.node-version }} + + - name: Enable Corepack for pnpm/yarn + if: inputs.package-manager != 'npm' + shell: bash + run: corepack enable + + - name: Get cache directory + id: cache-dir + shell: bash + run: | + if [ "${{ inputs.package-manager }}" == "npm" ]; then + echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT + elif [ "${{ inputs.package-manager }}" == "yarn" ]; then + echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + else + echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT + fi + + - name: Cache dependencies + id: cache + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: ${{ steps.cache-dir.outputs.dir }} + key: ${{ runner.os }}-${{ inputs.package-manager }}-${{ hashFiles(inputs.cache-dependency-path) }} + restore-keys: | + ${{ runner.os }}-${{ inputs.package-manager }}- + + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + run: | + if [ "${{ inputs.package-manager }}" == "npm" ]; then + npm ci + elif [ "${{ inputs.package-manager }}" == "yarn" ]; then + yarn install --frozen-lockfile + else + pnpm install --frozen-lockfile + fi + + - name: Report status + shell: bash + run: | + echo "::group::Setup Summary" + echo "Node.js version: $(node --version)" + echo "Package manager: ${{ inputs.package-manager }}" + echo "Cache hit: ${{ steps.cache.outputs.cache-hit }}" + echo "::endgroup::" diff --git a/.agents/skills/github-actions-generator/examples/caching/docker-buildkit.yml b/.agents/skills/github-actions-generator/examples/caching/docker-buildkit.yml new file mode 100644 index 0000000..4c85050 --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/caching/docker-buildkit.yml @@ -0,0 +1,256 @@ +# Docker BuildKit Cache Example +# Demonstrates: GitHub Actions cache backend, inline vs registry cache, multi-stage build optimization + +name: Docker Build with BuildKit Cache + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + # Example 1: Basic BuildKit with GitHub Actions Cache + build-with-gha-cache: + name: Build with GitHub Actions Cache Backend + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix={{branch}}- + + # GitHub Actions Cache Backend + # - Stores cache in GitHub Actions cache (10 GB free per repo) + # - mode=max caches all build layers (not just final image) + # - Fastest option for most use cases + - name: Build and push (GHA cache) + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ github.sha }} + + # Example 2: Multi-Stage Build with Optimized Caching + build-multistage-optimized: + name: Multi-Stage Build with Cache Optimization + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Multi-stage builds benefit most from mode=max + # All intermediate stages are cached + - name: Build multi-stage with optimized cache + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: ./Dockerfile.multistage + target: production # Only build production stage + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:multistage-${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + NODE_ENV=production + + # Example 3: Registry Cache (for cross-runner sharing) + build-with-registry-cache: + name: Build with Registry Cache Backend + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Registry Cache Backend + # - Stores cache in container registry + # - Useful for sharing cache across different CI systems + # - Slower than GHA cache but more portable + # - Good for self-hosted runners + - name: Build and push (registry cache) + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:registry-cache-${{ github.sha }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max + + # Example 4: Inline Cache (embedded in image) + build-with-inline-cache: + name: Build with Inline Cache + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Inline Cache + # - Embeds cache metadata in the image itself + # - No separate cache artifact + # - Only caches layers in final image (not intermediate stages) + # - Useful when you can't use separate cache backends + - name: Build and push (inline cache) + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:inline-cache-${{ github.sha }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + cache-to: type=inline + + # Example 5: Comparison and Best Practices + comparison-summary: + name: Cache Strategy Comparison + runs-on: ubuntu-latest + needs: [build-with-gha-cache, build-multistage-optimized, build-with-registry-cache, build-with-inline-cache] + if: always() + timeout-minutes: 5 + + steps: + - name: Generate cache strategy comparison + run: | + cat <<'EOF' >> $GITHUB_STEP_SUMMARY + ## Docker BuildKit Cache Strategy Comparison + + | Strategy | Speed | Cache Size | Use Case | Limitations | + |----------|-------|------------|----------|-------------| + | **GHA Cache** (`type=gha`) | ⚡ Fastest | Up to 10 GB free | Most workflows, GitHub-hosted runners | Limited to 10 GB per repo | + | **Registry Cache** (`type=registry`) | 🐌 Slower | Unlimited | Self-hosted runners, cross-CI sharing | Requires registry storage | + | **Inline Cache** (`type=inline`) | 🐌 Slowest | Embedded | When separate cache unavailable | Only caches final image layers | + + ### Recommendations + + ✅ **Use GHA Cache (`type=gha,mode=max`) for:** + - Standard CI/CD workflows + - Multi-stage builds (caches all stages) + - GitHub-hosted runners + - Fast rebuild times + + ✅ **Use Registry Cache (`type=registry`) for:** + - Self-hosted runners + - Sharing cache across different CI systems + - When you need >10 GB cache + - Cross-repository builds + + ⚠️ **Use Inline Cache (`type=inline`) only when:** + - You can't use GHA or registry cache + - Cache size is very small + - You only need final image layers cached + + ### Cache Modes + + - `mode=min` - Only caches layers from final image (default) + - `mode=max` - Caches all layers including intermediate stages (recommended) + + ### Multi-Stage Build Optimization + + For multi-stage Dockerfiles: + ```dockerfile + # Base stage - cached separately + FROM node:24-alpine AS base + WORKDIR /app + COPY package*.json ./ + RUN npm ci + + # Build stage - cached separately + FROM base AS build + COPY . . + RUN npm run build + + # Production stage - final image + FROM base AS production + COPY --from=build /app/dist ./dist + CMD ["node", "dist/index.js"] + ``` + + With `mode=max`, all stages (base, build, production) are cached independently, + maximizing cache reuse on subsequent builds. + + ### Cache Key Strategy + + BuildKit automatically generates cache keys based on: + - Dockerfile content + - Build context files referenced in COPY/ADD + - Build arguments + - Target platform + + No manual cache key management needed! 🎉 + EOF + + echo "## Build Status Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- GHA Cache: ${{ needs.build-with-gha-cache.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Multi-stage: ${{ needs.build-multistage-optimized.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Registry Cache: ${{ needs.build-with-registry-cache.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Inline Cache: ${{ needs.build-with-inline-cache.result }}" >> $GITHUB_STEP_SUMMARY diff --git a/.agents/skills/github-actions-generator/examples/security/dependency-review.yml b/.agents/skills/github-actions-generator/examples/security/dependency-review.yml new file mode 100644 index 0000000..51a158d --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/security/dependency-review.yml @@ -0,0 +1,62 @@ +# Dependency Review Workflow Example +# Demonstrates: Dependency scanning, license compliance, vulnerability detection + +name: Dependency Review + +on: + pull_request: + branches: [main, develop] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write # For commenting on PRs + +jobs: + dependency-review: + name: Review Dependencies and Licenses + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Dependency Review + uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3 + with: + # Fail on critical and high severity vulnerabilities + fail-on-severity: high + + # Allow specific licenses (adjust based on your policy) + allow-licenses: > + MIT, + Apache-2.0, + BSD-2-Clause, + BSD-3-Clause, + ISC, + 0BSD + + # Deny specific licenses (adjust based on your policy) + deny-licenses: > + GPL-3.0, + AGPL-3.0, + LGPL-3.0 + + # Fail on scopes (optional) + # fail-on-scopes: runtime + + # Comment on PR with results (default: true) + comment-summary-in-pr: always + + # Allow dependencies from specific sources + # allow-dependencies-licenses: pkg:npm/express@* + + - name: Upload dependency graph + if: always() + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: dependency-graph + path: dependency-graph.json + retention-days: 7 + if-no-files-found: ignore diff --git a/.agents/skills/github-actions-generator/examples/security/sbom-attestation.yml b/.agents/skills/github-actions-generator/examples/security/sbom-attestation.yml new file mode 100644 index 0000000..7fe0e7f --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/security/sbom-attestation.yml @@ -0,0 +1,119 @@ +# SBOM Attestation Workflow Example +# Demonstrates: Software Bill of Materials generation and attestation for container images + +name: Build and Attest Container Image + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + +permissions: + contents: read + packages: write + id-token: write + attestations: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-attest: + name: Build, Scan, and Attest Container Image + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix={{branch}}- + + - name: Build and push Docker image + id: build + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true + + - name: Generate SBOM with Syft + uses: anchore/sbom-action@fbfd9c6c189226748411491745178e0c2017392d # v0.20.10 + with: + image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} + format: spdx-json + output-file: sbom.spdx.json + upload-artifact: true + upload-release-assets: false + + - name: Attest SBOM + uses: actions/attest-sbom@bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b # v2.4.0 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build.outputs.digest }} + sbom-path: sbom.spdx.json + push-to-registry: true + + - name: Scan image for vulnerabilities with Trivy + uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH + + - name: Upload Trivy scan results to GitHub Security + uses: github/codeql-action/upload-sarif@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # v3.32.5 + if: always() + with: + sarif_file: trivy-results.sarif + category: container-scan + + - name: Generate build provenance + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true + + - name: Summary + run: | + echo "## Container Image Built and Attested ✅" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Image:** \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Digest:** \`${{ steps.build.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Tags:**" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ SBOM generated and attested" >> $GITHUB_STEP_SUMMARY + echo "✅ Build provenance attested" >> $GITHUB_STEP_SUMMARY + echo "✅ Security scan completed" >> $GITHUB_STEP_SUMMARY diff --git a/.agents/skills/github-actions-generator/examples/triggers/chatops-commands.yml b/.agents/skills/github-actions-generator/examples/triggers/chatops-commands.yml new file mode 100644 index 0000000..0454aad --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/triggers/chatops-commands.yml @@ -0,0 +1,475 @@ +# ChatOps Commands Example +# Demonstrates: issue_comment trigger, PR commands, permission validation, command parsing, feedback + +name: ChatOps Commands + +on: + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + deployments: write + issues: write + +jobs: + # Command router - determines which command to run + parse-command: + name: Parse ChatOps Command + # Only run on PRs, not issues + if: github.event.issue.pull_request + runs-on: ubuntu-latest + timeout-minutes: 5 + + outputs: + command: ${{ steps.parse.outputs.command }} + args: ${{ steps.parse.outputs.args }} + is-authorized: ${{ steps.check-permissions.outputs.authorized }} + + steps: + - name: Parse command + id: parse + env: + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + COMMENT="$COMMENT_BODY" + + # Extract command (first word starting with /) + COMMAND=$(echo "$COMMENT" | grep -oE '^/[-[:alnum:]_]+' | sed 's|^/||' || echo "") + + # Extract arguments (everything after command) + ARGS=$(echo "$COMMENT" | sed -E 's|^/[-[:alnum:]_]+\s*||' || echo "") + + echo "command=$COMMAND" >> $GITHUB_OUTPUT + echo "args=$ARGS" >> $GITHUB_OUTPUT + + echo "Detected command: /$COMMAND" + echo "Arguments: $ARGS" + + - name: Check permissions + id: check-permissions + env: + AUTHOR_ASSOCIATION: ${{ github.event.comment.author_association }} + COMMAND: ${{ steps.parse.outputs.command }} + run: | + # Define allowed associations per command + case "$COMMAND" in + deploy|approve|merge) + # Sensitive commands - only owners and members + ALLOWED="OWNER MEMBER" + ;; + run-tests|benchmark|lint) + # Less sensitive - include collaborators + ALLOWED="OWNER MEMBER COLLABORATOR" + ;; + help|status|info) + # Public commands - anyone + ALLOWED="OWNER MEMBER COLLABORATOR CONTRIBUTOR FIRST_TIME_CONTRIBUTOR FIRST_TIMER NONE" + ;; + *) + # Unknown command - no one + ALLOWED="" + ;; + esac + + if echo "$ALLOWED" | grep -q "$AUTHOR_ASSOCIATION"; then + echo "authorized=true" >> $GITHUB_OUTPUT + echo "✅ User authorized: ${{ github.event.comment.user.login }} ($AUTHOR_ASSOCIATION)" + else + echo "authorized=false" >> $GITHUB_OUTPUT + echo "❌ User not authorized: ${{ github.event.comment.user.login }} ($AUTHOR_ASSOCIATION)" + fi + + - name: React to unauthorized command + if: steps.check-permissions.outputs.authorized == 'false' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '-1' + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: `❌ @${{ github.event.comment.user.login }} You don't have permission to run this command.\n\nRequired role: OWNER or MEMBER` + }); + + # Command: /deploy [environment] + cmd-deploy: + name: Deploy Command + needs: [parse-command] + if: | + needs.parse-command.outputs.command == 'deploy' && + needs.parse-command.outputs.is-authorized == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: React with rocket + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket' + }); + + - name: Parse deploy arguments + id: parse-deploy + run: | + ARGS="${{ needs.parse-command.outputs.args }}" + + # Extract environment (default: staging) + ENV=$(echo "$ARGS" | awk '{print $1}' | tr '[:upper:]' '[:lower:]') + ENV=${ENV:-staging} + + # Validate environment + if [[ ! "$ENV" =~ ^(dev|staging|production)$ ]]; then + echo "error=Invalid environment: $ENV" >> $GITHUB_OUTPUT + exit 1 + fi + + echo "environment=$ENV" >> $GITHUB_OUTPUT + + - name: Get PR details + id: pr + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + result-encoding: string + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.issue.number + }); + + core.setOutput('ref', pr.data.head.ref); + core.setOutput('sha', pr.data.head.sha); + core.setOutput('mergeable', pr.data.mergeable); + + - name: Check if PR is mergeable + if: steps.pr.outputs.mergeable == 'false' + run: | + echo "❌ PR has merge conflicts" + exit 1 + + - name: Checkout PR code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.pr.outputs.ref }} + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + cache: 'npm' + + - name: Install and build + run: | + npm ci + npm run build + + - name: Deploy to environment + id: deploy + env: + ENVIRONMENT: ${{ steps.parse-deploy.outputs.environment }} + run: | + echo "🚀 Deploying to $ENVIRONMENT" + + # Deployment logic here + DEPLOY_URL="https://$ENVIRONMENT.example.com" + echo "url=$DEPLOY_URL" >> $GITHUB_OUTPUT + + - name: Comment deployment success + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const environment = '${{ steps.parse-deploy.outputs.environment }}'; + const url = '${{ steps.deploy.outputs.url }}'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: `## ✅ Deployment Successful + + **Environment:** ${environment} + **URL:** ${url} + **Triggered by:** @${{ github.event.comment.user.login }} + **Commit:** ${{ steps.pr.outputs.sha }} + + [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + ` + }); + + - name: Comment deployment failure + if: failure() + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: `## ❌ Deployment Failed + + [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + ` + }); + + # Command: /run-tests [suite] + cmd-run-tests: + name: Run Tests Command + needs: [parse-command] + if: | + needs.parse-command.outputs.command == 'run-tests' && + needs.parse-command.outputs.is-authorized == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: React with eyes + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'eyes' + }); + + - name: Parse test suite + id: parse-suite + run: | + ARGS="${{ needs.parse-command.outputs.args }}" + SUITE=$(echo "$ARGS" | awk '{print $1}') + SUITE=${SUITE:-all} + + echo "suite=$SUITE" >> $GITHUB_OUTPUT + + - name: Get PR details + id: pr + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.issue.number + }); + core.setOutput('ref', pr.data.head.ref); + + - name: Checkout PR code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.pr.outputs.ref }} + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + id: test + env: + TEST_SUITE: ${{ steps.parse-suite.outputs.suite }} + run: | + echo "🧪 Running test suite: $TEST_SUITE" + + case "$TEST_SUITE" in + unit) + npm run test:unit + ;; + integration) + npm run test:integration + ;; + e2e) + npm run test:e2e + ;; + all) + npm test + ;; + *) + echo "❌ Unknown test suite: $TEST_SUITE" + exit 1 + ;; + esac + + - name: Comment test results + if: always() + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const suite = '${{ steps.parse-suite.outputs.suite }}'; + const success = '${{ steps.test.outcome }}' === 'success'; + const emoji = success ? '✅' : '❌'; + const status = success ? 'Passed' : 'Failed'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: `## ${emoji} Test Suite: ${suite} - ${status} + + [View test results](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + ` + }); + + # Command: /benchmark + cmd-benchmark: + name: Benchmark Command + needs: [parse-command] + if: | + needs.parse-command.outputs.command == 'benchmark' && + needs.parse-command.outputs.is-authorized == 'true' + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: React with chart + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '+1' + }); + + - name: Get PR details + id: pr + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.issue.number + }); + core.setOutput('ref', pr.data.head.ref); + + - name: Checkout PR code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.pr.outputs.ref }} + + - name: Run benchmarks + run: | + echo "⚡ Running performance benchmarks" + # Benchmark logic here + + - name: Comment benchmark results + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: `## ⚡ Benchmark Results + + | Metric | Value | Baseline | Change | + |--------|-------|----------|--------| + | Response Time | 125ms | 130ms | -3.8% 🟢 | + | Throughput | 1,250 req/s | 1,200 req/s | +4.2% 🟢 | + | Memory Usage | 245 MB | 250 MB | -2.0% 🟢 | + + ✅ All metrics within acceptable range + + [View details](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + ` + }); + + # Command: /help + cmd-help: + name: Help Command + needs: [parse-command] + if: needs.parse-command.outputs.command == 'help' + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Show help + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: `## 🤖 ChatOps Commands + + Available commands: + + ### Deployment + - \`/deploy [env]\` - Deploy PR to environment (dev, staging, production) + - Example: \`/deploy staging\` + - Requires: OWNER or MEMBER permission + + ### Testing + - \`/run-tests [suite]\` - Run test suite (unit, integration, e2e, all) + - Example: \`/run-tests unit\` + - Requires: OWNER, MEMBER, or COLLABORATOR permission + + ### Performance + - \`/benchmark\` - Run performance benchmarks + - Requires: OWNER, MEMBER, or COLLABORATOR permission + + ### Utility + - \`/help\` - Show this help message + - Available to everyone + + --- + + **Your permission level:** ${{ github.event.comment.author_association }} + ` + }); + + # Unknown command handler + cmd-unknown: + name: Unknown Command + needs: [parse-command] + if: | + needs.parse-command.outputs.command != '' && + needs.parse-command.outputs.command != 'deploy' && + needs.parse-command.outputs.command != 'run-tests' && + needs.parse-command.outputs.command != 'benchmark' && + needs.parse-command.outputs.command != 'help' + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: React with confused + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'confused' + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: `❓ Unknown command: \`/${{ needs.parse-command.outputs.command }}\` + + Type \`/help\` to see available commands. + ` + }); diff --git a/.agents/skills/github-actions-generator/examples/triggers/repository-dispatch.yml b/.agents/skills/github-actions-generator/examples/triggers/repository-dispatch.yml new file mode 100644 index 0000000..ef4b154 --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/triggers/repository-dispatch.yml @@ -0,0 +1,418 @@ +# Repository Dispatch Example +# Demonstrates: External API triggers, webhook integration, payload validation, multiple event types + +name: Handle External Events + +on: + repository_dispatch: + types: + - deploy-prod + - deploy-staging + - deploy-dev + - run-migration + - rebuild-cache + - incident-response + - custom-task + +permissions: + contents: read + deployments: write + issues: write + +env: + AWS_REGION: 'us-east-1' + +jobs: + # Job 1: Handle deployment events + handle-deployment: + name: Handle Deployment Event + if: startsWith(github.event.action, 'deploy-') + runs-on: ubuntu-latest + timeout-minutes: 30 + + environment: + name: ${{ github.event.client_payload.environment || 'staging' }} + + steps: + - name: Validate deployment payload + id: validate + run: | + # Required fields validation + if [[ -z "${{ github.event.client_payload.version }}" ]]; then + echo "❌ Error: 'version' is required in client_payload" + exit 1 + fi + + if [[ -z "${{ github.event.client_payload.environment }}" ]]; then + echo "❌ Error: 'environment' is required in client_payload" + exit 1 + fi + + # Validate environment + ENV="${{ github.event.client_payload.environment }}" + if [[ ! "$ENV" =~ ^(dev|staging|production)$ ]]; then + echo "❌ Error: Invalid environment: $ENV" + echo "Allowed: dev, staging, production" + exit 1 + fi + + # Validate version format (semver) + VERSION="${{ github.event.client_payload.version }}" + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then + echo "⚠️ Warning: Version doesn't match semver format: $VERSION" + fi + + echo "environment=$ENV" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "requestor=${{ github.event.client_payload.requestor || 'unknown' }}" >> $GITHUB_OUTPUT + + - name: Log deployment request + run: | + cat <> $GITHUB_OUTPUT + + - name: Create deployment notification issue + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const environment = '${{ steps.validate.outputs.environment }}'; + const version = '${{ steps.validate.outputs.version }}'; + const requestor = '${{ steps.validate.outputs.requestor }}'; + + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `Deployment: ${version} to ${environment}`, + body: `## 🚀 Deployment Summary + + **Version:** ${version} + **Environment:** ${environment} + **Requestor:** ${requestor} + **Triggered via:** repository_dispatch API + + **Workflow Run:** https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId} + + **URL:** https://${environment}.example.com + + ✅ Deployment completed successfully + `, + labels: ['deployment', environment] + }); + + # Job 2: Handle migration events + handle-migration: + name: Run Database Migration + if: github.event.action == 'run-migration' + runs-on: ubuntu-latest + timeout-minutes: 15 + + environment: + name: ${{ github.event.client_payload.environment || 'staging' }} + + steps: + - name: Validate migration payload + run: | + if [[ -z "${{ github.event.client_payload.migration_name }}" ]]; then + echo "❌ Error: 'migration_name' is required" + exit 1 + fi + + if [[ -z "${{ github.event.client_payload.environment }}" ]]; then + echo "❌ Error: 'environment' is required" + exit 1 + fi + + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run migration + env: + MIGRATION_NAME: ${{ github.event.client_payload.migration_name }} + ENVIRONMENT: ${{ github.event.client_payload.environment }} + run: | + echo "🔄 Running migration: $MIGRATION_NAME on $ENVIRONMENT" + + # Migration logic here + # Example: npm run migrate:up $MIGRATION_NAME + + echo "✅ Migration completed" + + # Job 3: Handle cache rebuild + handle-cache-rebuild: + name: Rebuild Application Cache + if: github.event.action == 'rebuild-cache' + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + + - name: Clear existing cache + run: | + echo "🗑️ Clearing cache for: ${{ github.event.client_payload.cache_key || 'all' }}" + # Clear cache logic + + - name: Rebuild cache + run: | + echo "🔄 Rebuilding cache..." + npm ci + npm run build + + - name: Save cache + uses: actions/cache/save@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + path: | + ~/.npm + node_modules + dist/ + key: cache-rebuild-${{ github.sha }} + + # Job 4: Handle incident response + handle-incident: + name: Incident Response + if: github.event.action == 'incident-response' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Parse incident payload + id: incident + run: | + SEVERITY="${{ github.event.client_payload.severity || 'medium' }}" + MESSAGE="${{ github.event.client_payload.message }}" + AFFECTED_SERVICE="${{ github.event.client_payload.service }}" + + echo "severity=$SEVERITY" >> $GITHUB_OUTPUT + echo "message=$MESSAGE" >> $GITHUB_OUTPUT + echo "service=$AFFECTED_SERVICE" >> $GITHUB_OUTPUT + + - name: Create incident issue + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const severity = '${{ steps.incident.outputs.severity }}'; + const message = '${{ steps.incident.outputs.message }}'; + const service = '${{ steps.incident.outputs.service }}'; + + const severityEmoji = { + 'critical': '🔴', + 'high': '🟠', + 'medium': '🟡', + 'low': '🟢' + }; + + const emoji = severityEmoji[severity] || '⚪'; + + const issue = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `${emoji} Incident: ${service} - ${severity.toUpperCase()}`, + body: `## Incident Report + + **Severity:** ${severity.toUpperCase()} + **Service:** ${service} + **Message:** ${message} + + **Triggered:** ${new Date().toISOString()} + **Source:** Monitoring System (via repository_dispatch) + + ### Next Steps + - [ ] Investigate root cause + - [ ] Implement fix + - [ ] Deploy fix + - [ ] Verify resolution + - [ ] Post-mortem + `, + labels: ['incident', severity, 'automated'], + assignees: context.payload.client_payload.assignees || [] + }); + + console.log(`Created incident issue: ${issue.data.html_url}`); + + - name: Trigger automated response + if: steps.incident.outputs.severity == 'critical' + run: | + echo "🚨 CRITICAL INCIDENT DETECTED" + echo "Triggering automated response procedures..." + + # Automated incident response + # Example: Scale up services, enable debug mode, alert team + + # Job 5: Handle custom tasks + handle-custom-task: + name: Execute Custom Task + if: github.event.action == 'custom-task' + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Validate custom task + run: | + if [[ -z "${{ github.event.client_payload.task_name }}" ]]; then + echo "❌ Error: 'task_name' is required" + exit 1 + fi + + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Execute custom task + env: + TASK_NAME: ${{ github.event.client_payload.task_name }} + TASK_ARGS: ${{ toJSON(github.event.client_payload.args) }} + run: | + echo "⚙️ Executing task: $TASK_NAME" + echo "Arguments: $TASK_ARGS" + + # Custom task execution + # Use allowlist to prevent arbitrary code execution + case "$TASK_NAME" in + "cleanup-old-artifacts") + echo "Running artifact cleanup..." + # Cleanup logic + ;; + "regenerate-docs") + echo "Regenerating documentation..." + # Docs generation logic + ;; + "validate-config") + echo "Validating configuration..." + # Validation logic + ;; + *) + echo "❌ Error: Unknown task: $TASK_NAME" + echo "Allowed tasks: cleanup-old-artifacts, regenerate-docs, validate-config" + exit 1 + ;; + esac + + echo "✅ Task completed" + + # Job 6: Event summary + event-summary: + name: Event Processing Summary + runs-on: ubuntu-latest + needs: [handle-deployment, handle-migration, handle-cache-rebuild, handle-incident, handle-custom-task] + if: always() + timeout-minutes: 5 + + steps: + - name: Generate summary + run: | + cat <<'EOF' >> $GITHUB_STEP_SUMMARY + ## 📬 Repository Dispatch Event Summary + + **Event Type:** ${{ github.event.action }} + **Triggered:** ${{ github.event.created_at }} + **Sender:** ${{ github.event.sender.login }} + + ### Job Results + - Deployment: ${{ needs.handle-deployment.result }} + - Migration: ${{ needs.handle-migration.result }} + - Cache Rebuild: ${{ needs.handle-cache-rebuild.result }} + - Incident Response: ${{ needs.handle-incident.result }} + - Custom Task: ${{ needs.handle-custom-task.result }} + + ### Payload + ```json + ${{ toJSON(github.event.client_payload) }} + ``` + + --- + + ### How to Trigger This Workflow + + **Using curl:** + ```bash + curl -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/${{ github.repository }}/dispatches \ + -d '{ + "event_type": "deploy-prod", + "client_payload": { + "version": "v1.2.3", + "environment": "production", + "requestor": "api-user" + } + }' + ``` + + **Using Python:** + ```python + import requests + + url = "https://api.github.com/repos/${{ github.repository }}/dispatches" + headers = { + "Authorization": f"token {token}", + "Accept": "application/vnd.github.v3+json" + } + payload = { + "event_type": "deploy-prod", + "client_payload": { + "version": "v1.2.3", + "environment": "production" + } + } + requests.post(url, json=payload, headers=headers) + ``` + + **Using GitHub CLI:** + ```bash + gh api repos/${{ github.repository }}/dispatches \ + -X POST \ + -f event_type='deploy-prod' \ + -f client_payload[version]='v1.2.3' \ + -f client_payload[environment]='production' + ``` + EOF diff --git a/.agents/skills/github-actions-generator/examples/triggers/workflow-orchestration.yml b/.agents/skills/github-actions-generator/examples/triggers/workflow-orchestration.yml new file mode 100644 index 0000000..4b4fcfd --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/triggers/workflow-orchestration.yml @@ -0,0 +1,404 @@ +# Workflow Orchestration Example +# Demonstrates: workflow_run trigger, workflow chaining, artifact passing, secure external PR handling + +# This is the MAIN CI workflow that runs on all PRs +name: CI Pipeline + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + checks: write + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: '24' + +jobs: + # Standard CI jobs - safe for external PRs + test: + name: Run Tests + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Run tests + run: npm test -- --coverage + + - name: Upload coverage + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: coverage-report + path: coverage/ + retention-days: 7 + + build: + name: Build Application + runs-on: ubuntu-latest + needs: [test] + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + env: + NODE_ENV: production + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: build-artifacts + path: dist/ + retention-days: 30 + + ci-status: + name: CI Status Check + runs-on: ubuntu-latest + needs: [test, build] + if: always() + timeout-minutes: 5 + + steps: + - name: Check CI status + run: | + if [[ "${{ needs.test.result }}" == "failure" ]] || [[ "${{ needs.build.result }}" == "failure" ]]; then + echo "❌ CI failed" + exit 1 + fi + echo "✅ CI passed" + +--- +# SEPARATE WORKFLOW FILE: .github/workflows/security-scan.yml +# This workflow runs AFTER CI completes, triggered by workflow_run +# It's safe for external PRs because it runs with the target branch's code + +name: Security Scan + +on: + workflow_run: + workflows: ["CI Pipeline"] + types: [completed] + +# Critical: These permissions are safe because this workflow +# uses code from the target branch, not the PR +permissions: + security-events: write + contents: read + actions: read + issues: write # Required for PR comments via issues.createComment + +jobs: + security-scan: + name: Run Security Scan + # Only scan if CI passed + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + # Important: Check out the PR's code explicitly + # This is safe because we're in workflow_run (target branch context) + - name: Checkout PR code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run dependency audit + run: | + npm audit --audit-level=high || true + npm audit --json > audit-results.json || true + + - name: Run CodeQL analysis + uses: github/codeql-action/init@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # v3.32.5 + with: + languages: javascript + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # v3.32.5 + with: + category: security-scan + + - name: Upload security scan results + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + if: always() + with: + name: security-scan-results + path: | + audit-results.json + retention-days: 30 + + - name: Comment on PR with results + if: github.event.workflow_run.event == 'pull_request' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const fs = require('fs'); + + // Get PR number from workflow run + const prNumber = context.payload.workflow_run.pull_requests[0]?.number; + + if (!prNumber) { + console.log('Not a PR, skipping comment'); + return; + } + + // Read audit results + let auditResults = {}; + try { + auditResults = JSON.parse(fs.readFileSync('audit-results.json', 'utf8')); + } catch (error) { + console.log('No audit results found'); + } + + const vulnerabilities = auditResults.metadata?.vulnerabilities || {}; + const total = Object.values(vulnerabilities).reduce((a, b) => a + b, 0); + + let message = '## 🔒 Security Scan Results\n\n'; + + if (total === 0) { + message += '✅ No vulnerabilities found!\n'; + } else { + message += '⚠️ Found vulnerabilities:\n\n'; + message += `- Critical: ${vulnerabilities.critical || 0}\n`; + message += `- High: ${vulnerabilities.high || 0}\n`; + message += `- Moderate: ${vulnerabilities.moderate || 0}\n`; + message += `- Low: ${vulnerabilities.low || 0}\n`; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: message + }); + +--- +# SEPARATE WORKFLOW FILE: .github/workflows/deploy.yml +# This workflow deploys to staging/production AFTER CI completes successfully + +name: Deploy Application + +on: + workflow_run: + workflows: ["CI Pipeline"] + types: [completed] + branches: [main, staging] + +permissions: + contents: read + deployments: write + id-token: write + +jobs: + determine-environment: + name: Determine Deployment Environment + runs-on: ubuntu-latest + # Only deploy if CI passed + if: ${{ github.event.workflow_run.conclusion == 'success' }} + timeout-minutes: 5 + + outputs: + environment: ${{ steps.determine.outputs.environment }} + should-deploy: ${{ steps.determine.outputs.should-deploy }} + + steps: + - name: Determine environment + id: determine + run: | + BRANCH="${{ github.event.workflow_run.head_branch }}" + + case "$BRANCH" in + main) + echo "environment=production" >> $GITHUB_OUTPUT + echo "should-deploy=true" >> $GITHUB_OUTPUT + ;; + staging) + echo "environment=staging" >> $GITHUB_OUTPUT + echo "should-deploy=true" >> $GITHUB_OUTPUT + ;; + *) + echo "environment=none" >> $GITHUB_OUTPUT + echo "should-deploy=false" >> $GITHUB_OUTPUT + ;; + esac + + echo "Deploying to: ${{ steps.determine.outputs.environment }}" + + deploy: + name: Deploy to ${{ needs.determine-environment.outputs.environment }} + runs-on: ubuntu-latest + needs: [determine-environment] + if: needs.determine-environment.outputs.should-deploy == 'true' + timeout-minutes: 20 + + environment: + name: ${{ needs.determine-environment.outputs.environment }} + url: https://${{ needs.determine-environment.outputs.environment }}.example.com + + steps: + # Download artifacts from the CI workflow + - name: Download build artifacts from CI + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: build-artifacts + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Display downloaded files + run: | + echo "Downloaded artifacts:" + ls -lah + + - name: Deploy to environment + env: + ENVIRONMENT: ${{ needs.determine-environment.outputs.environment }} + run: | + echo "Deploying to $ENVIRONMENT" + echo "Commit SHA: ${{ github.event.workflow_run.head_sha }}" + echo "Branch: ${{ github.event.workflow_run.head_branch }}" + + # Deployment commands here + # Example: Upload to S3, deploy to Kubernetes, etc. + + - name: Run smoke tests + env: + ENVIRONMENT: ${{ needs.determine-environment.outputs.environment }} + run: | + URL="https://$ENVIRONMENT.example.com" + echo "Running smoke tests against $URL" + + # Basic health check + curl -f "$URL/health" || exit 1 + + echo "✅ Smoke tests passed" + + - name: Create deployment summary + run: | + cat <> $GITHUB_STEP_SUMMARY + ## 🚀 Deployment Complete + + **Environment:** ${{ needs.determine-environment.outputs.environment }} + **Commit:** ${{ github.event.workflow_run.head_sha }} + **Branch:** ${{ github.event.workflow_run.head_branch }} + **URL:** https://${{ needs.determine-environment.outputs.environment }}.example.com + + ### CI Workflow Details + - **Workflow:** ${{ github.event.workflow_run.name }} + - **Run ID:** ${{ github.event.workflow_run.id }} + - **Conclusion:** ${{ github.event.workflow_run.conclusion }} + + ✅ Deployment successful! + EOF + +--- +# SEPARATE WORKFLOW FILE: .github/workflows/performance-test.yml +# Run performance tests after CI completes (only on main branch) + +name: Performance Tests + +on: + workflow_run: + workflows: ["CI Pipeline"] + types: [completed] + branches: [main] + +permissions: + contents: write # Required for repos.createCommitComment + actions: read + +jobs: + performance-test: + name: Run Performance Tests + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Download build artifacts from CI + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: build-artifacts + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run performance benchmarks + run: | + echo "Running performance tests..." + # Performance test commands here + # Example: k6, lighthouse, etc. + + - name: Upload performance results + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: performance-results + path: performance-results/ + retention-days: 30 + + - name: Comment with performance results + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const message = `## ⚡ Performance Test Results + + **Commit:** ${{ github.event.workflow_run.head_sha }} + + ✅ Performance tests completed + + [View detailed results](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + `; + + await github.rest.repos.createCommitComment({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: '${{ github.event.workflow_run.head_sha }}', + body: message + }); diff --git a/.agents/skills/github-actions-generator/examples/workflows/docker-build-push.yml b/.agents/skills/github-actions-generator/examples/workflows/docker-build-push.yml new file mode 100644 index 0000000..fe7b05f --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/workflows/docker-build-push.yml @@ -0,0 +1,68 @@ +# Docker Build and Push Example +# Demonstrates: Multi-platform builds, Docker layer caching, GitHub Container Registry + +name: Build and Push Docker Image + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha,prefix={{branch}}- + + - name: Build and push Docker image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ github.sha }} + BUILD_DATE=${{ github.event.head_commit.timestamp || github.event.repository.updated_at }} diff --git a/.agents/skills/github-actions-generator/examples/workflows/go-ci.yml b/.agents/skills/github-actions-generator/examples/workflows/go-ci.yml new file mode 100644 index 0000000..61b5dce --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/workflows/go-ci.yml @@ -0,0 +1,161 @@ +# Go CI Pipeline Example +# Demonstrates: Matrix testing, caching, linting, testing, code coverage + +name: Go CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + GO_VERSION: '1.22' + +jobs: + lint: + name: Lint Code + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Go + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: go.sum + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v9.1.0 + with: + version: v1.55.3 + args: --timeout=5m + + - name: Run go vet + run: go vet ./... + + - name: Run go fmt check + run: | + if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then + echo "Code is not formatted. Run 'go fmt ./...'" + gofmt -s -l . + exit 1 + fi + + test: + name: Test on Go ${{ matrix.go-version }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + permissions: + contents: read + checks: write + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + go-version: ['1.21', '1.22', '1.23'] + exclude: + - os: macos-latest + go-version: '1.21' + fail-fast: false + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Go ${{ matrix.go-version }} + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: go.sum + + - name: Download dependencies + run: go mod download + + - name: Verify dependencies + run: go mod verify + + - name: Run tests + run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... + + - name: Generate coverage report + if: matrix.os == 'ubuntu-latest' + run: go tool cover -html=coverage.out -o coverage.html + + - name: Upload test results + if: always() + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: test-results-${{ matrix.os }}-go${{ matrix.go-version }} + path: | + coverage.out + coverage.html + retention-days: 7 + + - name: Upload coverage to Codecov + if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.22' + uses: codecov/codecov-action@e0b68c6749509c5f83f984dd99a76a1c1a231044 # v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.out + fail_ci_if_error: true + + build: + name: Build Application + needs: [lint, test] + runs-on: ubuntu-latest + timeout-minutes: 15 + + strategy: + matrix: + goos: [linux, darwin, windows] + goarch: [amd64, arm64] + exclude: + - goos: windows + goarch: arm64 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Go + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: go.sum + + - name: Build binary + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: 0 + run: | + OUTPUT_NAME="app-${{ matrix.goos }}-${{ matrix.goarch }}" + if [ "${{ matrix.goos }}" = "windows" ]; then + OUTPUT_NAME="${OUTPUT_NAME}.exe" + fi + go build -ldflags="-s -w" -o "${OUTPUT_NAME}" . + ls -lh "${OUTPUT_NAME}" + + - name: Upload build artifacts + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: binaries-${{ matrix.goos }}-${{ matrix.goarch }} + path: app-* + retention-days: 7 + if-no-files-found: error diff --git a/.agents/skills/github-actions-generator/examples/workflows/monorepo-ci.yml b/.agents/skills/github-actions-generator/examples/workflows/monorepo-ci.yml new file mode 100644 index 0000000..4e9648a --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/workflows/monorepo-ci.yml @@ -0,0 +1,340 @@ +# Monorepo CI Pipeline Example +# Demonstrates: Path filtering, affected package detection, conditional builds, matrix for packages + +name: Monorepo CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: '24' + +jobs: + # Detect which packages have changed + detect-changes: + name: Detect Changed Packages + runs-on: ubuntu-latest + timeout-minutes: 5 + + outputs: + packages: ${{ steps.filter.outputs.changes }} + any-changed: ${{ steps.filter.outputs.any-changed }} + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 # Need full history for path detection + + - name: Detect changed paths + uses: dorny/paths-filter@de90cc6551a0c30ca4af50ac82dafbf57eb22fab # v3.0.2 + id: filter + with: + filters: | + frontend: + - 'packages/frontend/**' + backend: + - 'packages/backend/**' + shared: + - 'packages/shared/**' + infrastructure: + - 'packages/infrastructure/**' + + - name: Set any-changed flag + id: any-changed + run: | + if [[ "${{ steps.filter.outputs.frontend }}" == "true" ]] || \ + [[ "${{ steps.filter.outputs.backend }}" == "true" ]] || \ + [[ "${{ steps.filter.outputs.shared }}" == "true" ]] || \ + [[ "${{ steps.filter.outputs.infrastructure }}" == "true" ]]; then + echo "any-changed=true" >> $GITHUB_OUTPUT + else + echo "any-changed=false" >> $GITHUB_OUTPUT + fi + + # Lint root configuration files + lint-root: + name: Lint Root Configuration + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install root dependencies + run: npm ci + + - name: Lint root files + run: npm run lint:root + + # Build and test shared package (dependency for others) + shared: + name: Build Shared Package + runs-on: ubuntu-latest + needs: [detect-changes] + if: needs.detect-changes.outputs.packages contains 'shared' || needs.detect-changes.outputs.any-changed == 'true' + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + working-directory: packages/shared + + - name: Run linter + run: npm run lint + working-directory: packages/shared + + - name: Run tests + run: npm test + working-directory: packages/shared + + - name: Build package + run: npm run build + working-directory: packages/shared + + - name: Upload build artifacts + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: shared-dist + path: packages/shared/dist/ + retention-days: 1 + + # Build and test frontend package + frontend: + name: Build Frontend Package + runs-on: ubuntu-latest + needs: [detect-changes, shared] + if: needs.detect-changes.outputs.packages contains 'frontend' || needs.detect-changes.outputs.packages contains 'shared' + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Download shared package + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: shared-dist + path: packages/shared/dist/ + + - name: Install dependencies + run: npm ci + working-directory: packages/frontend + + - name: Run linter + run: npm run lint + working-directory: packages/frontend + + - name: Run tests + run: npm test + working-directory: packages/frontend + + - name: Build frontend + run: npm run build + working-directory: packages/frontend + env: + NODE_ENV: production + + - name: Upload build artifacts + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: frontend-dist + path: packages/frontend/dist/ + retention-days: 7 + + # Build and test backend package + backend: + name: Build Backend Package + runs-on: ubuntu-latest + needs: [detect-changes, shared] + if: needs.detect-changes.outputs.packages contains 'backend' || needs.detect-changes.outputs.packages contains 'shared' + timeout-minutes: 20 + + strategy: + matrix: + node-version: [20, 24] + fail-fast: false + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Download shared package + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: shared-dist + path: packages/shared/dist/ + + - name: Install dependencies + run: npm ci + working-directory: packages/backend + + - name: Run linter + run: npm run lint + working-directory: packages/backend + + - name: Run unit tests + run: npm run test:unit + working-directory: packages/backend + + - name: Run integration tests + run: npm run test:integration + working-directory: packages/backend + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test + + - name: Build backend + run: npm run build + working-directory: packages/backend + + - name: Upload build artifacts + if: matrix.node-version == 24 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: backend-dist + path: packages/backend/dist/ + retention-days: 7 + + # Validate infrastructure code + infrastructure: + name: Validate Infrastructure + runs-on: ubuntu-latest + needs: [detect-changes] + if: needs.detect-changes.outputs.packages contains 'infrastructure' + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: 1.6.0 + + - name: Terraform Format Check + run: terraform fmt -check -recursive + working-directory: packages/infrastructure + + - name: Terraform Init + run: terraform init -backend=false + working-directory: packages/infrastructure + + - name: Terraform Validate + run: terraform validate + working-directory: packages/infrastructure + + # Integration test across packages + integration: + name: Integration Tests + runs-on: ubuntu-latest + needs: [frontend, backend] + if: | + always() && + (needs.frontend.result == 'success' || needs.frontend.result == 'skipped') && + (needs.backend.result == 'success' || needs.backend.result == 'skipped') + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Download all artifacts + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + + - name: Install test dependencies + run: npm ci + working-directory: tests/integration + + - name: Run integration tests + run: npm test + working-directory: tests/integration + + # Summary job - gates deployment + check-status: + name: CI Status Check + runs-on: ubuntu-latest + needs: [lint-root, shared, frontend, backend, infrastructure, integration] + if: always() + timeout-minutes: 5 + + steps: + - name: Check all job statuses + run: | + echo "Lint Root: ${{ needs.lint-root.result }}" + echo "Shared: ${{ needs.shared.result }}" + echo "Frontend: ${{ needs.frontend.result }}" + echo "Backend: ${{ needs.backend.result }}" + echo "Infrastructure: ${{ needs.infrastructure.result }}" + echo "Integration: ${{ needs.integration.result }}" + + # Fail if any job failed (skipped is ok) + if [[ "${{ needs.lint-root.result }}" == "failure" ]] || \ + [[ "${{ needs.shared.result }}" == "failure" ]] || \ + [[ "${{ needs.frontend.result }}" == "failure" ]] || \ + [[ "${{ needs.backend.result }}" == "failure" ]] || \ + [[ "${{ needs.infrastructure.result }}" == "failure" ]] || \ + [[ "${{ needs.integration.result }}" == "failure" ]]; then + echo "❌ One or more jobs failed" + exit 1 + fi + + echo "✅ All jobs passed or were skipped" + + - name: Generate summary + run: | + echo "## Monorepo CI Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** ✅ All checks passed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Job Results" >> $GITHUB_STEP_SUMMARY + echo "- Lint Root: ${{ needs.lint-root.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Shared Package: ${{ needs.shared.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Frontend Package: ${{ needs.frontend.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Backend Package: ${{ needs.backend.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Infrastructure: ${{ needs.infrastructure.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Integration Tests: ${{ needs.integration.result }}" >> $GITHUB_STEP_SUMMARY diff --git a/.agents/skills/github-actions-generator/examples/workflows/multi-environment-deploy.yml b/.agents/skills/github-actions-generator/examples/workflows/multi-environment-deploy.yml new file mode 100644 index 0000000..b9fa750 --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/workflows/multi-environment-deploy.yml @@ -0,0 +1,406 @@ +# Multi-Environment Deployment Example +# Demonstrates: Environment protection rules, approval gates, deployment strategies, rollback + +name: Multi-Environment Deployment + +on: + push: + branches: [main, develop, staging] + release: + types: [published] + workflow_dispatch: + inputs: + environment: + description: 'Target environment' + required: true + type: choice + options: + - dev + - staging + - production + default: 'dev' + skip-tests: + description: 'Skip pre-deployment tests' + required: false + type: boolean + default: false + +permissions: + contents: read + deployments: write + id-token: write # For OIDC authentication + +concurrency: + # Prevent concurrent deployments to same environment + group: deploy-${{ github.event.inputs.environment || (github.ref == 'refs/heads/main' && 'production' || github.ref == 'refs/heads/staging' && 'staging' || 'dev') }} + cancel-in-progress: false + +env: + NODE_VERSION: '24' + AWS_REGION: 'us-east-1' + +jobs: + # Determine deployment environment based on trigger + determine-environment: + name: Determine Deployment Environment + runs-on: ubuntu-latest + timeout-minutes: 5 + + outputs: + environment: ${{ steps.determine.outputs.environment }} + deploy: ${{ steps.determine.outputs.deploy }} + + steps: + - name: Determine environment + id: determine + run: | + # Manual dispatch takes precedence + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT + echo "deploy=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Release event goes to production + if [ "${{ github.event_name }}" == "release" ]; then + echo "environment=production" >> $GITHUB_OUTPUT + echo "deploy=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Branch-based deployment + case "${{ github.ref }}" in + refs/heads/main) + echo "environment=production" >> $GITHUB_OUTPUT + echo "deploy=true" >> $GITHUB_OUTPUT + ;; + refs/heads/staging) + echo "environment=staging" >> $GITHUB_OUTPUT + echo "deploy=true" >> $GITHUB_OUTPUT + ;; + refs/heads/develop) + echo "environment=dev" >> $GITHUB_OUTPUT + echo "deploy=true" >> $GITHUB_OUTPUT + ;; + *) + echo "environment=dev" >> $GITHUB_OUTPUT + echo "deploy=false" >> $GITHUB_OUTPUT + ;; + esac + + # Build application + build: + name: Build Application + runs-on: ubuntu-latest + needs: [determine-environment] + if: needs.determine-environment.outputs.deploy == 'true' + timeout-minutes: 20 + + outputs: + artifact-name: ${{ steps.build.outputs.artifact-name }} + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + if: github.event.inputs.skip-tests != 'true' + run: npm test + + - name: Build application + id: build + env: + NODE_ENV: production + ENVIRONMENT: ${{ needs.determine-environment.outputs.environment }} + run: | + npm run build + ARTIFACT_NAME="build-${{ github.sha }}" + echo "artifact-name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT + + - name: Upload build artifacts + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: ${{ steps.build.outputs.artifact-name }} + path: dist/ + retention-days: 30 + + # Deploy to Development + deploy-dev: + name: Deploy to Development + runs-on: ubuntu-latest + needs: [determine-environment, build] + if: needs.determine-environment.outputs.environment == 'dev' && needs.determine-environment.outputs.deploy == 'true' + timeout-minutes: 15 + + environment: + name: dev + url: https://dev.example.com + + steps: + - name: Download build artifacts + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: ${{ needs.build.outputs.artifact-name }} + path: dist/ + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + with: + role-to-assume: ${{ secrets.AWS_ROLE_DEV }} + role-session-name: github-actions-dev-deploy + aws-region: ${{ env.AWS_REGION }} + + - name: Deploy to S3 + run: | + aws s3 sync dist/ s3://${{ secrets.S3_BUCKET_DEV }} --delete + + - name: Invalidate CloudFront cache + run: | + aws cloudfront create-invalidation \ + --distribution-id ${{ secrets.CLOUDFRONT_DIST_DEV }} \ + --paths "/*" + + - name: Run smoke tests + run: | + echo "Running smoke tests against dev environment..." + curl -f https://dev.example.com/health || exit 1 + echo "✅ Smoke tests passed" + + # Deploy to Staging (with approval requirement) + deploy-staging: + name: Deploy to Staging + runs-on: ubuntu-latest + needs: [determine-environment, build] + if: needs.determine-environment.outputs.environment == 'staging' && needs.determine-environment.outputs.deploy == 'true' + timeout-minutes: 20 + + environment: + name: staging + url: https://staging.example.com + + steps: + - name: Download build artifacts + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: ${{ needs.build.outputs.artifact-name }} + path: dist/ + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + with: + role-to-assume: ${{ secrets.AWS_ROLE_STAGING }} + role-session-name: github-actions-staging-deploy + aws-region: ${{ env.AWS_REGION }} + + - name: Deploy to S3 + run: | + aws s3 sync dist/ s3://${{ secrets.S3_BUCKET_STAGING }} --delete + + - name: Invalidate CloudFront cache + run: | + aws cloudfront create-invalidation \ + --distribution-id ${{ secrets.CLOUDFRONT_DIST_STAGING }} \ + --paths "/*" + + - name: Run smoke tests + run: | + echo "Running smoke tests against staging environment..." + curl -f https://staging.example.com/health || exit 1 + echo "✅ Smoke tests passed" + + - name: Run integration tests + run: | + echo "Running integration tests..." + # Add your integration test commands here + echo "✅ Integration tests passed" + + # Deploy to Production (with approval requirement and blue-green strategy) + deploy-production: + name: Deploy to Production + runs-on: ubuntu-latest + needs: [determine-environment, build] + if: needs.determine-environment.outputs.environment == 'production' && needs.determine-environment.outputs.deploy == 'true' + timeout-minutes: 30 + + environment: + name: production + url: https://example.com + + steps: + - name: Download build artifacts + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: ${{ needs.build.outputs.artifact-name }} + path: dist/ + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + with: + role-to-assume: ${{ secrets.AWS_ROLE_PRODUCTION }} + role-session-name: github-actions-prod-deploy + aws-region: ${{ env.AWS_REGION }} + + - name: Backup current deployment + id: backup + run: | + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + BACKUP_BUCKET="s3://${{ secrets.S3_BACKUP_BUCKET }}/backups/${TIMESTAMP}" + + echo "Creating backup at ${BACKUP_BUCKET}" + aws s3 sync s3://${{ secrets.S3_BUCKET_PRODUCTION }} ${BACKUP_BUCKET} + echo "backup-path=${BACKUP_BUCKET}" >> $GITHUB_OUTPUT + + - name: Deploy to production (Blue-Green) + id: deploy + run: | + # Deploy to blue environment first + echo "Deploying to blue environment..." + aws s3 sync dist/ s3://${{ secrets.S3_BUCKET_PRODUCTION_BLUE }} --delete + + # Update CloudFront to point to blue environment + echo "Switching traffic to blue environment..." + aws cloudfront create-invalidation \ + --distribution-id ${{ secrets.CLOUDFRONT_DIST_PRODUCTION }} \ + --paths "/*" + + - name: Run smoke tests (production) + id: smoke-tests + run: | + echo "Running smoke tests against production..." + + # Wait for CloudFront to propagate + sleep 30 + + # Basic health check + curl -f https://example.com/health || exit 1 + + # Check critical endpoints + curl -f https://example.com/api/status || exit 1 + + echo "✅ Production smoke tests passed" + + - name: Monitor metrics + run: | + echo "Monitoring deployment metrics for 5 minutes..." + # In real scenario, this would monitor CloudWatch metrics + # Error rates, latency, etc. + sleep 10 + echo "✅ Metrics look good" + + - name: Rollback on failure + if: failure() + run: | + echo "❌ Deployment failed, initiating rollback..." + + # Restore from backup + aws s3 sync ${{ steps.backup.outputs.backup-path }} s3://${{ secrets.S3_BUCKET_PRODUCTION }} --delete + + # Invalidate cache + aws cloudfront create-invalidation \ + --distribution-id ${{ secrets.CLOUDFRONT_DIST_PRODUCTION }} \ + --paths "/*" + + echo "🔄 Rollback completed" + exit 1 + + # Deployment verification + post-deployment-verification: + name: Post-Deployment Verification + runs-on: ubuntu-latest + needs: [determine-environment, deploy-dev, deploy-staging, deploy-production] + if: always() && needs.determine-environment.outputs.deploy == 'true' + timeout-minutes: 10 + + steps: + - name: Verify deployment + env: + ENVIRONMENT: ${{ needs.determine-environment.outputs.environment }} + run: | + case "${ENVIRONMENT}" in + dev) + URL="https://dev.example.com" + ;; + staging) + URL="https://staging.example.com" + ;; + production) + URL="https://example.com" + ;; + esac + + echo "Verifying deployment at ${URL}" + + # Extended health check + for i in {1..5}; do + if curl -f "${URL}/health"; then + echo "✅ Health check passed (attempt $i)" + break + else + echo "⚠️ Health check failed (attempt $i/5)" + if [ $i -eq 5 ]; then + echo "❌ Health check failed after 5 attempts" + exit 1 + fi + sleep 10 + fi + done + + - name: Update deployment status + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const environment = '${{ needs.determine-environment.outputs.environment }}'; + const devResult = '${{ needs.deploy-dev.result }}'; + const stagingResult = '${{ needs.deploy-staging.result }}'; + const productionResult = '${{ needs.deploy-production.result }}'; + + let deploymentResult = 'skipped'; + if (environment === 'dev') deploymentResult = devResult; + if (environment === 'staging') deploymentResult = stagingResult; + if (environment === 'production') deploymentResult = productionResult; + + const status = deploymentResult === 'success' ? '✅' : '❌'; + + core.summary + .addHeading(`${status} Deployment to ${environment.toUpperCase()}`) + .addRaw(`**Environment:** ${environment}`) + .addBreak() + .addRaw(`**Status:** ${deploymentResult}`) + .addBreak() + .addRaw(`**Commit:** ${context.sha}`) + .addBreak() + .addRaw(`**Actor:** ${context.actor}`) + .write(); + + # Notify stakeholders + notify: + name: Notify Stakeholders + runs-on: ubuntu-latest + needs: [determine-environment, post-deployment-verification] + if: always() && needs.determine-environment.outputs.deploy == 'true' + timeout-minutes: 5 + + steps: + - name: Send notification + env: + ENVIRONMENT: ${{ needs.determine-environment.outputs.environment }} + VERIFICATION_RESULT: ${{ needs.post-deployment-verification.result }} + run: | + STATUS_EMOJI="✅" + if [ "$VERIFICATION_RESULT" != "success" ]; then + STATUS_EMOJI="❌" + fi + + echo "${STATUS_EMOJI} Deployment to ${ENVIRONMENT} completed with status: ${VERIFICATION_RESULT}" + + # In real scenario, send to Slack/Teams/Email + # Example: curl -X POST $SLACK_WEBHOOK -d "{\"text\":\"${STATUS_EMOJI} Deployment to ${ENVIRONMENT}: ${VERIFICATION_RESULT}\"}" diff --git a/.agents/skills/github-actions-generator/examples/workflows/nodejs-ci.yml b/.agents/skills/github-actions-generator/examples/workflows/nodejs-ci.yml new file mode 100644 index 0000000..4d7e8d3 --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/workflows/nodejs-ci.yml @@ -0,0 +1,122 @@ +# Node.js CI Pipeline Example +# Demonstrates: Matrix testing, caching, artifact uploading, code coverage + +name: Node.js CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: '24' + +jobs: + lint: + name: Lint Code + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + test: + name: Test on Node ${{ matrix.node-version }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + permissions: + contents: read + checks: write + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: [20, 22, 24] + exclude: + - os: macos-latest + node-version: 20 + fail-fast: false + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Upload test results + if: always() + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: test-results-${{ matrix.os }}-${{ matrix.node-version }} + path: test-results/ + retention-days: 7 + + - name: Upload coverage + if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 + uses: codecov/codecov-action@e0b68c6749509c5f83f984dd99a76a1c1a231044 # v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + + build: + name: Build Application + needs: [lint, test] + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: build-${{ github.sha }} + path: dist/ + retention-days: 7 + if-no-files-found: error diff --git a/.agents/skills/github-actions-generator/examples/workflows/python-ci.yml b/.agents/skills/github-actions-generator/examples/workflows/python-ci.yml new file mode 100644 index 0000000..f010831 --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/workflows/python-ci.yml @@ -0,0 +1,157 @@ +# Python CI Pipeline Example +# Demonstrates: Matrix testing, caching, linting, testing, code coverage + +name: Python CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHON_VERSION: '3.12' + +jobs: + lint: + name: Lint Code + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + + - name: Install linting tools + run: | + python -m pip install --upgrade pip + pip install black flake8 mypy pylint isort + + - name: Run Black + run: black --check . + + - name: Run isort + run: isort --check-only . + + - name: Run Flake8 + run: flake8 . + + - name: Run Pylint + run: | + FILES=$(git ls-files '*.py') + if [ -z "$FILES" ]; then + echo "No Python files found to lint" + exit 0 + fi + pylint $FILES + + - name: Run MyPy + run: mypy . + + test: + name: Test on Python ${{ matrix.python-version }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + permissions: + contents: read + checks: write + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ['3.10', '3.11', '3.12'] + exclude: + - os: macos-latest + python-version: '3.10' + fail-fast: false + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: | + requirements.txt + requirements-dev.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Run tests with pytest + run: | + pytest --cov=. --cov-report=xml --cov-report=html --junitxml=junit.xml + + - name: Upload test results + if: always() + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: test-results-${{ matrix.os }}-py${{ matrix.python-version }} + path: | + junit.xml + htmlcov/ + retention-days: 7 + + - name: Upload coverage to Codecov + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + uses: codecov/codecov-action@e0b68c6749509c5f83f984dd99a76a1c1a231044 # v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + fail_ci_if_error: true + + build: + name: Build Package + needs: [lint, test] + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Check package + run: twine check dist/* + + - name: Upload build artifacts + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: python-package-distributions + path: dist/ + retention-days: 7 + if-no-files-found: error diff --git a/.agents/skills/github-actions-generator/examples/workflows/scheduled-tasks.yml b/.agents/skills/github-actions-generator/examples/workflows/scheduled-tasks.yml new file mode 100644 index 0000000..cca2fc1 --- /dev/null +++ b/.agents/skills/github-actions-generator/examples/workflows/scheduled-tasks.yml @@ -0,0 +1,376 @@ +# Scheduled Tasks Workflow Example +# Demonstrates: Cron schedules, scheduled maintenance, dependency updates, cleanup tasks + +name: Scheduled Maintenance Tasks + +on: + # Run dependency audit daily at 2 AM UTC + schedule: + - cron: '0 2 * * *' + + # Allow manual trigger for testing + workflow_dispatch: + inputs: + task: + description: 'Task to run' + required: false + type: choice + options: + - all + - dependency-audit + - stale-branches + - cache-cleanup + - health-check + default: 'all' + +permissions: + contents: write + pull-requests: write + issues: write + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false # Don't cancel scheduled maintenance + +env: + NODE_VERSION: '24' + +jobs: + # Job 1: Security and dependency audit + dependency-audit: + name: Dependency Security Audit + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event.inputs.task == 'all' || github.event.inputs.task == 'dependency-audit' + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Run npm audit + id: audit + continue-on-error: true + run: | + npm audit --audit-level=moderate > audit-report.txt 2>&1 + AUDIT_EXIT_CODE=$? + echo "exit_code=$AUDIT_EXIT_CODE" >> $GITHUB_OUTPUT + cat audit-report.txt + + - name: Upload audit report + if: always() + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: dependency-audit-report + path: audit-report.txt + retention-days: 30 + + - name: Create issue if vulnerabilities found + if: steps.audit.outputs.exit_code != '0' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const fs = require('fs'); + const report = fs.readFileSync('audit-report.txt', 'utf8'); + + // Check if issue already exists + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'security,dependencies,automated' + }); + + const existingIssue = issues.find(issue => + issue.title.includes('Security Vulnerabilities Detected') + ); + + if (!existingIssue) { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: '🔒 Security Vulnerabilities Detected - ' + new Date().toISOString().split('T')[0], + body: `## Security Audit Report\n\n**Date:** ${new Date().toISOString()}\n\n### Audit Results\n\n\`\`\`\n${report}\n\`\`\`\n\n---\n*This issue was automatically created by the scheduled maintenance workflow.*`, + labels: ['security', 'dependencies', 'automated'] + }); + } + + # Job 2: Clean up stale branches + stale-branches: + name: Cleanup Stale Branches + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event.inputs.task == 'all' || github.event.inputs.task == 'stale-branches' + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 # Need full history for branch analysis + + - name: Find stale branches + id: find-stale + run: | + # Find branches not updated in 90 days + echo "## Stale Branches (90+ days old)" > stale-branches.txt + echo "" >> stale-branches.txt + + git for-each-ref --sort=-committerdate refs/remotes/origin/ --format='%(refname:short)|%(committerdate:relative)|%(authorname)' | \ + while IFS='|' read -r branch date author; do + # Skip main, develop, and release branches + if [[ "$branch" =~ ^origin/(main|develop|master|release/) ]]; then + continue + fi + + # Check if branch is older than 90 days + if [[ "$date" =~ "month" ]] || [[ "$date" =~ "year" ]]; then + echo "- **$branch** (Last updated: $date, Author: $author)" >> stale-branches.txt + fi + done + + cat stale-branches.txt + + - name: Create PR to document stale branches + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const fs = require('fs'); + const report = fs.readFileSync('stale-branches.txt', 'utf8'); + + // Create or update issue + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'maintenance,automated' + }); + + const existingIssue = issues.find(issue => + issue.title.includes('Stale Branches Report') + ); + + if (existingIssue) { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + body: `${report}\n\n---\n*Updated: ${new Date().toISOString()}*` + }); + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: '🧹 Stale Branches Report', + body: `${report}\n\n---\n*Created: ${new Date().toISOString()}*`, + labels: ['maintenance', 'automated'] + }); + } + + # Job 3: Clean up old caches + cache-cleanup: + name: Cache Cleanup + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event.inputs.task == 'all' || github.event.inputs.task == 'cache-cleanup' + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Cleanup old caches + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const { data: caches } = await github.rest.actions.getActionsCacheList({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100 + }); + + console.log(`Found ${caches.total_count} caches`); + + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + let deletedCount = 0; + for (const cache of caches.actions_caches) { + const cacheDate = new Date(cache.last_accessed_at); + + if (cacheDate < thirtyDaysAgo) { + console.log(`Deleting cache: ${cache.key} (last accessed: ${cache.last_accessed_at})`); + + await github.rest.actions.deleteActionsCacheById({ + owner: context.repo.owner, + repo: context.repo.repo, + cache_id: cache.id + }); + + deletedCount++; + } + } + + console.log(`Deleted ${deletedCount} old caches`); + core.summary + .addHeading('Cache Cleanup Summary') + .addRaw(`Deleted ${deletedCount} caches older than 30 days`) + .write(); + + # Job 4: Health check for external services + health-check: + name: External Services Health Check + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event.inputs.task == 'all' || github.event.inputs.task == 'health-check' + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Check production API health + id: api-health + continue-on-error: true + run: | + # Replace with your actual API endpoint + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health || echo "000") + echo "response_code=$RESPONSE" >> $GITHUB_OUTPUT + + if [ "$RESPONSE" = "200" ]; then + echo "✅ API is healthy (HTTP $RESPONSE)" + else + echo "❌ API health check failed (HTTP $RESPONSE)" + exit 1 + fi + + - name: Check database connectivity + id: db-health + continue-on-error: true + run: | + # Example: Check database connectivity + # Replace with your actual health check logic + echo "✅ Database connectivity check passed" + + - name: Check CDN status + id: cdn-health + continue-on-error: true + run: | + # Example: Check CDN health + CDN_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" https://cdn.example.com || echo "000") + echo "response_code=$CDN_RESPONSE" >> $GITHUB_OUTPUT + + if [ "$CDN_RESPONSE" = "200" ]; then + echo "✅ CDN is healthy (HTTP $CDN_RESPONSE)" + else + echo "❌ CDN health check failed (HTTP $CDN_RESPONSE)" + exit 1 + fi + + - name: Create incident if health checks fail + if: | + steps.api-health.outcome == 'failure' || + steps.db-health.outcome == 'failure' || + steps.cdn-health.outcome == 'failure' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const apiStatus = '${{ steps.api-health.outcome }}'; + const dbStatus = '${{ steps.db-health.outcome }}'; + const cdnStatus = '${{ steps.cdn-health.outcome }}'; + + const failedServices = []; + if (apiStatus === 'failure') failedServices.push('API'); + if (dbStatus === 'failure') failedServices.push('Database'); + if (cdnStatus === 'failure') failedServices.push('CDN'); + + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `🚨 Health Check Failed: ${failedServices.join(', ')}`, + body: `## Health Check Failure Report\n\n**Date:** ${new Date().toISOString()}\n\n### Failed Services\n\n${failedServices.map(s => `- ❌ ${s}`).join('\n')}\n\n### Details\n\n- API Status: ${apiStatus}\n- Database Status: ${dbStatus}\n- CDN Status: ${cdnStatus}\n\n---\n*This incident was automatically created by the scheduled maintenance workflow.*`, + labels: ['incident', 'automated', 'urgent'] + }); + + # Job 5: Generate weekly metrics report + weekly-metrics: + name: Weekly Metrics Report + runs-on: ubuntu-latest + # Run only on Mondays (schedule) or manual trigger + if: (github.event_name == 'schedule' && github.event.schedule == '0 2 * * 1') || github.event.inputs.task == 'all' + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Generate metrics + id: metrics + run: | + # Commits in last 7 days + COMMITS=$(git log --since="7 days ago" --oneline | wc -l) + echo "commits=$COMMITS" >> $GITHUB_OUTPUT + + # PRs merged in last 7 days + # This would typically use GitHub API + echo "prs=0" >> $GITHUB_OUTPUT + + # Contributors in last 7 days + CONTRIBUTORS=$(git log --since="7 days ago" --format='%ae' | sort -u | wc -l) + echo "contributors=$CONTRIBUTORS" >> $GITHUB_OUTPUT + + - name: Create metrics report + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const commits = '${{ steps.metrics.outputs.commits }}'; + const contributors = '${{ steps.metrics.outputs.contributors }}'; + + const weekStart = new Date(); + weekStart.setDate(weekStart.getDate() - 7); + + core.summary + .addHeading('📊 Weekly Repository Metrics') + .addRaw(`**Period:** ${weekStart.toISOString().split('T')[0]} to ${new Date().toISOString().split('T')[0]}`) + .addBreak() + .addTable([ + [{data: 'Metric', header: true}, {data: 'Value', header: true}], + ['Commits', commits], + ['Contributors', contributors] + ]) + .write(); + + # Summary job + summary: + name: Maintenance Summary + runs-on: ubuntu-latest + needs: [dependency-audit, stale-branches, cache-cleanup, health-check] + if: always() + timeout-minutes: 5 + + steps: + - name: Generate summary + run: | + echo "## Scheduled Maintenance Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Run Date:** $(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Task Results" >> $GITHUB_STEP_SUMMARY + echo "- Dependency Audit: ${{ needs.dependency-audit.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Stale Branches Cleanup: ${{ needs.stale-branches.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Cache Cleanup: ${{ needs.cache-cleanup.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Health Check: ${{ needs.health-check.result }}" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ needs.dependency-audit.result }}" == "failure" ]] || \ + [[ "${{ needs.stale-branches.result }}" == "failure" ]] || \ + [[ "${{ needs.cache-cleanup.result }}" == "failure" ]] || \ + [[ "${{ needs.health-check.result }}" == "failure" ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "⚠️ **Some tasks failed. Please check the logs.**" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ **All maintenance tasks completed successfully.**" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.agents/skills/github-actions-generator/references/advanced-triggers.md b/.agents/skills/github-actions-generator/references/advanced-triggers.md new file mode 100644 index 0000000..a984e78 --- /dev/null +++ b/.agents/skills/github-actions-generator/references/advanced-triggers.md @@ -0,0 +1,917 @@ +# Advanced GitHub Actions Triggers + +**Last Updated:** November 2025 + +## Overview + +This guide covers advanced trigger patterns for GitHub Actions workflows beyond the basic `push`, `pull_request`, and `schedule` triggers. These patterns enable workflow orchestration, external integrations, ChatOps, and complex automation scenarios. + +## Table of Contents + +1. [Workflow Orchestration](#workflow-orchestration) +2. [External Integration](#external-integration) +3. [ChatOps Patterns](#chatops-patterns) +4. [Deployment Triggers](#deployment-triggers) +5. [Advanced Path Filtering](#advanced-path-filtering) +6. [Security Patterns](#security-patterns) +7. [GitHub Services Integration](#github-services-integration) +8. [Best Practices](#best-practices) + +--- + +## Workflow Orchestration + +### workflow_run Trigger + +The `workflow_run` trigger allows you to chain workflows together, running one workflow after another completes. This is the **recommended pattern** for handling external pull requests securely. + +#### Basic Syntax + +```yaml +name: Deploy Application + +on: + workflow_run: + workflows: ["CI Pipeline"] + types: [completed] + branches: [main, staging] +``` + +#### Trigger Types + +- `requested` - Workflow run was requested +- `in_progress` - Workflow run is currently running +- `completed` - Workflow run has finished (success, failure, or cancelled) + +#### Use Cases + +**1. Deployment After CI Success** + +```yaml +# deploy.yml - Separate deployment workflow +name: Deploy to Production + +on: + workflow_run: + workflows: ["CI Pipeline"] + types: [completed] + branches: [main] + +jobs: + deploy: + # Only deploy if CI passed + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + + environment: + name: production + url: https://example.com + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download build artifacts from CI + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: build-artifacts + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy application + run: | + echo "Deploying commit ${{ github.event.workflow_run.head_sha }}" + # Deployment commands here +``` + +**2. Security Scanning for External PRs** + +```yaml +# security-scan.yml - Runs after CI for external PRs +name: Security Scan + +on: + workflow_run: + workflows: ["CI"] + types: [completed] + +permissions: + security-events: write + contents: read + +jobs: + scan: + # Only scan if CI passed and it was a PR + if: | + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + + steps: + - name: Checkout PR code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Run security scan + run: | + # Security scanning without exposing secrets to PR + npm audit --audit-level=high +``` + +#### Accessing Workflow Run Information + +```yaml +steps: + - name: Get workflow run details + run: | + echo "Workflow: ${{ github.event.workflow_run.name }}" + echo "Conclusion: ${{ github.event.workflow_run.conclusion }}" + echo "Head SHA: ${{ github.event.workflow_run.head_sha }}" + echo "Head Branch: ${{ github.event.workflow_run.head_branch }}" + echo "Run ID: ${{ github.event.workflow_run.id }}" + echo "Event: ${{ github.event.workflow_run.event }}" +``` + +#### Security Benefits + +✅ **Safer than `pull_request_target`** for external PRs: +- Runs with workflow file from target branch (not PR) +- No access to PR code by default +- Secrets are safe from malicious PRs +- Must explicitly checkout PR code if needed + +--- + +## External Integration + +### repository_dispatch Trigger + +The `repository_dispatch` trigger allows external systems to trigger workflows via the GitHub API. This enables integration with webhooks, custom dashboards, monitoring systems, and other external tools. + +#### Basic Syntax + +```yaml +name: Handle External Event + +on: + repository_dispatch: + types: [deploy-prod, deploy-staging, run-migration, rebuild-cache] +``` + +#### Event Types + +Event types are custom strings you define. Common patterns: +- `deploy-` - Deployment triggers +- `run-` - Task execution +- `notify-` - Notification handling + +#### Triggering via API + +**Using curl:** + +```bash +curl -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/OWNER/REPO/dispatches \ + -d '{ + "event_type": "deploy-prod", + "client_payload": { + "version": "v1.2.3", + "requestor": "monitoring-system", + "environment": "production", + "rollback": false + } + }' +``` + +**Using Python:** + +```python +import requests + +def trigger_deployment(repo, token, version, environment): + url = f"https://api.github.com/repos/{repo}/dispatches" + headers = { + "Authorization": f"token {token}", + "Accept": "application/vnd.github.v3+json" + } + payload = { + "event_type": f"deploy-{environment}", + "client_payload": { + "version": version, + "requestor": "api", + "environment": environment + } + } + response = requests.post(url, json=payload, headers=headers) + return response.status_code == 204 +``` + +**Using Node.js (Octokit):** + +```javascript +const { Octokit } = require("@octokit/rest"); + +const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); + +await octokit.repos.createDispatchEvent({ + owner: "OWNER", + repo: "REPO", + event_type: "deploy-prod", + client_payload: { + version: "v1.2.3", + requestor: "api", + environment: "production" + } +}); +``` + +#### Handling Dispatch Events + +```yaml +name: External Deployment Trigger + +on: + repository_dispatch: + types: [deploy-prod, deploy-staging, deploy-dev] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Parse dispatch payload + id: payload + run: | + echo "Event Type: ${{ github.event.action }}" + echo "Version: ${{ github.event.client_payload.version }}" + echo "Environment: ${{ github.event.client_payload.environment }}" + echo "Requestor: ${{ github.event.client_payload.requestor }}" + + # Set outputs for later steps + echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT + echo "environment=${{ github.event.client_payload.environment }}" >> $GITHUB_OUTPUT + + - name: Checkout specific version + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.payload.outputs.version }} + + - name: Deploy to environment + env: + ENVIRONMENT: ${{ steps.payload.outputs.environment }} + VERSION: ${{ steps.payload.outputs.version }} + run: | + echo "Deploying $VERSION to $ENVIRONMENT" + # Deployment logic here +``` + +#### Use Cases + +**1. Webhook Integration** + +Trigger workflows from external monitoring/alerting systems: + +```yaml +on: + repository_dispatch: + types: [incident-detected, performance-degradation] + +jobs: + handle-alert: + runs-on: ubuntu-latest + steps: + - name: Process alert + run: | + SEVERITY="${{ github.event.client_payload.severity }}" + MESSAGE="${{ github.event.client_payload.message }}" + + echo "Alert received: $MESSAGE (Severity: $SEVERITY)" + + if [[ "$SEVERITY" == "critical" ]]; then + # Trigger emergency procedures + echo "Initiating critical incident response" + fi +``` + +**2. Manual Trigger from Dashboard** + +Custom deployment dashboard that triggers GitHub Actions: + +```yaml +on: + repository_dispatch: + types: [dashboard-deploy] + +jobs: + deploy: + runs-on: ubuntu-latest + + environment: + name: ${{ github.event.client_payload.environment }} + + steps: + - name: Validate payload + run: | + # Validate required fields + if [[ -z "${{ github.event.client_payload.version }}" ]]; then + echo "Error: version is required" + exit 1 + fi + + if [[ -z "${{ github.event.client_payload.approver }}" ]]; then + echo "Error: approver is required" + exit 1 + fi + + - name: Deploy + run: | + echo "Deploying version ${{ github.event.client_payload.version }}" + echo "Approved by: ${{ github.event.client_payload.approver }}" +``` + +**3. Cross-Repository Triggers** + +Trigger workflow in repo A from repo B: + +```yaml +# In Repository A +on: + repository_dispatch: + types: [dependency-updated] + +jobs: + rebuild: + runs-on: ubuntu-latest + steps: + - name: Rebuild with new dependency + run: | + echo "Dependency ${{ github.event.client_payload.dependency }} updated to ${{ github.event.client_payload.version }}" + # Rebuild logic +``` + +#### Security Considerations + +🔒 **Token Security:** +- Use a Personal Access Token (PAT) or GitHub App token +- Minimum required scope: `repo` (for private repos) or `public_repo` (for public repos) +- Store token in secrets, never in code +- Rotate tokens regularly + +🔒 **Payload Validation:** +- Always validate `client_payload` fields +- Sanitize user input to prevent injection +- Use allowlists for critical fields + +```yaml +- name: Validate environment + run: | + ENV="${{ github.event.client_payload.environment }}" + + # Only allow specific environments + if [[ ! "$ENV" =~ ^(dev|staging|production)$ ]]; then + echo "Error: Invalid environment: $ENV" + exit 1 + fi +``` + +--- + +## ChatOps Patterns + +### issue_comment Trigger + +The `issue_comment` trigger allows you to implement ChatOps - executing workflows via commands in issue or PR comments. + +#### Basic Syntax + +```yaml +name: ChatOps Commands + +on: + issue_comment: + types: [created, edited] +``` + +#### Comment Types + +- `created` - New comment posted +- `edited` - Comment was edited +- `deleted` - Comment was deleted (rarely used) + +#### Implementing ChatOps Commands + +**Full ChatOps Example:** + +```yaml +name: ChatOps - Deploy Command + +on: + issue_comment: + types: [created] + +jobs: + deploy: + # Security checks (CRITICAL!) + if: | + github.event.issue.pull_request && + startsWith(github.event.comment.body, '/deploy') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) + + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + deployments: write + + steps: + # Step 1: React to comment to show command received + - name: Add reaction to comment + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket' + }); + + # Step 2: Parse command and arguments + - name: Parse deploy command + id: parse + run: | + COMMAND="${{ github.event.comment.body }}" + + # Extract environment (default: staging) + ENV=$(echo "$COMMAND" | grep -oP '/deploy\s+\K\w+' || echo 'staging') + + # Validate environment + if [[ ! "$ENV" =~ ^(dev|staging|production)$ ]]; then + echo "error=Invalid environment: $ENV" >> $GITHUB_OUTPUT + exit 1 + fi + + echo "environment=$ENV" >> $GITHUB_OUTPUT + echo "pr_number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT + + # Step 3: Get PR details + - name: Get PR branch + id: pr + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: ${{ steps.parse.outputs.pr_number }} + }); + + core.setOutput('ref', pr.data.head.ref); + core.setOutput('sha', pr.data.head.sha); + core.setOutput('repo', pr.data.head.repo.full_name); + + # Step 4: Checkout PR code + - name: Checkout PR code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.pr.outputs.ref }} + repository: ${{ steps.pr.outputs.repo }} + + # Step 5: Deploy + - name: Deploy to environment + id: deploy + env: + ENVIRONMENT: ${{ steps.parse.outputs.environment }} + PR_SHA: ${{ steps.pr.outputs.sha }} + run: | + echo "Deploying PR #${{ steps.parse.outputs.pr_number }} to $ENVIRONMENT" + echo "SHA: $PR_SHA" + + # Deployment logic here + DEPLOY_URL="https://$ENVIRONMENT.example.com" + echo "url=$DEPLOY_URL" >> $GITHUB_OUTPUT + + # Step 6: Comment with results + - name: Comment deployment result + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const environment = '${{ steps.parse.outputs.environment }}'; + const url = '${{ steps.deploy.outputs.url }}'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ steps.parse.outputs.pr_number }}, + body: `✅ Deployed to **${environment}**\n\n🔗 ${url}\n\nTriggered by: @${{ github.event.comment.user.login }}` + }); + + # Step 7: Handle failures + - name: Comment on failure + if: failure() + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ github.event.issue.number }}, + body: `❌ Deployment failed\n\nCheck the [workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.` + }); +``` + +#### Common ChatOps Commands + +**1. /deploy [environment]** +```yaml +startsWith(github.event.comment.body, '/deploy') +``` + +**2. /run-tests [suite]** +```yaml +startsWith(github.event.comment.body, '/run-tests') +``` + +**3. /benchmark** +```yaml +contains(github.event.comment.body, '/benchmark') +``` + +**4. /approve** +```yaml +github.event.comment.body == '/approve' +``` + +#### Permission Checking + +**Author Association Levels:** + +- `OWNER` - Repository owner +- `MEMBER` - Organization member +- `COLLABORATOR` - Repository collaborator +- `CONTRIBUTOR` - Has contributed to repo +- `FIRST_TIME_CONTRIBUTOR` - First contribution +- `FIRST_TIMER` - First time interacting +- `NONE` - No association + +**Check permissions:** + +```yaml +# Only owners and members +if: contains(fromJSON('["OWNER", "MEMBER"]'), github.event.comment.author_association) + +# More permissive +if: contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR"]'), github.event.comment.author_association) +``` + +**Advanced permission check with team membership:** + +```yaml +steps: + - name: Check team membership + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const teams = ['deployment-team', 'admin-team']; + const user = context.payload.comment.user.login; + + let authorized = false; + for (const team of teams) { + try { + await github.rest.teams.getMembershipForUserInOrg({ + org: context.repo.owner, + team_slug: team, + username: user + }); + authorized = true; + break; + } catch (error) { + // User not in this team + } + } + + if (!authorized) { + core.setFailed(`User ${user} not authorized`); + } +``` + +#### Security Best Practices for ChatOps + +🔒 **Always validate:** +1. Command is from a PR: `github.event.issue.pull_request` +2. User has permissions: `github.event.comment.author_association` +3. Command format is valid +4. Arguments are sanitized + +🔒 **Never:** +- Execute arbitrary code from comments +- Use comment content in shell commands without validation +- Trust external PR authors for sensitive operations + +🔒 **Use environment variables:** + +```yaml +# BAD - Command injection risk +- run: echo ${{ github.event.comment.body }} + +# GOOD - Safe +- env: + COMMENT: ${{ github.event.comment.body }} + run: echo "$COMMENT" +``` + +--- + +## Deployment Triggers + +### deployment and deployment_status + +These triggers integrate with GitHub's deployment API. + +#### deployment Trigger + +```yaml +name: Handle Deployment + +on: + deployment: + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Get deployment info + run: | + echo "Environment: ${{ github.event.deployment.environment }}" + echo "Ref: ${{ github.event.deployment.ref }}" + echo "Task: ${{ github.event.deployment.task }}" + echo "Payload: ${{ toJSON(github.event.deployment.payload) }}" +``` + +#### deployment_status Trigger + +```yaml +name: Post-Deployment Actions + +on: + deployment_status: + +jobs: + notify: + if: github.event.deployment_status.state == 'success' + runs-on: ubuntu-latest + + steps: + - name: Send notification + run: | + echo "Deployment to ${{ github.event.deployment.environment }} succeeded" + # Send Slack/email notification +``` + +--- + +## Advanced Path Filtering + +### Complex Path Patterns + +```yaml +on: + push: + paths: + # Include specific paths + - 'src/**' + - 'lib/**/*.js' + + # Exclude paths (ignore) + - '!src/**/*.md' + - '!src/**/*.test.js' + - '!**/__tests__/**' + + # Only specific file types + - '**.py' + - '**.yaml' + - '**.yml' +``` + +### Path Filters with Multiple Triggers + +```yaml +on: + pull_request: + paths: + - 'backend/**' + push: + branches: [main] + paths: + - 'backend/**' +``` + +### Monorepo Path Filtering + +```yaml +on: + pull_request: + paths: + - 'packages/frontend/**' + - 'packages/shared/**' + - '!packages/**/README.md' + - '!packages/**/*.test.*' +``` + +--- + +## Security Patterns + +### pull_request vs pull_request_target + +| Trigger | Context | Secrets | Use Case | Risk Level | +|---------|---------|---------|----------|------------| +| `pull_request` | PR branch | ❌ No access | Standard PR validation | ✅ Safe | +| `pull_request_target` | Target branch | ✅ Full access | Write to PR from fork | ⚠️ High risk | +| `workflow_run` | Target branch | ✅ Full access | Post-CI for external PRs | ✅ Safe (if used correctly) | + +### Safe Patterns + +**✅ Standard PR validation:** + +```yaml +on: + pull_request: + branches: [main] + +# Safe: No secrets exposed, runs PR code in isolation +``` + +**✅ Post-CI processing with workflow_run:** + +```yaml +on: + workflow_run: + workflows: ["CI"] + types: [completed] + +# Safe: Runs after CI, has secrets, but uses target branch code +``` + +**⚠️ Dangerous: pull_request_target** + +```yaml +on: + pull_request_target: + branches: [main] + +# DANGEROUS: External PRs can access secrets! +# Only use if you explicitly checkout target branch code +``` + +### Securing pull_request_target + +If you must use `pull_request_target`: + +```yaml +on: + pull_request_target: + +jobs: + comment: + runs-on: ubuntu-latest + + steps: + # SAFE: Don't checkout PR code + - name: Comment on PR + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Thanks for your contribution!' + }); + + # UNSAFE: Never do this! + # - uses: actions/checkout@v4 + # with: + # ref: ${{ github.event.pull_request.head.sha }} +``` + +--- + +## GitHub Services Integration + +### check_run and check_suite + +```yaml +on: + check_run: + types: [created, rerequested, completed] + +on: + check_suite: + types: [completed, requested] +``` + +### status + +```yaml +on: + status: + +jobs: + handle-status: + runs-on: ubuntu-latest + steps: + - name: Check status + run: | + echo "State: ${{ github.event.state }}" + echo "Context: ${{ github.event.context }}" +``` + +### package + +```yaml +on: + package: + types: [published, updated] +``` + +--- + +## Best Practices + +### 1. Choose the Right Trigger + +| Scenario | Recommended Trigger | +|----------|-------------------| +| Standard PR validation | `pull_request` | +| External PR with secrets | `workflow_run` after `pull_request` | +| Deploy after CI | `workflow_run` | +| Manual dashboard trigger | `repository_dispatch` | +| ChatOps commands | `issue_comment` | +| Scheduled cleanup | `schedule` | +| External webhook | `repository_dispatch` | + +### 2. Security Checklist + +- [ ] Validate user permissions +- [ ] Sanitize all inputs +- [ ] Use environment variables, not direct interpolation +- [ ] Never trust external PR code with secrets +- [ ] Use `workflow_run` instead of `pull_request_target` when possible +- [ ] Implement allowlists for critical operations +- [ ] Log all security-sensitive actions + +### 3. Performance Optimization + +- Use `workflow_run` to separate slow jobs from fast CI +- Filter triggers with `paths` to avoid unnecessary runs +- Use `concurrency` to cancel outdated runs +- Implement conditional job execution + +### 4. Debugging + +**Check trigger details:** + +```yaml +- name: Debug trigger info + run: | + echo "Event name: ${{ github.event_name }}" + echo "Event: ${{ toJSON(github.event) }}" +``` + +**Test repository_dispatch locally:** + +```bash +# Set token +export GITHUB_TOKEN="your_token" + +# Trigger workflow +curl -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/OWNER/REPO/dispatches \ + -d '{"event_type":"test","client_payload":{"debug":true}}' +``` + +--- + +## Example Workflows + +See the `examples/triggers/` directory for complete working examples: + +- `workflow-orchestration.yml` - CI → Deploy workflow chaining +- `repository-dispatch.yml` - External API triggers +- `chatops-commands.yml` - Full ChatOps implementation + +--- + +## Resources + +- [GitHub Actions Events Documentation](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows) +- [Security Hardening for GitHub Actions](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) +- [GitHub API - Repository Dispatch](https://docs.github.com/en/rest/repos/repos#create-a-repository-dispatch-event) \ No newline at end of file diff --git a/.agents/skills/github-actions-generator/references/best-practices.md b/.agents/skills/github-actions-generator/references/best-practices.md new file mode 100644 index 0000000..b0e227e --- /dev/null +++ b/.agents/skills/github-actions-generator/references/best-practices.md @@ -0,0 +1,755 @@ +# GitHub Actions Best Practices + +**Last Updated:** November 2025 +**Based on:** Official GitHub Actions documentation and Context7 verified sources + +## Table of Contents +1. [Security Best Practices](#security-best-practices) +2. [Performance Optimization](#performance-optimization) +3. [Workflow Design](#workflow-design) +4. [Action Selection and Versioning](#action-selection-and-versioning) +5. [Error Handling](#error-handling) +6. [Maintainability](#maintainability) +7. [Common Patterns](#common-patterns) +8. [Anti-Patterns to Avoid](#anti-patterns-to-avoid) + +## Security Best Practices + +### 1. Pin Actions to Full SHA (Critical Security Practice) + +**Best Practice:** +```yaml +# ✅ BEST: Pinned to specific full SHA (40 characters) with version comment +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +``` + +**Why:** +- Immutable: SHA cannot be changed, preventing supply chain attacks +- Reproducible: Same code runs every time +- Verifiable: Can audit exact code being executed + +**Acceptable Alternative:** +```yaml +# ✅ ACCEPTABLE: Major version tag (for official GitHub actions) +- uses: actions/checkout@v4 +``` + +**Avoid:** +```yaml +# ❌ BAD: Mutable references +- uses: actions/checkout@main +- uses: actions/checkout@master +- uses: actions/checkout@latest +``` + +### 2. Minimal Permissions + +**Best Practice:** +```yaml +# Top-level: Set default to read-only +permissions: + contents: read + +jobs: + build: + # Job-level: Grant only necessary permissions + permissions: + contents: read + packages: write + pull-requests: write +``` + +**Common Permission Scopes:** +- `contents`: Repository contents (read/write) +- `packages`: GitHub Packages (read/write) +- `pull-requests`: PR comments and labels (read/write) +- `issues`: Issue management (read/write) +- `statuses`: Commit statuses (write) +- `checks`: Check runs (write) +- `deployments`: Deployment status (write) + +### 3. Secrets Management + +**Best Practice:** +```yaml +# ✅ GOOD: Use secrets properly +- name: Deploy to production + env: + API_KEY: ${{ secrets.API_KEY }} + run: | + echo "::add-mask::$API_KEY" + ./deploy.sh + +# ✅ GOOD: Pass secrets to actions +- uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +**Avoid:** +```yaml +# ❌ BAD: Exposing secrets +- run: echo "API_KEY=${{ secrets.API_KEY }}" + +# ❌ BAD: Using secrets in URLs +- run: git clone https://${{ secrets.GITHUB_TOKEN }}@github.com/user/repo.git +``` + +### 4. Input Validation and Injection Prevention + +**Critical Security Issue:** Script injection through untrusted input is one of the most common security vulnerabilities in GitHub Actions. + +**Best Practice - Use Environment Variables:** +```yaml +# ✅ BEST: Always use environment variables for untrusted input (Bash) +- name: Check PR title + env: + TITLE: ${{ github.event.pull_request.title }} + run: | + if [[ "$TITLE" =~ ^octocat ]]; then + echo "PR title starts with 'octocat'" + exit 0 + else + echo "PR title did not start with 'octocat'" + exit 1 + fi + +# ✅ BEST: Validate inputs with strict patterns +- name: Build image + env: + IMAGE_NAME: ${{ github.event.inputs.image-name }} + run: | + if [[ ! "$IMAGE_NAME" =~ ^[a-z0-9-]+$ ]]; then + echo "::error::Invalid image name" + exit 1 + fi + docker build -t "$IMAGE_NAME" . +``` + +**Alternative - Use JavaScript Action:** +```yaml +# ✅ GOOD: Create a JavaScript action to process context values +- uses: fakeaction/checktitle@v3 + with: + title: ${{ github.event.pull_request.title }} +``` + +**Avoid:** +```yaml +# ❌ BAD: Direct interpolation of user input (vulnerable to injection) +- run: echo "PR: ${{ github.event.pull_request.title }}" +- run: docker build -t ${{ github.event.inputs.tag }} . +- run: echo "${{ github.event.pull_request.title }}" | grep "fix" +``` + +### 5. Dependency Review and SBOM Attestations (New in 2025) + +**Dependency Review Action:** +```yaml +name: Dependency Review +on: + pull_request: + paths-ignore: + - "README.md" + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + # Fail on critical vulnerabilities + fail-on-severity: critical + # Allow specific dependencies + allow-licenses: MIT, Apache-2.0, BSD-3-Clause +``` + +**SBOM Attestations for Container Images:** +```yaml +permissions: + id-token: write + contents: read + attestations: write + packages: write + +steps: + - name: Build container image + run: docker build -t ${{ env.REGISTRY }}/myapp:${{ github.sha }} . + + - name: Generate SBOM + uses: anchore/sbom-action@v0 + with: + image: ${{ env.REGISTRY }}/myapp:${{ github.sha }} + format: spdx-json + output-file: sbom.json + + - name: Generate SBOM attestation + uses: actions/attest-sbom@v2 + with: + subject-name: ${{ env.REGISTRY }}/myapp + subject-digest: sha256:${{ steps.build.outputs.digest }} + sbom-path: sbom.json + push-to-registry: true +``` + +## Performance Optimization + +### 1. Dependency Caching (Updated November 2025) + +**Important:** actions/cache v5.0.3 is recommended (Node 24 runtime). The cache service was rewritten for improved performance in 2025. Legacy cache service was sunset on February 1, 2025. + +**Cache Size Limits (New):** As of November 2025, repositories can exceed the previous 10 GB cache limit using a pay-as-you-go model. All repositories receive 10 GB free, with additional storage available. + +**NPM/Node.js with Built-in Caching:** +```yaml +- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' +``` + +**Manual Caching with actions/cache@v5:** +```yaml +- name: Cache node modules + id: cache-npm + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + +- name: Check cache hit + if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} + run: echo "Cache miss - installing dependencies" + +- name: Install dependencies + run: npm ci +``` + +**Maven with Built-in Caching:** +```yaml +- uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' +``` + +**Ruby Gems with Matrix Strategy:** +```yaml +- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: vendor/bundle + key: bundle-${{ matrix.os }}-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + bundle-${{ matrix.os }}-${{ matrix.ruby-version }}- +``` + +**.NET Dependencies:** +```yaml +- uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.x' + cache: true # Caches NuGet global-packages folder +``` + +### 2. Concurrency Control + +**Best Practice:** +```yaml +# Cancel in-progress runs when new commit pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +``` + +**Per-PR Concurrency:** +```yaml +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +``` + +### 3. Shallow Checkout + +**Best Practice:** +```yaml +# ✅ GOOD: Shallow clone when full history not needed +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + +# ✅ GOOD: Fetch specific depth for changelog generation +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 50 +``` + +### 4. Matrix Strategy Optimization + +**Best Practice:** +```yaml +strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node: [18, 20, 22] + exclude: + # Exclude expensive combinations + - os: macos-latest + node: 18 + fail-fast: false # Continue other jobs even if one fails + max-parallel: 3 # Limit concurrent jobs +``` + +## Workflow Design + +### 1. Job Dependencies + +**Best Practice:** +```yaml +jobs: + lint: + runs-on: ubuntu-latest + steps: + - run: npm run lint + + test: + runs-on: ubuntu-latest + steps: + - run: npm test + + build: + needs: [lint, test] # Wait for both + runs-on: ubuntu-latest + steps: + - run: npm run build + + deploy: + needs: build + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - run: ./deploy.sh +``` + +### 2. Conditional Execution + +**Best Practice:** +```yaml +# Job-level condition +jobs: + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + +# Step-level condition +steps: + - name: Deploy to staging + if: github.ref == 'refs/heads/develop' + run: ./deploy-staging.sh + + - name: Notify on failure + if: failure() + run: ./notify.sh +``` + +**Common Conditions:** +- `success()`: Previous steps succeeded +- `failure()`: Any previous step failed +- `always()`: Run regardless of status +- `cancelled()`: Workflow was cancelled + +### 3. Reusable Workflows + +**Caller Workflow:** +```yaml +# .github/workflows/ci.yml +jobs: + call-workflow: + uses: ./.github/workflows/reusable-build.yml + with: + environment: production + secrets: + token: ${{ secrets.DEPLOY_TOKEN }} +``` + +**Reusable Workflow:** +```yaml +# .github/workflows/reusable-build.yml +name: Reusable Build + +on: + workflow_call: + inputs: + environment: + required: true + type: string + node-version: + required: false + type: string + default: '20' + secrets: + token: + required: true + outputs: + build-id: + description: "Build identifier" + value: ${{ jobs.build.outputs.id }} + +jobs: + build: + runs-on: ubuntu-latest + outputs: + id: ${{ steps.build.outputs.id }} + steps: + - name: Build + id: build + run: echo "id=build-${{ github.sha }}" >> $GITHUB_OUTPUT +``` + +## Action Selection and Versioning + +### 1. Prefer Official GitHub Actions + +**Priority Order:** +1. Official GitHub actions (`actions/*`) +2. Official organization actions (`docker/*`, `aws-actions/*`) +3. Verified creators +4. Community actions (with careful review) + +### 2. Version Pinning Strategy + +**Recommended Approach:** +```yaml +# Format: @ # +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +``` + +**Finding SHAs:** +```bash +# Get SHA for specific tag +git ls-remote https://github.com/actions/checkout v4.1.1 +``` + +### 3. Regular Updates + +**Process:** +1. Monitor action releases and security advisories +2. Update SHAs with new versions +3. Test in PR before merging +4. Document version changes in commit message + +**Automated Updates:** +Use Dependabot for automatic action updates: +```yaml +# .github/dependabot.yml +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +``` + +## Error Handling + +### 1. Timeouts + +**Best Practice:** +```yaml +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 # Prevent hung jobs + steps: + - name: Run tests + timeout-minutes: 15 # Step-level timeout + run: npm test +``` + +### 2. Failure Handling + +**Best Practice:** +```yaml +jobs: + test: + steps: + - name: Run tests + id: tests + continue-on-error: true + run: npm test + + - name: Upload test results + if: always() + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: test-results + path: test-results/ + + - name: Check test results + if: steps.tests.outcome == 'failure' + run: exit 1 +``` + +### 3. Cleanup Steps + +**Best Practice:** +```yaml +steps: + - name: Start test environment + run: docker-compose up -d + + - name: Run tests + run: npm test + + - name: Cleanup + if: always() + run: docker-compose down +``` + +## Maintainability + +### 1. Naming Conventions + +**Best Practice:** +```yaml +# Workflow file: lowercase with hyphens +# File: .github/workflows/ci-pipeline.yml + +name: CI Pipeline # Descriptive workflow name + +jobs: + test-node: # Descriptive job ID + name: Test on Node ${{ matrix.version }} # Human-readable job name + steps: + - name: Install dependencies # Action-oriented step name + run: npm ci +``` + +### 2. Documentation + +**Best Practice:** +```yaml +# CI Pipeline +# +# This workflow runs on every push and pull request to validate code quality. +# It performs linting, testing, and builds the application. +# +# Required secrets: +# - CODECOV_TOKEN: For uploading coverage reports +# +# Required permissions: +# - contents: read +# - checks: write + +name: CI Pipeline +``` + +### 3. Environment Variables + +**Best Practice:** +```yaml +# Top-level environment variables +env: + NODE_VERSION: '20' + CACHE_VERSION: 'v1' + +jobs: + build: + env: + BUILD_ENV: production + steps: + - name: Build + env: + API_URL: ${{ secrets.API_URL }} + run: npm run build +``` + +## Common Patterns + +### 1. Multi-Environment Deployment + +```yaml +jobs: + deploy-staging: + if: github.ref == 'refs/heads/develop' + environment: + name: staging + url: https://staging.example.com + steps: + - name: Deploy to staging + run: ./deploy.sh staging + + deploy-production: + if: github.ref == 'refs/heads/main' + environment: + name: production + url: https://example.com + steps: + - name: Deploy to production + run: ./deploy.sh production +``` + +### 2. Manual Approval + +```yaml +jobs: + deploy: + environment: + name: production + # Requires manual approval from configured reviewers + steps: + - name: Deploy + run: ./deploy.sh +``` + +### 3. Artifact Sharing Between Jobs + +```yaml +jobs: + build: + steps: + - name: Build application + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: build-${{ github.sha }} + path: dist/ + retention-days: 7 + + test: + needs: build + steps: + - name: Download build artifacts + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: build-${{ github.sha }} + path: dist/ + + - name: Test build + run: npm run test:integration +``` + +### 4. Dynamic Matrix from JSON + +```yaml +jobs: + setup: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Set matrix + id: set-matrix + run: | + echo 'matrix={"version":["18","20","22"]}' >> $GITHUB_OUTPUT + + test: + needs: setup + strategy: + matrix: ${{ fromJSON(needs.setup.outputs.matrix) }} +``` + +## Anti-Patterns to Avoid + +### 1. Storing Secrets in Code + +```yaml +# ❌ NEVER DO THIS +env: + API_KEY: "hardcoded-secret-123" + PASSWORD: ${{ github.event.inputs.password }} +``` + +### 2. Using Deprecated Actions + +```yaml +# ❌ BAD: Deprecated actions +- uses: actions/setup-node@v1 # Use v6 instead (Node 24 runtime) +- uses: actions/cache@v1 # Use v5.0.3+ instead (Node 24 runtime, required as of Feb 2025) +``` + +### 3. Overly Broad Permissions + +```yaml +# ❌ BAD: Unnecessary permissions +permissions: write-all + +# ✅ GOOD: Minimal permissions +permissions: + contents: read + pull-requests: write +``` + +### 4. Long-Running Jobs Without Timeout + +```yaml +# ❌ BAD: No timeout +jobs: + build: + runs-on: ubuntu-latest + # Could run forever, consuming minutes + +# ✅ GOOD: With timeout +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 +``` + +### 5. Hardcoded Values + +```yaml +# ❌ BAD: Hardcoded +- name: Deploy + run: kubectl set image deployment/myapp myapp=myapp:1.0.0 + +# ✅ GOOD: Using variables +- name: Deploy + env: + IMAGE_TAG: ${{ github.sha }} + run: kubectl set image deployment/myapp myapp=myapp:$IMAGE_TAG +``` + +### 6. Unnecessary Checkouts + +```yaml +# ❌ BAD: Checkout when not needed +jobs: + notify: + steps: + - uses: actions/checkout@v4 # Not needed for notification + - run: ./notify.sh + +# ✅ GOOD: Only checkout when needed +jobs: + notify: + steps: + - run: curl -X POST ${{ secrets.WEBHOOK_URL }} +``` + +## Summary + +**Key Takeaways:** +1. Security first: Pin actions, use minimal permissions, protect secrets +2. Optimize performance: Cache dependencies, use concurrency controls +3. Design for maintainability: Clear naming, documentation, reusable components +4. Handle errors gracefully: Timeouts, cleanup, notifications +5. Follow conventions: Standard naming, proper versioning, community practices + +Always validate workflows with the github-actions-validator skill before deploying. diff --git a/.agents/skills/github-actions-generator/references/common-actions.md b/.agents/skills/github-actions-generator/references/common-actions.md new file mode 100644 index 0000000..16d9cfd --- /dev/null +++ b/.agents/skills/github-actions-generator/references/common-actions.md @@ -0,0 +1,715 @@ +# Common GitHub Actions Reference + +**Last Updated:** February 2026 +**Source:** Official GitHub Actions repositories and Context7 verified documentation + +This document catalogs frequently used GitHub Actions with current versions, inputs, outputs, and usage examples. + +**Important Notes for 2026:** +- All actions should be pinned to full 40-character SHA for security +- Node 24 runtime is now supported (Node 20 EOL: April 2026, default switch: March 4, 2026) +- actions/cache v5.0.3 recommended (Node 24 runtime, runner v2.327.1+) +- Cache size limits: 10 GB free per repository, additional storage available (as of February 2026) + +## Table of Contents +1. [Repository and Checkout](#repository-and-checkout) +2. [Language and Tool Setup](#language-and-tool-setup) +3. [Caching](#caching) +4. [Artifacts](#artifacts) +5. [Docker](#docker) +6. [Cloud Providers](#cloud-providers) +7. [Testing and Code Quality](#testing-and-code-quality) +8. [Notifications](#notifications) +9. [Release and Publishing](#release-and-publishing) +10. [Security](#security) + +## Repository and Checkout + +### actions/checkout + +**Latest Version:** v6 (v6.0.2) +**SHA:** `de0fac2e4500dabe0009e67214ff5f5447ce83dd` +**Minimum Runner:** v2.327.1+ + +**Description:** Checkout repository code with improved performance, tag handling, and security + +**Common Inputs:** +- `fetch-depth`: Number of commits to fetch (default: 1, use 0 for full history) +- `ref`: Branch, tag, or SHA to checkout +- `token`: PAT for private repos (default: `${{ github.token }}`) +- `submodules`: Whether to checkout submodules (`false`, `true`, `recursive`) +- `lfs`: Whether to download Git LFS files (default: `false`) +- `sparse-checkout`: Paths to checkout (cone mode or individual files) - **Available in v5+** +- `sparse-checkout-cone-mode`: Use cone mode for sparse checkout (default: `true`) + +**Required Permissions:** +```yaml +permissions: + contents: read +``` + +**Examples:** + +**Basic checkout:** +```yaml +- name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 +``` + +**Full history (for changelog/tags):** +```yaml +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 +``` + +**Sparse checkout (specific directories):** +```yaml +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .github + src + tests +``` + +**Checkout PR HEAD commit:** +```yaml +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.head.sha }} +``` + +**Checkout private repository:** +```yaml +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: my-org/my-private-repo + token: ${{ secrets.GH_PAT }} + path: my-repo +``` + +## Language and Tool Setup + +### actions/setup-node + +**Latest Version:** v6 (v6.2.0) +**SHA:** `6044e13b5dc448c55e2357c09f80417699197238` +**Minimum Runner:** v2.328.0+ (for Node 24 support) + +**Description:** Setup Node.js environment with Node 24 support + +**Important:** Node 24 runtime is now supported. Node 20 deprecation timeline: Default switch March 4, 2026 → EOL April 2026 → Complete removal Summer 2026. + +**Common Inputs:** +- `node-version`: Version to use (e.g., `'24'`, `'20'`, `'18.x'`, `'lts/*'`) +- `cache`: Package manager to cache (`'npm'`, `'yarn'`, `'pnpm'`) +- `cache-dependency-path`: Path to lock file(s) +- `registry-url`: NPM registry URL for publishing +- `always-auth`: Set always-auth in npmrc (default: `false`) + +**Examples:** + +**Basic setup with caching:** +```yaml +- name: Setup Node.js 24 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + cache: 'npm' +``` + +**Multi-lock-file caching:** +```yaml +- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + cache: 'npm' + cache-dependency-path: | + package-lock.json + packages/*/package-lock.json +``` + +**Setup for package publishing:** +```yaml +- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + +- run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} +``` + +### actions/setup-python + +**Latest Version:** v6 (v6.2.0) +**SHA:** `a309ff8b426b58ec0e2a45f0f869d46889d02405` + +**Description:** Setup Python environment + +**Common Inputs:** +- `python-version`: Version to use (e.g., `'3.11'`, `'3.x'`) +- `cache`: Package manager to cache (`'pip'`, `'pipenv'`, `'poetry'`) +- `cache-dependency-path`: Path to requirements file + +**Example:** +```yaml +- name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: 'requirements*.txt' +``` + +### actions/setup-java + +**Latest Version:** v4 (v4.0.0) +**SHA:** `387ac29b308b003ca37ba93a6cab5eb57c8f5f93` + +**Description:** Setup Java environment + +**Common Inputs:** +- `distribution`: Java distribution (`'temurin'`, `'zulu'`, `'adopt'`, etc.) +- `java-version`: Version to use (e.g., `'17'`, `'11'`) +- `cache`: Build tool to cache (`'maven'`, `'gradle'`, `'sbt'`) + +**Example:** +```yaml +- name: Setup Java + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 + with: + distribution: 'temurin' + java-version: '17' + cache: 'maven' +``` + +### actions/setup-go + +**Latest Version:** v5 (v5.0.0) +**SHA:** `0c52d547c9bc32b1aa3301fd7a9cb496313a4491` + +**Description:** Setup Go environment + +**Common Inputs:** +- `go-version`: Version to use (e.g., `'1.21'`, `'stable'`) +- `cache`: Whether to cache dependencies (default: `true`) +- `cache-dependency-path`: Path to go.sum + +**Example:** +```yaml +- name: Setup Go + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: '1.21' + cache-dependency-path: go.sum +``` + +## Caching + +### actions/cache + +**Latest Version:** v5 (v5.0.3) +**SHA:** `cdf6c1fa76f9f475f3d7449005a359c84ca0f306` + +**Description:** Cache dependencies and build outputs with Node 24 runtime support + +**Important:** actions/cache v5 requires GitHub Actions runner v2.327.1 or later. Repositories get 10 GB free cache storage, with additional storage available. + +**Required Inputs:** +- `path`: Directories to cache +- `key`: Cache key (must be unique) + +**Optional Inputs:** +- `restore-keys`: Fallback keys if exact key not found + +**Example:** +```yaml +- name: Cache dependencies + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: | + ~/.npm + ~/.cache + node_modules + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- +``` + +## Artifacts + +### actions/upload-artifact + +**Latest Version:** v4 (v4.3.1) +**SHA:** `5d5d22a31266ced268874388b861e4b58bb5c2f3` + +**Description:** Upload build artifacts + +**Required Inputs:** +- `name`: Artifact name +- `path`: Files to upload + +**Optional Inputs:** +- `retention-days`: How long to keep artifact (1-90, default: 90) +- `if-no-files-found`: What to do if no files found (`warn`, `error`, `ignore`) + +**Example:** +```yaml +- name: Upload build artifacts + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: build-${{ github.sha }} + path: dist/ + retention-days: 7 + if-no-files-found: error +``` + +### actions/download-artifact + +**Latest Version:** v4 (v4.1.4) +**SHA:** `c850b930e6ba138125429b7e5c93fc707a7f8427` + +**Description:** Download artifacts from previous jobs + +**Optional Inputs:** +- `name`: Artifact name (downloads all if not specified) +- `path`: Destination path + +**Example:** +```yaml +- name: Download build artifacts + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + with: + name: build-${{ github.sha }} + path: dist/ +``` + +## Docker + +### docker/setup-buildx-action + +**Latest Version:** v3 (v3.3.0) +**SHA:** `d70bba72b1f3fd22344832f00baa16ece964efeb` + +**Description:** Setup Docker Buildx for advanced builds + +**Example:** +```yaml +- name: Set up Docker Buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 +``` + +### docker/login-action + +**Latest Version:** v3 (v3.1.0) +**SHA:** `e92390c5fb421da1463c202d546fed0ec5c39f20` + +**Description:** Login to Docker registry + +**Common Inputs:** +- `registry`: Registry to login to (default: Docker Hub) +- `username`: Username +- `password`: Password or token + +**Example:** +```yaml +# Docker Hub +- name: Login to Docker Hub + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + +# GitHub Container Registry +- name: Login to GHCR + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} +``` + +### docker/build-push-action + +**Latest Version:** v5 (v5.3.0) +**SHA:** `2cdde995de11925a030ce8070c3d77a52ffcf1c0` + +**Description:** Build and push Docker images + +**Common Inputs:** +- `context`: Build context path +- `file`: Dockerfile path +- `push`: Whether to push image (default: `false`) +- `tags`: Image tags +- `platforms`: Target platforms (e.g., `linux/amd64,linux/arm64`) +- `cache-from`: Cache sources +- `cache-to`: Cache destinations +- `build-args`: Build arguments +- `secrets`: Build secrets + +**Example:** +```yaml +- name: Build and push Docker image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + user/app:latest + user/app:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ github.sha }} + BUILD_DATE=${{ github.event.head_commit.timestamp }} +``` + +## Cloud Providers + +### aws-actions/configure-aws-credentials + +**Latest Version:** v4 (v4.0.2) +**SHA:** `e3dd6a429d7300a6a4c196c26e071d42e0343502` + +**Description:** Configure AWS credentials for GitHub Actions + +**Common Inputs:** +- `aws-access-key-id`: AWS access key ID +- `aws-secret-access-key`: AWS secret access key +- `aws-region`: AWS region +- `role-to-assume`: IAM role ARN for OIDC +- `role-session-name`: Session name + +**Example (with secrets):** +```yaml +- name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 +``` + +**Example (with OIDC - preferred):** +```yaml +- name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + with: + role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole + role-session-name: GitHubActions-${{ github.run_id }} + aws-region: us-east-1 +``` + +### azure/login + +**Latest Version:** v2 (v2.0.0) +**SHA:** `6c251865b4e6290e7b78be643ea2d005a6c79ee5` + +**Description:** Login to Azure + +**Common Inputs:** +- `creds`: Azure credentials JSON +- `client-id`: Service principal client ID (for OIDC) +- `tenant-id`: Azure tenant ID (for OIDC) +- `subscription-id`: Azure subscription ID (for OIDC) + +**Example:** +```yaml +- name: Azure Login + uses: azure/login@6c251865b4e6290e7b78be643ea2d005a6c79ee5 # v2.0.0 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} +``` + +## Testing and Code Quality + +### codecov/codecov-action + +**Latest Version:** v4 (v4.0.1) +**SHA:** `e0b68c6749509c5f83f984dd99a76a1c1a231044` + +**Description:** Upload code coverage to Codecov + +**Common Inputs:** +- `token`: Codecov token +- `files`: Coverage files to upload +- `fail_ci_if_error`: Fail CI if upload fails + +**Example:** +```yaml +- name: Upload coverage to Codecov + uses: codecov/codecov-action@e0b68c6749509c5f83f984dd99a76a1c1a231044 # v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/lcov.info + fail_ci_if_error: true +``` + +### github/super-linter + +**Latest Version:** v5 (v5.7.2) +**SHA:** `45fc0d88288beee4701c62761281edfee85655d7` + +**Description:** Run multiple linters in one action + +**Common Inputs:** +- `validate_all_codebase`: Lint entire codebase or just changes +- `default_branch`: Default branch name +- `disable_errors`: Don't fail on errors + +**Example:** +```yaml +- name: Lint code + uses: github/super-linter@45fc0d88288beee4701c62761281edfee85655d7 # v5.7.2 + env: + VALIDATE_ALL_CODEBASE: false + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +## Notifications + +### slackapi/slack-github-action + +**Latest Version:** v1 (v1.25.0) +**SHA:** `6c661ce58804a1a20f6dc5fbee7f0381b469e001` + +**Description:** Send Slack notifications + +**Common Inputs:** +- `webhook-url`: Slack webhook URL +- `payload`: JSON payload to send + +**Example:** +```yaml +- name: Notify Slack + uses: slackapi/slack-github-action@6c661ce58804a1a20f6dc5fbee7f0381b469e001 # v1.25.0 + with: + webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} + payload: | + { + "text": "Build completed: ${{ job.status }}", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Status:* ${{ job.status }}\n*Branch:* ${{ github.ref }}" + } + } + ] + } +``` + +## Release and Publishing + +### actions/create-release + +**Note:** Deprecated. Use `gh release create` or `softprops/action-gh-release` instead. + +### softprops/action-gh-release + +**Latest Version:** v2 (v2.0.2) +**SHA:** `9d7c94cfd0a1f3ed45544c887983e9fa900f0564` + +**Description:** Create GitHub releases + +**Common Inputs:** +- `tag_name`: Release tag (default: from tag trigger) +- `name`: Release name +- `body`: Release description +- `draft`: Create as draft +- `prerelease`: Mark as prerelease +- `files`: Files to upload + +**Example:** +```yaml +- name: Create Release + uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 # v2.0.2 + with: + tag_name: ${{ github.ref }} + name: Release ${{ github.ref_name }} + body_path: CHANGELOG.md + draft: false + prerelease: false + files: | + dist/*.zip + dist/*.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +### actions/github-script + +**Latest Version:** v7 (v7.0.1) +**SHA:** `60a0d83039c74a4aee543508d2ffcb1c3799cdea` + +**Description:** Run JavaScript with GitHub API access + +**Common Inputs:** +- `script`: JavaScript code to execute +- `github-token`: GitHub token (default: `${{ github.token }}`) + +**Example:** +```yaml +- name: Create comment + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '👋 Thanks for reporting!' + }) +``` + +## Security + +### actions/dependency-review-action + +**Latest Version:** v4 (v4.8.3) +**SHA:** `05fe4576374b728f0c523d6a13d64c25081e0803` +**Description:** Scans pull requests for vulnerable dependency versions + +**Required Permissions:** +```yaml +permissions: + contents: read +``` + +**Common Inputs:** +- `fail-on-severity`: Severity level to fail on (`low`, `moderate`, `high`, `critical`) +- `allow-licenses`: Comma-separated list of allowed licenses +- `deny-licenses`: Comma-separated list of denied licenses + +**Example:** +```yaml +- name: Dependency Review + uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3 + with: + fail-on-severity: critical + allow-licenses: MIT, Apache-2.0, BSD-3-Clause +``` + +### actions/attest-sbom + +**Latest Version:** v2 (v2.4.0) +**SHA:** `bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b` +**Description:** Generate SBOM attestations for artifacts + +**Required Permissions:** +```yaml +permissions: + id-token: write + contents: read + attestations: write + packages: write # For container registry +``` + +**Example:** +```yaml +- name: Generate SBOM attestation + uses: actions/attest-sbom@bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b # v2.4.0 + with: + subject-name: ${{ env.REGISTRY }}/myapp + subject-digest: sha256:${{ steps.build.outputs.digest }} + sbom-path: sbom.json + push-to-registry: true +``` + +### actions/attest-build-provenance + +**Latest Version:** v2 (v2.4.0) +**SHA:** `e8998f949152b193b063cb0ec769d69d929409be` +**Description:** Generate build provenance attestations for build artifacts + +**Required Permissions:** +```yaml +permissions: + id-token: write + contents: read + attestations: write + packages: write # For container registry +``` + +**Example:** +```yaml +- name: Generate provenance attestation + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 + with: + subject-name: ${{ env.REGISTRY }}/myapp + subject-digest: sha256:${{ steps.build.outputs.digest }} + push-to-registry: true +``` + +### github/codeql-action + +**Latest Version:** v3 (v3.32.5) +**SHA:** `ae9ef3a1d2e3413523c3741725c30064970cc0d4` +**Description:** GitHub CodeQL scanning actions (`init`, `analyze`, `upload-sarif`) + +**Required Permissions:** +```yaml +permissions: + contents: read + security-events: write +``` + +**Example:** +```yaml +- name: Initialize CodeQL + uses: github/codeql-action/init@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # v3.32.5 + with: + languages: javascript + +- name: Upload SARIF + if: always() + uses: github/codeql-action/upload-sarif@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # v3.32.5 + with: + sarif_file: trivy-results.sarif +``` + +## Best Practices Summary (Updated November 2025) + +1. **Always pin to full SHA**: Use 40-character SHA with version comment +2. **Node 24 migration**: Migrate to Node 24 before March 2026 (Node 20 EOL April 2026) +3. **Cache v4.3.0**: Use latest cache version (v4.2.0+ required, legacy service retired Feb 2025) +4. **Use official actions**: Prefer verified `actions/*`, `docker/*`, etc. +5. **Security scanning**: Implement dependency review and SBOM attestations +6. **Minimal permissions**: Use explicit `permissions:` blocks +7. **Keep up to date**: Monitor releases and security advisories +8. **Document versions**: Add comments explaining version choices + +## Finding Action Documentation + +**Search Pattern:** +``` +"[owner/repo] [version] github action documentation" +``` + +**Example:** +``` +"docker/build-push-action v5 github documentation" +"actions/checkout v5 sparse-checkout" +``` + +**Official Sources:** +- GitHub Marketplace: https://github.com/marketplace +- Action repository: https://github.com/[owner]/[repo] +- Release notes: https://github.com/[owner]/[repo]/releases +- Context7: Use for structured documentation lookup + +**Version Verification:** +- Check releases page for latest version +- Find SHA from tags: `git ls-remote https://github.com/[owner]/[repo] [tag]` +- Verify minimum runner requirements + +Always verify action inputs and outputs from official documentation before use. diff --git a/.agents/skills/github-actions-generator/references/custom-actions.md b/.agents/skills/github-actions-generator/references/custom-actions.md new file mode 100644 index 0000000..ef30680 --- /dev/null +++ b/.agents/skills/github-actions-generator/references/custom-actions.md @@ -0,0 +1,320 @@ +# Custom GitHub Actions Guide + +**Last Updated:** December 2025 + +This guide covers creating custom GitHub Actions: composite, Docker, and JavaScript actions with proper metadata, directory structure, and versioning. + +## Table of Contents +1. [Action Types](#action-types) +2. [Action Metadata](#action-metadata) +3. [Directory Structure](#directory-structure) +4. [Versioning](#versioning) +5. [Publishing to Marketplace](#publishing-to-marketplace) + +--- + +## Action Types + +| Type | Runtime | Use Case | Performance | +|------|---------|----------|-------------| +| Composite | Shell/Actions | Combine multiple steps | Fast startup | +| Docker | Container | Custom environment/tools | Slower startup | +| JavaScript | Node.js | API interactions, complex logic | Fastest | + +--- + +## Action Metadata + +### Branding + +Add branding to make your action visually distinctive in GitHub Marketplace: + +```yaml +name: 'Setup Node.js with Cache' +description: 'Setup Node.js with automatic dependency caching' +author: 'Your Name or Organization' + +branding: + icon: 'package' # Feather icon name + color: 'blue' # Available: white, yellow, blue, green, orange, red, purple, gray-dark + +inputs: + node-version: + description: 'Node.js version to use' + required: true +``` + +**Available Icons:** See [Feather Icons](https://feathericons.com/) - e.g., `package`, `box`, `server`, `code`, `git-branch`, `shield`, `check-circle` + +**Best Practices:** +- Choose icons that represent the action's purpose +- Use consistent branding across related actions +- Branding is required for GitHub Marketplace publishing + +--- + +## Directory Structure + +### Local Repository Actions + +Use `.github/actions/` for actions within the same repository: + +``` +repository-root/ +├── .github/ +│ ├── actions/ # Local custom actions +│ │ ├── setup-node-cached/ # Composite action +│ │ │ ├── action.yml +│ │ │ └── README.md +│ │ ├── terraform-validator/ # Docker action +│ │ │ ├── action.yml +│ │ │ ├── Dockerfile +│ │ │ ├── entrypoint.sh +│ │ │ └── README.md +│ │ └── label-pr/ # JavaScript action +│ │ ├── action.yml +│ │ ├── dist/ +│ │ │ └── index.js # Compiled/bundled JS +│ │ ├── src/ +│ │ │ └── index.ts # Source TypeScript +│ │ ├── package.json +│ │ └── README.md +│ └── workflows/ +│ └── ci.yml +``` + +**Usage in Workflows:** +```yaml +steps: + # Local action (same repository) + - uses: ./.github/actions/setup-node-cached + with: + node-version: '20' + + # Action from another repository + - uses: owner/repo/.github/actions/action-name@v1 +``` + +### Standalone Action Repositories + +For actions intended for GitHub Marketplace or cross-repo reuse: + +``` +action-repository-root/ +├── action.yml # Action definition (MUST be in root) +├── README.md # Usage documentation +├── LICENSE # License file +├── CHANGELOG.md # Version history +├── dist/ # Compiled code (JS actions) +│ └── index.js +├── src/ # Source code (JS actions) +│ └── index.ts +└── Dockerfile # For Docker actions +``` + +**Best Practices:** +- Use `.github/actions/` for repository-local actions +- Create separate repos for reusable/Marketplace actions +- Always include README.md with usage examples +- For JS actions, commit compiled `dist/` (don't gitignore) + +--- + +## Versioning + +### Semantic Versioning + +Use MAJOR.MINOR.PATCH format: +- **MAJOR:** Breaking changes +- **MINOR:** New features (backward compatible) +- **PATCH:** Bug fixes + +### Git Tags + +```bash +# Create version tag +git tag -a v1.0.0 -m "Release version 1.0.0" +git push origin v1.0.0 + +# Update major version tag (v1 → latest v1.x.x) +git tag -fa v1 -m "Update v1 to v1.0.0" +git push origin v1 --force +``` + +### Tag Strategy + +Maintain multiple tag levels: +- **Specific:** `v1.0.0`, `v1.0.1`, `v1.1.0` +- **Major:** `v1`, `v2` (points to latest minor/patch) + +**User options:** +```yaml +- uses: owner/action@v1.0.0 # Pinned to exact version +- uses: owner/action@v1 # Latest v1.x.x +- uses: owner/action@abc123 # Pinned to SHA (most secure) +``` + +### Release Workflow + +```yaml +# .github/workflows/release.yml +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Create GitHub Release + run: gh release create ${{ github.ref_name }} --generate-notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Update major version tag + run: | + MAJOR=$(echo ${{ github.ref_name }} | cut -d. -f1) + git tag -fa $MAJOR -m "Update $MAJOR tag" + git push origin $MAJOR --force +``` + +### Breaking Changes + +When introducing breaking changes: +1. Document in CHANGELOG.md and release notes +2. Increment MAJOR version +3. Provide migration guide +4. Consider maintaining previous major version branch + +--- + +## Publishing to Marketplace + +### Requirements + +1. **Repository must be public** +2. **action.yml in repository root** +3. **Branding metadata** (icon and color) +4. **README.md** with usage examples +5. **Semantic version tags** + +### Marketplace Listing + +1. Go to repository Settings → Actions → "Create Action" +2. Or visit: `https://github.com/marketplace/actions/your-action` +3. Fill in marketplace details +4. Submit for review + +### Pre-release Testing + +Use pre-release versions for testing: +```bash +git tag -a v1.0.0-beta.1 -m "Beta release" +git push origin v1.0.0-beta.1 +``` + +--- + +## Action Templates + +### Composite Action Template + +```yaml +name: '[Action Name]' +description: '[Brief description]' +author: '[Author]' + +branding: + icon: 'check-circle' + color: 'green' + +inputs: + input-name: + description: '[Description]' + required: true + default: '[default value]' + +outputs: + output-name: + description: '[Description]' + value: ${{ steps.step-id.outputs.value }} + +runs: + using: 'composite' + steps: + - name: Step name + id: step-id + shell: bash + run: echo "value=result" >> $GITHUB_OUTPUT +``` + +### Docker Action Template + +```yaml +name: '[Action Name]' +description: '[Brief description]' +author: '[Author]' + +branding: + icon: 'box' + color: 'blue' + +inputs: + input-name: + description: '[Description]' + required: true + +outputs: + output-name: + description: '[Description]' + +runs: + using: 'docker' + image: 'Dockerfile' + args: + - ${{ inputs.input-name }} +``` + +### JavaScript Action Template + +```yaml +name: '[Action Name]' +description: '[Brief description]' +author: '[Author]' + +branding: + icon: 'code' + color: 'purple' + +inputs: + github-token: + description: 'GitHub token for API access' + required: true + +outputs: + result: + description: 'Action result' + +runs: + using: 'node20' + main: 'dist/index.js' +``` + +--- + +## Summary + +| Aspect | Recommendation | +|--------|---------------| +| Location | `.github/actions/` for local, separate repo for shared | +| Branding | Required for Marketplace, recommended for all | +| Versioning | Semantic versions with major tag updates | +| Documentation | README.md with examples, CHANGELOG.md | +| Security | Pin to SHA, minimal permissions | \ No newline at end of file diff --git a/.agents/skills/github-actions-generator/references/expressions-and-contexts.md b/.agents/skills/github-actions-generator/references/expressions-and-contexts.md new file mode 100644 index 0000000..bb14460 --- /dev/null +++ b/.agents/skills/github-actions-generator/references/expressions-and-contexts.md @@ -0,0 +1,688 @@ +# GitHub Actions Expressions and Contexts + +**Last Updated:** November 2025 +**Source:** Official GitHub Actions documentation and Context7 verified examples + +## Table of Contents +1. [Expression Syntax](#expression-syntax) +2. [Contexts](#contexts) +3. [Functions](#functions) +4. [Operators](#operators) +5. [Common Patterns](#common-patterns) +6. [Debugging and Troubleshooting](#debugging-and-troubleshooting) + +## Expression Syntax + +GitHub Actions expressions use `${{ }}` syntax to evaluate values dynamically. + +**Basic Usage:** +```yaml +- name: Print environment + run: echo "Running on ${{ runner.os }}" + +- name: Conditional step + if: ${{ github.ref == 'refs/heads/main' }} + run: echo "On main branch" + +# Note: 'if' doesn't require ${{ }}, it's implicit +- name: Conditional step (preferred) + if: github.ref == 'refs/heads/main' + run: echo "On main branch" +``` + +**Where Expressions Can Be Used:** +- `if` conditionals (implicit `${{ }}`, can omit) +- `env` values (must use `${{ }}`) +- `with` inputs (must use `${{ }}`) +- Step `name` values (must use `${{ }}`) +- Job `outputs` (must use `${{ }}`) +- `environment.name` (can use expressions for dynamic environments) + +**Important Notes:** +- Expressions are interpolated **before** the job is sent to the runner +- Use environment variables via `env` context for safer variable handling +- Avoid direct interpolation of untrusted input (use `env` context instead) + +## Contexts + +Contexts are objects containing information about workflow runs, variables, environments, and more. + +### github context + +Contains information about the workflow run and triggering event. + +**Common Properties:** +```yaml +# Event information +${{ github.event_name }} # Event that triggered workflow (push, pull_request, etc.) +${{ github.event.action }} # Action that triggered event (opened, synchronize, etc.) + +# Repository information +${{ github.repository }} # owner/repo +${{ github.repository_owner }} # Repository owner +${{ github.ref }} # Full ref (refs/heads/main, refs/tags/v1.0.0) +${{ github.ref_name }} # Short ref (main, v1.0.0) +${{ github.sha }} # Commit SHA that triggered workflow + +# Actor information +${{ github.actor }} # Username that triggered workflow +${{ github.triggering_actor }} # User that initiated the workflow run + +# Workflow information +${{ github.workflow }} # Workflow name +${{ github.run_id }} # Unique workflow run ID +${{ github.run_number }} # Workflow run number +${{ github.job }} # Current job ID + +# Pull request information (when event is pull_request) +${{ github.event.pull_request.number }} +${{ github.event.pull_request.title }} +${{ github.event.pull_request.head.ref }} # Source branch +${{ github.event.pull_request.base.ref }} # Target branch +${{ github.event.pull_request.head.sha }} + +# Push information (when event is push) +${{ github.event.head_commit.message }} +${{ github.event.head_commit.author.name }} +``` + +**Examples:** +```yaml +# Build image tagged with commit SHA +- name: Build Docker image + run: docker build -t myapp:${{ github.sha }} . + +# Deploy only on main branch +- name: Deploy + if: github.ref == 'refs/heads/main' + run: ./deploy.sh + +# Different behavior for PR vs push +- name: Set environment + run: | + if [ "${{ github.event_name }}" == "pull_request" ]; then + echo "ENV=preview" >> $GITHUB_ENV + else + echo "ENV=production" >> $GITHUB_ENV + fi +``` + +### env context + +Access environment variables defined in workflow, job, or step. + +```yaml +env: + NODE_VERSION: '20' + BUILD_TYPE: 'production' + +jobs: + build: + env: + API_URL: 'https://api.example.com' + steps: + - name: Print variables + run: | + echo "Node: ${{ env.NODE_VERSION }}" + echo "API: ${{ env.API_URL }}" +``` + +### runner context + +Information about the runner executing the job. + +**Common Properties:** +```yaml +${{ runner.os }} # OS (Linux, Windows, macOS) +${{ runner.arch }} # Architecture (X64, ARM64) +${{ runner.name }} # Runner name +${{ runner.temp }} # Temp directory path +${{ runner.tool_cache }} # Tool cache directory path +``` + +**Examples:** +```yaml +# OS-specific commands +- name: Install dependencies + run: | + if [ "${{ runner.os }}" == "Linux" ]; then + sudo apt-get update + elif [ "${{ runner.os }}" == "macOS" ]; then + brew update + fi + +# Cache key with OS +- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: ~/.cache + key: ${{ runner.os }}-cache-${{ hashFiles('**/lock.file') }} +``` + +### secrets context + +Access encrypted secrets defined in repository or organization settings. + +```yaml +- name: Deploy to production + env: + API_KEY: ${{ secrets.API_KEY }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + run: ./deploy.sh +``` + +**Security Notes:** +- Secrets are automatically masked in logs +- Use `echo "::add-mask::$VALUE"` to mask additional values +- Pass secrets via environment variables, not command arguments + +### matrix context + +Access matrix configuration values when using matrix strategy. + +```yaml +strategy: + matrix: + os: [ubuntu-latest, windows-latest] + node: [18, 20, 22] + include: + - os: ubuntu-latest + node: 20 + experimental: true + +steps: + - name: Setup Node.js ${{ matrix.node }} + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ matrix.node }} + + - name: Mark experimental + if: matrix.experimental + run: echo "Experimental build" +``` + +### steps context + +Access information about steps that have already run. + +```yaml +steps: + - name: Run tests + id: tests + run: npm test + + - name: Upload results + if: steps.tests.outcome == 'success' + run: ./upload-results.sh + + - name: Use step output + run: echo "Test result: ${{ steps.tests.outputs.result }}" +``` + +**Step Properties:** +- `steps..outputs.`: Output value +- `steps..outcome`: Result before `continue-on-error` (`success`, `failure`, `cancelled`, `skipped`) +- `steps..conclusion`: Final result after `continue-on-error` + +### job context + +Access information about currently running job. + +```yaml +jobs: + build: + outputs: + build-id: ${{ steps.build.outputs.id }} + steps: + - name: Build + id: build + run: echo "id=build-123" >> $GITHUB_OUTPUT + + deploy: + needs: build + steps: + - name: Deploy build + run: ./deploy.sh ${{ needs.build.outputs.build-id }} +``` + +### inputs context + +Access workflow or reusable workflow inputs. + +```yaml +on: + workflow_dispatch: + inputs: + environment: + description: 'Environment to deploy' + required: true + type: choice + options: [dev, staging, production] + debug: + description: 'Enable debug mode' + required: false + type: boolean + default: false + +jobs: + deploy: + steps: + - name: Deploy to ${{ inputs.environment }} + run: ./deploy.sh ${{ inputs.environment }} + + - name: Enable debug + if: inputs.debug + run: echo "Debug mode enabled" +``` + +## Functions + +### String Functions + +**contains()** +```yaml +# Check if string contains substring +if: contains(github.ref, 'refs/tags/') +if: contains(github.event.head_commit.message, '[skip ci]') +if: contains(fromJSON('["main", "develop"]'), github.ref_name) +``` + +**startsWith()** +```yaml +# Check if string starts with prefix +if: startsWith(github.ref, 'refs/tags/v') +if: startsWith(github.event.pull_request.title, 'feat:') +``` + +**endsWith()** +```yaml +# Check if string ends with suffix +if: endsWith(github.ref, '/main') +if: endsWith(github.event.pull_request.head.ref, '-hotfix') +``` + +**format()** +```yaml +# Format string with placeholders +- name: Print message + run: echo "${{ format('Building {0} on {1}', github.ref_name, runner.os) }}" +``` + +### Type Conversion Functions + +**toJSON()** +```yaml +# Convert object to JSON string +- name: Print context + run: echo '${{ toJSON(github) }}' + +- name: Print matrix + run: echo '${{ toJSON(matrix) }}' +``` + +**fromJSON()** +```yaml +# Parse JSON string to object +strategy: + matrix: + config: ${{ fromJSON('{"versions":[18,20,22]}') }} + +# Use with dynamic matrix +jobs: + setup: + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - id: set-matrix + run: echo 'matrix={"version":["18","20"]}' >> $GITHUB_OUTPUT + + build: + needs: setup + strategy: + matrix: ${{ fromJSON(needs.setup.outputs.matrix) }} +``` + +### Hash Functions + +**hashFiles()** +```yaml +# Generate hash of file contents (for cache keys) +- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} + +# Multiple patterns +key: ${{ hashFiles('**/*.go', '**/go.sum') }} +``` + +### Status Check Functions + +**success()** +```yaml +# Run only if all previous steps succeeded +- name: Deploy + if: success() + run: ./deploy.sh +``` + +**failure()** +```yaml +# Run only if any previous step failed +- name: Notify on failure + if: failure() + run: ./notify-failure.sh +``` + +**always()** +```yaml +# Run regardless of previous step status +- name: Cleanup + if: always() + run: ./cleanup.sh +``` + +**cancelled()** +```yaml +# Run if workflow was cancelled +- name: Cleanup on cancel + if: cancelled() + run: ./cancel-cleanup.sh +``` + +## Operators + +### Comparison Operators + +```yaml +# Equality +if: github.ref == 'refs/heads/main' +if: runner.os != 'Windows' + +# Logical +if: github.event_name == 'push' && github.ref == 'refs/heads/main' +if: github.event_name == 'pull_request' || github.event_name == 'push' +if: "!(github.event_name == 'pull_request')" + +# Comparison +if: github.event.pull_request.changed_files < 10 +if: matrix.node-version >= 20 +``` + +### Operator Precedence + +1. `()` +2. `!` +3. `<`, `<=`, `>`, `>=` +4. `==`, `!=` +5. `&&` +6. `||` + +## Common Patterns + +### Branch-Based Conditions + +```yaml +# Main branch only +if: github.ref == 'refs/heads/main' + +# Any branch except main +if: github.ref != 'refs/heads/main' + +# Specific branches +if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' + +# Tag pushes only +if: startsWith(github.ref, 'refs/tags/') + +# Version tags only (v1.0.0 format) +if: startsWith(github.ref, 'refs/tags/v') +``` + +### Event-Based Conditions + +```yaml +# Push event only +if: github.event_name == 'push' + +# PR opened or synchronized +if: | + github.event_name == 'pull_request' && + (github.event.action == 'opened' || github.event.action == 'synchronize') + +# Manual dispatch only +if: github.event_name == 'workflow_dispatch' + +# Scheduled run only +if: github.event_name == 'schedule' +``` + +### Step Status Patterns + +```yaml +# Run if specific step succeeded +if: steps.tests.outcome == 'success' + +# Run if step failed but continue +if: steps.tests.outcome == 'failure' + +# Run cleanup always +if: always() + +# Run only on failure +if: failure() +``` + +### Matrix Patterns + +```yaml +# Specific matrix combination +if: matrix.os == 'ubuntu-latest' && matrix.node == 20 + +# Exclude certain combinations +strategy: + matrix: + os: [ubuntu, windows, macos] + node: [18, 20, 22] + exclude: + - os: windows + node: 18 +``` + +### Dynamic Values + +```yaml +# Build tags with multiple values +tags: | + myapp:latest + myapp:${{ github.sha }} + myapp:${{ github.ref_name }} + +# Conditional environment +environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} + +# Dynamic timeout +timeout-minutes: ${{ github.event_name == 'schedule' && 120 || 30 }} +``` + +### Combining Contexts + +```yaml +# Artifact name with context values +- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: build-${{ runner.os }}-${{ github.sha }} + path: dist/ + +# Cache key with multiple factors +- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: ~/.cache + key: ${{ runner.os }}-${{ hashFiles('**/*.lock') }}-${{ github.ref_name }} +``` + +### Safe String Interpolation + +```yaml +# ❌ UNSAFE: Direct interpolation of user input +- run: echo "Title: ${{ github.event.pull_request.title }}" + +# ✅ SAFE: Use environment variables +- name: Print PR title + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: echo "Title: $PR_TITLE" +``` + +### JSON Manipulation + +```yaml +# Create dynamic matrix +- id: set-matrix + run: | + if [ "${{ github.event_name }}" == "push" ]; then + echo 'matrix={"os":["ubuntu","windows","macos"]}' >> $GITHUB_OUTPUT + else + echo 'matrix={"os":["ubuntu"]}' >> $GITHUB_OUTPUT + fi + +# Use matrix +strategy: + matrix: ${{ fromJSON(steps.set-matrix.outputs.matrix) }} +``` + +## Debugging and Troubleshooting + +### Dumping Contexts to Logs + +The most effective way to debug workflow issues is to dump context information to logs: + +**Dump All Contexts:** +```yaml +name: Context Debugging +on: push + +jobs: + dump_contexts: + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + + - name: Dump job context + env: + JOB_CONTEXT: ${{ toJson(job) }} + run: echo "$JOB_CONTEXT" + + - name: Dump steps context + env: + STEPS_CONTEXT: ${{ toJson(steps) }} + run: echo "$STEPS_CONTEXT" + + - name: Dump runner context + env: + RUNNER_CONTEXT: ${{ toJson(runner) }} + run: echo "$RUNNER_CONTEXT" + + - name: Dump strategy context + env: + STRATEGY_CONTEXT: ${{ toJson(strategy) }} + run: echo "$STRATEGY_CONTEXT" + + - name: Dump matrix context + env: + MATRIX_CONTEXT: ${{ toJson(matrix) }} + run: echo "$MATRIX_CONTEXT" +``` + +**Example Runner Context Output:** +```json +{ + "os": "Linux", + "arch": "X64", + "name": "GitHub Actions 2", + "tool_cache": "/opt/hostedtoolcache", + "temp": "/home/runner/work/_temp" +} +``` + +### Safe Variable Interpolation + +**Best Practice - Use env context:** +```yaml +# ✅ SAFE: Interpolate before runner execution +- name: Greet user + env: + GREETING: ${{ env.Greeting }} + FIRST_NAME: ${{ env.First_Name }} + DAY: ${{ env.DAY_OF_WEEK }} + run: echo "$GREETING $FIRST_NAME. Today is $DAY!" +``` + +This approach ensures variables are resolved by GitHub Actions before execution, providing consistent behavior. + +## Tips and Best Practices + +1. **Implicit vs Explicit `${{ }}`:** + - `if` conditions don't need `${{ }}` (implicit) + - Other contexts require explicit `${{ }}` + +2. **String Comparisons:** + - Always use quotes for string literals + - Case-sensitive by default + +3. **Boolean Values:** + - Use `true` and `false` without quotes + - Empty strings evaluate to `false` + +4. **Default Values:** + ```yaml + # Use || for default values + environment: ${{ inputs.environment || 'dev' }} + ``` + +5. **Multi-line Expressions:** + ```yaml + if: | + github.event_name == 'push' && + github.ref == 'refs/heads/main' && + !contains(github.event.head_commit.message, '[skip ci]') + ``` + +6. **Security Best Practices:** + - Always use `env` context for untrusted input + - Never directly interpolate user-controlled values + - Use `::add-mask::` for sensitive values + +7. **Dynamic Environment Names:** + ```yaml + environment: + name: ${{ github.ref_name }} # Dynamic based on branch + ``` + +8. **Debugging Expressions:** + ```yaml + # Print entire context with pretty formatting + - run: echo '${{ toJson(github) }}' + + # Print specific values + - run: | + echo "Event: ${{ github.event_name }}" + echo "Ref: ${{ github.ref }}" + echo "SHA: ${{ github.sha }}" + echo "Actor: ${{ github.actor }}" + ``` + +## Summary + +- Use `${{ }}` for dynamic values +- Access contexts like `github`, `env`, `secrets`, `matrix`, `runner`, etc. +- Use functions for string manipulation, hashing, and type conversion (`toJSON`, `hashFiles`, `contains`, etc.) +- Combine operators for complex conditions +- **Always validate and sanitize user inputs for security** +- Use `env` context for untrusted input instead of direct interpolation +- Debug with `toJSON()` to dump context information +- Expressions are evaluated before job execution on the runner + +**Security Warning:** Never directly interpolate untrusted input (PR titles, issue bodies, user input) in `run` commands. Always use environment variables via the `env` context. diff --git a/.agents/skills/github-actions-generator/references/modern-features.md b/.agents/skills/github-actions-generator/references/modern-features.md new file mode 100644 index 0000000..081a5e9 --- /dev/null +++ b/.agents/skills/github-actions-generator/references/modern-features.md @@ -0,0 +1,421 @@ +# Modern GitHub Actions Features + +**Last Updated:** December 2025 + +This guide covers modern GitHub Actions capabilities for enhanced workflow output, deployment control, and containerized builds. + +## Table of Contents +1. [Job Summaries](#job-summaries) +2. [Deployment Environments](#deployment-environments) +3. [Container Jobs](#container-jobs) +4. [Workflow Annotations](#workflow-annotations) +5. [Integration Examples](#integration-examples) + +--- + +## Job Summaries + +Create rich markdown summaries in the Actions UI using `$GITHUB_STEP_SUMMARY`. + +### When to Use +- Display test results, coverage reports, benchmarks +- Show deployment status and URLs +- Present security scan findings +- Summarize workflow execution + +### Basic Usage + +```yaml +- name: Generate summary + run: | + echo "## Build Results :rocket:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Tests | ${{ steps.test.outputs.passed }} passed |" >> $GITHUB_STEP_SUMMARY + echo "| Coverage | ${{ steps.test.outputs.coverage }}% |" >> $GITHUB_STEP_SUMMARY +``` + +### Advanced Patterns + +**Test Results Table:** +```yaml +- name: Test summary + if: always() + run: | + echo "## Test Results :test_tube:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Suite | Status | Duration |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|----------|" >> $GITHUB_STEP_SUMMARY + echo "| Unit Tests | :white_check_mark: | 45s |" >> $GITHUB_STEP_SUMMARY + echo "| Integration | :white_check_mark: | 2m 30s |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Deployment URLs" >> $GITHUB_STEP_SUMMARY + echo "- [Staging](https://staging.example.com)" >> $GITHUB_STEP_SUMMARY + echo "- [Production](https://example.com)" >> $GITHUB_STEP_SUMMARY +``` + +**Collapsible Details:** +```yaml +- name: Detailed summary + run: | + echo "## Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "Click to expand test details" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat test-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY +``` + +### Best Practices +- Use `if: always()` to show summaries even on failure +- Include emojis for visual scanning +- Use markdown tables for structured data +- Add links to deployed environments +- Clear summary at start if needed: `> $GITHUB_STEP_SUMMARY` + +--- + +## Deployment Environments + +Use GitHub environments with protection rules, approval gates, and environment-specific secrets. + +### When to Use +- Multi-stage deployments (dev, staging, production) +- Deployments requiring manual approval +- Environment-specific configuration +- Deployment tracking and rollback + +### Basic Usage + +```yaml +jobs: + deploy-staging: + runs-on: ubuntu-latest + environment: + name: staging + url: https://staging.example.com + steps: + - name: Deploy + run: ./deploy.sh staging + + deploy-production: + needs: deploy-staging + runs-on: ubuntu-latest + environment: + name: production + url: https://example.com + steps: + - name: Deploy + run: ./deploy.sh production +``` + +### Protection Rules + +Configure in repository Settings → Environments: + +| Rule | Description | +|------|-------------| +| Required reviewers | Manual approval before deployment | +| Wait timer | Delay deployment by N minutes | +| Deployment branches | Restrict which branches can deploy | +| Environment secrets | Secrets available only in this environment | + +### Multi-Environment Pattern + +```yaml +name: Deploy + +on: + push: + branches: [main, develop] + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: ${{ github.ref_name == 'main' && 'production' || 'staging' }} + url: ${{ github.ref_name == 'main' && 'https://example.com' || 'https://staging.example.com' }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Deploy to ${{ github.ref_name == 'main' && 'production' || 'staging' }} + env: + API_KEY: ${{ secrets.API_KEY }} # Environment-specific secret + run: ./deploy.sh +``` + +### Best Practices +- Set environment URLs for easy access +- Use required reviewers for production +- Configure branch policies +- Leverage environment-specific secrets +- Use `needs` to enforce deployment order + +--- + +## Container Jobs + +Run jobs inside Docker containers for consistent, isolated build environments. + +### When to Use +- Require specific OS/tool versions +- Need isolated build environment +- Want to match local dev environment +- Building for specific Linux distributions + +### Basic Container Job + +```yaml +jobs: + build: + runs-on: ubuntu-latest + container: + image: node:20-alpine + env: + NODE_ENV: production + options: --cpus 2 --memory 4g + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - run: npm ci + - run: npm run build +``` + +### With Service Containers + +```yaml +jobs: + test: + runs-on: ubuntu-latest + container: + image: node:20 + + services: + postgres: + image: postgres:15 + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + redis: + image: redis:7 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run tests + env: + DATABASE_URL: postgres://postgres:postgres@postgres:5432/test + REDIS_URL: redis://redis:6379 + run: npm test +``` + +### Best Practices +- Use specific image tags (avoid `latest`) +- Configure health checks for services +- Set resource limits with `options` +- Use volumes for persistent data +- Prefer official images + +--- + +## Workflow Annotations + +Create annotations (notices, warnings, errors) in the Actions UI and PR files. + +### Annotation Commands + +| Command | Level | Appearance | +|---------|-------|------------| +| `::notice::` | Info | Blue | +| `::warning::` | Warning | Yellow | +| `::error::` | Error | Red (doesn't fail step) | + +### Basic Usage + +```yaml +- name: Validate + run: | + # Simple annotations + echo "::notice::Build completed successfully" + echo "::warning::Deprecated API usage detected" + echo "::error::Configuration issue found" +``` + +### File/Line Annotations + +Annotations with file location appear in PR Files tab: + +```yaml +- name: Lint results + run: | + # With file and line info + echo "::error file=src/app.js,line=10,col=5::Type mismatch detected" + echo "::warning file=config.js,line=23::Deprecated option used" + echo "::notice file=utils.js,line=100,endLine=105::Consider refactoring" +``` + +### Log Groups + +Collapse verbose output: + +```yaml +- name: Build with groups + run: | + echo "::group::Installing dependencies" + npm ci + echo "::endgroup::" + + echo "::group::Running tests" + npm test + echo "::endgroup::" +``` + +### Masking Secrets + +```yaml +- name: Process secret + run: | + SENSITIVE="$(./get-secret.sh)" + echo "::add-mask::$SENSITIVE" + echo "Using secret safely" +``` + +### All Workflow Commands + +| Command | Purpose | +|---------|---------| +| `::notice::` | Info annotation | +| `::warning::` | Warning annotation | +| `::error::` | Error annotation | +| `::group::` | Start collapsed section | +| `::endgroup::` | End collapsed section | +| `::add-mask::` | Mask value in logs | +| `::stop-commands::TOKEN` | Disable command processing | +| `::TOKEN::` | Re-enable commands | +| `::debug::` | Debug message (requires debug logging) | + +--- + +## Integration Examples + +### Complete CI/CD with Modern Features + +```yaml +name: Full-Featured CI/CD + +on: + push: + branches: [main, develop] + pull_request: + +permissions: + contents: read + deployments: write + +jobs: + test: + runs-on: ubuntu-latest + container: + image: node:20-alpine + + services: + postgres: + image: postgres:15 + env: + POSTGRES_PASSWORD: postgres + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run tests + id: test + env: + DATABASE_URL: postgres://postgres:postgres@postgres:5432/test + run: | + npm ci + npm test -- --coverage + echo "coverage=85" >> $GITHUB_OUTPUT + + - name: Coverage check + run: | + COVERAGE=${{ steps.test.outputs.coverage }} + if [ $COVERAGE -lt 80 ]; then + echo "::warning::Coverage $COVERAGE% below 80% threshold" + else + echo "::notice::Coverage $COVERAGE% meets threshold" + fi + + - name: Test summary + if: always() + run: | + echo "## Test Results :test_tube:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Coverage | ${{ steps.test.outputs.coverage }}% |" >> $GITHUB_STEP_SUMMARY + echo "| Status | :white_check_mark: Passed |" >> $GITHUB_STEP_SUMMARY + + deploy-staging: + needs: test + if: github.ref == 'refs/heads/develop' + runs-on: ubuntu-latest + environment: + name: staging + url: https://staging.example.com + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Deploy + run: ./deploy.sh staging + + - name: Deployment summary + run: | + echo "## Deployment :rocket:" >> $GITHUB_STEP_SUMMARY + echo "- **Environment**: Staging" >> $GITHUB_STEP_SUMMARY + echo "- **URL**: https://staging.example.com" >> $GITHUB_STEP_SUMMARY + echo "- **Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + + deploy-production: + needs: test + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: + name: production + url: https://example.com + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Deploy + run: ./deploy.sh production + + - name: Deployment summary + run: | + echo "## Production Deployment :rocket:" >> $GITHUB_STEP_SUMMARY + echo "- **Environment**: Production" >> $GITHUB_STEP_SUMMARY + echo "- **URL**: https://example.com" >> $GITHUB_STEP_SUMMARY + echo "- **Note**: Required manual approval" >> $GITHUB_STEP_SUMMARY +``` + +--- + +## Summary + +| Feature | Use Case | Key Benefits | +|---------|----------|--------------| +| Job Summaries | Rich output display | Markdown support, persists in UI | +| Environments | Deployment control | Approvals, secrets, tracking | +| Container Jobs | Consistent builds | Isolation, reproducibility | +| Annotations | Inline feedback | PR integration, visual alerts | \ No newline at end of file diff --git a/.agents/skills/github-actions-generator/scripts/test_generator.sh b/.agents/skills/github-actions-generator/scripts/test_generator.sh new file mode 100644 index 0000000..8d4b167 --- /dev/null +++ b/.agents/skills/github-actions-generator/scripts/test_generator.sh @@ -0,0 +1,344 @@ +#!/usr/bin/env bash +# +# Regression test suite for github-actions-generator +# +# Tests: +# 1. YAML syntax validity — all templates and examples parse without errors +# 2. SHA pinning compliance — no unpinned @vN action refs in positive examples +# 3. EOF newlines — all YAML files end with a newline +# 4. SHA consistency — templates/examples use the canonical SHAs from +# references/common-actions.md +# 5. Required workflow keys — example workflows contain mandatory top-level keys +# 6. Template placeholders — workflow/docker templates keep safe placeholders +# +# Prerequisites: yamllint must be installed (pip install yamllint) +# +# Exit 0 when all assertions pass; non-zero on any failure. +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +PASS=0 +FAIL=0 + +# ─── helpers ──────────────────────────────────────────────────────────────── + +pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } +fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } + +assert_true() { + local label="$1" + shift + if "$@" >/dev/null 2>&1; then + pass "$label" + else + fail "$label" + fi +} + +assert_false() { + local label="$1" + shift + if ! "$@" >/dev/null 2>&1; then + pass "$label" + else + fail "$label" + fi +} + +assert_file_contains() { + local label="$1" + local file="$2" + local pattern="$3" + if grep -qE -- "$pattern" "$file"; then + pass "$label" + else + fail "$label — pattern '$pattern' not found in $file" + fi +} + +assert_file_not_contains() { + local label="$1" + local file="$2" + local pattern="$3" + if ! grep -qE -- "$pattern" "$file"; then + pass "$label" + else + fail "$label — unexpected pattern '$pattern' found in $file" + grep -nE -- "$pattern" "$file" | sed 's/^/ /' || true + fi +} + +assert_file_ends_with_newline() { + local label="$1" + local file="$2" + if [ -z "$(tail -c1 "$file")" ]; then + pass "$label" + else + fail "$label — $file is missing a trailing newline" + fi +} + +assert_text_matches_pattern() { + local label="$1" + local text="$2" + local pattern="$3" + if echo "$text" | grep -qE "$pattern"; then + pass "$label" + else + fail "$label — text did not match pattern '$pattern': $text" + fi +} + +assert_text_not_matches_pattern() { + local label="$1" + local text="$2" + local pattern="$3" + if ! echo "$text" | grep -qE "$pattern"; then + pass "$label" + else + fail "$label — text unexpectedly matched pattern '$pattern': $text" + fi +} + +# ─── discovery ────────────────────────────────────────────────────────────── + +# Collect all YAML files under templates and examples +TEMPLATE_FILES=() +while IFS= read -r f; do + TEMPLATE_FILES+=("$f") +done < <(find "$SKILL_DIR/assets/templates" -name "*.yml" -type f) + +EXAMPLE_FILES=() +while IFS= read -r f; do + EXAMPLE_FILES+=("$f") +done < <(find "$SKILL_DIR/examples" -name "*.yml" -type f) + +ALL_YAML_FILES=("${TEMPLATE_FILES[@]}" "${EXAMPLE_FILES[@]}") + +echo "Discovered ${#TEMPLATE_FILES[@]} template file(s) and ${#EXAMPLE_FILES[@]} example file(s)." +echo "" + +# ─── canonical SHAs (sourced from references/common-actions.md) ───────────── + +# These are the authoritative SHAs the generator must use. +# If common-actions.md is updated, update these constants to match. +readonly SHA_CHECKOUT="de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2 +readonly SHA_SETUP_NODE="6044e13b5dc448c55e2357c09f80417699197238" # v6.2.0 +readonly SHA_CACHE="cdf6c1fa76f9f475f3d7449005a359c84ca0f306" # v5.0.3 +readonly SHA_UPLOAD_ARTIFACT="5d5d22a31266ced268874388b861e4b58bb5c2f3" # v4.3.1 +readonly SHA_DOWNLOAD_ARTIFACT="c850b930e6ba138125429b7e5c93fc707a7f8427" # v4.1.4 +readonly SHA_GITHUB_SCRIPT="60a0d83039c74a4aee543508d2ffcb1c3799cdea" # v7.0.1 +readonly SHA_DEPENDENCY_REVIEW="05fe4576374b728f0c523d6a13d64c25081e0803" # v4.8.3 +readonly SHA_ATTEST_SBOM="bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b" # v2.4.0 +readonly SHA_ATTEST_BUILD_PROVENANCE="e8998f949152b193b063cb0ec769d69d929409be" # v2.4.0 +readonly SHA_CODEQL_ACTION="ae9ef3a1d2e3413523c3741725c30064970cc0d4" # v3.32.5 + +# ─── 1. YAML syntax validity ───────────────────────────────────────────────── + +echo "[1] YAML syntax validity" + +if ! command -v yamllint >/dev/null 2>&1; then + fail "yamllint not installed — skipping YAML syntax checks" +else + for f in "${ALL_YAML_FILES[@]}"; do + rel="${f#"$SKILL_DIR/"}" + errors=$(yamllint -d relaxed "$f" 2>&1 | grep "error" | grep -v "line-length" || true) + if [ -z "$errors" ]; then + pass "valid YAML: $rel" + else + fail "YAML error in: $rel" + echo "$errors" | sed 's/^/ /' + fi + done +fi +echo "" + +# ─── 2. SHA pinning compliance ─────────────────────────────────────────────── + +echo "[2] SHA pinning compliance (no bare @vN refs in positive examples)" + +# Pattern matches mutable tag refs in uses:, including nested action paths. +# Examples detected: +# uses: actions/checkout@v6 +# uses: github/codeql-action/upload-sarif@v3 +# uses: owner/repo/sub/path@v1.2.3 +# We check examples/templates only — reference .md files are excluded. +readonly UNPINNED_PATTERN='^[[:space:]]*uses:[[:space:]]*[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*@v[0-9]+(\.[0-9]+){0,2}([[:space:]]|$)' + +echo " [2a] Regex regression checks" +assert_text_matches_pattern \ + "matches top-level action major tag" \ + " uses: actions/dependency-review-action@v4" \ + "$UNPINNED_PATTERN" +assert_text_matches_pattern \ + "matches nested action path major tag" \ + " uses: github/codeql-action/upload-sarif@v3" \ + "$UNPINNED_PATTERN" +assert_text_matches_pattern \ + "matches nested action path semver tag" \ + " uses: owner/repo/sub-path@v3.2.1 # mutable" \ + "$UNPINNED_PATTERN" +assert_text_not_matches_pattern \ + "does not match full SHA pin" \ + " uses: github/codeql-action/upload-sarif@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # v3.32.5" \ + "$UNPINNED_PATTERN" + +for f in "${TEMPLATE_FILES[@]}" "${EXAMPLE_FILES[@]}"; do + rel="${f#"$SKILL_DIR/"}" + unpinned=$(grep -nE "$UNPINNED_PATTERN" "$f" || true) + if [ -z "$unpinned" ]; then + pass "no unpinned refs: $rel" + else + fail "unpinned action ref(s) in: $rel" + echo "$unpinned" | sed 's/^/ /' + fi +done +echo "" + +# ─── 3. EOF newlines ───────────────────────────────────────────────────────── + +echo "[3] EOF newlines (all YAML files must end with a newline)" + +for f in "${ALL_YAML_FILES[@]}"; do + rel="${f#"$SKILL_DIR/"}" + assert_file_ends_with_newline "EOF newline: $rel" "$f" +done +echo "" + +# ─── 4. SHA consistency ────────────────────────────────────────────────────── + +echo "[4] SHA consistency (templates and examples match canonical SHAs)" + +# Check that wherever a well-known action is used, it uses the canonical SHA. +# This catches version drift between common-actions.md and the rest of the files. + +check_sha_consistency() { + local action="$1" + local expected_sha="$2" + local file="$3" + local rel="${file#"$SKILL_DIR/"}" + + # Extract all @ references for this action + local wrong + wrong=$(grep -nE "uses: ${action}@" "$file" 2>/dev/null \ + | grep -v "$expected_sha" \ + | grep -v "# .*BAD\|# .*UNSAFE\|# .*AVOID\|#.*-.*uses:" \ + || true) + + if [ -z "$wrong" ]; then + pass "SHA consistent for ${action}: $rel" + else + fail "Wrong SHA for ${action} in: $rel" + echo "$wrong" | sed 's/^/ /' + echo " Expected SHA: $expected_sha" + fi +} + +for f in "${TEMPLATE_FILES[@]}" "${EXAMPLE_FILES[@]}"; do + if grep -qE "uses: actions/checkout@" "$f" 2>/dev/null; then + check_sha_consistency "actions/checkout" "$SHA_CHECKOUT" "$f" + fi + if grep -qE "uses: actions/setup-node@" "$f" 2>/dev/null; then + check_sha_consistency "actions/setup-node" "$SHA_SETUP_NODE" "$f" + fi + if grep -qE "uses: actions/cache@" "$f" 2>/dev/null; then + check_sha_consistency "actions/cache" "$SHA_CACHE" "$f" + fi + if grep -qE "uses: actions/upload-artifact@" "$f" 2>/dev/null; then + check_sha_consistency "actions/upload-artifact" "$SHA_UPLOAD_ARTIFACT" "$f" + fi + if grep -qE "uses: actions/download-artifact@" "$f" 2>/dev/null; then + check_sha_consistency "actions/download-artifact" "$SHA_DOWNLOAD_ARTIFACT" "$f" + fi + if grep -qE "uses: actions/github-script@" "$f" 2>/dev/null; then + check_sha_consistency "actions/github-script" "$SHA_GITHUB_SCRIPT" "$f" + fi + if grep -qE "uses: actions/dependency-review-action@" "$f" 2>/dev/null; then + check_sha_consistency "actions/dependency-review-action" "$SHA_DEPENDENCY_REVIEW" "$f" + fi + if grep -qE "uses: actions/attest-sbom@" "$f" 2>/dev/null; then + check_sha_consistency "actions/attest-sbom" "$SHA_ATTEST_SBOM" "$f" + fi + if grep -qE "uses: actions/attest-build-provenance@" "$f" 2>/dev/null; then + check_sha_consistency "actions/attest-build-provenance" "$SHA_ATTEST_BUILD_PROVENANCE" "$f" + fi + if grep -qE "uses: github/codeql-action/init@" "$f" 2>/dev/null; then + check_sha_consistency "github/codeql-action/init" "$SHA_CODEQL_ACTION" "$f" + fi + if grep -qE "uses: github/codeql-action/analyze@" "$f" 2>/dev/null; then + check_sha_consistency "github/codeql-action/analyze" "$SHA_CODEQL_ACTION" "$f" + fi + if grep -qE "uses: github/codeql-action/upload-sarif@" "$f" 2>/dev/null; then + check_sha_consistency "github/codeql-action/upload-sarif" "$SHA_CODEQL_ACTION" "$f" + fi +done +echo "" + +# ─── 5. Required workflow keys ─────────────────────────────────────────────── + +echo "[5] Required workflow keys (name, on, permissions, jobs)" + +WORKFLOW_EXAMPLES=() +while IFS= read -r f; do + WORKFLOW_EXAMPLES+=("$f") +done < <(find "$SKILL_DIR/examples/workflows" "$SKILL_DIR/assets/templates/workflow" \ + -name "*.yml" -type f) + +for f in "${WORKFLOW_EXAMPLES[@]}"; do + rel="${f#"$SKILL_DIR/"}" + assert_file_contains "has 'name:' key: $rel" "$f" "^name:" + assert_file_contains "has 'on:' trigger: $rel" "$f" "^on:" + assert_file_contains "has 'permissions:' key: $rel" "$f" "permissions:" + assert_file_contains "has 'jobs:' key: $rel" "$f" "^jobs:" +done +echo "" + +# ─── 6. Template placeholder integrity ─────────────────────────────────────── + +echo "[6] Template placeholder integrity" + +BASIC_TEMPLATE="$SKILL_DIR/assets/templates/workflow/basic_workflow.yml" +DOCKER_TEMPLATE="$SKILL_DIR/assets/templates/action/docker/Dockerfile" + +# Verify the step name with brackets is quoted (prevents YAML parse error) +assert_file_contains \ + "basic_workflow.yml: bracketed step name is quoted" \ + "$BASIC_TEMPLATE" \ + 'name: "\[SETUP_STEP_NAME\]' + +# Verify the file has no raw YAML syntax errors (already covered in test 1, +# but surfaced here explicitly for the template that had the known bug) +if command -v yamllint >/dev/null 2>&1; then + errors=$(yamllint -d relaxed "$BASIC_TEMPLATE" 2>&1 | grep "error" | grep -v "line-length" || true) + if [ -z "$errors" ]; then + pass "basic_workflow.yml: no YAML syntax errors" + else + fail "basic_workflow.yml: YAML syntax errors found" + echo "$errors" | sed 's/^/ /' + fi +fi + +assert_file_contains \ + "docker action template: has package-install flags placeholder" \ + "$DOCKER_TEMPLATE" \ + '\[PACKAGE_INSTALL_FLAGS\]' + +assert_file_not_contains \ + "docker action template: no apt-only hardcoded install flags" \ + "$DOCKER_TEMPLATE" \ + '--no-install-recommends' + +echo "" + +# ─── summary ───────────────────────────────────────────────────────────────── + +echo "Results: $PASS passed, $FAIL failed" +echo "" + +if [[ $FAIL -gt 0 ]]; then + exit 1 +fi diff --git a/.agents/skills/install-anti-slop/SKILL.md b/.agents/skills/install-anti-slop/SKILL.md new file mode 100644 index 0000000..8c07e0b --- /dev/null +++ b/.agents/skills/install-anti-slop/SKILL.md @@ -0,0 +1,127 @@ +--- +name: install-anti-slop +description: Install, configure, update, or upgrade vendored anti-slop Oxlint plugins. Use when adding anti-slop, picking up upstream rules or fixes, or migrating an existing installation while preserving local customizations. +--- + +# Install or update anti-slop + +Anti-slop is vendored code: the target repository owns its rules, diagnostics, tests, and configuration. Preserve those choices when bringing in upstream changes. + +## Choose the path + +Read the repository's agent instructions and `git status`. Identify its package manager, Oxlint/Vite+ configuration, and any existing anti-slop entry points, including renamed or relocated copies referenced by `jsPlugins`. + +- **Existing installation — update, upgrade, migrate, or reconfigure:** read [Update a vendored installation](references/update.md) and follow that procedure instead of the fresh-install steps below. +- **No installation — fresh install:** follow the procedure below. If the user requested an update but no installation can be found, confirm the target before installing. + +Complete when the operation and target path are established and pre-existing work is identified. + +## Fresh install + +1. Copy the bundled plugin from this skill. Run from the target repository: + + ```bash + node /scripts/install.mjs + ``` + + This creates `tools/oxlint/anti-slop/`. Pass another relative destination as the first argument when the repository has an established tooling layout. The script refuses to replace an existing destination; route existing copies through the update procedure rather than `--force`. + + Preserve the nested `vendor/eslint-stylistic/LICENSE` and `UPSTREAM.md`; they travel with the copied rule. Readability enforcement is self-contained and requires no Stylistic plugin dependency. + + Complete when the files, including vendored license and provenance, exist at the agreed destination without replacing an existing copy. + +2. Install current compatible dependencies rather than trusting versions remembered by the agent: + - If the repository already depends on `oxlint`, read its installed version from the package manager or lockfile and install `@oxlint/plugins` at exactly that version. Pin it exactly rather than by range so future upgrades move both packages together. + - Only when the repository has no `oxlint` dependency, query `npm view oxlint version` and `npm view @oxlint/plugins version`, then install the same current version of both packages. + - `oxlint` is a development dependency. The copied source imports `@oxlint/plugins`, so install it as a development dependency for a local-only plugin. + - Do not replace the package manager or rewrite unrelated dependency ranges. + + Complete when matching compatible versions are installed and unrelated dependency ranges are preserved. + +3. Register the generic plugin, configure ignores, and enable all generic rules. For `oxlint.config.ts` or `.oxlintrc.json`, merge these fields with the existing configuration: + + ```ts + ignorePatterns: [ + ".agent/**", + ".agents/**", + ".claude/**", + ".codex/**", + ".continue/**", + ".cursor/**", + ".gemini/**", + ".opencode/**", + ".pi/**", + ".roo/**", + ".windsurf/**", + "tools/oxlint/anti-slop/**", + ], + jsPlugins: [ + { name: "anti-slop", specifier: "./tools/oxlint/anti-slop/index.ts" }, + ], + ``` + + Keep every existing ignore. Adjust the final pattern when the plugin was copied elsewhere. Inspect the repository for other project-local agent tooling directories and add them rather than linting installed skills, hooks, or generated agent configuration as application source. Do not broadly ignore all dot-directories, because some repositories keep owned source or checks in them. + + For Vite+, add these fields to `lint.ignorePatterns` and `lint.jsPlugins`. Also merge the same patterns into `fmt.ignorePatterns` so `vp check` does not reformat installed agent assets or the vendored plugin. Merge existing entries instead of replacing them. + + Enable these rules at `"error"`, including the native Oxlint companion rule: + + ```json + { + "oxc/no-accumulating-spread": "error", + "anti-slop/no-array-filter-map": "error", + "anti-slop/no-reduce-accumulator-copy": "error", + "anti-slop/no-chained-type-assertions": "error", + "anti-slop/no-conditional-empty-object-spread": "error", + "anti-slop/no-known-value-widening": "error", + "anti-slop/no-module-mocking": "error", + "anti-slop/no-object-parameters": "error", + "anti-slop/no-reflect-apply": "error", + "anti-slop/no-reflect-get": "error", + "anti-slop/no-runtime-typeof": "error", + "anti-slop/no-shape-in-symbol-names": "error", + "anti-slop/no-unknown-parameters": "error", + "anti-slop/no-unknown-returns": "error", + "anti-slop/no-unknown-type-aliases": "error", + "anti-slop/no-unsafe-dictionary-type": "error", + "anti-slop/no-widen-then-assert": "error", + "anti-slop/require-readable-spacing": "error", + "anti-slop/require-safety-comment-for-type-assertion": "error" + } + ``` + + For `no-array-filter-map`, prefer lazy `.values().filter(...).map(...).toArray()` pipelines only when the target runtime supports iterator helpers; otherwise use an appropriate single `flatMap` or locally mutating reducer. Review callback order, indexes, sparse arrays, `thisArg`, and filtering semantics rather than mechanically rewriting chains. Unknown receiver types are deliberately not inferred by this AST/scope rule. + + Pair `no-reduce-accumulator-copy` with native `oxc/no-accumulating-spread`: the custom rule catches supported non-spread copies such as `Object.assign({}, acc, item)`, `Array.from(acc)`, and array accumulator `concat`/`slice` calls. Mutating a fresh local accumulator is allowed; copying individual input items is also allowed. Named callbacks, indirect helpers, and nested accumulator properties are not fully analyzed, so do not claim all quadratic reducers are ruled out. + + If the repository declares `effect` in a package manifest, or the user explicitly requests Effect rules, also register the opt-in Effect plugin: + + ```ts + jsPlugins: [ + { + name: "anti-slop-effect", + specifier: "./tools/oxlint/anti-slop/effect/index.ts", + }, + ], + rules: { + "anti-slop-effect/no-manual-effect-error-tag": "error", + "anti-slop-effect/no-manual-tag-comparison": "error", + "anti-slop-effect/no-manual-tagged-construction": "error", + "anti-slop-effect/no-service-constructor-imports": "error", + "anti-slop-effect/prefer-effect-match": "error", + }, + ``` + + Merge these entries with the generic plugin configuration rather than replacing it. Do not enable the Effect plugin merely because Effect appears transitively in a lockfile; require a direct package-manifest dependency or an explicit user request. The rule covers relative project imports. Report package-alias imports as a current limitation rather than pretending they are enforced. + + Complete when the generic rules and eligible Effect rules are registered and existing configuration is preserved. + +4. Run the repository's lint command and typecheck. For Vite+, run the repository's full `vp check` command after adding both lint and format ignores. If findings appear in owned project source, report them and fix them only when the user asked for migration/cleanup. Do not suppress rules, weaken rule severity, add unsafe casts, or mechanically launder types to make lint pass. + + When cleanup is authorized, apply `require-readable-spacing` with lint autofix, then run the repository's formatter and lint again. Confirm a second fix/format pass leaves files unchanged. Keep whitespace fixes separate from semantic edits, preserve documentation attachment and overload groups, and do not enable an entire competing formatting preset. + + Complete when checks have run, fix/format stability has been verified for authorized cleanup, and every failure is resolved or reported with its diagnostics. + +5. Record provenance in `UPSTREAM.md` beside the vendored entry point: source repository, exact source commit or recoverable pristine snapshot when available, installed plugin paths, and intentional deviations. Verify that the revision identifies the actual copied assets; a package version or the current upstream HEAD alone is insufficient. If provenance cannot be established, record it as unknown rather than guessing. + + Review the final diff and report the installed path, source identity, dependency/configuration changes, and check results. Complete when the record and report describe the files actually installed and any remaining findings. diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/effect/index.ts b/.agents/skills/install-anti-slop/assets/anti-slop/effect/index.ts new file mode 100644 index 0000000..55eb13d --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/effect/index.ts @@ -0,0 +1,21 @@ +import { eslintCompatPlugin } from "@oxlint/plugins"; + +import { noManualEffectErrorTagRule } from "./rules/no-manual-effect-error-tag.ts"; +import { noManualTagComparisonRule } from "./rules/no-manual-tag-comparison.ts"; +import { noManualTaggedConstructionRule } from "./rules/no-manual-tagged-construction.ts"; +import { noServiceConstructorImportsRule } from "./rules/no-service-constructor-imports.ts"; +import { preferEffectMatchRule } from "./rules/prefer-effect-match.ts"; + +/** Opt-in Oxlint rules for Effect service and Layer architecture. */ +const antiSlopEffectPlugin = eslintCompatPlugin({ + meta: { name: "anti-slop-effect" }, + rules: { + "no-manual-effect-error-tag": noManualEffectErrorTagRule, + "no-manual-tag-comparison": noManualTagComparisonRule, + "no-manual-tagged-construction": noManualTaggedConstructionRule, + "no-service-constructor-imports": noServiceConstructorImportsRule, + "prefer-effect-match": preferEffectMatchRule, + }, +}); + +export default antiSlopEffectPlugin; diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-effect-error-tag.ts b/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-effect-error-tag.ts new file mode 100644 index 0000000..daaa109 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-effect-error-tag.ts @@ -0,0 +1,52 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + isInsideBroadEffectHandler, + isReasonTagMember, + isTagMember, + tagMemberFromComparison, +} from "../shared/tagged-values.ts"; + +export const noManualEffectErrorTagRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Use Effect tagged error handlers instead of manually branching on `_tag` in a catch handler.", + }, + messages: { + tag: "Use Effect.catchTag or Effect.catchTags instead of manually discriminating a tagged error in a broad Effect catch handler.", + reason: + "Use Effect.catchReason or Effect.catchReasons instead of manually discriminating a tagged `reason` in a broad Effect catch handler.", + }, + }, + createOnce(context) { + return { + BinaryExpression(node) { + const tagMember = tagMemberFromComparison(node); + if ( + tagMember === undefined || + !isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ + node, + messageId: isReasonTagMember(tagMember) ? "reason" : "tag", + }); + }, + SwitchStatement(node) { + if ( + !isTagMember(node.discriminant) || + !isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ + node, + messageId: isReasonTagMember(node.discriminant) ? "reason" : "tag", + }); + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tag-comparison.ts b/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tag-comparison.ts new file mode 100644 index 0000000..868d1d2 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tag-comparison.ts @@ -0,0 +1,45 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + isInsideBroadEffectHandler, + isTagMember, + tagMemberFromComparison, +} from "../shared/tagged-values.ts"; + +export const noManualTagComparisonRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Use Effect Match or Predicate helpers instead of manually branching on `_tag`.", + }, + messages: { + manualComparison: + "Use Match.tag/Match.tags for tagged-value branching, or Predicate.isTagged for a simple reusable predicate.", + manualSwitch: + "Use Match.value(value).pipe(Match.tag/Match.tags/Match.tagsExhaustive) or the tagged enum `$match` helper instead of switching on `_tag`.", + }, + }, + createOnce(context) { + return { + BinaryExpression(node) { + if ( + tagMemberFromComparison(node) === undefined || + isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ node, messageId: "manualComparison" }); + }, + SwitchStatement(node) { + if ( + !isTagMember(node.discriminant) || + isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ node, messageId: "manualSwitch" }); + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tagged-construction.ts b/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tagged-construction.ts new file mode 100644 index 0000000..382f166 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tagged-construction.ts @@ -0,0 +1,37 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + isMatchPatternObject, + isStringLiteral, + propertyName, +} from "../shared/tagged-values.ts"; + +export const noManualTaggedConstructionRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Construct tagged values with their existing Effect constructor instead of writing `_tag` manually.", + }, + messages: { + manualConstruction: + "Use the existing Schema tagged `.make`, tagged class/error constructor, or Data.taggedEnum variant constructor instead of writing a literal `_tag` object.", + }, + }, + createOnce(context) { + return { + ObjectExpression(node) { + if (isMatchPatternObject(node)) return; + const tag = node.properties.find( + (property) => + property.type === "Property" && + propertyName(property) === "_tag" && + isStringLiteral(property.value), + ); + if (tag !== undefined) { + context.report({ node: tag, messageId: "manualConstruction" }); + } + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-service-constructor-imports.ts b/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-service-constructor-imports.ts new file mode 100644 index 0000000..55cefb7 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/no-service-constructor-imports.ts @@ -0,0 +1,52 @@ +import { defineRule } from "@oxlint/plugins"; + +import type { ESTree } from "@oxlint/plugins"; + +const SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u; +const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u; + +function isProjectLocalImport(source: string): boolean { + return source.startsWith("./") || source.startsWith("../"); +} + +function getImportedName(specifier: ESTree.ImportSpecifier): string { + if (specifier.imported.type === "Identifier") return specifier.imported.name; + return specifier.imported.value; +} + +/** Keep dependency-bearing Effect service constructors local to their owning capability modules. */ +export const noServiceConstructorImportsRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow project-local make imports outside test and spec files.", + }, + messages: { + serviceConstructorImport: + 'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.', + }, + }, + create(context) { + const isTestFile = TEST_FILE.test(context.filename.replaceAll("\\", "/")); + + return { + ImportDeclaration(node) { + if (isTestFile || !isProjectLocalImport(node.source.value)) return; + + for (const specifier of node.specifiers) { + if (specifier.type !== "ImportSpecifier") continue; + + const importedName = getImportedName(specifier); + if (!SERVICE_CONSTRUCTOR_NAME.test(importedName)) continue; + + context.report({ + node: specifier, + messageId: "serviceConstructorImport", + data: { name: importedName }, + }); + } + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/prefer-effect-match.ts b/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/prefer-effect-match.ts new file mode 100644 index 0000000..d834540 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/effect/rules/prefer-effect-match.ts @@ -0,0 +1,54 @@ +import { defineRule, type ESTree } from "@oxlint/plugins"; + +const equalityOperators = new Set(["==", "===", "!=", "!=="]); + +export const preferEffectMatchRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Use Match from Effect for chained literal ternaries over the same value.", + }, + messages: { + preferMatch: + "Use Match from Effect instead of a chained literal ternary.", + }, + }, + createOnce(context) { + const isLiteral = (node: ESTree.Node): boolean => + node.type === "Literal" || + (node.type === "TemplateLiteral" && node.expressions.length === 0); + + const comparedValue = (node: ESTree.Expression): string | undefined => { + if ( + node.type !== "BinaryExpression" || + !equalityOperators.has(node.operator) + ) { + return undefined; + } + if (isLiteral(node.left)) return context.sourceCode.getText(node.right); + if (isLiteral(node.right)) return context.sourceCode.getText(node.left); + return undefined; + }; + + return { + ConditionalExpression(node) { + if (node.parent?.type === "ConditionalExpression") return; + const value = comparedValue(node.test); + if (value === undefined) return; + + let alternate = node.alternate; + let literalChecks = 1; + while (alternate.type === "ConditionalExpression") { + if (comparedValue(alternate.test) !== value) return; + literalChecks += 1; + alternate = alternate.alternate; + } + + if (literalChecks > 1) { + context.report({ node, messageId: "preferMatch" }); + } + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/effect/shared/tagged-values.ts b/.agents/skills/install-anti-slop/assets/anti-slop/effect/shared/tagged-values.ts new file mode 100644 index 0000000..21d10a2 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/effect/shared/tagged-values.ts @@ -0,0 +1,97 @@ +import type { ESTree } from "@oxlint/plugins"; + +const equalityOperators = new Set(["==", "===", "!=", "!=="]); +const broadEffectCatchMethods = new Set(["catch", "catchAll", "catchIf"]); + +export const isStringLiteral = ( + node: ESTree.Node | null | undefined, +): node is ESTree.StringLiteral => + node?.type === "Literal" && typeof node.value === "string"; + +export const isTagMember = ( + node: ESTree.Node | null | undefined, +): node is ESTree.MemberExpression => + node?.type === "MemberExpression" && + ((!node.computed && + node.property.type === "Identifier" && + node.property.name === "_tag") || + (node.computed && + isStringLiteral(node.property) && + node.property.value === "_tag")); + +export const tagMemberFromComparison = ( + node: ESTree.BinaryExpression, +): ESTree.MemberExpression | undefined => { + if (!equalityOperators.has(node.operator)) return undefined; + if (isTagMember(node.left) && isStringLiteral(node.right)) return node.left; + if (isTagMember(node.right) && isStringLiteral(node.left)) return node.right; + return undefined; +}; + +const isBroadEffectCatchCall = ( + node: ESTree.Node | null | undefined, +): node is ESTree.CallExpression => + node?.type === "CallExpression" && + node.callee.type === "MemberExpression" && + node.callee.object.type === "Identifier" && + node.callee.object.name === "Effect" && + !node.callee.computed && + node.callee.property.type === "Identifier" && + broadEffectCatchMethods.has(node.callee.property.name); + +export const isInsideBroadEffectHandler = (node: ESTree.Node): boolean => { + let current: ESTree.Node | null | undefined = node.parent; + while (current !== null && current !== undefined) { + if ( + current.type === "ArrowFunctionExpression" || + current.type === "FunctionExpression" + ) { + return ( + isBroadEffectCatchCall(current.parent) && + current.parent.arguments.includes(current) + ); + } + current = current.parent; + } + return false; +}; + +export const isReasonTagMember = (node: ESTree.MemberExpression): boolean => + node.object.type === "MemberExpression" && + ((!node.object.computed && + node.object.property.type === "Identifier" && + node.object.property.name === "reason") || + (node.object.computed && + isStringLiteral(node.object.property) && + node.object.property.value === "reason")); + +export const propertyName = ( + property: ESTree.ObjectProperty, +): string | undefined => { + if (!property.computed && property.key.type === "Identifier") { + return property.key.name; + } + if ( + property.key.type === "Literal" && + typeof property.key.value === "string" + ) { + return property.key.value; + } + return undefined; +}; + +export const isMatchPatternObject = (node: ESTree.ObjectExpression): boolean => { + const call = node.parent; + if (call?.type !== "CallExpression" || !call.arguments.includes(node)) { + return false; + } + const callee = call.callee; + return ( + callee.type === "MemberExpression" && + callee.object.type === "Identifier" && + callee.object.name === "Match" && + !callee.computed && + callee.property.type === "Identifier" && + (callee.property.name === "when" || callee.property.name === "not") + ); +}; diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/index.ts b/.agents/skills/install-anti-slop/assets/anti-slop/index.ts new file mode 100644 index 0000000..a3876ec --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/index.ts @@ -0,0 +1,47 @@ +import { eslintCompatPlugin } from "@oxlint/plugins"; + +import { noArrayFilterMapRule } from "./rules/no-array-filter-map.ts"; +import { noReduceAccumulatorCopyRule } from "./rules/no-reduce-accumulator-copy.ts"; +import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.ts"; +import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.ts"; +import { noKnownValueWideningRule } from "./rules/no-known-value-widening.ts"; +import { noModuleMockingRule } from "./rules/no-module-mocking.ts"; +import { noObjectParametersRule } from "./rules/no-object-parameters.ts"; +import { noReflectApplyRule } from "./rules/no-reflect-apply.ts"; +import { noReflectGetRule } from "./rules/no-reflect-get.ts"; +import { noRuntimeTypeofRule } from "./rules/no-runtime-typeof.ts"; +import { noForbiddenTermInSymbolNamesRule } from "./rules/no-shape-in-symbol-names.ts"; +import { noUnknownParametersRule } from "./rules/no-unknown-parameters.ts"; +import { noUnknownReturnsRule } from "./rules/no-unknown-returns.ts"; +import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.ts"; +import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.ts"; +import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.ts"; +import { requireReadableSpacingRule } from "./rules/require-readable-spacing.ts"; +import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.ts"; + +/** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */ +const antiSlopPlugin = eslintCompatPlugin({ + meta: { name: "anti-slop" }, + rules: { + "no-array-filter-map": noArrayFilterMapRule, + "no-reduce-accumulator-copy": noReduceAccumulatorCopyRule, + "no-chained-type-assertions": noChainedTypeAssertionsRule, + "no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule, + "no-known-value-widening": noKnownValueWideningRule, + "no-module-mocking": noModuleMockingRule, + "no-object-parameters": noObjectParametersRule, + "no-reflect-apply": noReflectApplyRule, + "no-reflect-get": noReflectGetRule, + "no-runtime-typeof": noRuntimeTypeofRule, + "no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule, + "no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule, + "no-unknown-parameters": noUnknownParametersRule, + "no-unknown-returns": noUnknownReturnsRule, + "no-unknown-type-aliases": noUnknownTypeAliasesRule, + "no-widen-then-assert": noWidenThenAssertRule, + "require-readable-spacing": requireReadableSpacingRule, + "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule, + }, +}); + +export default antiSlopPlugin; diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-array-filter-map.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-array-filter-map.ts new file mode 100644 index 0000000..dc001b3 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-array-filter-map.ts @@ -0,0 +1,28 @@ +import { defineRule } from "@oxlint/plugins"; + +import { arrayMethodTarget, isKnownArrayExpression, unwrapArrayExpression } from "../shared/array-method.ts"; + +/** Reject eager array filter/map pipelines; lazy iterator helpers remain allowed. */ +export const noArrayFilterMapRule = defineRule({ + meta: { + type: "suggestion", + docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." }, + messages: { + arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics.", + }, + }, + createOnce(context) { + return { + CallExpression(node) { + const outer = arrayMethodTarget(node.callee); + if (outer === null || (outer.name !== "map" && outer.name !== "filter")) return; + const innerCall = unwrapArrayExpression(outer.object); + if (innerCall.type !== "CallExpression") return; + const inner = arrayMethodTarget(innerCall.callee); + if (inner === null || inner.name !== (outer.name === "map" ? "filter" : "map")) return; + if (!isKnownArrayExpression(context.sourceCode, inner.object)) return; + context.report({ node, messageId: "arrayFilterMap", data: { first: inner.name, second: outer.name } }); + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-chained-type-assertions.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-chained-type-assertions.ts new file mode 100644 index 0000000..0d11852 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-chained-type-assertions.ts @@ -0,0 +1,77 @@ +import { defineRule } from "@oxlint/plugins"; +import type { ESTree } from "@oxlint/plugins"; + +type TypeAssertionExpression = ESTree.TSAsExpression | ESTree.TSTypeAssertion; + +function isTypeAssertionExpression(node: ESTree.Node): node is TypeAssertionExpression { + return node.type === "TSAsExpression" || node.type === "TSTypeAssertion"; +} + +function unwrapParenthesizedExpression(expression: ESTree.Expression): ESTree.Expression { + let current = expression; + while (current.type === "ParenthesizedExpression") { + current = current.expression; + } + return current; +} + +function isConstAssertion(node: TypeAssertionExpression): boolean { + const { typeAnnotation } = node; + return ( + typeAnnotation.type === "TSTypeReference" && + typeAnnotation.typeName.type === "Identifier" && + typeAnnotation.typeName.name === "const" + ); +} + +function isOutermostAssertionInChain(node: TypeAssertionExpression): boolean { + let current: ESTree.Expression = node; + let parent = node.parent; + + while (parent.type === "ParenthesizedExpression" && parent.expression === current) { + current = parent; + parent = parent.parent; + } + + return !isTypeAssertionExpression(parent) || parent.expression !== current; +} + +function isForbiddenAssertionChain(node: TypeAssertionExpression): boolean { + let assertionCount = 0; + let hasNonConstAssertion = false; + let current: ESTree.Expression = node; + + while (isTypeAssertionExpression(current)) { + assertionCount += 1; + hasNonConstAssertion ||= !isConstAssertion(current); + current = unwrapParenthesizedExpression(current.expression); + } + + return assertionCount > 1 && hasNonConstAssertion; +} + +/** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */ +export const noChainedTypeAssertionsRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains.", + }, + messages: { + chained: + "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.", + }, + }, + createOnce(context) { + const checkTypeAssertion = (node: TypeAssertionExpression) => { + if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return; + context.report({ node, messageId: "chained" }); + }; + + return { + TSAsExpression: checkTypeAssertion, + TSTypeAssertion: checkTypeAssertion, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-conditional-empty-object-spread.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-conditional-empty-object-spread.ts new file mode 100644 index 0000000..ae7248d --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-conditional-empty-object-spread.ts @@ -0,0 +1,49 @@ +import { defineRule } from "@oxlint/plugins"; +import type { ESTree } from "@oxlint/plugins"; + +function unwrapParentheses(node: ESTree.Expression): ESTree.Expression { + let current = node; + while (current.type === "ParenthesizedExpression") { + current = current.expression; + } + return current; +} + +function isEmptyObjectExpression(node: ESTree.Expression): boolean { + return node.type === "ObjectExpression" && node.properties.length === 0; +} + +function isConditionalEmptyObjectSpread(node: ESTree.Expression): boolean { + const conditional = unwrapParentheses(node); + return ( + conditional.type === "ConditionalExpression" && + (isEmptyObjectExpression(conditional.consequent) || + isEmptyObjectExpression(conditional.alternate)) + ); +} + +/** Ban conditional empty-object spreads without changing their omission semantics. */ +export const noConditionalEmptyObjectSpreadRule = defineRule({ + meta: { + type: "suggestion", + docs: { + description: + "Disallow object spreads that conditionally spread an empty object to omit fields.", + }, + messages: { + avoid: + "This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.", + }, + }, + createOnce(context) { + return { + SpreadElement(node) { + if (node.parent.type !== "ObjectExpression") return; + + if (isConditionalEmptyObjectSpread(node.argument)) { + context.report({ node, messageId: "avoid" }); + } + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-known-value-widening.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-known-value-widening.ts new file mode 100644 index 0000000..0df39ab --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-known-value-widening.ts @@ -0,0 +1,415 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + classifyUnsafeDictionaryValue, + classifyWideningTarget, + createTypeEnvironment, + isKnownEvidenceExpression, + type TypeEnvironment, + type WideningTarget, +} from "../shared/dictionary-types.ts"; +import { + containsUnknownType, + functionParameterBindingName, + functionParameterTypeAnnotation, +} from "../shared/function-parameters.ts"; +import { resolveVariable } from "../shared/scope.ts"; + +import type { ESTree, SourceCode, Variable } from "@oxlint/plugins"; + +type FunctionExpression = ESTree.ArrowFunctionExpression | ESTree.Function; + +function unwrapExpression(expression: ESTree.Expression): ESTree.Expression { + let current = expression; + while ( + current.type === "ParenthesizedExpression" || + current.type === "TSAsExpression" || + current.type === "TSSatisfiesExpression" || + current.type === "TSTypeAssertion" || + current.type === "TSNonNullExpression" + ) { + current = current.expression; + } + return current; +} + +function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null { + if (variable.defs.length !== 1) return null; + const [definition] = variable.defs; + return definition?.type === "Variable" && definition.node.type === "VariableDeclarator" + ? definition.node + : null; +} + +function isStableConstVariable(variable: Variable, declarator: ESTree.VariableDeclarator): boolean { + return ( + declarator.parent.type === "VariableDeclaration" && + declarator.parent.kind === "const" && + variable.references.every((reference) => reference.init || !reference.isWrite()) + ); +} + +function hasKnownEvidence( + sourceCode: SourceCode, + expression: ESTree.Expression, + visitedVariables = new Set(), +): boolean { + if (isKnownEvidenceExpression(expression)) return true; + const unwrapped = unwrapExpression(expression); + if (unwrapped.type !== "Identifier") return false; + const variable = resolveVariable(sourceCode, unwrapped); + if (variable === null || visitedVariables.has(variable)) return false; + const declarator = variableDeclarator(variable); + if ( + declarator === null || + declarator.init === null || + !isStableConstVariable(variable, declarator) + ) { + return false; + } + visitedVariables.add(variable); + return hasKnownEvidence(sourceCode, declarator.init, visitedVariables); +} + +function isFunctionExpression(node: ESTree.Node): node is FunctionExpression { + return ( + node.type === "ArrowFunctionExpression" || + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression" || + node.type === "TSDeclareFunction" || + node.type === "TSEmptyBodyFunctionExpression" + ); +} + +function localFunctionForCall( + sourceCode: SourceCode, + callee: ESTree.Expression, +): FunctionExpression | null { + const unwrapped = unwrapExpression(callee); + if (isFunctionExpression(unwrapped)) return unwrapped; + if (unwrapped.type !== "Identifier") return null; + const variable = resolveVariable(sourceCode, unwrapped); + if (variable === null || variable.defs.length !== 1) return null; + const [definition] = variable.defs; + if (definition === undefined) return null; + if (definition.type === "FunctionName" && isFunctionExpression(definition.node)) { + return definition.node; + } + if (definition.type !== "Variable" || definition.node.type !== "VariableDeclarator") { + return null; + } + const initializer = definition.node.init; + if (initializer === null) return null; + const unwrappedInitializer = unwrapExpression(initializer); + return isFunctionExpression(unwrappedInitializer) ? unwrappedInitializer : null; +} + +function variableTypeAnnotation( + sourceCode: SourceCode, + variable: Variable, +): ESTree.TSTypeAnnotation | null { + if (variable.defs.length !== 1) return null; + const [definition] = variable.defs; + if (definition === undefined) return null; + if ( + definition.type === "Variable" && + definition.node.type === "VariableDeclarator" && + definition.node.id.type === "Identifier" + ) { + return definition.node.id.typeAnnotation ?? null; + } + if (definition.type !== "Parameter" || !isFunctionExpression(definition.node)) { + return null; + } + const parameter = definition.node.params.find( + (candidate) => + functionParameterBindingName(candidate, sourceCode) === variable.name, + ); + return parameter === undefined ? null : (functionParameterTypeAnnotation(parameter) ?? null); +} + +function hasInformativeType( + type: ESTree.TSType, + environment: TypeEnvironment, +): boolean { + return classifyUnsafeDictionaryValue(type, environment) === null; +} + +function hasKnownCallArgumentEvidence( + sourceCode: SourceCode, + expression: ESTree.Expression, + environment: TypeEnvironment, + visitedVariables = new Set(), +): boolean { + if (expression.type === "ParenthesizedExpression" || expression.type === "TSNonNullExpression") { + return hasKnownCallArgumentEvidence( + sourceCode, + expression.expression, + environment, + visitedVariables, + ); + } + if (expression.type === "TSAsExpression" || expression.type === "TSTypeAssertion") { + return hasInformativeType(expression.typeAnnotation, environment); + } + if (expression.type === "TSSatisfiesExpression") { + return hasKnownCallArgumentEvidence( + sourceCode, + expression.expression, + environment, + visitedVariables, + ); + } + if (expression.type === "CallExpression") { + const owner = localFunctionForCall(sourceCode, expression.callee); + const returnType = owner?.returnType?.typeAnnotation; + return returnType !== undefined && hasInformativeType(returnType, environment); + } + if (expression.type !== "Identifier") return isKnownEvidenceExpression(expression); + const variable = resolveVariable(sourceCode, expression); + if (variable === null || visitedVariables.has(variable)) return false; + const annotation = variableTypeAnnotation(sourceCode, variable); + if (annotation !== null) { + return hasInformativeType(annotation.typeAnnotation, environment); + } + const declarator = variableDeclarator(variable); + if ( + declarator === null || + declarator.init === null || + !isStableConstVariable(variable, declarator) + ) { + return false; + } + visitedVariables.add(variable); + return hasKnownCallArgumentEvidence( + sourceCode, + declarator.init, + environment, + visitedVariables, + ); +} + +function typePredicateSubjectIndex( + sourceCode: SourceCode, + owner: FunctionExpression, +): number | null { + const predicate = owner.returnType?.typeAnnotation; + if (predicate?.type !== "TSTypePredicate" || predicate.parameterName.type !== "Identifier") { + return null; + } + const predicateParameterName = predicate.parameterName.name; + const index = owner.params.findIndex( + (parameter) => + functionParameterBindingName(parameter, sourceCode) === predicateParameterName, + ); + return index === -1 ? null : index; +} + +function annotationTarget( + annotation: ESTree.TSTypeAnnotation | null | undefined, + environment: TypeEnvironment, +): WideningTarget | null { + return annotation === null || annotation === undefined + ? null + : classifyWideningTarget(annotation.typeAnnotation, environment); +} + +function enclosingFunction(node: ESTree.Node): FunctionExpression | null { + let current: ESTree.Node | null = node.parent; + while (current !== null && current.type !== "Program") { + if ( + current.type === "ArrowFunctionExpression" || + current.type === "FunctionDeclaration" || + current.type === "FunctionExpression" + ) { + return current; + } + current = current.parent; + } + return null; +} + +function sourceKeyName(sourceCode: SourceCode, key: ESTree.PropertyKey): string { + if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name; + if (key.type === "Literal") return String(key.value); + return sourceCode.getText(key); +} + +function functionName(sourceCode: SourceCode, owner: FunctionExpression | null): string { + if (owner === null) return "anonymous function"; + if (owner.id !== null) return owner.id.name; + const parent = owner.parent; + if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") + return parent.id.name; + if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key); + return "anonymous function"; +} + +function isEmptyObjectExpression(expression: ESTree.Expression): boolean { + const unwrapped = unwrapExpression(expression); + return unwrapped.type === "ObjectExpression" && unwrapped.properties.length === 0; +} + +function isDictionaryAccumulatorTarget(destination: WideningTarget): boolean { + return destination.kind === "open dictionary" || destination.kind === "generic container"; +} + +function hasParentAssertion(node: ESTree.Node): boolean { + return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion"; +} + +/** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */ +export const noKnownValueWideningRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence.", + }, + messages: { + widening: + "The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract.", + }, + }, + createOnce(context) { + let environment: TypeEnvironment | null = null; + + const reportFlow = ( + expression: ESTree.Expression, + destination: WideningTarget | null, + subject: string, + ) => { + if (destination === null) return; + if ( + isDictionaryAccumulatorTarget(destination) && + isEmptyObjectExpression(expression) + ) { + return; + } + if (!hasKnownEvidence(context.sourceCode, expression)) return; + context.report({ + node: expression, + messageId: "widening", + data: { subject, target: destination.kind }, + }); + }; + + const targetFromAnnotation = (annotation: ESTree.TSTypeAnnotation | null | undefined) => + environment === null ? null : annotationTarget(annotation, environment); + + return { + Program(node) { + environment = createTypeEnvironment( + node, + context.sourceCode.visitorKeys, + ); + }, + VariableDeclarator(node) { + if (node.init === null || node.id.type !== "Identifier") return; + reportFlow( + node.init, + targetFromAnnotation(node.id.typeAnnotation), + `binding \`${node.id.name}\``, + ); + }, + PropertyDefinition(node) { + if (node.value === null) return; + reportFlow( + node.value, + targetFromAnnotation(node.typeAnnotation), + `property \`${sourceKeyName(context.sourceCode, node.key)}\``, + ); + }, + AccessorProperty(node) { + if (node.value === null) return; + reportFlow( + node.value, + targetFromAnnotation(node.typeAnnotation), + `property \`${sourceKeyName(context.sourceCode, node.key)}\``, + ); + }, + AssignmentExpression(node) { + if (node.operator !== "=" || node.left.type !== "Identifier") return; + const variable = resolveVariable(context.sourceCode, node.left); + if (variable === null) return; + const declarator = variableDeclarator(variable); + if (declarator === null || declarator.id.type !== "Identifier") return; + reportFlow( + node.right, + targetFromAnnotation(declarator.id.typeAnnotation), + `binding \`${declarator.id.name}\``, + ); + }, + CallExpression(node) { + if (environment === null) return; + const owner = localFunctionForCall(context.sourceCode, node.callee); + if (owner === null) return; + const parameterIndex = typePredicateSubjectIndex(context.sourceCode, owner); + if (parameterIndex === null) return; + const parameter = owner.params[parameterIndex]; + const argument = node.arguments[parameterIndex]; + if (parameter === undefined || argument === undefined || argument.type === "SpreadElement") { + return; + } + const parameterAnnotation = functionParameterTypeAnnotation(parameter); + if ( + parameterAnnotation === null || + parameterAnnotation === undefined || + !containsUnknownType(parameterAnnotation.typeAnnotation) + ) { + return; + } + if ( + !hasKnownCallArgumentEvidence( + context.sourceCode, + argument, + environment, + ) + ) { + return; + } + context.report({ + node: argument, + messageId: "widening", + data: { + subject: `argument for parameter \`${functionParameterBindingName(parameter, context.sourceCode)}\` of \`${functionName(context.sourceCode, owner)}\``, + target: "unknown", + }, + }); + }, + ReturnStatement(node) { + if (node.argument === null) return; + const owner = enclosingFunction(node); + reportFlow( + node.argument, + targetFromAnnotation(owner?.returnType), + `return value of \`${functionName(context.sourceCode, owner)}\``, + ); + }, + ArrowFunctionExpression(node) { + if (node.body.type === "BlockStatement") return; + reportFlow( + node.body, + targetFromAnnotation(node.returnType), + `return value of \`${functionName(context.sourceCode, node)}\``, + ); + }, + TSAsExpression(node) { + if (environment === null || hasParentAssertion(node)) return; + reportFlow( + node.expression, + classifyWideningTarget(node.typeAnnotation, environment), + "assertion", + ); + }, + TSTypeAssertion(node) { + if (environment === null || hasParentAssertion(node)) return; + reportFlow( + node.expression, + classifyWideningTarget(node.typeAnnotation, environment), + "assertion", + ); + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-module-mocking.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-module-mocking.ts new file mode 100644 index 0000000..2d60d48 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-module-mocking.ts @@ -0,0 +1,80 @@ +import { defineRule } from "@oxlint/plugins"; + +import { resolveVariable } from "../shared/scope.ts"; + +import type { ESTree, SourceCode } from "@oxlint/plugins"; + +const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]); + +function importedName(node: ESTree.Node): string | null { + if (node.type !== "ImportSpecifier") return null; + return node.imported.type === "Identifier" ? node.imported.name : node.imported.value; +} + +function isTestFrameworkObject( + sourceCode: SourceCode, + expression: ESTree.Expression, +): expression is ESTree.IdentifierReference { + if (expression.type !== "Identifier") return false; + if ( + (expression.name === "vi" || expression.name === "jest") && + sourceCode.isGlobalReference(expression) + ) { + return true; + } + + const variable = resolveVariable(sourceCode, expression); + if (variable === null || variable.defs.length === 0) { + return expression.name === "vi" || expression.name === "jest"; + } + return variable.defs.some((definition) => { + if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") { + return false; + } + const source = definition.parent.source.value; + const name = importedName(definition.node); + return (source === "vitest" && name === "vi") || (source === "@jest/globals" && name === "jest"); + }); +} + +function moduleMockCall(sourceCode: SourceCode, callee: ESTree.Expression): boolean { + if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false; + if (!isTestFrameworkObject(sourceCode, callee.object)) return false; + const property = callee.property; + const method = callee.computed + ? property.type === "Literal" && + (property.value === "doMock" || + property.value === "mock" || + property.value === "unstable_mockModule") + ? property.value + : null + : property.type === "Identifier" + ? property.name + : null; + return method !== null && moduleMockMethods.has(method); +} + +/** Ban test framework module mocking in favor of real dependency seams. */ +export const noModuleMockingRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow Vitest and Jest module mocking; tests must replace dependencies through real interfaces.", + }, + messages: { + moduleMock: + "Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation.", + }, + }, + createOnce(context) { + return { + CallExpression(node) { + if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return; + if (moduleMockCall(context.sourceCode, node.callee)) { + context.report({ node, messageId: "moduleMock" }); + } + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-object-parameters.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-object-parameters.ts new file mode 100644 index 0000000..6589ebf --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-object-parameters.ts @@ -0,0 +1,83 @@ +import { defineRule } from "@oxlint/plugins"; + +import type { ESTree } from "@oxlint/plugins"; + +import { + functionParameterBindingName, + functionParameterTypeAnnotation, +} from "../shared/function-parameters.ts"; +import { + createTypeAliasEnvironment, + resolvedTypeMatches, + type TypeAliasEnvironment, +} from "../shared/type-alias-resolution.ts"; +type ParameterOwner = + | ESTree.ArrowFunctionExpression + | ESTree.Function + | ESTree.TSCallSignatureDeclaration + | ESTree.TSConstructSignatureDeclaration + | ESTree.TSConstructorType + | ESTree.TSFunctionType + | ESTree.TSMethodSignature; + +/** Ban the broad object type on function inputs, including local aliases to object. */ +export const noObjectParametersRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary.", + }, + messages: { + objectParameter: + "Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function.", + }, + }, + createOnce(context) { + let environment: TypeAliasEnvironment | null = null; + + const resolvesToObject = (type: ESTree.TSType): boolean => + environment !== null && + resolvedTypeMatches(type, environment, (resolved, matches) => { + if (resolved.type === "TSObjectKeyword") return true; + if (resolved.type === "TSParenthesizedType") { + return matches(resolved.typeAnnotation); + } + return ( + resolved.type === "TSUnionType" && resolved.types.some(matches) + ); + }); + + const checkParameters = (node: ParameterOwner) => { + for (const parameter of node.params) { + const annotation = functionParameterTypeAnnotation(parameter); + if (annotation === null || annotation === undefined) continue; + if (!resolvesToObject(annotation.typeAnnotation)) continue; + context.report({ + node: annotation.typeAnnotation, + messageId: "objectParameter", + data: { parameter: functionParameterBindingName(parameter, context.sourceCode) }, + }); + } + }; + + return { + Program(node) { + environment = createTypeAliasEnvironment( + node, + context.sourceCode.visitorKeys, + ); + }, + ArrowFunctionExpression: checkParameters, + FunctionDeclaration: checkParameters, + FunctionExpression: checkParameters, + TSCallSignatureDeclaration: checkParameters, + TSConstructSignatureDeclaration: checkParameters, + TSConstructorType: checkParameters, + TSDeclareFunction: checkParameters, + TSEmptyBodyFunctionExpression: checkParameters, + TSFunctionType: checkParameters, + TSMethodSignature: checkParameters, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-reduce-accumulator-copy.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-reduce-accumulator-copy.ts new file mode 100644 index 0000000..47d8e2c --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-reduce-accumulator-copy.ts @@ -0,0 +1,109 @@ +import { defineRule } from "@oxlint/plugins"; +import type { ESTree, SourceCode, Variable } from "@oxlint/plugins"; + +import { + arrayMethodTarget, + isKnownArrayExpression, + resolveArrayBinding, + unwrapArrayExpression, +} from "../shared/array-method.ts"; + +function enclosingReducer(node: ESTree.Node) { + let parent = node.parent; + while (parent !== null) { + if (parent.type === "FunctionDeclaration") return null; + if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") { + const callback = parent; + let owner: ESTree.Node | null = callback.parent; + while (owner !== null && unwrapArrayExpression(owner) === callback) owner = owner.parent; + if (owner?.type !== "CallExpression") return null; + const method = arrayMethodTarget(owner.callee); + const firstArgument = owner.arguments[0]; + if ( + method === null || (method.name !== "reduce" && method.name !== "reduceRight") || + owner.arguments.length > 2 || firstArgument === undefined || + unwrapArrayExpression(firstArgument) !== callback + ) return null; + const firstParameter = callback.params[0]; + const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter; + if (accumulator?.type !== "Identifier") return null; + return { callback, accumulator, initialValue: owner.arguments[1] }; + } + parent = parent.parent; + } + return null; +} + +function referencesAccumulator( + sourceCode: SourceCode, + node: ESTree.Node, + accumulator: Variable, + visited = new Set(), +): boolean { + const variable = resolveArrayBinding(sourceCode, node); + if (variable === null || visited.has(variable)) return false; + if (variable === accumulator) return true; + visited.add(variable); + if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false; + for (const definition of variable.defs) { + if ( + definition.type === "Variable" && definition.node.type === "VariableDeclarator" && + definition.node.id.type === "Identifier" && definition.node.init !== null && + definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const" + ) { + return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited); + } + } + return false; +} + +function isGlobalCopyOwner(sourceCode: SourceCode, node: ESTree.Node, name: string): boolean { + node = unwrapArrayExpression(node); + if (node.type !== "Identifier" || node.name !== name) return false; + const variable = resolveArrayBinding(sourceCode, node); + return variable === null || variable.defs.length === 0; +} + +/** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */ +export const noReduceAccumulatorCopyRule = defineRule({ + meta: { + type: "problem", + docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." }, + messages: { + accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap.", + }, + }, + createOnce(context) { + return { + CallExpression(node) { + const method = arrayMethodTarget(node.callee); + if (method === null) return; + const reducer = enclosingReducer(node); + if (reducer === null) return; + const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find(variable => + variable.identifiers.some(identifier => identifier.start === reducer.accumulator.start), + ); + if (accumulator === undefined) return; + const isAccumulator = (expression: ESTree.Node) => + referencesAccumulator(context.sourceCode, expression, accumulator); + let copiesAccumulator = false; + if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) { + const target = node.arguments[0]; + copiesAccumulator = ( + target !== undefined && unwrapArrayExpression(target).type === "ObjectExpression" && + node.arguments.slice(1).some(isAccumulator) + ); + } else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) { + const source = node.arguments[0]; + copiesAccumulator = source !== undefined && isAccumulator(source); + } else if (["concat", "slice", "toSpliced", "toSorted", "toReversed", "with"].includes(method.name)) { + const initialValue = reducer.initialValue; + const arrayAccumulator = initialValue !== undefined && + isKnownArrayExpression(context.sourceCode, initialValue); + copiesAccumulator = arrayAccumulator && isAccumulator(method.object); + } + if (copiesAccumulator) context.report({ node, messageId: "accumulatorCopy" }); + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-reflect-apply.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-reflect-apply.ts new file mode 100644 index 0000000..2cc3045 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-reflect-apply.ts @@ -0,0 +1,28 @@ +import { defineRule } from "@oxlint/plugins"; + +import { isGlobalReflectMethodCall } from "../shared/reflect-method.ts"; + +/** Ban Reflect.apply, which bypasses ordinary typed function calls. */ +export const noReflectApplyRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface.", + }, + messages: { + reflectApply: + "Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface.", + }, + }, + createOnce(context) { + return { + CallExpression(node) { + if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return; + if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "apply")) { + context.report({ node, messageId: "reflectApply" }); + } + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-reflect-get.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-reflect-get.ts new file mode 100644 index 0000000..cf630ec --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-reflect-get.ts @@ -0,0 +1,28 @@ +import { defineRule } from "@oxlint/plugins"; + +import { isGlobalReflectMethodCall } from "../shared/reflect-method.ts"; + +/** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */ +export const noReflectGetRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow Reflect.get; use typed property access or parse dynamic input into a domain type.", + }, + messages: { + reflectGet: + "Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it.", + }, + }, + createOnce(context) { + return { + CallExpression(node) { + if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return; + if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "get")) { + context.report({ node, messageId: "reflectGet" }); + } + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-runtime-typeof.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-runtime-typeof.ts new file mode 100644 index 0000000..43259eb --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-runtime-typeof.ts @@ -0,0 +1,77 @@ +import { defineRule } from "@oxlint/plugins"; + +import type { ESTree } from "@oxlint/plugins"; + +type RuntimeFunction = ESTree.ArrowFunctionExpression | ESTree.Function; + +function isRuntimeFunction(node: ESTree.Node): node is RuntimeFunction { + return ( + node.type === "ArrowFunctionExpression" || + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression" + ); +} + +function isInsideTypeGuard(node: ESTree.Node): boolean { + let current: ESTree.Node | null = node.parent; + while (current !== null && current.type !== "Program") { + if (isRuntimeFunction(current)) { + return current.returnType?.typeAnnotation.type === "TSTypePredicate"; + } + current = current.parent; + } + return false; +} + +/** Return whether typeof safely probes for the existence of a possibly absent binding. */ +function isExistenceProbe(node: ESTree.UnaryExpression): boolean { + const parent = node.parent; + if (parent.type !== "BinaryExpression") return false; + if (!["===", "!==", "==", "!="].includes(parent.operator)) return false; + const other = parent.left === node ? parent.right : parent.left; + return other.type === "Literal" && other.value === "undefined"; +} + +/** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */ +export const noRuntimeTypeofRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary.", + }, + messages: { + runtimeTypeof: + "A `typeof` check narrows a representation without establishing its contract. Parse input at its I/O boundary, then branch on the domain value.", + }, + schema: [ + { + type: "object", + properties: { + allowInTypeGuards: { type: "boolean" }, + }, + additionalProperties: false, + }, + ], + defaultOptions: [{ allowInTypeGuards: false }], + }, + createOnce(context) { + return { + UnaryExpression(node) { + const option = context.options?.[0]; + const allowInTypeGuards = + typeof option === "object" && + option !== null && + !Array.isArray(option) && + option.allowInTypeGuards === true; + if ( + node.operator === "typeof" && + !isExistenceProbe(node) && + (!allowInTypeGuards || !isInsideTypeGuard(node)) + ) { + context.report({ node, messageId: "runtimeTypeof" }); + } + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-shape-in-symbol-names.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-shape-in-symbol-names.ts new file mode 100644 index 0000000..436d2a2 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-shape-in-symbol-names.ts @@ -0,0 +1,46 @@ +import { defineRule } from "@oxlint/plugins"; +import type { ESTree } from "@oxlint/plugins"; + +const FORBIDDEN_SYMBOL_NAME = "shape"; + +function containsForbiddenSymbolName(name: string): boolean { + return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME); +} + +/** Return whether an identifier names a statically accessed member owned by another value. */ +function isBorrowedMemberName(node: ESTree.Node): boolean { + const parent = node.parent; + if (parent === null || parent.type !== "MemberExpression") return false; + return parent.property === node && parent.computed === false; +} + +/** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */ +export const noForbiddenTermInSymbolNamesRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + 'Disallow the case-insensitive substring "shape" in JavaScript, TypeScript, private, and JSX symbol names.', + }, + messages: { + forbiddenSymbolName: + 'Rename symbol "{{name}}" for its domain role; "shape" describes structure rather than ownership.', + }, + }, + createOnce(context) { + const reportForbiddenSymbolName = (node: ESTree.Node & { name: string }) => { + if (!containsForbiddenSymbolName(node.name) || isBorrowedMemberName(node)) return; + context.report({ + node, + messageId: "forbiddenSymbolName", + data: { name: node.name }, + }); + }; + + return { + Identifier: reportForbiddenSymbolName, + PrivateIdentifier: reportForbiddenSymbolName, + JSXIdentifier: reportForbiddenSymbolName, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-parameters.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-parameters.ts new file mode 100644 index 0000000..b4a1545 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-parameters.ts @@ -0,0 +1,69 @@ +import { defineRule } from "@oxlint/plugins"; +import type { ESTree } from "@oxlint/plugins"; + +import { + containsUnknownType, + functionParameterBindingName, + functionParameterTypeAnnotation, +} from "../shared/function-parameters.ts"; +type ParameterOwner = + | ESTree.ArrowFunctionExpression + | ESTree.Function + | ESTree.TSCallSignatureDeclaration + | ESTree.TSConstructSignatureDeclaration + | ESTree.TSConstructorType + | ESTree.TSFunctionType + | ESTree.TSMethodSignature; + +function isTypePredicateSubject(owner: ParameterOwner, parameterName: string): boolean { + const predicate = owner.returnType?.typeAnnotation; + return ( + predicate?.type === "TSTypePredicate" && + predicate.parameterName.type === "Identifier" && + predicate.parameterName.name === parameterName + ); +} + +/** Disallow unknown inputs except explicitly named error-cause enrichment. */ +export const noUnknownParametersRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow explicitly unknown function parameters except `cause` and type-predicate subjects; decode unknown input at its I/O boundary instead.", + }, + messages: { + unknownParameter: + "Parameter `{{parameter}}` leaves input unparsed. Accept a named domain type; run the expected schema or parser at the I/O boundary before calling this function.", + }, + }, + createOnce(context) { + const checkParameters = (node: ParameterOwner) => { + for (const parameter of node.params) { + const annotation = functionParameterTypeAnnotation(parameter); + if (annotation === null || annotation === undefined) continue; + if (!containsUnknownType(annotation.typeAnnotation)) continue; + const name = functionParameterBindingName(parameter, context.sourceCode); + if (name === "cause" || isTypePredicateSubject(node, name)) continue; + context.report({ + node: annotation.typeAnnotation, + messageId: "unknownParameter", + data: { parameter: name }, + }); + } + }; + + return { + ArrowFunctionExpression: checkParameters, + FunctionDeclaration: checkParameters, + FunctionExpression: checkParameters, + TSCallSignatureDeclaration: checkParameters, + TSConstructSignatureDeclaration: checkParameters, + TSConstructorType: checkParameters, + TSDeclareFunction: checkParameters, + TSEmptyBodyFunctionExpression: checkParameters, + TSFunctionType: checkParameters, + TSMethodSignature: checkParameters, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-returns.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-returns.ts new file mode 100644 index 0000000..e1f43f8 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-returns.ts @@ -0,0 +1,82 @@ +import { defineRule } from "@oxlint/plugins"; + +import type { ESTree } from "@oxlint/plugins"; + +import { + createTypeAliasEnvironment, + resolvedTypeMatches, + type TypeAliasEnvironment, +} from "../shared/type-alias-resolution.ts"; + +type FunctionWithReturnType = + | ESTree.ArrowFunctionExpression + | ESTree.Function + | ESTree.TSCallSignatureDeclaration + | ESTree.TSConstructSignatureDeclaration + | ESTree.TSConstructorType + | ESTree.TSFunctionType + | ESTree.TSMethodSignature; + +/** Ban function contracts that return unknown instead of a parsed domain type. */ +export const noUnknownReturnsRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow functions whose explicit return contract is unknown or Promise.", + }, + messages: { + unknownReturn: + "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type.", + }, + }, + createOnce(context) { + let environment: TypeAliasEnvironment | null = null; + + const resolvesToUnknown = (type: ESTree.TSType): boolean => + environment !== null && + resolvedTypeMatches(type, environment, (resolved, matches) => { + if (resolved.type === "TSUnknownKeyword") return true; + if (resolved.type === "TSParenthesizedType") { + return matches(resolved.typeAnnotation); + } + if (resolved.type === "TSUnionType") return resolved.types.some(matches); + if ( + resolved.type !== "TSTypeReference" || + resolved.typeName.type !== "Identifier" || + (resolved.typeName.name !== "Promise" && + resolved.typeName.name !== "PromiseLike") + ) { + return false; + } + const value = resolved.typeArguments?.params[0]; + return value !== undefined && matches(value); + }); + + const checkReturnType = (node: FunctionWithReturnType) => { + const annotation = node.returnType; + if (annotation === null || annotation === undefined) return; + if (!resolvesToUnknown(annotation.typeAnnotation)) return; + context.report({ node: annotation.typeAnnotation, messageId: "unknownReturn" }); + }; + + return { + Program(node) { + environment = createTypeAliasEnvironment( + node, + context.sourceCode.visitorKeys, + ); + }, + ArrowFunctionExpression: checkReturnType, + FunctionDeclaration: checkReturnType, + FunctionExpression: checkReturnType, + TSCallSignatureDeclaration: checkReturnType, + TSConstructSignatureDeclaration: checkReturnType, + TSConstructorType: checkReturnType, + TSDeclareFunction: checkReturnType, + TSEmptyBodyFunctionExpression: checkReturnType, + TSFunctionType: checkReturnType, + TSMethodSignature: checkReturnType, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-type-aliases.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-type-aliases.ts new file mode 100644 index 0000000..af5f08a --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unknown-type-aliases.ts @@ -0,0 +1,54 @@ +import { defineRule } from "@oxlint/plugins"; + +import type { ESTree } from "@oxlint/plugins"; + +import { + createTypeAliasEnvironment, + resolvedTypeMatches, + type TypeAliasEnvironment, +} from "../shared/type-alias-resolution.ts"; + +/** Ban named aliases that merely conceal TypeScript's unknown top type. */ +export const noUnknownTypeAliasesRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.", + }, + messages: { + unknownAlias: + "Type alias `{{alias}}` hides `unknown`. Keep `unknown` explicit at the parsing boundary or on an allowed `cause` field; otherwise use the parsed owner type.", + }, + }, + createOnce(context) { + let environment: TypeAliasEnvironment | null = null; + + const resolvesToUnknown = (type: ESTree.TSType): boolean => + environment !== null && + resolvedTypeMatches(type, environment, (resolved, matches) => { + if (resolved.type === "TSUnknownKeyword") return true; + if (resolved.type === "TSParenthesizedType") { + return matches(resolved.typeAnnotation); + } + return resolved.type === "TSUnionType" && resolved.types.some(matches); + }); + + return { + Program(node) { + environment = createTypeAliasEnvironment( + node, + context.sourceCode.visitorKeys, + ); + }, + TSTypeAliasDeclaration(node) { + if (!resolvesToUnknown(node.typeAnnotation)) return; + context.report({ + node: node.id, + messageId: "unknownAlias", + data: { alias: node.id.name }, + }); + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unsafe-dictionary-type.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unsafe-dictionary-type.ts new file mode 100644 index 0000000..8cb615a --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-unsafe-dictionary-type.ts @@ -0,0 +1,154 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + classifyUnsafeDictionary, + classifyUnsafeDictionaryValue, + createTypeEnvironment, + type TypeEnvironment, +} from "../shared/dictionary-types.ts"; +import { visibleTypeAlias } from "../shared/type-alias-resolution.ts"; + +import type { ESTree } from "@oxlint/plugins"; + +const typeNodeKinds: ReadonlySet = new Set([ + "JSDocNonNullableType", + "JSDocNullableType", + "JSDocUnknownType", + "TSAnyKeyword", + "TSArrayType", + "TSBigIntKeyword", + "TSBooleanKeyword", + "TSConditionalType", + "TSConstructorType", + "TSFunctionType", + "TSImportType", + "TSIndexedAccessType", + "TSInferType", + "TSIntersectionType", + "TSIntrinsicKeyword", + "TSLiteralType", + "TSMappedType", + "TSNamedTupleMember", + "TSNeverKeyword", + "TSNullKeyword", + "TSNumberKeyword", + "TSObjectKeyword", + "TSParenthesizedType", + "TSStringKeyword", + "TSSymbolKeyword", + "TSTemplateLiteralType", + "TSThisType", + "TSTupleType", + "TSTypeLiteral", + "TSTypeOperator", + "TSTypePredicate", + "TSTypeQuery", + "TSTypeReference", + "TSUndefinedKeyword", + "TSUnionType", + "TSUnknownKeyword", + "TSVoidKeyword", +]); + +function isTypeNode(node: ESTree.Node): node is ESTree.TSType { + return typeNodeKinds.has(node.type); +} + +function typeReferenceName(type: ESTree.TSTypeReference): string | null { + return type.typeName.type === "Identifier" ? type.typeName.name : null; +} + +function isInsideTypeAliasDeclaration(node: ESTree.Node): boolean { + let current: ESTree.Node | null = node.parent; + while (current !== null && current.type !== "Program") { + if (current.type === "TSTypeAliasDeclaration") return true; + current = current.parent; + } + return false; +} + +function isPlainAliasConsumerUse(node: ESTree.TSType, environment: TypeEnvironment): boolean { + if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false; + const name = typeReferenceName(node); + return ( + name !== null && + visibleTypeAlias(name, node, environment.typeAliases) !== null && + !isInsideTypeAliasDeclaration(node) + ); +} + +function isInsideTypeParameterConstraint(node: ESTree.TSType): boolean { + let child: ESTree.Node = node; + let parent: ESTree.Node | null = child.parent; + while (parent !== null && parent.type !== "Program") { + if (parent.type === "TSTypeParameter" && parent.constraint === child) return true; + child = parent; + parent = child.parent; + } + return false; +} + +function shouldReportType(node: ESTree.TSType, environment: TypeEnvironment): boolean { + if (isInsideTypeParameterConstraint(node)) return false; + if (isPlainAliasConsumerUse(node, environment)) return false; + if (classifyUnsafeDictionary(node, environment) === null) return false; + let current: ESTree.Node | null = node.parent; + while (current !== null && current.type !== "Program") { + if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) + return false; + current = current.parent; + } + return true; +} + +/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */ +export const noUnsafeDictionaryTypeRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches.", + }, + messages: { + unsafeDictionary: + "This dictionary's {{value}} value type gives callers no concrete value contract. Use an owner/schema-derived value type; parse external payloads before insertion.", + }, + }, + createOnce(context) { + let environment: TypeEnvironment | null = null; + const report = (node: ESTree.Node, value: string) => { + context.report({ node, messageId: "unsafeDictionary", data: { value } }); + }; + const reportIfUnsafe = (node: ESTree.TSType) => { + if (environment === null || !shouldReportType(node, environment)) return; + const unsafe = classifyUnsafeDictionary(node, environment); + if (unsafe === null) return; + report(node, unsafe.unsafeValue); + }; + + return { + Program(node) { + environment = createTypeEnvironment( + node, + context.sourceCode.visitorKeys, + ); + }, + TSTypeReference: reportIfUnsafe, + TSTypeLiteral: reportIfUnsafe, + TSMappedType: reportIfUnsafe, + TSIndexSignature(node) { + if ( + environment === null || + node.typeAnnotation === null || + node.parent.type === "TSTypeLiteral" + ) + return; + const unsafe = classifyUnsafeDictionaryValue( + node.typeAnnotation.typeAnnotation, + environment, + ); + if (unsafe !== null) report(node, unsafe.unsafeValue); + }, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-widen-then-assert.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-widen-then-assert.ts new file mode 100644 index 0000000..c5e07f7 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/no-widen-then-assert.ts @@ -0,0 +1,366 @@ +import { defineRule } from "@oxlint/plugins"; +import type { ESTree, Variable } from "@oxlint/plugins"; + +type BroadTypeKind = "top" | "object" | "record"; + +type KnownValueEvidence = { + readonly type: ESTree.TSType | null; +}; + +const functionBoundaryTypes = new Set([ + "ArrowFunctionExpression", + "FunctionDeclaration", + "FunctionExpression", + "TSDeclareFunction", + "TSEmptyBodyFunctionExpression", +]); + +function unwrapExpressionParentheses(expression: ESTree.Expression): ESTree.Expression { + let current = expression; + while (current.type === "ParenthesizedExpression") current = current.expression; + return current; +} + +function unwrapTypeParentheses(type: ESTree.TSType): ESTree.TSType { + let current = type; + while (current.type === "TSParenthesizedType") current = current.typeAnnotation; + return current; +} + +function typeReferenceName(type: ESTree.TSTypeReference): string | null { + return type.typeName.type === "Identifier" ? type.typeName.name : null; +} + +function isUnknownOrAnyType(type: ESTree.TSType): boolean { + const unwrapped = unwrapTypeParentheses(type); + return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword"; +} + +function isBroadRecordKeyType(type: ESTree.TSType): boolean { + const unwrapped = unwrapTypeParentheses(type); + if ( + unwrapped.type === "TSStringKeyword" || + unwrapped.type === "TSNumberKeyword" || + unwrapped.type === "TSSymbolKeyword" + ) { + return true; + } + if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType); + return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey"; +} + +function isBroadRecordType(type: ESTree.TSType): boolean { + const unwrapped = unwrapTypeParentheses(type); + + if (unwrapped.type === "TSTypeReference") { + if (typeReferenceName(unwrapped) === "Readonly") { + const [inner] = unwrapped.typeArguments?.params ?? []; + return inner !== undefined && isBroadRecordType(inner); + } + + if (typeReferenceName(unwrapped) !== "Record") return false; + const parameters = unwrapped.typeArguments?.params ?? []; + return ( + parameters.length === 2 && + parameters[0] !== undefined && + parameters[1] !== undefined && + isBroadRecordKeyType(parameters[0]) && + isUnknownOrAnyType(parameters[1]) + ); + } + + if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false; + const [member] = unwrapped.members; + const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : []; + return ( + member?.type === "TSIndexSignature" && + member.parameters.length === 1 && + parameter !== undefined && + isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && + isUnknownOrAnyType(member.typeAnnotation.typeAnnotation) + ); +} + +function broadTypeKind(type: ESTree.TSType): BroadTypeKind | null { + const unwrapped = unwrapTypeParentheses(type); + if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top"; + if (unwrapped.type === "TSObjectKeyword") return "object"; + return isBroadRecordType(unwrapped) ? "record" : null; +} + +function assertedExpression( + node: ESTree.TSAsExpression | ESTree.TSTypeAssertion, +): ESTree.Expression { + return unwrapExpressionParentheses(node.expression); +} + +function assertionFromExpression( + expression: ESTree.Expression, +): ESTree.TSAsExpression | ESTree.TSTypeAssertion | null { + const unwrapped = unwrapExpressionParentheses(expression); + return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion" + ? unwrapped + : null; +} + +function normalizedTypeText(sourceText: string, type: ESTree.TSType): string { + return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, ""); +} + +function typesHaveSameSyntax( + sourceText: string, + left: ESTree.TSType | null, + right: ESTree.TSType, +): boolean { + return ( + left !== null && + normalizedTypeText(sourceText, unwrapTypeParentheses(left)) === + normalizedTypeText(sourceText, unwrapTypeParentheses(right)) + ); +} + +function isDefinitelyObjectType(type: ESTree.TSType): boolean { + const unwrapped = unwrapTypeParentheses(type); + switch (unwrapped.type) { + case "TSArrayType": + case "TSConstructorType": + case "TSFunctionType": + case "TSMappedType": + case "TSObjectKeyword": + case "TSTupleType": + return true; + case "TSTypeLiteral": + return unwrapped.members.length > 0; + case "TSIntersectionType": + return unwrapped.types.every(isDefinitelyObjectType); + case "TSTypeOperator": + return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation); + default: + return false; + } +} + +function isDefinitelyNarrowerRecordType(type: ESTree.TSType): boolean { + const unwrapped = unwrapTypeParentheses(type); + if (unwrapped.type === "TSTypeLiteral") { + return unwrapped.members.some((member) => member.type !== "TSIndexSignature"); + } + + if (unwrapped.type !== "TSTypeReference") return false; + if (typeReferenceName(unwrapped) === "Readonly") { + const [inner] = unwrapped.typeArguments?.params ?? []; + return inner !== undefined && isDefinitelyNarrowerRecordType(inner); + } + if (typeReferenceName(unwrapped) !== "Record") return false; + + const parameters = unwrapped.typeArguments?.params ?? []; + return ( + parameters.length === 2 && parameters[1] !== undefined && !isUnknownOrAnyType(parameters[1]) + ); +} + +function functionBoundary(node: ESTree.Node): ESTree.Node | null { + let current = node.parent; + while (current !== null && current.type !== "Program") { + if (functionBoundaryTypes.has(current.type)) return current; + current = current.parent; + } + return null; +} + +function resolvedVariableForIdentifier( + scopes: readonly { + readonly references: readonly { + readonly identifier: ESTree.Node; + readonly resolved: Variable | null; + }[]; + }[], + identifier: ESTree.IdentifierReference, +): Variable | null { + for (const scope of scopes) { + const reference = scope.references.find( + (candidate) => + candidate.identifier.start === identifier.start && + candidate.identifier.end === identifier.end, + ); + if (reference !== undefined) return reference.resolved; + } + return null; +} + +function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null { + for (const definition of variable.defs) { + if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") { + return definition.node; + } + } + return null; +} + +function knownValueEvidence( + expression: ESTree.Expression, + scopes: Parameters[0], + boundary: ESTree.Node | null, + visitedVariables: ReadonlySet, +): KnownValueEvidence | null { + const unwrapped = unwrapExpressionParentheses(expression); + + if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") { + if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null; + return { type: unwrapped.typeAnnotation }; + } + + if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") { + return { type: null }; + } + + if ( + unwrapped.type === "ArrayExpression" || + unwrapped.type === "ArrowFunctionExpression" || + unwrapped.type === "ClassExpression" || + unwrapped.type === "FunctionExpression" || + unwrapped.type === "NewExpression" || + unwrapped.type === "ObjectExpression" + ) { + return { type: null }; + } + + if (unwrapped.type !== "Identifier") return null; + const variable = resolvedVariableForIdentifier(scopes, unwrapped); + if (variable === null || visitedVariables.has(variable)) return null; + + const annotatedIdentifier = variable.identifiers.find( + (identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== undefined, + ); + const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation; + if (annotation !== undefined && annotatedIdentifier !== undefined) { + if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) { + return null; + } + return { type: annotation }; + } + + const declarator = variableDeclarator(variable); + if ( + declarator === null || + declarator.parent.type !== "VariableDeclaration" || + declarator.parent.kind !== "const" || + declarator.init === null || + variable.references.some((reference) => reference.isWrite() && !reference.init) || + functionBoundary(declarator) !== boundary + ) { + return null; + } + + return knownValueEvidence( + declarator.init, + scopes, + boundary, + new Set([...visitedVariables, variable]), + ); +} + +function widenedBinding( + variable: Variable, + scopes: Parameters[0], +): { + readonly broadKind: BroadTypeKind; + readonly evidence: KnownValueEvidence; + readonly declaredAt: number; + readonly boundary: ESTree.Node | null; +} | null { + const declarator = variableDeclarator(variable); + if ( + declarator === null || + declarator.parent.type !== "VariableDeclaration" || + declarator.parent.kind !== "const" || + declarator.id.type !== "Identifier" || + declarator.init === null || + variable.references.some((reference) => reference.isWrite() && !reference.init) + ) { + return null; + } + + const boundary = functionBoundary(declarator); + const declaredType = declarator.id.typeAnnotation?.typeAnnotation; + const initializerAssertion = assertionFromExpression(declarator.init); + const initializerBroadKind = + initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation); + const declaredBroadKind = declaredType === undefined ? null : broadTypeKind(declaredType); + const broadKind = declaredBroadKind ?? initializerBroadKind; + if (broadKind === null) return null; + + const originalExpression = + initializerAssertion !== null && initializerBroadKind !== null + ? assertedExpression(initializerAssertion) + : declarator.init; + const evidence = knownValueEvidence(originalExpression, scopes, boundary, new Set([variable])); + return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary }; +} + +function assertionIsNarrower( + sourceText: string, + broadKind: BroadTypeKind, + evidence: KnownValueEvidence, + assertedType: ESTree.TSType, +): boolean { + if (broadTypeKind(assertedType) !== null) return false; + if (broadKind === "top") return true; + if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true; + if (broadKind === "object") return isDefinitelyObjectType(assertedType); + return isDefinitelyNarrowerRecordType(assertedType); +} + +/** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */ +export const noWidenThenAssertRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.", + }, + messages: { + widenThenAssert: + 'Binding "{{name}}" discards type evidence and later recreates it with an assertion. Keep the precise type from initialization through use; parse boundary input once.', + }, + }, + createOnce(context) { + let scopes: Parameters[0] = []; + + const checkAssertion = (node: ESTree.TSAsExpression | ESTree.TSTypeAssertion) => { + const expression = assertedExpression(node); + if (expression.type !== "Identifier") return; + + const variable = resolvedVariableForIdentifier(scopes, expression); + if (variable === null) return; + const widened = widenedBinding(variable, scopes); + if ( + widened === null || + node.start <= widened.declaredAt || + functionBoundary(node) !== widened.boundary || + !assertionIsNarrower( + context.sourceCode.text, + widened.broadKind, + widened.evidence, + node.typeAnnotation, + ) + ) { + return; + } + + context.report({ + node, + messageId: "widenThenAssert", + data: { name: expression.name }, + }); + }; + + return { + Program() { + scopes = context.sourceCode.scopeManager.scopes; + }, + TSAsExpression: checkAssertion, + TSTypeAssertion: checkAssertion, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/require-readable-spacing.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/require-readable-spacing.ts new file mode 100644 index 0000000..40735c7 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/require-readable-spacing.ts @@ -0,0 +1,80 @@ +import type { CreateRule, ESTree, SourceCode } from "@oxlint/plugins"; + +import createPaddingLineRule from "../vendor/eslint-stylistic/padding-line-between-statements.ts"; + +import type { NodeTestObject } from "../vendor/eslint-stylistic/padding-line-between-statements.ts"; + +/** A `const`/`let`/`var`/`using` declaration, optionally exported, written on one line. */ +function isSinglelineBinding(node: ESTree.Node): boolean { + const declaration = node.type === "ExportNamedDeclaration" ? node.declaration : node; + return declaration?.type === "VariableDeclaration" && node.loc.start.line === node.loc.end.line; +} + +/** Comments above a statement belong to it; a comment trailing the previous line does not. */ +function hasLeadingComment(node: ESTree.Node, sourceCode: SourceCode): boolean { + const previousLine = sourceCode.getTokenBefore(node)?.loc.end.line; + return sourceCode + .getCommentsBefore(node) + .some((comment) => comment.loc.start.line !== previousLine); +} + +const singlelineBinding: NodeTestObject = { + test: (node) => isSinglelineBinding(node), +}; + +const undocumentedSinglelineBinding: NodeTestObject = { + test: (node, sourceCode) => isSinglelineBinding(node) && !hasLeadingComment(node, sourceCode), +}; + +const paddingRule = createPaddingLineRule( + [ + { blankLine: "always", prev: "import", next: "*" }, + { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } }, + { blankLine: "always", prev: { selector: "Program > :not(ImportDeclaration)" }, next: "*" }, + { blankLine: "always", prev: "*", next: ["function", "class", "interface", "type"] }, + { blankLine: "always", prev: ["function", "class", "interface", "type"], next: "*" }, + { + blankLine: "always", + prev: "*", + next: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"], + }, + { + blankLine: "always", + prev: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"], + next: "*", + }, + { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] }, + { blankLine: "always", prev: "block-like", next: "*" }, + { blankLine: "any", prev: "import", next: "import" }, + // Consecutive single-line bindings form one group at any scope, including + // module scope. A binding with its own leading comment starts a new group. + { blankLine: "any", prev: "singleline-binding", next: "undocumented-singleline-binding" }, + { + blankLine: "any", + prev: { + selector: + ':matches(TSDeclareFunction, ExportNamedDeclaration[declaration.type="TSDeclareFunction"])', + }, + next: { + selector: + ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])', + }, + }, + ], + { + "singleline-binding": singlelineBinding, + "undocumented-singleline-binding": undocumentedSinglelineBinding, + }, +); + +/** Restore structural blank lines with whitespace-only fixes; keep single-line binding groups and overloads grouped. */ +export const requireReadableSpacingRule: CreateRule = { + ...paddingRule, + meta: { + ...paddingRule.meta, + docs: { + description: "Require readable spacing between declarations and logical statement groups.", + }, + schema: [], + }, +}; diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/rules/require-safety-comment-for-type-assertion.ts b/.agents/skills/install-anti-slop/assets/anti-slop/rules/require-safety-comment-for-type-assertion.ts new file mode 100644 index 0000000..bfea1a2 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/rules/require-safety-comment-for-type-assertion.ts @@ -0,0 +1,131 @@ +import { defineRule } from "@oxlint/plugins"; + +import type { ESTree, SourceCode } from "@oxlint/plugins"; + +type TypeAssertion = ESTree.TSAsExpression | ESTree.TSTypeAssertion; + +const DEFAULT_SAFETY_MARKERS = ["SAFETY"] as const; + +const commentOwnerKinds = new Set([ + "ExpressionStatement", + "PropertyDefinition", + "ReturnStatement", + "ThrowStatement", + "VariableDeclaration", +]); + +function isConstAssertion(node: TypeAssertion): boolean { + return ( + node.typeAnnotation.type === "TSTypeReference" && + node.typeAnnotation.typeName.type === "Identifier" && + node.typeAnnotation.typeName.name === "const" + ); +} + +function configuredSafetyMarkers(option: unknown): readonly string[] { + if (typeof option !== "object" || option === null || !("markers" in option)) { + return DEFAULT_SAFETY_MARKERS; + } + const configured = option.markers; + if (!Array.isArray(configured)) return DEFAULT_SAFETY_MARKERS; + const markers = configured.flatMap((marker) => + typeof marker === "string" && marker.trim().length > 0 ? [marker.trim()] : [], + ); + return markers.length > 0 ? markers : DEFAULT_SAFETY_MARKERS; +} + +function markerPattern(markers: readonly string[]): RegExp { + const alternation = markers + .map((marker) => marker.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`)) + .join("|"); + return new RegExp( + String.raw`(?:^|[^\p{L}\p{N}_])(?:${alternation})\s*:\s*\S`, + "u", + ); +} + +function hasSafetyJustificationBefore( + sourceCode: SourceCode, + owner: ESTree.Node, + assertion: TypeAssertion, + pattern: RegExp, +): boolean { + return sourceCode + .getCommentsBefore(owner) + .some( + (comment) => comment.end <= assertion.start && pattern.test(comment.value), + ); +} + +function hasSafetyComment( + sourceCode: SourceCode, + node: TypeAssertion, + pattern: RegExp, +): boolean { + let current: ESTree.Node = node; + while (true) { + if (hasSafetyJustificationBefore(sourceCode, current, node, pattern)) return true; + if (commentOwnerKinds.has(current.type)) { + const exportDeclaration = current.parent; + return ( + exportDeclaration.type === "ExportNamedDeclaration" && + exportDeclaration.declaration === current && + hasSafetyJustificationBefore(sourceCode, exportDeclaration, node, pattern) + ); + } + if (current.parent.type === "Program") return false; + current = current.parent; + } +} + +/** Require every non-const type assertion to state the invariant TypeScript cannot express. */ +export const requireSafetyCommentForTypeAssertionRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Require a nearby SAFETY comment for every TypeScript type assertion except const assertions.", + }, + messages: { + missingSafetyComment: + "This type assertion has no `{{marker}}:` justification. State the checked invariant immediately before the assertion or its containing statement.", + }, + schema: [ + { + type: "object", + properties: { + markers: { + type: "array", + items: { type: "string", minLength: 1 }, + minItems: 1, + uniqueItems: true, + }, + }, + additionalProperties: false, + }, + ], + defaultOptions: [{ markers: ["SAFETY"] }], + }, + createOnce(context) { + const patterns = new Map(); + + const checkAssertion = (node: TypeAssertion) => { + if (isConstAssertion(node)) return; + const markers = configuredSafetyMarkers(context.options?.[0]); + const patternKey = markers.join("\u0000"); + const pattern = patterns.get(patternKey) ?? markerPattern(markers); + patterns.set(patternKey, pattern); + if (hasSafetyComment(context.sourceCode, node, pattern)) return; + context.report({ + node, + messageId: "missingSafetyComment", + data: { marker: markers[0] ?? DEFAULT_SAFETY_MARKERS[0] }, + }); + }; + + return { + TSAsExpression: checkAssertion, + TSTypeAssertion: checkAssertion, + }; + }, +}); diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/shared/array-method.ts b/.agents/skills/install-anti-slop/assets/anti-slop/shared/array-method.ts new file mode 100644 index 0000000..87eb9d8 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/shared/array-method.ts @@ -0,0 +1,94 @@ +import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; + +/** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */ +export function unwrapArrayExpression(node: ESTree.Node): ESTree.Node { + while ( + node.type === "ParenthesizedExpression" || + node.type === "ChainExpression" || + node.type === "TSAsExpression" || + node.type === "TSTypeAssertion" || + node.type === "TSNonNullExpression" || + node.type === "TSSatisfiesExpression" + ) { + node = node.expression; + } + return node; +} + +/** Resolve a local binding by scope, not by identifier spelling. */ +export function resolveArrayBinding(sourceCode: SourceCode, node: ESTree.Node): Variable | null { + node = unwrapArrayExpression(node); + if (node.type !== "Identifier") return null; + let scope: Scope | null = sourceCode.getScope(node); + while (scope !== null) { + const variable = scope.set.get(node.name); + if (variable !== undefined) return variable; + scope = scope.upper; + } + return null; +} + +/** Read static method names, including computed string literals, without evaluating expressions. */ +export function arrayMethodTarget( + node: ESTree.Node, +): { readonly name: string; readonly object: ESTree.Node } | null { + node = unwrapArrayExpression(node); + if (node.type !== "MemberExpression") return null; + const property = node.property; + if (!node.computed && property.type === "Identifier") { + return { name: property.name, object: node.object }; + } + if (node.computed && property.type === "Literal" && typeof property.value === "string") { + return { name: property.value, object: node.object }; + } + return null; +} + +function isArrayAnnotation(type: ESTree.TSType): boolean { + if (type.type === "TSArrayType" || type.type === "TSTupleType") return true; + if (type.type === "TSParenthesizedType") return isArrayAnnotation(type.typeAnnotation); + if (type.type === "TSTypeOperator" && type.operator === "readonly") { + return isArrayAnnotation(type.typeAnnotation); + } + return ( + type.type === "TSTypeReference" && type.typeName.type === "Identifier" && + (type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray") + ); +} + +/** Recognize local array evidence; unknown receivers and iterator pipelines are deliberately excluded. */ +export function isKnownArrayExpression( + sourceCode: SourceCode, + node: ESTree.Node, + visited = new Set(), +): boolean { + node = unwrapArrayExpression(node); + if (node.type === "ArrayExpression") return true; + if (node.type === "CallExpression") { + const method = arrayMethodTarget(node.callee); + return ( + method !== null && + ["map", "filter", "flatMap", "slice", "concat", "toSorted", "toReversed", "toSpliced"].includes(method.name) && + isKnownArrayExpression(sourceCode, method.object, visited) + ); + } + if (node.type !== "Identifier") return false; + const variable = resolveArrayBinding(sourceCode, node); + if (variable === null || visited.has(variable)) return false; + visited.add(variable); + if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false; + for (const identifier of variable.identifiers) { + const annotation = identifier.typeAnnotation?.typeAnnotation; + if (annotation !== undefined) return isArrayAnnotation(annotation); + } + for (const definition of variable.defs) { + if ( + definition.type === "Variable" && definition.node.type === "VariableDeclarator" && + definition.node.id.type === "Identifier" && definition.node.init !== null && + definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const" + ) { + return isKnownArrayExpression(sourceCode, definition.node.init, visited); + } + } + return false; +} diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/shared/dictionary-types.ts b/.agents/skills/install-anti-slop/assets/anti-slop/shared/dictionary-types.ts new file mode 100644 index 0000000..8db73ff --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/shared/dictionary-types.ts @@ -0,0 +1,515 @@ +import type { ESTree } from "@oxlint/plugins"; + +import { + createTypeAliasEnvironment, + hasVisibleTypeBinding, + visibleTypeAlias, + type TypeAliasEnvironment as LexicalTypeAliasEnvironment, +} from "./type-alias-resolution.ts"; + +const BUILT_INS = new Set([ + "Record", + "Readonly", + "Partial", + "Required", + "Pick", + "Omit", + "PropertyKey", + "NonNullable", +]); +const TRANSPARENT_WRAPPERS = new Set(["Readonly", "Partial", "Required", "NonNullable"]); + +type TypeAliasEnvironment = ReadonlyMap; + +type ResolvedType = { + readonly type: ESTree.TSType; + readonly substitutions: TypeAliasEnvironment; +}; + +export type UnsafeDictionary = { + readonly kind: "unsafe-dictionary"; + readonly unsafeValue: "any" | "empty-object" | "object" | "union" | "unknown"; +}; + +export type WideningTargetKind = + | "anonymous object" + | "generic container" + | "object" + | "open dictionary" + | "unknown"; + +export type WideningTarget = { + readonly kind: WideningTargetKind; +}; + +export type TypeEnvironment = { + readonly interfaces: ReadonlyMap; + readonly typeAliases: LexicalTypeAliasEnvironment; +}; + +function declaredStatement(statement: ESTree.Statement): ESTree.Node | null { + return statement.type === "ExportNamedDeclaration" || + statement.type === "ExportDefaultDeclaration" + ? (statement.declaration ?? null) + : statement; +} + +export function createTypeEnvironment( + program: ESTree.Program, + visitorKeys: Readonly>, +): TypeEnvironment { + const interfaces = new Map(); + + for (const statement of program.body) { + const declaration = declaredStatement(statement); + if (declaration?.type !== "TSInterfaceDeclaration") continue; + const declarations = interfaces.get(declaration.id.name) ?? []; + declarations.push(declaration); + interfaces.set(declaration.id.name, declarations); + } + + return { + interfaces, + typeAliases: createTypeAliasEnvironment(program, visitorKeys), + }; +} + +function typeReferenceName(type: ESTree.TSTypeReference): string | null { + return type.typeName.type === "Identifier" ? type.typeName.name : null; +} + +function isBuiltIn( + name: string, + use: ESTree.Node, + environment: TypeEnvironment, +): boolean { + return ( + BUILT_INS.has(name) && + !hasVisibleTypeBinding(name, use, environment.typeAliases) + ); +} + +function isUnappliedReferenceTo(type: ESTree.TSType, name: string): boolean { + const unwrapped = unwrapTransparentType(type); + return ( + unwrapped.type === "TSTypeReference" && + typeReferenceName(unwrapped) === name && + (unwrapped.typeArguments === null || + unwrapped.typeArguments === undefined || + unwrapped.typeArguments.params.length === 0) + ); +} + +function unwrapTransparentType(type: ESTree.TSType): ESTree.TSType { + let current = type; + while ( + current.type === "TSParenthesizedType" || + (current.type === "TSTypeOperator" && current.operator === "readonly") + ) { + current = current.typeAnnotation; + } + return current; +} + +function isNeverType(type: ESTree.TSType): boolean { + return unwrapTransparentType(type).type === "TSNeverKeyword"; +} + +function isEffectivelyEmptyMember(member: ESTree.TSSignature): boolean { + return ( + member.type === "TSPropertySignature" && + member.optional === true && + member.typeAnnotation !== null && + member.typeAnnotation !== undefined && + isNeverType(member.typeAnnotation.typeAnnotation) + ); +} + +function isEffectivelyEmptyTypeLiteral(type: ESTree.TSTypeLiteral): boolean { + return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember); +} + +function isEffectivelyEmptyInterface( + declarations: readonly ESTree.TSInterfaceDeclaration[], +): boolean { + if (declarations.length !== 1) return false; + const [type] = declarations; + return ( + type !== undefined && + type.extends.length === 0 && + (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember)) + ); +} + +function resolvedSubstitutionArgument( + type: ESTree.TSType, + base: TypeAliasEnvironment, + resolving: ReadonlySet = new Set(), +): ESTree.TSType { + const unwrapped = unwrapTransparentType(type); + if (unwrapped.type !== "TSTypeReference") return type; + const name = typeReferenceName(unwrapped); + if (name === null || resolving.has(name)) return type; + const substitution = base.get(name); + if (substitution === undefined) return type; + const nextResolving = new Set(resolving); + nextResolving.add(name); + return resolvedSubstitutionArgument(substitution, base, nextResolving); +} + +function aliasSubstitution( + alias: ESTree.TSTypeAliasDeclaration, + type: ESTree.TSTypeReference, + base: TypeAliasEnvironment, +): TypeAliasEnvironment | null { + const parameters = alias.typeParameters?.params ?? []; + const arguments_ = type.typeArguments?.params ?? []; + const next = new Map(base); + for (const [index, parameter] of parameters.entries()) { + const argument = arguments_[index] ?? parameter.default; + if (argument === null || argument === undefined) return null; + next.set(parameter.name.name, resolvedSubstitutionArgument(argument, next)); + } + return next; +} + +function unsafeDirectValue( + type: ESTree.TSType, + environment: TypeEnvironment, + substitutions: TypeAliasEnvironment, + resolvingAliases: ReadonlySet, +): UnsafeDictionary["unsafeValue"] | null { + const unwrapped = unwrapTransparentType(type); + if (unwrapped.type === "TSUnknownKeyword") return "unknown"; + if (unwrapped.type === "TSAnyKeyword") return "any"; + if (unwrapped.type === "TSObjectKeyword") return "object"; + if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) + return "empty-object"; + if (unwrapped.type === "TSUnionType") { + return unwrapped.types.some( + (member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases) !== null, + ) + ? "union" + : null; + } + if (unwrapped.type === "TSIntersectionType") { + const unsafeMembers = unwrapped.types.map((member) => + unsafeDirectValue(member, environment, substitutions, resolvingAliases), + ); + if (unsafeMembers.includes("any")) return "any"; + return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null) + ? unsafeMembers[0] + : null; + } + if (unwrapped.type !== "TSTypeReference") return null; + const name = typeReferenceName(unwrapped); + if (name === null) return null; + if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) { + const wrapped = unwrapped.typeArguments?.params[0]; + return wrapped === undefined + ? null + : unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases); + } + const substitution = substitutions.get(name); + if (substitution !== undefined) { + return isUnappliedReferenceTo(substitution, name) + ? null + : unsafeDirectValue(substitution, environment, substitutions, resolvingAliases); + } + const interfaceDeclarations = environment.interfaces.get(name); + if (interfaceDeclarations !== undefined) { + return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null; + } + const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases); + if (alias === null || resolvingAliases.has(name)) return null; + const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions); + if (nextSubstitutions === null) return null; + const nextResolving = new Set(resolvingAliases); + nextResolving.add(name); + return unsafeDirectValue(alias.typeAnnotation, environment, nextSubstitutions, nextResolving); +} + +function dictionaryValueTypes( + type: ESTree.TSType, + environment: TypeEnvironment, + substitutions: TypeAliasEnvironment, + resolvingAliases: ReadonlySet, +): readonly ResolvedType[] { + const unwrapped = unwrapTransparentType(type); + + if (unwrapped.type === "TSTypeLiteral") { + return unwrapped.members.flatMap((member): readonly ResolvedType[] => + member.type === "TSIndexSignature" && member.typeAnnotation !== null + ? [{ type: member.typeAnnotation.typeAnnotation, substitutions }] + : [], + ); + } + + if (unwrapped.type === "TSMappedType") { + return unwrapped.typeAnnotation === null + ? [] + : [{ type: unwrapped.typeAnnotation, substitutions }]; + } + + if (unwrapped.type !== "TSTypeReference") return []; + const name = typeReferenceName(unwrapped); + if (name === null) return []; + + const substitution = substitutions.get(name); + if (substitution !== undefined) { + return isUnappliedReferenceTo(substitution, name) + ? [] + : dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases); + } + + if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) { + const wrapped = unwrapped.typeArguments?.params[0]; + return wrapped === undefined + ? [] + : dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases); + } + + if (name === "Record" && isBuiltIn(name, unwrapped, environment)) { + const value = unwrapped.typeArguments?.params[1] ?? null; + return value === null ? [] : [{ type: value, substitutions }]; + } + + if ( + (name === "Pick" || name === "Omit") && + isBuiltIn(name, unwrapped, environment) + ) { + const source = unwrapped.typeArguments?.params[0]; + return source === undefined + ? [] + : dictionaryValueTypes(source, environment, substitutions, resolvingAliases); + } + + const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases); + if (alias === null || resolvingAliases.has(name)) return []; + const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions); + if (nextSubstitutions === null) return []; + const nextResolving = new Set(resolvingAliases); + nextResolving.add(name); + return dictionaryValueTypes(alias.typeAnnotation, environment, nextSubstitutions, nextResolving); +} + +export function classifyUnsafeDictionaryValue( + valueType: ESTree.TSType, + environment: TypeEnvironment, +): UnsafeDictionary | null { + const unsafeValue = unsafeDirectValue(valueType, environment, new Map(), new Set()); + return unsafeValue === null ? null : { kind: "unsafe-dictionary", unsafeValue }; +} + +export function classifyUnsafeDictionary( + type: ESTree.TSType, + environment: TypeEnvironment, +): UnsafeDictionary | null { + for (const valueType of dictionaryValueTypes(type, environment, new Map(), new Set())) { + const unsafeValue = unsafeDirectValue( + valueType.type, + environment, + valueType.substitutions, + new Set(), + ); + if (unsafeValue !== null) return { kind: "unsafe-dictionary", unsafeValue }; + } + return null; +} + +export function classifyWideningTarget( + type: ESTree.TSType, + environment: TypeEnvironment, +): WideningTarget | null { + const unwrapped = unwrapTransparentType(type); + if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" }; + if (unwrapped.type === "TSObjectKeyword") return { kind: "object" }; + if (unwrapped.type === "TSTypeLiteral") { + return unwrapped.members.some((member) => member.type === "TSIndexSignature") + ? { kind: "open dictionary" } + : unwrapped.members.length > 0 + ? { kind: "anonymous object" } + : null; + } + if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" }; + if (unwrapped.type !== "TSTypeReference") return null; + const name = typeReferenceName(unwrapped); + if (name === null) return null; + if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) { + const wrapped = unwrapped.typeArguments?.params[0]; + return wrapped === undefined ? null : classifyWideningTarget(wrapped, environment); + } + if (name === "Record" && isBuiltIn(name, unwrapped, environment)) { + return hasBroadRecordKey(unwrapped, environment, new Map()) + ? { kind: "open dictionary" } + : null; + } + const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases); + if (alias === null) return null; + if ((alias.typeParameters?.params.length ?? 0) > 0) { + const substitutions = aliasSubstitution(alias, unwrapped, new Map()); + const resolved = + substitutions === null + ? null + : classifyAliasBroadTarget( + alias.typeAnnotation, + environment, + substitutions, + new Set([name]), + ); + return resolved?.kind === "open dictionary" ? { kind: "generic container" } : null; + } + const substitutions = aliasSubstitution(alias, unwrapped, new Map()); + if (substitutions === null) return null; + const resolved = classifyAliasBroadTarget( + alias.typeAnnotation, + environment, + substitutions, + new Set([name]), + ); + return resolved; +} + +function hasBroadRecordKey( + type: ESTree.TSTypeReference, + environment: TypeEnvironment, + substitutions: TypeAliasEnvironment, +): boolean { + const key = type.typeArguments?.params[0]; + return key === undefined || isBroadMappedKey(key, environment, substitutions); +} + +function isBroadMappedKey( + type: ESTree.TSType, + environment: TypeEnvironment, + substitutions: TypeAliasEnvironment, + visitedAliases: ReadonlySet = new Set(), +): boolean { + const unwrapped = unwrapTransparentType(type); + if ( + unwrapped.type === "TSStringKeyword" || + unwrapped.type === "TSNumberKeyword" || + unwrapped.type === "TSSymbolKeyword" + ) { + return true; + } + if (unwrapped.type === "TSUnionType") { + return unwrapped.types.some((member) => + isBroadMappedKey(member, environment, substitutions, visitedAliases), + ); + } + if (unwrapped.type !== "TSTypeReference") return false; + const name = typeReferenceName(unwrapped); + if (name === null) return false; + const substitution = substitutions.get(name); + if (substitution !== undefined && !isUnappliedReferenceTo(substitution, name)) { + return isBroadMappedKey(substitution, environment, substitutions, visitedAliases); + } + if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) return true; + const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases); + if ( + alias === null || + (alias.typeParameters?.params.length ?? 0) > 0 || + visitedAliases.has(name) + ) { + return false; + } + const nextVisited = new Set(visitedAliases); + nextVisited.add(name); + return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited); +} + +function classifyAliasBroadTarget( + type: ESTree.TSType, + environment: TypeEnvironment, + substitutions: TypeAliasEnvironment, + resolvingAliases: ReadonlySet, +): WideningTarget | null { + const unwrapped = unwrapTransparentType(type); + if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" }; + if (unwrapped.type === "TSObjectKeyword") return { kind: "object" }; + if (unwrapped.type === "TSTypeLiteral") { + return unwrapped.members.some((member) => member.type === "TSIndexSignature") + ? { kind: "open dictionary" } + : null; + } + if (unwrapped.type === "TSMappedType") { + return isBroadMappedKey(unwrapped.constraint, environment, substitutions) + ? { kind: "open dictionary" } + : null; + } + if (unwrapped.type !== "TSTypeReference") return null; + const name = typeReferenceName(unwrapped); + if (name === null) return null; + const substitution = substitutions.get(name); + if (substitution !== undefined) { + return isUnappliedReferenceTo(substitution, name) + ? null + : classifyAliasBroadTarget( + substitution, + environment, + substitutions, + resolvingAliases, + ); + } + if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) { + const wrapped = unwrapped.typeArguments?.params[0]; + return wrapped === undefined + ? null + : classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases); + } + if (name === "Record" && isBuiltIn(name, unwrapped, environment)) { + return hasBroadRecordKey(unwrapped, environment, substitutions) + ? { kind: "open dictionary" } + : null; + } + const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases); + if (alias === null || resolvingAliases.has(name)) return null; + const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions); + if (nextSubstitutions === null) return null; + const nextResolving = new Set(resolvingAliases); + nextResolving.add(name); + return classifyAliasBroadTarget( + alias.typeAnnotation, + environment, + nextSubstitutions, + nextResolving, + ); +} + +export function isPopulatedObjectExpression(expression: ESTree.Expression): boolean { + let current = expression; + while ( + current.type === "ParenthesizedExpression" || + current.type === "TSAsExpression" || + current.type === "TSTypeAssertion" || + current.type === "TSNonNullExpression" + ) { + current = current.expression; + } + return current.type === "ObjectExpression" && current.properties.length > 0; +} + +export function isKnownEvidenceExpression(expression: ESTree.Expression): boolean { + let current = expression; + while ( + current.type === "ParenthesizedExpression" || + current.type === "TSAsExpression" || + current.type === "TSTypeAssertion" || + current.type === "TSNonNullExpression" || + current.type === "TSSatisfiesExpression" + ) { + current = current.expression; + } + if (current.type === "ObjectExpression") return true; + return ( + current.type === "ArrayExpression" || + current.type === "ArrowFunctionExpression" || + current.type === "ClassExpression" || + current.type === "FunctionExpression" || + current.type === "NewExpression" || + current.type === "Literal" || + current.type === "TemplateLiteral" || + current.type === "UnaryExpression" + ); +} diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/shared/function-parameters.ts b/.agents/skills/install-anti-slop/assets/anti-slop/shared/function-parameters.ts new file mode 100644 index 0000000..80de91d --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/shared/function-parameters.ts @@ -0,0 +1,49 @@ +import type { ESTree, SourceCode } from "@oxlint/plugins"; + +export type FunctionParameter = ESTree.ParamPattern; + +/** Return whether a type is or contains TypeScript's absorbing unknown top type. */ +export function containsUnknownType(type: ESTree.TSType): boolean { + if (type.type === "TSUnknownKeyword") return true; + if (type.type === "TSParenthesizedType") return containsUnknownType(type.typeAnnotation); + return type.type === "TSUnionType" && type.types.some(containsUnknownType); +} + +/** Return the TypeScript annotation attached to a function parameter or its wrapped binding. */ +export function functionParameterTypeAnnotation( + parameter: FunctionParameter, +): ESTree.TSTypeAnnotation | null | undefined { + if (parameter.type === "TSParameterProperty") { + return functionParameterTypeAnnotation(parameter.parameter); + } + if (parameter.type === "RestElement") { + return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.argument); + } + if (parameter.type === "AssignmentPattern") { + return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.left); + } + return parameter.typeAnnotation; +} + +/** Return only a function parameter's local binding, excluding its annotation and default value. */ +export function functionParameterBindingName( + parameter: FunctionParameter, + sourceCode: SourceCode, +): string { + if (parameter.type === "TSParameterProperty") { + return functionParameterBindingName(parameter.parameter, sourceCode); + } + if (parameter.type === "AssignmentPattern") { + return functionParameterBindingName(parameter.left, sourceCode); + } + if (parameter.type === "RestElement") { + return functionParameterBindingName(parameter.argument, sourceCode); + } + if (parameter.type === "Identifier") return parameter.name; + + const sourceText = sourceCode.getText(parameter); + const annotationStart = parameter.typeAnnotation?.start; + return annotationStart === undefined + ? sourceText + : sourceText.slice(0, annotationStart - parameter.start).trimEnd(); +} diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/shared/lexical-type-parameters.ts b/.agents/skills/install-anti-slop/assets/anti-slop/shared/lexical-type-parameters.ts new file mode 100644 index 0000000..7cdb18c --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/shared/lexical-type-parameters.ts @@ -0,0 +1,61 @@ +import type { ESTree } from "@oxlint/plugins"; + +type VisitorKeys = Readonly>; + +function isNode(value: unknown): value is ESTree.Node { + return ( + typeof value === "object" && + value !== null && + "type" in value && + typeof value.type === "string" + ); +} + +function collectInferTypeParameterNames( + node: ESTree.Node, + visitorKeys: VisitorKeys, + names: Set, +): void { + if (node.type === "TSInferType") names.add(node.typeParameter.name.name); + const record = node as unknown as Readonly>; + for (const key of visitorKeys[node.type] ?? []) { + const value = record[key]; + if (isNode(value)) { + collectInferTypeParameterNames(value, visitorKeys, names); + continue; + } + if (!Array.isArray(value)) continue; + for (const child of value) { + if (isNode(child)) collectInferTypeParameterNames(child, visitorKeys, names); + } + } +} + +/** Collect type binders that are in scope at a node and can shadow module aliases. */ +export function lexicalTypeParameterNames( + node: ESTree.Node, + visitorKeys: VisitorKeys, +): ReadonlySet { + const names = new Set(); + let descendant: ESTree.Node = node; + let current: ESTree.Node | null = node; + while (current !== null && current.type !== "Program") { + if ("typeParameters" in current) { + for (const parameter of current.typeParameters?.params ?? []) { + names.add(parameter.name.name); + } + } + if ( + current.type === "TSMappedType" && + (descendant === current.nameType || descendant === current.typeAnnotation) + ) { + names.add(current.key.name); + } + if (current.type === "TSConditionalType" && descendant === current.trueType) { + collectInferTypeParameterNames(current.extendsType, visitorKeys, names); + } + descendant = current; + current = current.parent; + } + return names; +} diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/shared/reflect-method.ts b/.agents/skills/install-anti-slop/assets/anti-slop/shared/reflect-method.ts new file mode 100644 index 0000000..43a7cfc --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/shared/reflect-method.ts @@ -0,0 +1,24 @@ +import { resolveVariable } from "./scope.ts"; + +import type { ESTree, SourceCode } from "@oxlint/plugins"; + +function isGlobalReflect(sourceCode: SourceCode, expression: ESTree.Expression): boolean { + if (expression.type !== "Identifier" || expression.name !== "Reflect") return false; + if (sourceCode.isGlobalReference(expression)) return true; + const variable = resolveVariable(sourceCode, expression); + return variable === null || variable.defs.length === 0; +} + +/** Reports whether a call target names one method on the global Reflect object. */ +export function isGlobalReflectMethodCall( + sourceCode: SourceCode, + callee: ESTree.Expression, + methodName: string, +): boolean { + if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false; + if (!isGlobalReflect(sourceCode, callee.object)) return false; + const property = callee.property; + return callee.computed + ? property.type === "Literal" && property.value === methodName + : property.type === "Identifier" && property.name === methodName; +} diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/shared/scope.ts b/.agents/skills/install-anti-slop/assets/anti-slop/shared/scope.ts new file mode 100644 index 0000000..602414a --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/shared/scope.ts @@ -0,0 +1,15 @@ +import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; + +/** Resolve an identifier to its binding by walking lexical scopes upward. */ +export function resolveVariable( + sourceCode: SourceCode, + identifier: ESTree.IdentifierReference, +): Variable | null { + let scope: Scope | null = sourceCode.getScope(identifier); + while (scope !== null) { + const variable = scope.set.get(identifier.name); + if (variable !== undefined) return variable; + scope = scope.upper; + } + return null; +} diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/shared/type-alias-resolution.ts b/.agents/skills/install-anti-slop/assets/anti-slop/shared/type-alias-resolution.ts new file mode 100644 index 0000000..4744222 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/shared/type-alias-resolution.ts @@ -0,0 +1,250 @@ +import type { ESTree } from "@oxlint/plugins"; + +import { lexicalTypeParameterNames } from "./lexical-type-parameters.ts"; + +type VisitorKeys = Readonly>; +type TypeScope = ESTree.Node; + +type TypeBinding = { + readonly alias: ESTree.TSTypeAliasDeclaration | null; + readonly name: string; + readonly scope: TypeScope; +}; + +type Substitution = { + readonly substitutions: Substitutions; + readonly type: ESTree.TSType; +}; + +type Substitutions = ReadonlyMap; + +export type TypeAliasEnvironment = { + readonly aliases: readonly ESTree.TSTypeAliasDeclaration[]; + readonly bindingsByName: ReadonlyMap; + readonly visitorKeys: VisitorKeys; +}; + +export type ResolvedTypeMatcher = ( + type: ESTree.TSType, + matches: (child: ESTree.TSType) => boolean, +) => boolean; + +const environmentsByProgram = new WeakMap(); + +function isNode(value: unknown): value is ESTree.Node { + return ( + typeof value === "object" && + value !== null && + "type" in value && + typeof value.type === "string" + ); +} + +function enclosingTypeScope(node: ESTree.Node): TypeScope { + let current: ESTree.Node | null = node.parent; + while (current !== null) { + if ( + current.type === "Program" || + current.type === "BlockStatement" || + current.type === "TSModuleBlock" || + current.type === "StaticBlock" || + current.type === "SwitchStatement" + ) { + return current; + } + current = current.parent; + } + return node; +} + +function declaredTypeBinding(node: ESTree.Node): { + readonly alias: ESTree.TSTypeAliasDeclaration | null; + readonly name: string; +} | null { + if (node.type === "TSTypeAliasDeclaration") { + return { alias: node, name: node.id.name }; + } + if ( + node.type === "TSInterfaceDeclaration" || + node.type === "TSEnumDeclaration" || + node.type === "ClassDeclaration" || + node.type === "ClassExpression" + ) { + return node.id === null ? null : { alias: null, name: node.id.name }; + } + if ( + node.type === "ImportSpecifier" || + node.type === "ImportDefaultSpecifier" || + node.type === "ImportNamespaceSpecifier" + ) { + return { alias: null, name: node.local.name }; + } + return null; +} + +function collectTypeBindings( + node: ESTree.Node, + visitorKeys: VisitorKeys, + bindingsByName: Map, + aliases: ESTree.TSTypeAliasDeclaration[], +): void { + const declared = declaredTypeBinding(node); + if (declared !== null) { + const bindings = bindingsByName.get(declared.name) ?? []; + bindings.push({ ...declared, scope: enclosingTypeScope(node) }); + bindingsByName.set(declared.name, bindings); + if (declared.alias !== null) aliases.push(declared.alias); + } + + // SAFETY: Oxlint's visitor keys identify only ESTree child-node properties. + const fields = node as unknown as Readonly>; + for (const key of visitorKeys[node.type] ?? []) { + const value = fields[key]; + if (isNode(value)) { + collectTypeBindings(value, visitorKeys, bindingsByName, aliases); + continue; + } + if (!Array.isArray(value)) continue; + for (const child of value) { + if (isNode(child)) { + collectTypeBindings(child, visitorKeys, bindingsByName, aliases); + } + } + } +} + +/** Collect every lexical type alias and competing type binding in a program. */ +export function createTypeAliasEnvironment( + program: ESTree.Program, + visitorKeys: VisitorKeys, +): TypeAliasEnvironment { + const cached = environmentsByProgram.get(program); + if (cached !== undefined) return cached; + const bindingsByName = new Map(); + const aliases: ESTree.TSTypeAliasDeclaration[] = []; + collectTypeBindings(program, visitorKeys, bindingsByName, aliases); + const environment = { aliases, bindingsByName, visitorKeys }; + environmentsByProgram.set(program, environment); + return environment; +} + +function ancestorDistance(ancestor: ESTree.Node, node: ESTree.Node): number | null { + let current: ESTree.Node | null = node; + let distance = 0; + while (current !== null) { + if (current === ancestor) return distance; + current = current.parent; + distance += 1; + } + return null; +} + +function nearestTypeBindings( + name: string, + use: ESTree.Node, + environment: TypeAliasEnvironment, +): readonly TypeBinding[] { + const candidates = environment.bindingsByName.get(name) ?? []; + let nearestDistance = Number.POSITIVE_INFINITY; + let nearest: TypeBinding[] = []; + for (const candidate of candidates) { + const distance = ancestorDistance(candidate.scope, use); + if (distance === null || distance > nearestDistance) continue; + if (distance === nearestDistance) { + nearest.push(candidate); + continue; + } + nearestDistance = distance; + nearest = [candidate]; + } + return nearest; +} + +/** Resolve the nearest visible alias with this name, respecting lexical shadowing. */ +export function visibleTypeAlias( + name: string, + use: ESTree.Node, + environment: TypeAliasEnvironment, +): ESTree.TSTypeAliasDeclaration | null { + if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return null; + const bindings = nearestTypeBindings(name, use, environment); + return bindings.length === 1 ? (bindings[0]?.alias ?? null) : null; +} + +/** Return whether a local declaration shadows a built-in type at this use. */ +export function hasVisibleTypeBinding( + name: string, + use: ESTree.Node, + environment: TypeAliasEnvironment, +): boolean { + return ( + lexicalTypeParameterNames(use, environment.visitorKeys).has(name) || + nearestTypeBindings(name, use, environment).length > 0 + ); +} + +function typeReferenceName(type: ESTree.TSTypeReference): string | null { + return type.typeName.type === "Identifier" ? type.typeName.name : null; +} + +function aliasSubstitutions( + alias: ESTree.TSTypeAliasDeclaration, + reference: ESTree.TSTypeReference, + base: Substitutions, +): Substitutions | null { + const parameters = alias.typeParameters?.params ?? []; + const arguments_ = reference.typeArguments?.params ?? []; + const next = new Map(base); + for (const [index, parameter] of parameters.entries()) { + const explicitArgument = arguments_[index]; + const argument = explicitArgument ?? parameter.default; + if (argument === null || argument === undefined) return null; + const argumentSubstitutions = explicitArgument === undefined ? next : base; + next.set(parameter.name.name, { + type: argument, + substitutions: new Map(argumentSubstitutions), + }); + } + return next; +} + +/** Match a type after resolving visible aliases and substituting their type parameters. */ +export function resolvedTypeMatches( + type: ESTree.TSType, + environment: TypeAliasEnvironment, + matcher: ResolvedTypeMatcher, +): boolean { + const evaluate = ( + current: ESTree.TSType, + substitutions: Substitutions, + resolvingAliases: ReadonlySet, + ): boolean => { + if (current.type === "TSTypeReference") { + const name = typeReferenceName(current); + if (name !== null) { + const substitution = substitutions.get(name); + if (substitution !== undefined && !current.typeArguments?.params.length) { + return evaluate( + substitution.type, + substitution.substitutions, + resolvingAliases, + ); + } + const alias = visibleTypeAlias(name, current, environment); + if (alias !== null && !resolvingAliases.has(alias)) { + const nextSubstitutions = aliasSubstitutions(alias, current, substitutions); + if (nextSubstitutions !== null) { + const nextResolving = new Set(resolvingAliases); + nextResolving.add(alias); + return evaluate(alias.typeAnnotation, nextSubstitutions, nextResolving); + } + } + } + } + return matcher(current, (child) => + evaluate(child, substitutions, resolvingAliases), + ); + }; + + return evaluate(type, new Map(), new Set()); +} diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/LICENSE b/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/LICENSE new file mode 100644 index 0000000..38dbc35 --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright OpenJS Foundation and other contributors, +Copyright (c) 2023-PRESENT ESLint Stylistic contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/UPSTREAM.md b/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/UPSTREAM.md new file mode 100644 index 0000000..95d15ed --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/UPSTREAM.md @@ -0,0 +1,28 @@ +# Vendored padding-line-between-statements + +Source: [ESLint Stylistic](https://github.com/eslint-stylistic/eslint-stylistic), commit `435c3ea0fd26a5fef9042c4b36b6e165fbbf8d08`. + +Copied files: + +- `packages/eslint-plugin/rules/padding-line-between-statements/padding-line-between-statements.ts` +- `packages/eslint-plugin/rules/padding-line-between-statements/types.d.ts` → `padding-line-options.d.ts` +- Root `LICENSE`, retained verbatim. Both OpenJS Foundation and ESLint Stylistic notices apply. + +The rule is MIT-licensed. Keep `LICENSE` with every redistributed copy, including skill assets. No Stylistic, ESLint, TypeScript-ESLint, or additional parser runtime dependency is required. + +## Local adaptations + +- Replace upstream type aliases with Oxlint's ESTree, context, token/comment, and rule types. Upstream's token type includes comments; Oxlint exposes those separately. +- Replace `AST_NODE_TYPES` enum members with identical string literals. +- Guard indexed reads for consuming repositories with `noUncheckedIndexedAccess`. Impossible missing AST/configuration entries raise explicit invariant errors rather than introducing new non-null assertions. +- Replace the repository-specific `createRule` factory with `createPaddingLineRule(options, statementTypes?)`. The anti-slop wrapper supplies typed options directly; it exposes no user configuration options. The optional second argument registers caller-defined statement types, consulted before the upstream table, so a policy can match syntax the upstream matchers do not distinguish (exported bindings, comment ownership) without editing the vendored tables. +- Implement the small required AST helper surface in `padding-line-ast.ts` using Oxlint's public source-code/token API. `isParenthesized` only needs the one-pair check used to exclude parenthesized directive strings, not the upstream general-purpose overloads. +- Retain the upstream statement matchers, scope tracking, comment-aware insertion/removal, selector support, and diagnostic text. Upstream naming and non-null assumptions remain localized here to keep future diffs reviewable; the file is not a model for new application code. + +The opinionated policy lives outside this directory in `../../rules/require-readable-spacing.ts`. It adds spacing without collapsing existing blank lines. Short local bindings, consecutive imports, and adjacent overload signatures/implementation remain grouped. Spacing is syntactic, not an inference of business-logic boundaries. + +## Updating and verification + +Fetch an explicit upstream revision, compare the original rule and types against this revision, and port relevant fixes while retaining the adapters above. Update this record and preserve the license. Run `pnpm check` and `pnpm sync:skill-assets` as required by repository guidance. + +Focused Oxlint RuleTester cases live in `../../rules/require-readable-spacing.test.ts`; they test exact fixes, JSDoc/trailing comments, same-line statements, semicolon-free code, TypeScript exports/overloads, Effect-style generators, and upstream removal behavior. `../../rules/require-readable-spacing-cli.test.ts` verifies the exported plugin through the native Oxlint CLI on multiple files, including rejection, autofix, and repeated-fix stability. The complete upstream JS/TS test suites have not been ported; this is focused compatibility evidence, not a claim of full upstream conformance. diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts b/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts new file mode 100644 index 0000000..10f34cc --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts @@ -0,0 +1,51 @@ +// Local replacements for the upstream helper imports. See UPSTREAM.md. +import type { ESTree, SourceCode, Token as SyntaxToken, Comment, Location } from "@oxlint/plugins"; + +type Token = SyntaxToken | Comment; + +/** Line terminators recognized by the upstream padding matcher. */ +export const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]); + +/** Test a closing brace without treating comment text as punctuation. */ +export const isClosingBraceToken = (token: Token): boolean => + token.type === "Punctuator" && token.value === "}"; + +/** Test a semicolon without treating comment text as punctuation. */ +export const isSemicolonToken = (token: Token): boolean => + token.type === "Punctuator" && token.value === ";"; + +/** Filter the optional final semicolon when identifying block-like statements. */ +export const isNotSemicolonToken = (token: Token): boolean => !isSemicolonToken(token); + +/** Compare token/node boundaries, including attached comments. */ +export const isTokenOnSameLine = (left: { loc: Location }, right: { loc: Location }): boolean => + left.loc.end.line === right.loc.start.line; + +/** Recognize declarations and expressions used by the upstream IIFE matcher. */ +export const isFunction = (node: ESTree.Node): boolean => + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression" || + node.type === "ArrowFunctionExpression"; + +/** Preserve the upstream multiline statement heuristic. */ +export const isSingleLine = (node: ESTree.Node): boolean => + node.loc.start.line === node.loc.end.line; + +/** Unwrap optional chaining before checking IIFE syntax. */ +export const skipChainExpression = (node: ESTree.Node): ESTree.Node => + node.type === "ChainExpression" ? node.expression : node; + +/** Only a program or function-body expression can begin a directive prologue. */ +export const isTopLevelExpressionStatement = ( + node: ESTree.Node, +): node is ESTree.ExpressionStatement => + node.type === "ExpressionStatement" && + (node.parent.type === "Program" || + (node.parent.type === "BlockStatement" && isFunction(node.parent.parent))); + +/** A single wrapping pair suffices to exclude a string from directive syntax. */ +export function isParenthesized(node: ESTree.Node, sourceCode: SourceCode): boolean { + const before = sourceCode.getTokenBefore(node); + const after = sourceCode.getTokenAfter(node); + return before?.value === "(" && after?.value === ")"; +} diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts b/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts new file mode 100644 index 0000000..8e2c99f --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts @@ -0,0 +1,929 @@ +// Vendored from ESLint Stylistic; see UPSTREAM.md and LICENSE in this directory. +import type { ESTree, Context as RuleContext, SourceCode, Token as SyntaxToken, Comment, CreateRule, Location } from '@oxlint/plugins' +type ASTNode = ESTree.Node +type Token = SyntaxToken | Comment +import type { + PaddingType, + SelectorOption, + StatementMatcher, +} from './padding-line-options.d.ts' +import { + isClosingBraceToken, + isFunction, + isNotSemicolonToken, + isParenthesized, + isSemicolonToken, + isSingleLine, + isTokenOnSameLine, + isTopLevelExpressionStatement, + LINEBREAKS, + skipChainExpression, +} from './padding-line-ast.ts' + +const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u +const CJS_IMPORT = /^require\(/u + +/** + * This rule is a replica of padding-line-between-statements. + * + * Ideally we would want to extend the rule support typescript specific support. + * But since not all the state is exposed by the eslint and eslint has frozen stylistic rules, + * (see - https://eslint.org/blog/2020/05/changes-to-rules-policies for details.) + * we are forced to re-implement the rule here. + * + * We have tried to keep the implementation as close as possible to the eslint implementation, to make + * patching easier for future contributors. + * + * Reference rule - https://github.com/eslint/eslint/blob/main/lib/rules/padding-line-between-statements.js + */ + +type NodeTest = ( + node: ASTNode, + sourceCode: SourceCode, +) => boolean + +export interface NodeTestObject { + test: NodeTest +} + +/** + * A policy entry may name a caller-defined statement type (registered through + * `createPaddingLineRule`) anywhere the upstream statement types are accepted. + */ +export type PolicyStatementOption = + | StatementMatcher + | Extra + | [StatementMatcher | Extra, ...(StatementMatcher | Extra)[]] + +export type PolicyOptions = { + blankLine: PaddingType + prev: PolicyStatementOption + next: PolicyStatementOption +}[] + +const LT = `[${Array.from(LINEBREAKS).join('')}]` +const PADDING_LINE_SEQUENCE = new RegExp( + String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`, + 'u', +) + +function isSelectorOption(option: PolicyStatementOption): option is SelectorOption { + return typeof option === 'object' && !Array.isArray(option) +} + +/** + * Creates tester which check if a node starts with specific keyword with the + * appropriate AST_NODE_TYPES. + * @param keyword The keyword to test. + * @returns the created tester. + * @private + */ +function newKeywordTester( + type: string | string[], + keyword: string, +): NodeTestObject { + return { + test(node, sourceCode): boolean { + const isSameKeyword = sourceCode.getFirstToken(node)?.value === keyword + const isSameType = Array.isArray(type) + ? type.includes(node.type) + : type === node.type + + return isSameKeyword && isSameType + }, + } +} + +/** + * Creates tester which check if a node is specific type. + * @param type The node type to test. + * @returns the created tester. + * @private + */ +function newNodeTypeTester(type: string): NodeTestObject { + return { + test: (node): boolean => node.type === type, + } +} + +/** + * Checks the given node is an expression statement of IIFE. + * @param node The node to check. + * @returns `true` if the node is an expression statement of IIFE. + * @private + */ +function isIIFEStatement(node: ASTNode): boolean { + if (node.type === 'ExpressionStatement') { + let expression = skipChainExpression(node.expression) + if (expression.type === 'UnaryExpression') + expression = skipChainExpression(expression.argument) + + if (expression.type === 'CallExpression') { + let node: ASTNode = expression.callee + while (node.type === 'SequenceExpression') { + const lastExpression = node.expressions.at(-1) + if (lastExpression === undefined) + throw new Error('Padding rule invariant: sequence expression is empty') + node = lastExpression + } + + return isFunction(node) + } + } + return false +} + +/** + * Checks the given node is a CommonJS require statement + * @param node The node to check. + * @returns `true` if the node is a CommonJS require statement. + * @private + */ +function isCJSRequire(node: ASTNode): boolean { + if (node.type === 'VariableDeclaration') { + const declaration = node.declarations[0] + if (declaration?.init) { + let call = declaration?.init + while (call.type === 'MemberExpression') + call = call.object + + if ( + call.type === 'CallExpression' + && call.callee.type === 'Identifier' + ) { + return call.callee.name === 'require' + } + } + } + return false +} + +/** + * Checks whether the given node is a block-like statement. + * This checks the last token of the node is the closing brace of a block. + * @param sourceCode The source code to get tokens. + * @param node The node to check. + * @returns `true` if the node is a block-like statement. + * @private + */ +function isBlockLikeStatement( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + // do-while with a block is a block-like statement. + if ( + node.type === 'DoWhileStatement' + && node.body.type === 'BlockStatement' + ) { + return true + } + + /** + * IIFE is a block-like statement specially from + * JSCS#disallowPaddingNewLinesAfterBlocks. + */ + if (isIIFEStatement(node)) + return true + + // Checks the last token is a closing brace of blocks. + const lastToken = sourceCode.getLastToken(node, isNotSemicolonToken) + const belongingNode + = lastToken && isClosingBraceToken(lastToken) + ? sourceCode.getNodeByRangeIndex(lastToken.range[0]) + : null + + return ( + !!belongingNode + && (belongingNode.type === 'BlockStatement' + || belongingNode.type === 'SwitchStatement') + ) +} + +/** + * Check whether the given node is a directive or not. + * @param node The node to check. + * @param sourceCode The source code object to get tokens. + * @returns `true` if the node is a directive. + */ +function isDirective( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + return ( + isTopLevelExpressionStatement(node) + && node.expression.type === 'Literal' + && typeof node.expression.value === 'string' + && !isParenthesized(node.expression, sourceCode) + ) +} + +/** + * Check whether the given node is a part of directive prologue or not. + * @param node The node to check. + * @param sourceCode The source code object to get tokens. + * @returns `true` if the node is a part of directive prologue. + */ +function isDirectivePrologue( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + if ( + isDirective(node, sourceCode) + && node.parent + && 'body' in node.parent + && Array.isArray(node.parent.body) + ) { + for (const sibling of node.parent.body) { + if (sibling === node) + break + + if (!isDirective(sibling, sourceCode)) + return false + } + return true + } + return false +} + +/** + * Checks the given node is a CommonJS export statement + * @param node The node to check. + * @returns `true` if the node is a CommonJS export statement. + * @private + */ +function isCJSExport(node: ASTNode): boolean { + if (node.type === 'ExpressionStatement') { + const expression = node.expression + if (expression.type === 'AssignmentExpression') { + let left = expression.left + if (left.type === 'MemberExpression') { + while (left.object.type === 'MemberExpression') + left = left.object + + return ( + left.object.type === 'Identifier' + && (left.object.name === 'exports' + || (left.object.name === 'module' + && left.property.type === 'Identifier' + && left.property.name === 'exports')) + ) + } + } + } + return false +} + +/** + * Check whether the given node is an expression + * @param node The node to check. + * @param sourceCode The source code object to get tokens. + * @returns `true` if the node is an expression + */ +function isExpression( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + return ( + node.type === 'ExpressionStatement' + && !isDirectivePrologue(node, sourceCode) + ) +} + +/** + * Gets the actual last token. + * + * If a semicolon is semicolon-less style's semicolon, this ignores it. + * For example: + * + * foo() + * ;[1, 2, 3].forEach(bar) + * @param sourceCode The source code to get tokens. + * @param node The node to get. + * @returns The actual last token. + * @private + */ +function getActualLastToken( + node: ASTNode, + sourceCode: SourceCode, +): Token | null { + const semiToken = sourceCode.getLastToken(node)! + const prevToken = sourceCode.getTokenBefore(semiToken) + const nextToken = sourceCode.getTokenAfter(semiToken) + const isSemicolonLessStyle + = prevToken + && nextToken + && prevToken.range[0] >= node.range[0] + && isSemicolonToken(semiToken) + && !isTokenOnSameLine(prevToken, semiToken) + && isTokenOnSameLine(semiToken, nextToken) + + return isSemicolonLessStyle ? prevToken : semiToken +} + +/** + * This returns the concatenation of the first 2 captured strings. + * @param _ Unused. Whole matched string. + * @param trailingSpaces The trailing spaces of the first line. + * @param indentSpaces The indentation spaces of the last line. + * @returns The concatenation of trailingSpaces and indentSpaces. + * @private + */ +function replacerToRemovePaddingLines( + _: string, + trailingSpaces: string, + indentSpaces: string, +): string { + return trailingSpaces + indentSpaces +} + +function getReportLoc(node: ASTNode, sourceCode: SourceCode): Location { + if (isSingleLine(node)) + return node.loc + + const line = node.loc.start.line + const sourceLine = sourceCode.lines[line - 1] + if (sourceLine === undefined) + throw new Error('Padding rule invariant: statement source line is missing') + + return { + start: node.loc.start, + end: { + line, + column: sourceLine.length, + }, + } +} + +/** + * Check and report statements for `any` configuration. + * It does nothing. + * + * @private + */ +function verifyForAny(): void { + // Empty +} + +/** + * Check and report statements for `never` configuration. + * This autofix removes blank lines between the given 2 statements. + * However, if comments exist between 2 blank lines, it does not remove those + * blank lines automatically. + * @param context The rule context to report. + * @param _ Unused. The previous node to check. + * @param nextNode The next node to check. + * @param paddingLines The array of token pairs that blank + * lines exist between the pair. + * + * @private + */ +function verifyForNever( + context: RuleContext, + _: ASTNode, + nextNode: ASTNode, + paddingLines: [Token, Token][], +): void { + if (paddingLines.length === 0) + return + + context.report({ + node: nextNode, + messageId: 'unexpectedBlankLine', + loc: getReportLoc(nextNode, context.sourceCode), + fix(fixer) { + if (paddingLines.length >= 2) + return null + + const paddingPair = paddingLines[0] + if (paddingPair === undefined) + throw new Error('Padding rule invariant: reported padding pair is missing') + const [prevToken, nextToken] = paddingPair + const start = prevToken.range[1] + const end = nextToken.range[0] + const text = context + .sourceCode + .text + .slice(start, end) + .replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines) + + return fixer.replaceTextRange([start, end], text) + }, + }) +} + +/** + * Check and report statements for `always` configuration. + * This autofix inserts a blank line between the given 2 statements. + * If the `prevNode` has trailing comments, it inserts a blank line after the + * trailing comments. + * @param context The rule context to report. + * @param prevNode The previous node to check. + * @param nextNode The next node to check. + * @param paddingLines The array of token pairs that blank + * lines exist between the pair. + * + * @private + */ +function verifyForAlways( + context: RuleContext, + prevNode: ASTNode, + nextNode: ASTNode, + paddingLines: [Token, Token][], +): void { + if (paddingLines.length > 0) + return + + context.report({ + node: nextNode, + messageId: 'expectedBlankLine', + loc: getReportLoc(nextNode, context.sourceCode), + fix(fixer) { + const sourceCode = context.sourceCode + let prevToken = getActualLastToken(prevNode, sourceCode)! + const nextToken + = sourceCode.getFirstTokenBetween(prevToken, nextNode, { + includeComments: true, + + /** + * Skip the trailing comments of the previous node. + * This inserts a blank line after the last trailing comment. + * + * For example: + * + * foo(); // trailing comment. + * // comment. + * bar(); + * + * Get fixed to: + * + * foo(); // trailing comment. + * + * // comment. + * bar(); + * @param token The token to check. + * @returns `true` if the token is not a trailing comment. + * @private + */ + filter(token) { + if (isTokenOnSameLine(prevToken, token)) { + prevToken = token + return false + } + return true + }, + })! || nextNode + const insertText = isTokenOnSameLine(prevToken, nextToken) + ? '\n\n' + : '\n' + + return fixer.insertTextAfter(prevToken, insertText) + }, + }) +} + +/** + * Types of blank lines. + * `any`, `never`, and `always` are defined. + * Those have `verify` method to check and report statements. + * @private + */ +const PaddingTypes = { + any: { verify: verifyForAny }, + never: { verify: verifyForNever }, + always: { verify: verifyForAlways }, +} + +const MaybeMultilineStatementType: Record = { + 'block-like': { test: isBlockLikeStatement }, + 'expression': { test: isExpression }, + 'return': newKeywordTester('ReturnStatement', 'return'), + 'export': newKeywordTester( + [ + 'ExportAllDeclaration', + 'ExportDefaultDeclaration', + 'ExportNamedDeclaration', + ], + 'export', + ), + 'var': newKeywordTester('VariableDeclaration', 'var'), + 'let': newKeywordTester('VariableDeclaration', 'let'), + 'const': newKeywordTester('VariableDeclaration', 'const'), + 'using': { + test: node => node.type === 'VariableDeclaration' + && (node.kind === 'using' || node.kind === 'await using'), + }, + 'type': newKeywordTester('TSTypeAliasDeclaration', 'type'), +} + +/** + * Types of statements. + * Those have `test` method to check it matches to the given statement. + * @private + */ +const StatementTypes: Record = { + '*': { test: (): boolean => true }, + 'exports': { test: isCJSExport }, + 'require': { test: isCJSRequire }, + 'directive': { test: isDirectivePrologue }, + 'iife': { test: isIIFEStatement }, + + 'block': newNodeTypeTester('BlockStatement'), + 'empty': newNodeTypeTester('EmptyStatement'), + 'function': newNodeTypeTester('FunctionDeclaration'), + 'ts-method': newNodeTypeTester('TSMethodSignature'), + + 'break': newKeywordTester('BreakStatement', 'break'), + 'case': newKeywordTester('SwitchCase', 'case'), + 'class': newKeywordTester('ClassDeclaration', 'class'), + 'continue': newKeywordTester('ContinueStatement', 'continue'), + 'debugger': newKeywordTester('DebuggerStatement', 'debugger'), + 'default': newKeywordTester( + ['SwitchCase', 'ExportDefaultDeclaration'], + 'default', + ), + 'do': newKeywordTester('DoWhileStatement', 'do'), + 'for': newKeywordTester( + [ + 'ForStatement', + 'ForInStatement', + 'ForOfStatement', + ], + 'for', + ), + 'if': newKeywordTester('IfStatement', 'if'), + 'import': newKeywordTester('ImportDeclaration', 'import'), + 'switch': newKeywordTester('SwitchStatement', 'switch'), + 'throw': newKeywordTester('ThrowStatement', 'throw'), + 'try': newKeywordTester('TryStatement', 'try'), + 'while': newKeywordTester( + ['WhileStatement', 'DoWhileStatement'], + 'while', + ), + 'with': newKeywordTester('WithStatement', 'with'), + + 'cjs-export': { + test: (node, sourceCode) => node.type === 'ExpressionStatement' + && node.expression.type === 'AssignmentExpression' + && CJS_EXPORT.test(sourceCode.getText(node.expression.left)), + }, + 'cjs-import': { + test: (node, sourceCode) => node.type === 'VariableDeclaration' + && node.declarations.length > 0 + && node.declarations[0]?.init != null + && CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init)), + }, + + 'enum': newKeywordTester( + 'TSEnumDeclaration', + 'enum', + ), + 'interface': newKeywordTester( + 'TSInterfaceDeclaration', + 'interface', + ), + 'function-overload': newNodeTypeTester('TSDeclareFunction'), + ...Object.fromEntries( + Object.entries(MaybeMultilineStatementType) + .flatMap(([key, value]) => [ + [key, value], + [ + `singleline-${key}`, + { + ...value, + test: (node, sourceCode) => value.test(node, sourceCode) && isSingleLine(node), + }, + ], + [ + `multiline-${key}`, + { + ...value, + test: (node, sourceCode) => value.test(node, sourceCode) && !isSingleLine(node), + }, + ], + ]), + ), +} + +/** + * Build the vendored padding rule with caller-owned, typed policy options. + * `statementTypes` registers caller-defined statement types, looked up before + * the upstream table, for syntax the upstream matchers do not distinguish. + */ +export default function createPaddingLineRule( + options: PolicyOptions>, + statementTypes?: Record, +): CreateRule { +const customStatementTypes: Partial> = statementTypes ?? {} +return { + meta: { + type: 'layout', + docs: { + description: 'Require or disallow padding lines between statements', + }, + fixable: 'whitespace', + hasSuggestions: false, + // This is intentionally an array schema as you can pass 0..n config objects + schema: { + $defs: { + paddingType: { + type: 'string', + enum: Object.keys(PaddingTypes), + }, + statementType: { + type: 'string', + enum: Object.keys(StatementTypes), + }, + selectorOption: { + type: 'object', + properties: { + selector: { + type: 'string', + }, + lineMode: { + type: 'string', + enum: ['any', 'singleline', 'multiline'], + }, + }, + required: ['selector'], + additionalProperties: false, + }, + statementMatcher: { + anyOf: [ + { $ref: '#/$defs/statementType' }, + { $ref: '#/$defs/selectorOption' }, + ], + }, + statementOption: { + anyOf: [ + { $ref: '#/$defs/statementMatcher' }, + { + type: 'array', + items: { $ref: '#/$defs/statementMatcher' }, + minItems: 1, + uniqueItems: true, + additionalItems: false, + }, + ], + }, + }, + type: 'array', + additionalItems: false, + items: { + type: 'object', + properties: { + blankLine: { $ref: '#/$defs/paddingType' }, + prev: { $ref: '#/$defs/statementOption' }, + next: { $ref: '#/$defs/statementOption' }, + }, + additionalProperties: false, + required: ['blankLine', 'prev', 'next'], + }, + }, + messages: { + unexpectedBlankLine: 'Unexpected blank line before this statement.', + expectedBlankLine: 'Expected blank line before this statement.', + }, + }, + create(context) { + const sourceCode = context.sourceCode + + const selectorMatchedNodes = new Map>() + const pendingPairs: { prevNode: ASTNode, nextNode: ASTNode }[] = [] + + function collectSelectorOption(option: PolicyStatementOption): void { + if (Array.isArray(option)) { + for (const item of option) + collectSelectorOption(item) + return + } + + if (!isSelectorOption(option)) + return + + selectorMatchedNodes.set(option.selector, new Set()) + } + + for (const configure of options) { + collectSelectorOption(configure.prev) + collectSelectorOption(configure.next) + } + + type Scope = { + upper: Scope + prevNode: ASTNode | null + } | null + + let scopeInfo: Scope = null + + /** + * Processes to enter to new scope. + * This manages the current previous statement. + * + * @private + */ + function enterScope(): void { + scopeInfo = { + upper: scopeInfo, + prevNode: null, + } + } + + /** + * Processes to exit from the current scope. + * + * @private + */ + function exitScope(): void { + if (scopeInfo) + scopeInfo = scopeInfo.upper + } + + /** + * Checks whether the given node matches the given type. + * @param node The statement node to check. + * @param type The statement type to check. + * @returns `true` if the statement node matched the type. + * @private + */ + function match(node: ASTNode, type: PolicyStatementOption): boolean { + let innerStatementNode = node + + while (innerStatementNode.type === 'LabeledStatement') + innerStatementNode = innerStatementNode.body + + if (Array.isArray(type)) + return type.some(match.bind(null, innerStatementNode)) + + if (isSelectorOption(type)) { + const matchedNodes = selectorMatchedNodes.get(type.selector) + if (!matchedNodes?.has(innerStatementNode)) + return false + + const lineMode = type.lineMode + + if (lineMode === 'singleline') + return isSingleLine(innerStatementNode) + else if (lineMode === 'multiline') + return !isSingleLine(innerStatementNode) + + return true + } + else { + const statementType = customStatementTypes[type] ?? StatementTypes[type] + if (statementType === undefined) + throw new Error(`Padding rule invariant: unsupported statement type ${type}`) + return statementType.test(innerStatementNode, sourceCode) + } + } + + /** + * Finds the last matched configure from options. + * @param prevNode The previous statement to match. + * @param nextNode The current statement to match. + * @returns The tester of the last matched configure. + * @private + */ + function getPaddingType( + prevNode: ASTNode, + nextNode: ASTNode, + ): (typeof PaddingTypes)[keyof typeof PaddingTypes] { + for (let i = options.length - 1; i >= 0; --i) { + const configure = options[i] + if (configure === undefined) + throw new Error('Padding rule invariant: configuration entry is missing') + if ( + match(prevNode, configure.prev) + && match(nextNode, configure.next) + ) { + return PaddingTypes[configure.blankLine] + } + } + return PaddingTypes.any + } + + /** + * Gets padding line sequences between the given 2 statements. + * Comments are separators of the padding line sequences. + * @param prevNode The previous statement to count. + * @param nextNode The current statement to count. + * @returns The array of token pairs. + * @private + */ + function getPaddingLineSequences( + prevNode: ASTNode, + nextNode: ASTNode, + ): [Token, Token][] { + const pairs: [Token, Token][] = [] + let prevToken: Token = getActualLastToken(prevNode, sourceCode)! + + if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) { + do { + const token: Token = sourceCode.getTokenAfter(prevToken, { + includeComments: true, + })! + + if (token.loc.start.line - prevToken.loc.end.line >= 2) + pairs.push([prevToken, token]) + + prevToken = token + } while (prevToken.range[0] < nextNode.range[0]) + } + + return pairs + } + + /** + * Verify padding lines between the given node and the previous node. + * @param node The node to verify. + * + * @private + */ + function verify(node: ASTNode): void { + if ( + !node.parent + || ![ + 'BlockStatement', + 'Program', + 'StaticBlock', + 'SwitchCase', + 'SwitchStatement', + 'TSInterfaceBody', + 'TSModuleBlock', + 'TSTypeLiteral', + ].includes(node.parent.type) + ) { + return + } + + // Save this node as the current previous statement. + const prevNode = scopeInfo!.prevNode + + // Verify. + if (prevNode) + pendingPairs.push({ prevNode, nextNode: node }) + + scopeInfo!.prevNode = node + } + + function verifyPendingPairs(): void { + for (const { prevNode, nextNode } of pendingPairs) { + const type = getPaddingType(prevNode, nextNode) + const paddingLines = getPaddingLineSequences(prevNode, nextNode) + + type.verify(context, prevNode, nextNode, paddingLines) + } + } + + /** + * Verify padding lines between the given node and the previous node. + * Then process to enter to new scope. + * @param node The node to verify. + * + * @private + */ + function verifyThenEnterScope(node: ASTNode): void { + verify(node) + enterScope() + } + + const selectorMatchListeners = Object.fromEntries( + Array.from(selectorMatchedNodes.keys(), selector => [ + selector, + (node: ASTNode): void => { + selectorMatchedNodes.get(selector)?.add(node) + }, + ]), + ) + + return { + 'Program': enterScope, + 'Program:exit': () => { + verifyPendingPairs() + exitScope() + }, + 'BlockStatement': enterScope, + 'BlockStatement:exit': exitScope, + 'SwitchStatement': enterScope, + 'SwitchStatement:exit': exitScope, + 'SwitchCase': verifyThenEnterScope, + 'SwitchCase:exit': exitScope, + 'StaticBlock': enterScope, + 'StaticBlock:exit': exitScope, + + 'TSInterfaceBody': enterScope, + 'TSInterfaceBody:exit': exitScope, + 'TSModuleBlock': enterScope, + 'TSModuleBlock:exit': exitScope, + 'TSTypeLiteral': enterScope, + 'TSTypeLiteral:exit': exitScope, + 'TSDeclareFunction': verifyThenEnterScope, + 'TSDeclareFunction:exit': exitScope, + 'TSMethodSignature': verifyThenEnterScope, + 'TSMethodSignature:exit': exitScope, + + ':statement': verify, + ...selectorMatchListeners, + } + }, +} +} diff --git a/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts b/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts new file mode 100644 index 0000000..45068ec --- /dev/null +++ b/.agents/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts @@ -0,0 +1,87 @@ +/* GENERATED, DO NOT EDIT DIRECTLY */ + +/* @checksum: 3QCTtOH6rJM5_AGJ58rGpeEaBEfaJz17MSCxWB4X_PU */ + +export type PaddingType = 'any' | 'never' | 'always' +export type StatementOption = + | StatementMatcher + | [StatementMatcher, ...StatementMatcher[]] +export type StatementMatcher = + | StatementType + | SelectorOption +export type StatementType = + | '*' + | 'exports' + | 'require' + | 'directive' + | 'iife' + | 'block' + | 'empty' + | 'function' + | 'ts-method' + | 'break' + | 'case' + | 'class' + | 'continue' + | 'debugger' + | 'default' + | 'do' + | 'for' + | 'if' + | 'import' + | 'switch' + | 'throw' + | 'try' + | 'while' + | 'with' + | 'cjs-export' + | 'cjs-import' + | 'enum' + | 'interface' + | 'function-overload' + | 'block-like' + | 'singleline-block-like' + | 'multiline-block-like' + | 'expression' + | 'singleline-expression' + | 'multiline-expression' + | 'return' + | 'singleline-return' + | 'multiline-return' + | 'export' + | 'singleline-export' + | 'multiline-export' + | 'var' + | 'singleline-var' + | 'multiline-var' + | 'let' + | 'singleline-let' + | 'multiline-let' + | 'const' + | 'singleline-const' + | 'multiline-const' + | 'using' + | 'singleline-using' + | 'multiline-using' + | 'type' + | 'singleline-type' + | 'multiline-type' +export type PaddingLineBetweenStatementsSchema0 = { + blankLine: PaddingType + prev: StatementOption + next: StatementOption +}[] + +export interface SelectorOption { + selector: string + lineMode?: 'any' | 'singleline' | 'multiline' +} + +export type PaddingLineBetweenStatementsRuleOptions + = PaddingLineBetweenStatementsSchema0 + +export type RuleOptions + = PaddingLineBetweenStatementsRuleOptions +export type MessageIds = + | 'unexpectedBlankLine' + | 'expectedBlankLine' diff --git a/.agents/skills/install-anti-slop/references/update.md b/.agents/skills/install-anti-slop/references/update.md new file mode 100644 index 0000000..b2e7f67 --- /dev/null +++ b/.agents/skills/install-anti-slop/references/update.md @@ -0,0 +1,101 @@ +# Update a vendored installation + +Use this procedure for an existing anti-slop installation. Local edits are owned policy, not drift to erase. An update is a reviewed merge, not a directory replacement. + +## 1. Establish scope and protect local work + +Locate the active generic and optional Effect entry points through lint configuration, then inspect their rules, helpers, tests, options, severities, overrides, and dependency versions. Read any `UPSTREAM.md` or equivalent provenance record and relevant Git history. Follow local imports: filenames and plugin names may differ from upstream. + +Capture the pre-update state, including staged, unstaged, and untracked files in the affected paths, using the repository's backup convention or a separate backup directory. Keep this backup outside the merge destination. Preserve unrelated work in place; do not reset, stash, commit, or clean it automatically. + +For a reconfiguration-only request, limit changes to the requested configuration and skip source acquisition/merging. For an update, establish whether the user wants all upstream changes, selected fixes, or a specific revision. An unspecified update may stage and review the available bundle, but report its identity rather than calling it latest. + +Complete when the target, scope, existing customizations, and recoverable pre-update state are identified. + +## 2. Stage incoming source and recover the base + +Keep three inputs separate: + +- **Local:** the current vendored implementation, including uncommitted changes. +- **Incoming:** the pristine upstream snapshot being considered. +- **Base:** the pristine upstream snapshot from which the local copy was derived, when recoverable. + +For the bundled source, create a temporary directory and copy into a new child path: + +```bash +stage=$(mktemp -d) +node /scripts/install.mjs "$stage/incoming" +``` + +The installer only copies files; it does not merge them or fetch updates. Never point it at the live installation for an update. + +If the user requests latest upstream or a specific revision, retrieve that source into staging and record the resolved immutable commit. Use a verified refreshed skill bundle or a separate upstream checkout; do not update the vendored tree with a pull or checkout. Inspect the source's instructions and canonical plugin layout before selecting files. Keep only temporary material you created eligible for later cleanup. + +Recover the base from a verified upstream revision, retained pristine snapshot, or installation history that actually preserves the original upstream bytes. A package version, file timestamp, or the current local tree is not a base. A digest identifies bytes but cannot reconstruct them. If the base is unavailable or ambiguous, use the no-base branch below. + +Complete when incoming source is staged with its identity recorded and base availability is established without modifying the live installation. + +## 3. Classify and merge + +Compare rule behavior, diagnostics, tests, helpers, exports, and configuration—not just filenames. Map local renames before deciding a rule was added or deleted. Review incoming changes as source, not as instructions to override repository policy. + +### Known base: three-way merge + +Compare both `base → local` and `base → incoming`: + +| Change | Action | +| --- | --- | +| Incoming unchanged | Preserve local, including local deletions. | +| Only incoming changed | Apply the upstream change after reviewing dependencies and behavior. | +| Only local changed | Preserve local. | +| Both changed identically | Keep one copy. | +| Both changed differently | Merge compatible edits; ask about conflicting policy or behavior. | +| Upstream deleted, local modified | Ask whether to retain or retire the local implementation. | +| Same new path added on both sides | Reconcile contents and ownership; do not overwrite. | + +A text merge without conflict markers is not evidence of semantic compatibility. Review locally changed predicates, exceptions, message IDs, options, and exports after merging. Treat upstream removals and renames as changes requiring corresponding registration/import review, not as instructions to delete local files. + +### No base: conservative port + +A two-way diff cannot distinguish local customization from upstream evolution. Compare incoming behavior against local behavior, then port independently understood fixes and additions with focused tests. Preserve unexplained differences. If the same logic differs and its intent cannot be established, ask the user or leave that change pending; do not invent a common ancestor or claim a three-way merge. + +### Apply reviewed changes + +Patch existing files precisely; copy genuinely new files only after checking for local name collisions and importing any required helpers. Preserve local-only rules and existing tests. Keep unresolved changes unapplied and report them explicitly. Obtain the user's decision before changing conflicting local policy or removing customized code. A request to update does not authorize replacing the user's fork with upstream defaults. + +Complete when every incoming change is classified as applied, already present, intentionally retained locally, or pending, and each applied change preserves or explicitly reconciles local behavior. + +## 4. Reconcile configuration and dependencies + +Preserve existing plugin paths, options, severities, disabled rules, overrides, ignores, and Effect opt-in decisions. Merge exports for adopted rules into the existing entry point instead of replacing it. Check native companion rules when adopting related custom rules. + +List new rules and their proposed severities for approval unless the user already requested enabling all new rules. Existing disabled rules stay disabled. Merely adding a new rule implementation need not enable it. + +Check incoming API requirements against the installed `oxlint` and `@oxlint/plugins` versions. Keep a compatible pair and follow repository version policy; change dependencies only when required by the adopted source or requested by the user. Preserve unrelated dependency ranges and the package manager. + +Complete when every adopted rule is reachable through the intended entry point, configuration reflects approved policy, and the dependency pair supports the merged implementation. + +## 5. Verify local and incoming behavior + +Run existing tests and add focused RuleTester cases for changed rule semantics, including regressions that preserve local customizations. Incoming tests are useful evidence, not replacements for local tests. When a skill bundle lacks upstream tests, retrieve relevant tests from the identified upstream revision or write focused cases for the adopted changes. + +Exercise the registered plugin with representative accepted and rejected code. Run the repository's lint, typecheck, and required checks; for Vite+, run `vp check`. Vendored files may be excluded from application lint, so verify changed rules explicitly rather than treating an ignored directory as tested. + +Report application findings separately from plugin/test failures. Application cleanup requires the user's authorization; retain rule severity and safety checks while reporting findings. If a required check is unavailable, name the blocker and mark verification incomplete. + +Complete when adopted behavior and preserved local behavior have test evidence, checks have run, and every failure or verification gap is accounted for. + +## 6. Record the merge and hand back ownership + +Update `UPSTREAM.md` beside the vendored entry point, or the repository's existing provenance record, with: + +- incoming source identity and the recoverable base used, or explicit unknown-base status; +- adopted changes and intentional local deviations; +- deferred/conflicting changes and the decisions still needed; +- dependency/configuration changes and verification results. + +Advance the whole-installation baseline only when the complete incoming snapshot has been reconciled and remaining differences are recorded as intentional local changes. For a partial update, retain the prior baseline and record exactly which changes were adopted or remain pending; do not label the entire tree as matching the new revision. Without a recoverable base, keep that limitation explicit for the next update. + +Review the final diff against the pre-update state. Report applied updates, preserved customizations, pending decisions, checks, and the backup location. Keep backups until the user accepts the result; remove only disposable staging material you created. + +Complete when the user can distinguish upstream updates from local policy, recover the previous state, and identify what a future update must still reconcile. diff --git a/.agents/skills/install-anti-slop/scripts/install.mjs b/.agents/skills/install-anti-slop/scripts/install.mjs new file mode 100644 index 0000000..db1f155 --- /dev/null +++ b/.agents/skills/install-anti-slop/scripts/install.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import { cpSync, existsSync, mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const skillRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const source = resolve(skillRoot, "assets/anti-slop"); +const arguments_ = process.argv.slice(2); +const targetArgument = arguments_.find((argument) => !argument.startsWith("--")); +const target = resolve(process.cwd(), targetArgument ?? "tools/oxlint/anti-slop"); +const force = arguments_.includes("--force"); + +if (existsSync(target) && !force) { + console.error(`Refusing to overwrite ${target}. Re-run with --force only after reviewing the existing files.`); + process.exit(1); +} + +mkdirSync(dirname(target), { recursive: true }); +cpSync(source, target, { recursive: true, force }); +console.log(`Copied the anti-slop plugin to ${target}`); +console.log(`Configure Oxlint with: ${target}/index.ts`); diff --git a/.agents/skills/modern-css/SKILL.md b/.agents/skills/modern-css/SKILL.md new file mode 100644 index 0000000..49d70cc --- /dev/null +++ b/.agents/skills/modern-css/SKILL.md @@ -0,0 +1,50 @@ +--- +name: modern-css +description: Implement or debug CSS layouts, responsive styles, themes, and motion. Use when choosing native CSS features or replacing legacy styling with browser-compatible CSS; not for unrelated frontend logic. +--- + +# Modern CSS + +Choose native CSS features that fit the requested behavior and supported browsers. The user's explicit instructions take precedence over this skill's guidelines. Preserve the project's framework, preprocessor, tokens, and styling conventions unless changing them is part of the request. + +## Resolve the styling problem + +Use the affected markup/styles, desired appearance or interaction, and browser targets as input. Infer targets from project configuration when available. If missing support information changes the implementation, ask for it or state a conservative fallback. + +Bundled support tables are dated research snapshots, not a current compatibility guarantee. Check [MDN compatibility data](https://developer.mozilla.org/en-US/docs/Web/CSS) or [Web Platform Status](https://webstatus.dev/) for the particular feature when support matters. + +| Need | Starting point | +|---|---| +| Rows and columns, or aligned nested tracks | Grid / Subgrid | +| One-dimensional arrangement | Flexbox | +| Layout based on a component's available space | Container queries | +| Cascading overrides or specificity conflict | Layers, scope, selector specificity | +| Themes and derived colors | Custom properties, color functions, color-scheme | +| Motion, popovers, or scroll effects | Feature-specific reference and fallback | + +## Keep the result usable + +- Preserve layout and interaction in supported browsers. Use progressive enhancement or feature detection where a feature is unavailable. +- Match cascade changes to the existing origin, importance, and layer ordering; adding a layer can change precedence. +- Respect reduced-motion preferences and preserve focus/keyboard behavior when changing interactive components. +- Use modern CSS as an option, not a mandate to remove JavaScript or rebuild working controls. Support and equivalent behavior determine whether a replacement fits. +- Scope resets and stylesheet reorganization to the task; a component edit does not require a global reset. + +Deliver the requested style change or explanation and verify affected layout/states when implementing. State unverified browser limitations rather than treating an example as proof of support. + +## References + +Load only the relevant area, using its contents to find specific features. + +| Task | Reference | +|---|---| +| Layers, scope, nesting, specificity | [CASCADE.md](references/CASCADE.md) | +| Grid, Flexbox, containers, intrinsic sizing | [LAYOUT.md](references/LAYOUT.md) | +| Relational selectors, focus, pseudo-elements | [SELECTORS.md](references/SELECTORS.md) | +| Color spaces, palettes, dark mode | [COLOR.md](references/COLOR.md) | +| Custom properties, functions, design tokens | [TOKENS.md](references/TOKENS.md) | +| Transitions, keyframes, entry/exit, view transitions | [ANIMATION.md](references/ANIMATION.md) | +| Scroll timelines, sticky states, carousels | [SCROLL.md](references/SCROLL.md) | +| Dialogs, popovers, anchors, selects, field sizing | [COMPONENTS.md](references/COMPONENTS.md) | +| Rendering performance, typography, writing modes | [PERFORMANCE.md](references/PERFORMANCE.md) | +| Compact syntax or feature-detection lookup | [CHEATSHEET.md](references/CHEATSHEET.md) | diff --git a/.agents/skills/modern-css/references/ANIMATION.md b/.agents/skills/modern-css/references/ANIMATION.md new file mode 100644 index 0000000..99983ab --- /dev/null +++ b/.agents/skills/modern-css/references/ANIMATION.md @@ -0,0 +1,613 @@ +# Animation — Making Things Move + +> Sources: [CSS Transitions Level 2](https://www.w3.org/TR/css-transitions-2/), [CSS Animations Level 2](https://www.w3.org/TR/css-animations-2/), [CSS View Transitions Level 2](https://www.w3.org/TR/css-view-transitions-2/), [CSS Easing Functions Level 2](https://www.w3.org/TR/css-easing-2/), [Interop 2025](https://webkit.org/blog/16458/interop-2025/) + +CSS now handles entry/exit animations, intrinsic size interpolation, custom easing curves, cross-document transitions, and responsive shape morphing — all without JavaScript. + +Respect `prefers-reduced-motion` for the animation being implemented. A global +reset is one project-level option; component-level alternatives appear inline. + +## Contents + +- [Universal Reduced-Motion Reset](#universal-reduced-motion-reset) +- [Animation Strategy Decision Flowchart](#animation-strategy-decision-flowchart) +- [1. `@starting-style` — Entry Animations](#1-starting-style--entry-animations) +- [2. `transition-behavior: allow-discrete`](#2-transition-behavior-allow-discrete) +- [3. The Canonical Entry/Exit Pattern](#3-the-canonical-entryexit-pattern) +- [4. `interpolate-size: allow-keywords` — Animate to/from `auto`](#4-interpolate-size-allow-keywords--animate-tofrom-auto) +- [5. `calc-size()` — Math on Intrinsic Sizes](#5-calc-size--math-on-intrinsic-sizes) +- [6. `linear()` Easing — Custom Curves](#6-linear-easing--custom-curves) +- [7. View Transitions API](#7-view-transitions-api) +- [8. `shape()` Function — Responsive Shapes](#8-shape-function--responsive-shapes) +- [9. `corner-shape` — Non-Rounded Corners](#9-corner-shape--non-rounded-corners) +- [10. `prefers-reduced-motion` — The Accessibility Contract](#10-prefers-reduced-motion--the-accessibility-contract) +- [Anti-Patterns](#anti-patterns) + +--- + +## Universal Reduced-Motion Reset + +Consider this reset when establishing a project's motion policy. For a local +animation change, use the project's existing policy or scope reduced-motion +styles to the affected component. Preserve essential feedback and lifecycle events. + +```css +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} +``` + +Use `0.01ms` not `0s` — keeps `transitionend`/`animationend` events firing so JS listeners do not break. + +--- + +## Animation Strategy Decision Flowchart + +```mermaid +flowchart TD + START(["What kind of animation?"]) --> Q1{"Element entering
or exiting DOM?"} + Q1 -->|Yes| ENTRY["@starting-style +
transition-behavior:
allow-discrete"] + Q1 -->|No| Q2{"Animating to/from
auto or intrinsic size?"} + Q2 -->|Yes| Q2a{"Need math on
the size value?"} + Q2a -->|No| INTERP["interpolate-size:
allow-keywords"] + Q2a -->|Yes| CALC["calc-size()"] + Q2 -->|No| Q3{"Custom easing
(bounce/spring)?"} + Q3 -->|Yes| LINEAR["linear() easing"] + Q3 -->|No| Q4{"Page or view
navigation?"} + Q4 -->|Yes| VT["View Transitions API"] + Q4 -->|No| Q5{"Shape morphing
or path animation?"} + Q5 -->|Yes| SHAPE["shape() function"] + Q5 -->|No| Q6{"Simple A→B
property change?"} + Q6 -->|Yes| TRANS["CSS transition"] + Q6 -->|No| KF["@keyframes animation"] + + style START fill:#1a1a2e,color:#eee + style ENTRY fill:#0d3b66,color:#eee + style INTERP fill:#0d3b66,color:#eee + style CALC fill:#0d3b66,color:#eee + style LINEAR fill:#0d3b66,color:#eee + style VT fill:#0d3b66,color:#eee + style SHAPE fill:#0d3b66,color:#eee + style TRANS fill:#0d3b66,color:#eee + style KF fill:#0d3b66,color:#eee +``` + +--- + +## 1. `@starting-style` — Entry Animations + +Before `@starting-style`, there was no CSS way to animate an element's first render. Elements appeared at their final state instantly. Developers used `requestAnimationFrame` double-wrapping or class-toggle-after-a-frame hacks. + +`@starting-style` defines the "before" state for first paint. The browser applies these styles on frame one, then transitions to normal styles. + +```css +/* ❌ Before: JavaScript hack for fade-in */ +``` +```js +element.style.display = 'block'; +requestAnimationFrame(() => { + requestAnimationFrame(() => { + element.classList.add('visible'); + }); +}); +``` + +```css +/* ✅ After: Pure CSS entry animation */ +.tooltip { + opacity: 1; + transform: translateY(0); + transition: opacity 0.3s, transform 0.3s; + + @starting-style { + opacity: 0; + transform: translateY(-8px); + } +} +``` + +Nested form (above) is preferred — keeps entry state co-located. Standalone form also works: + +```css +@starting-style { + .tooltip { opacity: 0; transform: translateY(-8px); } +} +``` + +`@starting-style` alone handles elements always in the DOM. For `display: none` toggling (popovers, dialogs), pair with `transition-behavior: allow-discrete`. + +**Reduced motion:** Keep fade, remove transform. `@media (prefers-reduced-motion: reduce) { .tooltip { transition-duration: 0.15s; @starting-style { transform: none; } } }` + +**Browser support:** Baseline. + +--- + +## 2. `transition-behavior: allow-discrete` + +`display` and `visibility` are discrete properties — no intermediate values. Before `allow-discrete`, exit animations were impossible without JS to delay `display: none`. + +How it works: +- **Entry:** `display` flips to visible at **0%** of the transition. Element is visible immediately, animates in. +- **Exit:** `display` flips to hidden at **100%**. Element stays visible throughout, then disappears. + +```css +.panel { + transition: opacity 0.3s, display 0.3s allow-discrete; +} +.panel[hidden] { + opacity: 0; + display: none; +} +``` + +### The `overlay` Property + +For top-layer elements (popovers, dialogs), transition `overlay` to keep them in the top layer during exit. Without it, a popover exits the top layer immediately and the exit animation is clipped. + +```css +[popover] { + transition: opacity 0.3s, display 0.3s, overlay 0.3s; + transition-behavior: allow-discrete; +} +``` + +**Browser support:** Baseline. + +--- + +## 3. The Canonical Entry/Exit Pattern + +The unified pattern combining all three primitives. Use for popovers, dialogs, `[hidden]` toggling, and DOM insertion/removal. + +```css +@layer animations { + .animated-presence { + opacity: 1; + transform: translateY(0); + transition: + opacity 0.3s ease, + transform 0.3s ease, + display 0.3s ease allow-discrete, + overlay 0.3s ease allow-discrete; + + @starting-style { + opacity: 0; + transform: translateY(10px); + } + } + + .animated-presence[hidden], + .animated-presence:not(:popover-open), + .animated-presence:not([open]) { + opacity: 0; + transform: translateY(10px); + } +} +``` + +| Piece | Role | Without It | +|-------|------|------------| +| `@starting-style` | "from" state on entry | Appears at final state instantly | +| `allow-discrete` | `display` participates in transition | `display: none` instant, no exit animation | +| `overlay` transition | Keeps top-layer visible during exit | Popover disappears before animation completes | + +### Dialog with Backdrop + +```css +dialog { + opacity: 1; + transform: translateY(0); + transition: opacity 0.3s, transform 0.3s, + display 0.3s allow-discrete, overlay 0.3s allow-discrete; + + @starting-style { opacity: 0; transform: translateY(-20px); } +} +dialog:not([open]) { opacity: 0; transform: translateY(-20px); } + +dialog::backdrop { + background: hsl(0 0% 0% / 0); + transition: background 0.3s, display 0.3s allow-discrete, + overlay 0.3s allow-discrete; +} +dialog[open]::backdrop { background: hsl(0 0% 0% / 0.4); } +@starting-style { + dialog[open]::backdrop { background: hsl(0 0% 0% / 0); } +} +``` + +**Reduced motion:** `@media (prefers-reduced-motion: reduce) { .animated-presence { transition-duration: 0.01ms; } }` + +--- + +## Entry/Exit Lifecycle State Diagram + +```mermaid +stateDiagram-v2 + direction LR + + state "display: none" as Hidden + state "@starting-style applied" as Starting + state "Visible (final styles)" as Visible + state "Exit styles applied" as Exiting + + [*] --> Hidden + Hidden --> Starting : display toggled to visible + Starting --> Visible : transition runs (opacity, transform) + Visible --> Visible : interactive / stable + Visible --> Exiting : removal triggered + Exiting --> Hidden : transition completes, display flips at 100% + Hidden --> [*] + + note right of Starting + @starting-style provides the + "from" snapshot. display flips + at 0% (entry). + end note + + note right of Exiting + Element remains visible throughout + transition. display flips at 100%. + overlay keeps top-layer. + end note +``` + +--- + +## 4. `interpolate-size: allow-keywords` — Animate to/from `auto` + +CSS could never transition `height: auto`, `min-content`, `max-content`, or `fit-content`. The workaround was the `max-height: 9999px` hack — always wrong because duration maps to 9999px, not actual content height. + +```css +/* ❌ max-height hack — duration is always wrong */ +.accordion-body { + max-height: 0; + overflow: hidden; + transition: max-height 0.5s ease; +} +.accordion.open .accordion-body { + max-height: 9999px; /* 200px panel finishes in ~10ms */ +} +``` + +```css +/* ✅ interpolate-size — correct duration, smooth animation */ +:root { + interpolate-size: allow-keywords; +} + +.accordion-body { + height: 0; + overflow: hidden; + transition: height 0.4s ease; +} +.accordion.open .accordion-body { + height: auto; /* transitions to actual content height */ +} +``` + +Works with all intrinsic keywords: `auto`, `min-content`, `max-content`, `fit-content`. + +Setting on `:root` is safe — only affects elements that already have transitions on size properties. Does not change layout or computed values. Set once, forget it. + +**Reduced motion:** Keep functional open/close, just make it instant: `transition-duration: 0.01ms`. + +**Browser support:** Chromium-only (Chrome/Edge 129+) as of mid-2026. Not Baseline — other browsers snap instantly instead of animating, which is an acceptable degradation. Feature-detect: + +```css +@supports (interpolate-size: allow-keywords) { + :root { interpolate-size: allow-keywords; } +} +``` + +--- + +## 5. `calc-size()` — Math on Intrinsic Sizes + +`interpolate-size` enables interpolation but not arithmetic on keywords. `calc-size()` does. + +```css +.panel { height: calc-size(auto, size * 0.5); } /* half of auto */ +.tag { width: calc-size(fit-content, size + 2rem); } /* fit-content + padding */ +.cell { width: calc-size(min-content, max(size, 100px)); } /* floor */ +``` + +First argument: sizing keyword. Second: calculation using `size` as the resolved value. + +Animatable when both states use the same keyword: + +```css +.drawer { + height: calc-size(auto, size * 0); + overflow: hidden; + transition: height 0.3s ease; +} +.drawer.open { + height: calc-size(auto, size * 1); +} +``` + +| Scenario | Use | +|----------|-----| +| Animate `height: 0` to `height: auto` | `interpolate-size: allow-keywords` | +| Animate to 50% of `auto` height | `calc-size(auto, size * 0.5)` | +| Add padding to `fit-content` | `calc-size(fit-content, size + 1rem)` | +| Clamp `min-content` with a floor | `calc-size(min-content, max(size, 80px))` | + +**Browser support:** Chromium-only (Chrome/Edge 129+) as of mid-2026. Always feature-detect with `@supports (width: calc-size(auto, size))` and provide a static fallback. + +--- + +## 6. `linear()` Easing — Custom Curves + +`cubic-bezier()` is confined to a unit box — no bounce, spring, or overshoot. `linear()` defines unlimited control points. Values above 1 create overshoot; below 0 create undershoot. + +```css +.bounce { + transition: transform 0.6s linear( + 0, 0.004, 0.016, 0.035, 0.063, 0.098, 0.141, 0.191, + 0.25, 0.316, 0.391, 0.472, 0.562, 0.66, 0.765, 0.878, + 1, 0.956, 0.922, 0.898, 0.883, 0.878, 0.883, 0.898, + 0.922, 0.956, 1, 0.988, 0.981, 0.978, 0.981, 0.988, 1 + ); +} +``` + +With explicit positions: `linear(0, 0.5 25%, 1 50%, 0.8 75%, 1)` + +Store as custom properties for reuse: + +```css +:root { + --ease-bounce: linear(0, 0.004, 0.016, 0.035, 0.063, 0.098, 0.141, + 0.191, 0.25, 0.316, 0.391, 0.472, 0.562, 0.66, 0.765, 0.878, + 1, 0.956, 0.922, 0.898, 0.883, 0.878, 0.883, 0.898, 0.922, + 0.956, 1, 0.988, 0.981, 0.978, 0.981, 0.988, 1); + --ease-spring: linear(0, 0.009, 0.035, 0.078, 0.141, 0.223, 0.326, + 0.45, 0.594, 0.758, 0.938, 1.026, 1.063, 1.064, 1.042, 1.007, + 0.968, 0.938, 0.923, 0.925, 0.942, 0.966, 0.99, 1.006, 1.012, + 1.008, 0.998, 0.99, 0.988, 0.992, 0.998, 1.002, 1.003, 1.001, 1); +} +``` + +For complex easing curves, these tools can generate control points: +- [linear-easing-generator.netlify.app](https://linear-easing-generator.netlify.app/) — paste a JS easing function, get `linear()` output +- [easingwizard.com](https://easingwizard.com) — visual editor + +**Reduced motion:** Spring/bounce imply spatial motion. Replace with simple ease or remove. + +**Browser support:** Baseline. + +--- + +## 7. View Transitions API + +Animate state changes across the page — navigations, DOM updates, layout shifts. Browser captures a "before" snapshot, you apply the change, browser crossfades to "after." + +### Same-Document (Level 1) + +```js +document.startViewTransition(() => updateDOM()); +``` + +Name specific elements for independent transitions: + +```css +.hero-image { view-transition-name: hero; } +.page-title { view-transition-name: title; } + +::view-transition-old(hero) { animation: fade-out 0.3s; } +::view-transition-new(hero) { animation: fade-in 0.3s; } +::view-transition-group(hero) { animation-duration: 0.4s; } +``` + +Names must be unique on the page at any given time. + +### Cross-Document (Level 2) + +Opt in on both source and destination pages: + +```css +@view-transition { navigation: auto; } +``` + +Matching `view-transition-name` values across pages create shared-element transitions. + +### `view-transition-class` — Bulk Styling + +```css +.card { view-transition-class: card; } + +::view-transition-group(*.card) { + animation-duration: 0.3s; + animation-timing-function: var(--ease-spring); +} +``` + +### `view-transition-name: match-element` + +Auto-assigns unique names — essential for reorderable lists: + +```css +.list-item { view-transition-name: match-element; } +``` + +### Nested Groups + +Child transitions animate independently within a parent group: + +```css +.card { view-transition-name: card-1; } +.card img { view-transition-name: card-1-img; } + +::view-transition-group(card-1-img) { + view-transition-group: card-1; /* nest inside parent */ +} +``` + +**Reduced motion:** +```css +@media (prefers-reduced-motion: reduce) { + ::view-transition-group(*), + ::view-transition-old(*), + ::view-transition-new(*) { + animation-duration: 0.01ms !important; + } +} +``` + +**Browser support (mid-2026):** Same-document view transitions — including `view-transition-class` and `match-element` — are Baseline Newly Available since October 2025 (Chrome 111+, Safari 18+, Firefox 144+). Cross-document view transitions (`@view-transition`) are NOT Baseline: Chromium 126+ and Safari 18.2+ support them; Firefox has them behind a flag (an Interop 2026 focus area). Cross-document transitions degrade gracefully — unsupported browsers get a normal navigation — so use them freely as an enhancement. + +--- + +## 8. `shape()` Function — Responsive Shapes + +`path()` uses SVG coordinates — fixed pixels, not responsive. `shape()` uses CSS units and percentages. + +```css +/* ❌ path() — fixed pixel coordinates */ +.clip { clip-path: path('M 0 0 L 200 0 L 200 150 Q 100 200 0 150 Z'); } + +/* ✅ shape() — responsive to element size */ +.clip { + clip-path: shape(from 0% 0%, line to 100% 0%, line to 100% 70%, + curve to 0% 70% with 50% 100%, close); +} +``` + +Commands: `line to`, `curve to ... with`, `smooth to`, `arc to ... of`, `hline to`, `vline to`. + +Animatable when both states have the same number and type of commands: + +```css +.morph { + clip-path: shape(from 0% 0%, line to 100% 0%, + line to 100% 100%, line to 0% 100%, close); + transition: clip-path 0.5s var(--ease-spring); +} +.morph:hover { + clip-path: shape(from 10% 0%, line to 90% 0%, + line to 100% 100%, line to 0% 100%, close); +} +``` + +**Reduced motion:** `transition: none;` — show final state. + +**Browser support:** Not Baseline as of mid-2026 — Chrome/Edge 135+ and Safari 18.4+ support `shape()` in `clip-path`; Firefox does not. Feature-detect with `@supports (clip-path: shape(from 0% 0%, line to 100% 100%))`. + +--- + +## 9. `corner-shape` — Non-Rounded Corners + +`border-radius` only makes circles/ellipses. `corner-shape` adds geometric alternatives. + +```css +.card { border-radius: 20px; corner-shape: squircle; } /* superellipse */ +.tag { border-radius: 8px; corner-shape: bevel; } /* 45-deg chamfer */ +.badge { border-radius: 12px; corner-shape: notch; } /* inward rectangle */ +.frame { border-radius: 16px; corner-shape: scoop; } /* concave curve */ +``` + +`corner-shape` uses the `border-radius` value to determine treatment size. + +Per-corner: `corner-shape: squircle bevel squircle bevel;` (TL, TR, BR, BL). + +Animatable between shapes: + +```css +.card { + border-radius: 20px; + corner-shape: round; + transition: corner-shape 0.4s ease; +} +.card:hover { corner-shape: squircle; } +``` + +**Reduced motion:** `transition: none; corner-shape: squircle;` (apply preferred shape statically). + +**Browser support:** Chromium-only (Chrome/Edge 139+) as of mid-2026. Always feature-detect; plain `border-radius` is the automatic fallback: + +```css +@supports (corner-shape: squircle) { + .card { corner-shape: squircle; } +} +``` + +--- + +## 10. `prefers-reduced-motion` — The Accessibility Contract + +Every animation must have a reduced-motion path. Motion triggers vestibular disorders, nausea, and seizures. + +### Strategy: Universal Reset + Selective Override + +```css +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Re-enable essential animations */ + .spinner { + animation-duration: 1s !important; + animation-iteration-count: infinite !important; + } +} +``` + +### Per-Feature Summary + +| Feature | Reduced-Motion Approach | +|---------|------------------------| +| `@starting-style` | Keep opacity fade (short), remove transform | +| Entry/exit transitions | `transition-duration: 0.01ms` | +| `interpolate-size` | Instant expand/collapse (functional) | +| `linear()` bounce/spring | Replace with `ease` or remove | +| View Transitions | `animation-duration: 0.01ms` on pseudo-elements | +| `shape()` morphing | `transition: none`, show final state | +| `corner-shape` | `transition: none`, apply shape statically | + +### Motion-Aware Custom Properties + +```css +:root { + --motion-duration: 0.3s; + --motion-distance: 10px; +} +@media (prefers-reduced-motion: reduce) { + :root { + --motion-duration: 0.01ms; + --motion-distance: 0; + } +} + +.element { + transition: transform var(--motion-duration) ease; + @starting-style { transform: translateY(var(--motion-distance)); } +} +``` + +> For non-Baseline features, always feature-detect with `@supports` or use progressive enhancement. Check [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS) or [Baseline](https://web.dev/baseline) for current browser support. + +--- + +## Anti-Patterns + +| Anti-Pattern | Problem | Modern Replacement | +|---|---|---| +| `max-height: 9999px` | Wrong timing, always janky | `interpolate-size: allow-keywords` | +| `rAF` double-wrap for entry | Fragile timing hack | `@starting-style` | +| JS `setTimeout` to delay `display: none` | Race conditions, flickering | `transition-behavior: allow-discrete` | +| `cubic-bezier()` for bounce/spring | Cannot exceed unit box | `linear()` easing | +| Fixed-pixel SVG `path()` | Not responsive | `shape()` with CSS units | +| JS page transition libraries | Bundle size, complexity | View Transitions API | +| Ignoring `prefers-reduced-motion` | Accessibility violation | Universal reset + selective override | +| `animation: none` for reduced motion | Breaks JS event listeners | `animation-duration: 0.01ms` | +| `will-change` on everything | Wastes GPU memory | Only on elements being animated | diff --git a/.agents/skills/modern-css/references/CASCADE.md b/.agents/skills/modern-css/references/CASCADE.md new file mode 100644 index 0000000..78fb76e --- /dev/null +++ b/.agents/skills/modern-css/references/CASCADE.md @@ -0,0 +1,559 @@ +# Cascade Control — Controlling Which Styles Win + +> Sources: +> - [CSS Cascading and Inheritance Level 5](https://www.w3.org/TR/css-cascade-5/) — W3C (`@layer`) +> - [CSS Cascading and Inheritance Level 6](https://www.w3.org/TR/css-cascade-6/) — W3C (`@scope`, proximity) +> - [CSS Nesting Module](https://www.w3.org/TR/css-nesting-1/) — W3C +> - [CSS Conditional Rules Level 5](https://www.w3.org/TR/css-conditional-5/) — W3C (`@supports`, `@media`) +> - [CSS Display Level 4](https://www.w3.org/TR/css-display-4/) — W3C (`reading-flow`) +> - [MDN: CSS Cascade](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade) — Mozilla + +## Contents + +- [How the Cascade Resolves Styles](#how-the-cascade-resolves-styles) +- [1. Native CSS Nesting](#1-native-css-nesting) +- [2. Cascade Layers (`@layer`)](#2-cascade-layers-layer) +- [3. `@scope` — Proximity-Based Scoping](#3-scope--proximity-based-scoping) +- [4. `@supports` — Feature Queries](#4-supports--feature-queries) +- [5. `@media` Range Syntax](#5-media-range-syntax) +- [6. `reading-flow` — Tab Order Matching Visual Layout](#6-reading-flow--tab-order-matching-visual-layout) +- [Putting It All Together](#putting-it-all-together) + +## How the Cascade Resolves Styles + +Every style conflict resolves through this hierarchy. The first decisive step wins: + +```mermaid +flowchart TD + A["Two+ declarations target the same property"] --> B{"1. Transitions active?"} + B -- "Yes" --> B1["Transition value wins"] + B -- "No" --> C{"2. !important?"} + C -- "Yes" --> C1["Highest-origin !important wins
(layer order INVERTS for !important)"] + C -- "No" --> D{"3. @layer order?"} + D -- "Different layers" --> D1["Unlayered beats all layers.
Among layers: LAST declared wins.
(For !important: FIRST layer wins)"] + D -- "Same layer" --> E{"4. Specificity?"} + E -- "Different" --> E1["Higher specificity wins
(ID > class > element)"] + E -- "Tied" --> F{"5. @scope proximity?"} + F -- "Different" --> F1["Closer scope root wins"] + F -- "Same" --> G{"6. Source order?"} + G --> G1["Last declaration wins"] + + style B1 fill:#2d5016,color:#fff + style C1 fill:#2d5016,color:#fff + style D1 fill:#2d5016,color:#fff + style E1 fill:#2d5016,color:#fff + style F1 fill:#2d5016,color:#fff + style G1 fill:#2d5016,color:#fff +``` + +Memorize: **Transitions > Importance > Layers > Specificity > Proximity > Source Order**. + +--- + +## 1. Native CSS Nesting + +Use native nesting to co-locate related rules when supported by the project's +browser targets. Preserve an existing preprocessor unless its removal is part +of the task; native and preprocessor nesting are not interchangeable syntax. + +**Rules:** +- Nested rules implicitly start with `&` (the parent selector). +- Use explicit `&` to place the parent reference elsewhere (`a&`, `.parent &`). +- Bare element selectors (`div`, `span`) work directly without `&`. +- Nesting produces the same specificity as the equivalent flat selector. +- Nest at most 2-3 levels deep. Deeper nesting creates fragile, DOM-coupled selectors. + +Nest when rules are subordinate to a parent (states, pseudo-elements, media queries). Keep flat when selectors are independent concepts. + +```css +/* ❌ Preprocessor — requires build step, proprietary syntax */ +.card { + padding: 1rem; + &__title { font-size: 1.25rem; } + &:hover { box-shadow: 0 2px 8px rgb(0 0 0 / 0.15); } +} +``` + +```css +/* ✅ Native CSS nesting — no build step */ +.card { + padding: 1rem; + + .card-title { font-size: 1.25rem; } + + &:hover { box-shadow: 0 2px 8px rgb(0 0 0 / 0.15); } +} +``` + +### `&` Placement Patterns + +```css +.button { + background: steelblue; + &:hover { background: darkblue; } /* .button:hover */ + .icon { width: 1em; } /* .button .icon */ + .wrapper & { margin: 0; } /* .wrapper .button */ + a& { text-decoration: none; } /* a.button */ +} +``` + +### Nesting `@media` Inside a Rule + +```css +.grid { + display: grid; + grid-template-columns: 1fr; + + @media (width >= 768px) { grid-template-columns: repeat(2, 1fr); } + @media (width >= 1200px) { grid-template-columns: repeat(4, 1fr); } +} +``` + +--- + +## 2. Cascade Layers (`@layer`) + +Define layer order once at the top of your CSS and specificity wars disappear. A declaration in a later layer always beats an earlier layer, regardless of selector specificity. + +### The Core Insight + +Without layers, overriding styles requires higher specificity (leading to `!important` arms races) or later source order (fragile). Layers decouple "which styles win" from "how specific the selector is." + +### Layer Ordering + +```css +/* This single line controls your entire cascade architecture */ +@layer reset, base, components, utilities; +``` + +Priority (lowest to highest): `reset` < `base` < `components` < `utilities`. Styles in `utilities` beat `components` regardless of specificity. + +### Layer Stacking + +```mermaid +block-beta + columns 1 + block:cascade["Cascade Priority (top wins)"]:1 + U["Unlayered styles (highest — beats ALL layers)"] + L4["@layer utilities (last declared)"] + L3["@layer components"] + L2["@layer base"] + L1["@layer reset (first declared = lowest)"] + end + + style U fill:#7c2d12,color:#fff + style L4 fill:#1e40af,color:#fff + style L3 fill:#1e3a5f,color:#fff + style L2 fill:#1a3350,color:#fff + style L1 fill:#172540,color:#fff +``` + +**Critical:** Unlayered styles beat all layered styles. Use this intentionally for page-specific overrides. Avoid accidentally leaving styles unlayered. + +### Specificity Hacks vs `@layer` + +```css +/* ❌ Specificity arms race — fragile, escalates */ +.nav .menu .item a { color: gray; } +.nav .menu .item a.active { color: blue; } +#main-nav .nav .menu .item a.active { color: blue !important; } +``` + +```css +/* ✅ @layer — simple selectors, architectural control */ +@layer base, components, utilities; + +@layer base { a { color: gray; } } +@layer components { .active { color: blue; } } +@layer utilities { .text-primary { color: blue; } } +``` + +### Three Ways to Define Layers + +```css +/* 1. Block syntax */ +@layer components { + .card { padding: 1rem; } +} + +/* 2. Import into a layer */ +@import url("reset.css") layer(reset); +@import url("vendor.css") layer(vendor); + +/* 3. Anonymous layer (cannot append to later — use sparingly) */ +@layer { body { margin: 0; } } +``` + +### Sub-layers + +Use dot notation when a layer needs internal ordering: + +```css +@layer components.card, components.button, components.modal; + +@layer components.card { .card { border: 1px solid #ddd; } } +@layer components.modal { .modal .card { border: none; } } +``` + +Sub-layers resolve within their parent. `components.modal` never competes directly with `utilities`. + +### `!important` Inverts Layer Order + +When `!important` is used, layer priority **inverts**. The first-declared layer's `!important` beats the last-declared layer's `!important`. This lets resets protect critical styles: + +```css +@layer reset, components, utilities; + +@layer reset { + *, *::before, *::after { box-sizing: border-box !important; } + /* Beats utilities !important because reset is first */ +} +``` + +Avoid `!important` in layered architectures. The layer order should handle priority. + +### Recommended Architecture + +```css +@layer reset, base, layout, components, utilities; + +@import url("modern-reset.css") layer(reset); + +@layer base { + body { font-family: system-ui, sans-serif; line-height: 1.6; } + h1, h2, h3 { line-height: 1.2; text-wrap: balance; } +} + +@layer layout { + .page { display: grid; grid-template-rows: auto 1fr auto; min-height: 100dvh; } +} + +@layer components { + .card { padding: 1rem; border-radius: 8px; } + .button { padding: 0.5em 1em; border: none; cursor: pointer; } +} + +@layer utilities { + .visually-hidden { + clip: rect(0 0 0 0); clip-path: inset(50%); + height: 1px; overflow: hidden; + position: absolute; white-space: nowrap; width: 1px; + } +} +``` + +--- + +## 3. `@scope` — Proximity-Based Scoping + +Use `@scope` to limit where styles apply and to resolve conflicts by DOM proximity. Use it when component styles should not leak into nested sub-components. + +**Support:** Baseline Newly Available since December 2025 (Chrome 118+, Safari 17.4+, Firefox 146+). Older browsers ignore the whole `@scope` block, so keep a plain-selector fallback for anything essential. + +### Basic Scoping + +```css +@scope (.card) { + h2 { font-size: 1.25rem; } + p { color: #555; } + .actions { display: flex; gap: 1rem; } +} +``` + +`@scope` does not add specificity. The advantage is containment and proximity. + +### Donut Scoping — Excluding Inner Regions + +Use `to` to stop matching before a nested boundary. This prevents parent styles from leaking into child components: + +```css +@scope (.card) to (.card) { + p { color: #333; } + /* Does NOT match

inside a nested .card */ +} +``` + +```html +

+

Styled by the scope.

+
+

NOT styled — excluded by the "to .card" boundary.

+
+
+``` + +Use donut scoping for recursive/nested component patterns (comments, tree views, nested cards). + +### Proximity Resolution + +When two `@scope` blocks target the same element with equal specificity, the closer scope root wins. This is checked after specificity but before source order: + +```css +@scope (.light-theme) { p { color: #333; background: #fff; } } +@scope (.dark-theme) { p { color: #eee; background: #222; } } +``` + +```html +
+

Light (closest root is .light-theme).

+
+

Dark (closest root is .dark-theme — proximity wins).

+
+

Light again (inner .light-theme is closest).

+
+
+
+``` + +Proximity makes nested theming work without specificity tricks. + +### Inline ` +

Scoped to this widget only.

+ +``` + +### Theming with Donut Scopes + +Combine donut scoping with custom properties to prevent theme bleed-through: + +```css +.light-theme { --surface: #fff; --text: #1a1a1a; } +.dark-theme { --surface: #1a1a1a; --text: #e5e5e5; } + +@scope (.light-theme) to (.dark-theme) { + .card { background: var(--surface); color: var(--text); } +} +@scope (.dark-theme) to (.light-theme) { + .card { background: var(--surface); color: var(--text); } +} +``` + +### The Full Cascade Hierarchy (with Scope) + +1. **Importance** — `!important` (with inverted layer order) +2. **Layers** — later `@layer` beats earlier; unlayered beats all +3. **Specificity** — ID (1,0,0) > class (0,1,0) > element (0,0,1) +4. **Proximity** — closer `@scope` root wins +5. **Source order** — last declaration wins + +--- + +## 4. `@supports` — Feature Queries + +Use `@supports` when a supported target may lack the feature. Baseline status +does not cover every older browser or embedded WebView; omit detection when +the project's actual targets support the feature and no fallback is needed. + +### Feature Detection Patterns + +```css +/* Fallback */ +.grid { display: flex; flex-wrap: wrap; } + +/* Enhance when available */ +@supports (grid-template-columns: subgrid) { + .grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + } + .grid > * { display: grid; grid-template-rows: subgrid; } +} +``` + +### Detecting Modern Features + +```css +@supports selector(:has(a)) { /* :has() available */ } +@supports (container-type: inline-size) { /* container queries */ } +@supports (anchor-name: --tip) { /* anchor positioning */ } +@supports (reading-flow: grid-rows) { /* reading-flow */ } +``` + +`@supports` can only test declarations (`property: value`) and selectors (`selector(...)`). There is no cross-browser test for at-rules like `@scope` or `@starting-style` — the `at-rule()` function exists in the spec but only Chromium ships it. At-rules degrade safely anyway: unsupported browsers skip the whole block. + +Also note: `@supports (--x: anything)` is always true — custom properties accept any value, so they cannot be used to detect new value syntax. Test the new syntax in a real property instead (e.g. `@supports (transition-delay: calc(sibling-index() * 1ms))`). + +### Negation and Combination + +```css +@supports not (display: grid) { .layout { display: flex; } } +@supports (display: grid) and (grid-template-columns: subgrid) { /* both */ } +@supports (overflow: clip) or (overflow: hidden) { /* either */ } +``` + +### Progressive Enhancement Pattern + +Structure as enhancement layers — base works everywhere, enhancements add capabilities: + +```css +.card { padding: 1rem; background: white; } + +@supports (color: oklch(0.7 0.15 200)) { + .card { background: oklch(0.98 0.01 200); } +} + +/* At-rules need no @supports wrapper — unsupported browsers skip the block */ +@scope (.card) to (.card) { + p { margin-block: 0.5em; } +} +``` + +--- + +## 5. `@media` Range Syntax + +Use modern range syntax for media queries. More readable, less error-prone, Baseline in all modern browsers. + +```css +/* ❌ Legacy — verbose, off-by-one risk */ +@media (min-width: 768px) and (max-width: 1199px) { + .sidebar { width: 250px; } +} +``` + +```css +/* ✅ Modern — reads like math */ +@media (768px <= width < 1200px) { .sidebar { width: 250px; } } +@media (width >= 1200px) { .sidebar { width: 300px; } } +``` + +### Common Patterns + +```css +@media (width >= 768px) { /* tablet and up */ } +@media (width < 768px) { /* mobile only */ } +@media (768px <= width < 1200px) { /* tablet only */ } +@media (height >= 600px) { /* sufficient vertical space */ } +@media (aspect-ratio > 16/9) { /* ultrawide */ } +``` + +Combine with nesting to co-locate responsive rules: + +```css +.nav { + display: flex; + flex-direction: column; + @media (width >= 768px) { flex-direction: row; gap: 2rem; } +} +``` + +--- + +## 6. `reading-flow` — Tab Order Matching Visual Layout + +When CSS reorders content visually (`order`, `grid-row`, `flex-direction: row-reverse`), keyboard tab order still follows DOM order. `reading-flow` fixes this accessibility gap. + +**Support:** Chromium-only (Chrome/Edge 137+) as of mid-2026 — always feature-detect. In other browsers, prefer fixing the DOM order instead of relying on this property. + +```css +/* ❌ Visual order differs from tab order */ +.grid { display: grid; } +.grid .featured { order: -1; /* Visually first, tabbed last */ } +``` + +```css +/* ✅ Tab order follows visual layout */ +.grid { + display: grid; + reading-flow: grid-rows; +} +.flex-container { + display: flex; + reading-flow: flex-visual; +} +``` + +| Value | Behavior | +|-------|----------| +| `normal` | Tab follows DOM order (default) | +| `flex-visual` | Follows visual flex item order | +| `flex-flow` | Follows `flex-flow` direction | +| `grid-rows` | Follows visual grid rows | +| `grid-columns` | Follows visual grid columns | +| `grid-order` | Follows `order` property in grid | + +Apply whenever CSS changes visual order. Feature-detect first: + +```css +@supports (reading-flow: grid-rows) { + .reordered-grid { reading-flow: grid-rows; } +} +``` + +--- + +## Putting It All Together + +```css +@layer reset, base, layout, components, utilities; +@import url("reset.css") layer(reset); + +@layer base { + :root { --surface: oklch(0.99 0 0); --text: oklch(0.15 0 0); } + body { font-family: system-ui, sans-serif; color: var(--text); } +} + +@layer layout { + .page { + display: grid; grid-template-rows: auto 1fr auto; min-height: 100dvh; + @media (width >= 1200px) { grid-template-columns: 250px 1fr; } + } +} + +@layer components { + @scope (.card) to (.card) { + :scope { padding: 1rem; background: var(--surface); } + h2 { font-size: 1.25rem; } + .actions { display: flex; gap: 0.5rem; margin-block-start: 1rem; } + } +} + +@layer utilities { + .sr-only { + clip: rect(0 0 0 0); clip-path: inset(50%); + height: 1px; overflow: hidden; position: absolute; + white-space: nowrap; width: 1px; + } +} + +/* Unlayered — page-specific overrides (beats ALL layers) */ +.page-home .card { border: 2px solid oklch(0.6 0.15 250); } +``` + +### Decision Guide + +| Situation | Tool | +|-----------|------| +| Control which stylesheet wins | `@layer` | +| Prevent styles leaking to child components | `@scope` with donut scoping | +| Nested themes without specificity tricks | `@scope` proximity | +| Co-locate responsive + base styles | Native nesting + `@media` | +| Fallbacks for newer features | `@supports` | +| Readable breakpoints | `@media` range syntax | +| Fix tab order after visual reordering | `reading-flow` | +| Override layered styles without `!important` | Unlayered styles | +| Protect critical resets | `!important` in first-declared layer | + +### Anti-Patterns + +| Do Not | Instead | +|--------|---------| +| Leave styles unlayered in a layered codebase | Place all styles in an explicit layer | +| Use `!important` to win specificity battles | Use `@layer` ordering | +| Nest selectors deeper than 3 levels | Flatten or use `@scope` | +| Wrap Baseline features in `@supports` | Use them directly | +| Use `min-width`/`max-width` media syntax | Range syntax (`width >= 768px`) | +| Reorder visually without `reading-flow` | Add `reading-flow` or fix DOM order | +| Create anonymous layers for important styles | Name all layers | +| Rely on source order for critical overrides | Use explicit `@layer` ordering | diff --git a/.agents/skills/modern-css/references/CHEATSHEET.md b/.agents/skills/modern-css/references/CHEATSHEET.md new file mode 100644 index 0000000..c85076c --- /dev/null +++ b/.agents/skills/modern-css/references/CHEATSHEET.md @@ -0,0 +1,625 @@ +# Quick Reference Cheatsheet +> See [SKILL.md](../SKILL.md#references) for topic-specific references and their sources. + +## Contents + +- [Cascade Resolution Hierarchy](#cascade-resolution-hierarchy) +- [Layout Decision Flow](#layout-decision-flow) +- [Legacy to Modern Quick Upgrades](#legacy-to-modern-quick-upgrades) +- [CSS Replaces JavaScript](#css-replaces-javascript) +- [Property Quick Reference by Category](#property-quick-reference-by-category) +- [Color Function Syntax](#color-function-syntax) +- [Unit Reference](#unit-reference) +- [Logical Properties Mapping](#logical-properties-mapping) +- [Grid Pattern Recipes](#grid-pattern-recipes) +- [@supports Feature Detection](#supports-feature-detection) +- [Common Patterns](#common-patterns) +- [Progressive Enhancement Template](#progressive-enhancement-template) +- [Anti-Patterns Recap](#anti-patterns-recap) + +--- + +## Cascade Resolution Hierarchy + +```mermaid +flowchart TB + subgraph WINS["Highest Priority"] + style WINS fill:#2d6a2d,color:#fff + T["1. Transitions
(active transition wins)"] + end + + subgraph IMPORTANT["!important (reversed origin)"] + style IMPORTANT fill:#8b2500,color:#fff + I1["2a. UA !important"] + I2["2b. User !important"] + I3["2c. Author !important"] + end + + subgraph LAYERS["Cascade Layers"] + style LAYERS fill:#1a4a6e,color:#fff + L1["3. Unlayered styles
(beat ALL layers)"] + L2["4. @layer order
(last declared layer wins)"] + end + + subgraph SPECIFICITY["Specificity & Proximity"] + style SPECIFICITY fill:#4a3560,color:#fff + S["5. Specificity
(ID > class > element)"] + P["6. @scope proximity
(closer root wins)"] + end + + subgraph FALLBACK["Lowest Priority"] + style FALLBACK fill:#555,color:#fff + O["7. Source order
(later declaration wins)"] + end + + T --> I1 --> I2 --> I3 --> L1 --> L2 --> S --> P --> O +``` + +**Key rule:** Unlayered styles > Last layer > ... > First layer. Define order once: +```css +@layer reset, base, components, utilities; +/* reset = lowest priority, unlayered = highest */ +``` + +**Layer priority shorthand:** +``` +Unlayered > utilities > components > base > reset +``` + +**Specificity weight:** `(ID, CLASS, ELEMENT)` -- `:is()/:not()/:has()` take highest argument. `:where()` always 0. + +--- + +## Layout Decision Flow + +```mermaid +flowchart TB + START(["What layout do I need?"]) + D2D{"Need rows
AND columns?"} + D1D{"Need 1D
row or column?"} + DCQ{"Component must
adapt to its
container?"} + DMAS{"Masonry /
waterfall?"} + DSUB{"Children must
align to parent
grid tracks?"} + DCENTER{"Just centering
one element?"} + + GRID["CSS Grid"] + SUBGRID["Grid + Subgrid"] + FLEX["Flexbox"] + CQ["Container Query
+ Grid / Flex"] + LANES["grid-lanes
(experimental)"] + INTRINSIC["fit-content /
min-content /
stretch"] + CENTER["align-content: center
(no wrapper needed)"] + + START --> D2D + D2D -- Yes --> DSUB + DSUB -- Yes --> SUBGRID + DSUB -- No --> DMAS + DMAS -- Yes --> LANES + DMAS -- No --> GRID + D2D -- No --> D1D + D1D -- Yes --> DCQ + DCQ -- Yes --> CQ + DCQ -- No --> FLEX + D1D -- No --> DCENTER + DCENTER -- Yes --> CENTER + DCENTER -- No --> INTRINSIC + + style GRID fill:#1a6e3a,color:#fff + style SUBGRID fill:#1a6e3a,color:#fff + style FLEX fill:#1a4a6e,color:#fff + style CQ fill:#4a3560,color:#fff + style LANES fill:#8b6500,color:#fff + style INTRINSIC fill:#555,color:#fff + style CENTER fill:#2d6a2d,color:#fff +``` + +--- + +## Legacy to Modern Quick Upgrades + +### 1. Color syntax +```css +/* ❌ */ background: rgba(100, 50, 200, 0.5); +/* ✅ */ background: rgb(100 50 200 / 0.5); +``` + +### 2. Physical to logical +```css +/* ❌ */ margin-left: 1rem; +/* ✅ */ margin-inline-start: 1rem; +``` + +### 3. Preprocessor nesting to native +```css +/* ❌ */ .card { .title { /* requires Sass */ } } +/* ✅ */ .card { .title { color: inherit; } } /* native CSS nesting */ +``` + +### 4. Media query syntax +```css +/* ❌ */ @media (min-width: 768px) { } +/* ✅ */ @media (width >= 768px) { } +``` + +### 5. Specificity wars to layers +```css +/* ❌ */ .btn { color: red !important; } +/* ✅ */ @layer base { .btn { color: red; } } +``` + +### 6. JS parent selection to :has() +```css +/* ❌ */ /* JS: card.querySelector('img') && card.classList.add('has-img') */ +/* ✅ */ .card:has(img) { grid-template-rows: 200px 1fr; } +``` + +### 7. JS scroll listener to scroll-driven animation +```css +/* ❌ */ /* JS: window.addEventListener('scroll', updateProgress) */ +/* ✅ */ .progress { animation: grow linear; animation-timeline: scroll(); } +``` + +### 8. JS tooltip position to anchor positioning +```css +/* ❌ */ /* JS: Floating UI / Popper.js for tooltip placement */ +/* ✅ */ .tooltip { position: fixed; position-anchor: --trigger; position-area: block-start; } +``` + +### 9. Max-height hack to interpolate-size +```css +/* ❌ */ .panel { max-height: 9999px; transition: max-height 0.3s; } +/* ✅ */ .panel { interpolate-size: allow-keywords; transition: height 0.3s; height: auto; } +``` + +### 10. Sass variables to custom properties +```css +/* ❌ */ $primary: #3b82f6; /* Sass — compile-time only */ +/* ✅ */ :root { --primary: oklch(0.59 0.2 260); } /* runtime, inherits, responds to context */ +``` + +### 11. Sass color functions to color-mix / relative color +```css +/* ❌ */ darken($primary, 10%); /* Sass only */ +/* ✅ */ color-mix(in oklch, var(--primary), black 20%); +/* ✅ */ oklch(from var(--primary) calc(l - 0.1) c h); /* relative color */ +``` + +### 12. appearance: none to base-select +```css +/* ❌ */ select { appearance: none; } /* destroys all native behavior */ +/* ✅ */ select { appearance: base-select; } /* styleable AND functional */ +``` + +### 13. JS auto-resize textarea to field-sizing +```css +/* ❌ */ /* JS: textarea.style.height = textarea.scrollHeight + 'px' */ +/* ✅ */ textarea { field-sizing: content; } +``` + +### 14. Manual stagger delays to sibling-index() +```css +/* ❌ */ li:nth-child(1) { --d: 0ms; } li:nth-child(2) { --d: 50ms; } /* ... */ +/* ✅ */ li { transition-delay: calc(sibling-index() * 50ms); } +``` + +### 15. Hex colors to oklch +```css +/* ❌ */ color: #3b82f6; +/* ✅ */ color: oklch(0.59 0.2 260); /* perceptually uniform, P3 gamut */ +``` + +--- + +## CSS Replaces JavaScript + +| JS Pattern | CSS Replacement | +|---|---| +| Scroll position listeners | `animation-timeline: scroll()` | +| IntersectionObserver for reveal | `animation-timeline: view()` | +| Sticky header shadow toggle | `scroll-state(stuck: top)` | +| Floating UI / Popper.js | Anchor positioning (`position-anchor`, `position-area`) | +| Carousel prev/next/dots | `::scroll-button()`, `::scroll-marker` | +| Auto-expanding textarea | `field-sizing: content` | +| Staggered animation delays | `sibling-index()` in `calc()` | +| `max-height: 9999px` hack | `interpolate-size: allow-keywords` | +| Parent element selection | `:has()` | +| Theme toggle logic | `light-dark()` + `color-scheme` | +| Tooltip/popover show/hide | Popover API + invoker commands | +| Color manipulation | `color-mix()`, relative color syntax | + +--- + +## Property Quick Reference by Category + +### Layout + +| Property / At-Rule | Values / Syntax | Purpose | +|---|---|---| +| `display` | `grid`, `flex`, `contents`, `none` | Layout mode | +| `grid-template-columns` | `repeat(auto-fill, minmax(250px, 1fr))` | Responsive columns | +| `grid-template-rows` | `subgrid` | Inherit parent tracks | +| `grid-auto-flow` | `dense` | Fill gaps automatically | +| `container-type` | `inline-size`, `size`, `normal`, `scroll-state` | Enable container queries | +| `container-name` | `` | Name the container | +| `@container` | `(inline-size >= 400px)` | Query container dimensions | +| `@container style()` | `style(--theme: dark)` | Query container custom props | +| `align-content` | `center` (on block elements) | Vertical centering without flex/grid | +| `place-items` | `center` | Shorthand for align + justify items | +| `gap` | `1rem`, `1rem 2rem` | Row and column gap | + +### Cascade & Scope + +| Syntax | Purpose | Key Behavior | +|---|---|---| +| `@layer reset, base, components;` | Declare layer order | First = lowest priority | +| `@layer base { ... }` | Add rules to a layer | Unlayered beats all layers | +| `@scope (.card) to (.card-footer)` | Limit style reach | Proximity wins over specificity | +| `.parent { .child { } }` | Native nesting | `&` optional — bare element selectors also work (`div { }`) | +| `.parent { & .child { } }` | Explicit nesting | Use `&` to reposition parent: `a&`, `.wrap &` | +| `.parent { @media (...) { } }` | Nested media query | Scoped to parent context | +| `@import url() layer(name)` | Import into a layer | Third-party CSS isolation | + +### Color + +| Function | Syntax | Notes | +|---|---|---| +| `oklch()` | `oklch(L C H)` or `oklch(L C H / A)` | L: 0-1, C: 0-0.4, H: 0-360 | +| `oklab()` | `oklab(L a b)` or `oklab(L a b / A)` | Perceptual, good for gradients | +| `color-mix()` | `color-mix(in oklch, color1, color2 %)` | Mix two colors in any space | +| `light-dark()` | `light-dark(lightVal, darkVal)` | Requires `color-scheme` set | +| Relative color | `oklch(from var(--c) calc(l - 0.1) c h)` | Derive colors from base | +| `color()` | `color(display-p3 1 0 0)` | Wide-gamut P3 colors | + +### Animation & Transitions + +| Property / At-Rule | Syntax | Purpose | +|---|---|---| +| `@starting-style` | `@starting-style { .el { opacity: 0; } }` | Entry animation initial state | +| `transition-behavior` | `allow-discrete` | Animate `display`, `overlay` | +| `interpolate-size` | `allow-keywords` | Animate to/from `auto` | +| `animation-timeline` | `scroll()`, `view()`, `--name` | Scroll-driven animations | +| `animation-range` | `entry 0% entry 100%` | Visible range for `view()` | +| `view-transition-name` | `` | Opt element into view transition | +| `linear()` | `linear(0, 0.5 25%, 1)` | Custom easing with stops | +| `overlay` | `auto`, `none` | Keep element in top layer during exit | + +### Selectors + +| Selector | Specificity | Purpose | +|---|---|---| +| `:has()` | Highest in list | Parent/relational selection | +| `:is()` | Highest in list | Grouping (carries specificity) | +| `:where()` | Always 0 | Grouping (zero specificity) | +| `:not()` | Highest in list | Negation with selector list | +| `:focus-visible` | (0,1,0) | Keyboard-only focus ring | +| `:user-valid` / `:user-invalid` | (0,1,0) | Form validation after interaction | +| `:nth-child(... of S)` | (0,1,0) + S | Filter nth-child by selector | + +--- + +## Color Function Syntax + +| Function | Parameters | Example | +|---|---|---| +| `oklch(L C H)` | L: 0-1 lightness, C: 0-0.4 chroma, H: 0-360 hue | `oklch(0.7 0.15 145)` | +| `oklch(L C H / A)` | + alpha 0-1 | `oklch(0.7 0.15 145 / 0.5)` | +| `oklab(L a b)` | L: 0-1, a/b: -0.4 to 0.4 | `oklab(0.7 -0.1 0.1)` | +| `color(space R G B)` | space: `srgb`, `display-p3`, `rec2020` | `color(display-p3 1 0.5 0)` | +| `color-mix(in space, c1, c2 %)` | Any color space, two colors, optional % | `color-mix(in oklch, red, blue 30%)` | +| `light-dark(light, dark)` | Two color values | `light-dark(#fff, #1a1a1a)` | +| Relative: `oklch(from ...)` | `from L C H` with `calc()` | `oklch(from var(--c) calc(l+0.1) c h)` | +| `rgb(R G B / A)` | Space-separated, no commas | `rgb(100 200 50 / 0.8)` | +| `hsl(H S L / A)` | H: deg, S/L: % | `hsl(260 80% 50% / 0.9)` | + +### OKLCH Hue Reference + +| Hue Range | Color | Common Use | +|---|---|---| +| 0-30 | Pink/Red | Error, danger | +| 30-70 | Orange/Yellow | Warning | +| 70-140 | Yellow/Green | Success | +| 140-200 | Green/Cyan | Confirmation | +| 200-270 | Blue | Primary, info, links | +| 270-330 | Purple/Magenta | Accent | +| 330-360 | Pink/Red | Back to error | + +### Common Color Recipes + +```css +/* Auto dark/light theme */ +:root { color-scheme: light dark; } +body { color: light-dark(#1a1a1a, #e5e5e5); background: light-dark(#fff, #111); } + +/* Palette from one base color */ +--base: oklch(0.6 0.2 260); +--lighter: oklch(from var(--base) calc(l + 0.2) c h); +--darker: oklch(from var(--base) calc(l - 0.2) c h); +--muted: oklch(from var(--base) l calc(c * 0.5) h); +--transparent: oklch(from var(--base) l c h / 0.2); + +/* Semi-transparent overlay */ +--overlay: color-mix(in oklch, var(--base), transparent 60%); +``` + +--- + +## Unit Reference + +### Viewport Units + +| Unit | Measures | When to Use | +|---|---|---| +| `dvh` / `dvw` | Dynamic viewport (adjusts for mobile URL bar) | Full-screen hero sections, mobile layouts | +| `svh` / `svw` | Small viewport (URL bar visible) | Minimum safe area sizing | +| `lvh` / `lvw` | Large viewport (URL bar hidden) | Maximum available space | +| `dvb` / `dvi` | Dynamic block/inline (logical) | Writing-mode-aware viewports | +| `vmin` / `vmax` | Smaller/larger of `vw`/`vh` | Responsive typography | + +### Container Units + +| Unit | Measures | When to Use | +|---|---|---| +| `cqw` | 1% of container's width | Component-relative sizing | +| `cqh` | 1% of container's height | Requires `container-type: size` | +| `cqi` | 1% of container's inline size | Writing-mode-aware (preferred) | +| `cqb` | 1% of container's block size | Writing-mode-aware block | +| `cqmin` / `cqmax` | Smaller/larger of `cqi`/`cqb` | Adaptive within container | + +### Other Modern Units + +| Unit | Measures | When to Use | +|---|---|---| +| `lh` | Computed `line-height` of element | Vertical spacing relative to text | +| `rlh` | Computed `line-height` of root | Consistent baseline grid | +| `cap` | Cap height of font | Optical alignment with text | +| `rex` | Root element's x-height | Consistent small sizing | +| `ic` | Width of CJK ideograph | CJK typography spacing | + +--- + +## Logical Properties Mapping + +| Physical | Logical | Axis | +|---|---|---| +| `width` | `inline-size` | Inline | +| `height` | `block-size` | Block | +| `min-width` | `min-inline-size` | Inline | +| `max-height` | `max-block-size` | Block | +| `margin-top` | `margin-block-start` | Block | +| `margin-bottom` | `margin-block-end` | Block | +| `margin-left` | `margin-inline-start` | Inline | +| `margin-right` | `margin-inline-end` | Inline | +| `padding-left` | `padding-inline-start` | Inline | +| `padding-right` | `padding-inline-end` | Inline | +| `border-top` | `border-block-start` | Block | +| `border-bottom` | `border-block-end` | Block | +| `top` | `inset-block-start` | Block | +| `bottom` | `inset-block-end` | Block | +| `left` | `inset-inline-start` | Inline | +| `right` | `inset-inline-end` | Inline | +| `text-align: left` | `text-align: start` | Inline | +| `float: left` | `float: inline-start` | Inline | +| `border-radius: 8px 0 0 8px` | `border-start-start-radius: 8px; border-end-start-radius: 8px` | Both | + +Shorthands: `margin-block`, `margin-inline`, `padding-block`, `padding-inline`, `inset-block`, `inset-inline`. + +--- + +## Grid Pattern Recipes + +### Responsive auto-fill columns +```css +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(250px, 100%), 1fr)); + gap: 1rem; +} +``` + +### Holy grail layout +```css +body { + display: grid; + grid-template: "header header" auto + "sidebar main" 1fr + "footer footer" auto / 250px 1fr; + min-block-size: 100dvh; +} +``` + +### Subgrid card alignment +```css +.card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; } +.card { + display: grid; + grid-template-rows: subgrid; + grid-row: span 3; /* title, content, footer */ +} +``` + +### Full-bleed inside constrained parent +```css +.full-bleed { + inline-size: 100vw; + margin-inline: calc(50% - 50vw); +} +``` + +--- + +## @supports Feature Detection + +| Feature | `@supports` Test | +|---|---| +| Container queries | `(container-type: inline-size)` | +| `:has()` | `selector(:has(*))` | +| Nesting | `selector(&)` | +| Anchor positioning | `(anchor-name: --a)` | +| Scroll-driven animations | `(animation-timeline: scroll())` | +| `interpolate-size` | `(interpolate-size: allow-keywords)` | +| `field-sizing` | `(field-sizing: content)` | +| Customizable select | `(appearance: base-select)` | +| Scroll-state queries | `(container-type: scroll-state)` | +| View transitions | `(view-transition-name: a)` | +| `sibling-index()` | `(transition-delay: calc(sibling-index() * 1ms))` | +| Typed `attr()` | `(width: attr(data-x type(), 0px))` | +| `oklch()` | `(color: oklch(0 0 0))` | +| `light-dark()` | `(color: light-dark(#000, #fff))` | +| Subgrid | `(grid-template-rows: subgrid)` | +| At-rules (`@layer`, `@scope`, `@starting-style`) | _No cross-browser test (`at-rule()` is Chromium-only). Unsupported browsers skip the block — usually no test needed._ | + +Never test new value syntax through a custom property (`@supports (--x: if(...))`) — custom properties accept any value, so the test is always true. + +--- + +## Common Patterns + +### Entry/exit animation (display: none toggling) +```css +.dialog { + opacity: 1; + scale: 1; + transition: opacity 0.3s, scale 0.3s, display 0.3s allow-discrete, overlay 0.3s allow-discrete; + + @starting-style { + opacity: 0; + scale: 0.95; + } +} +.dialog:not([open]) { + opacity: 0; + scale: 0.95; + display: none; +} +``` + +### Scroll progress bar +```css +.progress-bar { + position: fixed; + inset-block-start: 0; + inset-inline: 0; + block-size: 3px; + background: oklch(0.6 0.2 260); + transform-origin: inline-start; + animation: progress-grow linear; + animation-timeline: scroll(root); +} +@keyframes progress-grow { from { scale: 0 1; } to { scale: 1 1; } } +``` + +### Anchor-positioned tooltip +```css +.trigger { anchor-name: --trigger; } +.tooltip { + position: fixed; + position-anchor: --trigger; + position-area: block-start; + margin-block-end: 0.5rem; + position-try-fallbacks: flip-block, flip-inline; +} +``` + +### Dark mode with light-dark() +```css +:root { + color-scheme: light dark; + --surface: light-dark(oklch(0.98 0 0), oklch(0.15 0 0)); + --text: light-dark(oklch(0.2 0 0), oklch(0.9 0 0)); + --border: light-dark(oklch(0.85 0 0), oklch(0.3 0 0)); +} +``` + +### Container query responsive card +```css +.card-wrapper { container-type: inline-size; container-name: card; } + +@container card (inline-size < 300px) { + .card { flex-direction: column; } + .card img { aspect-ratio: 16/9; inline-size: 100%; } +} +@container card (inline-size >= 300px) { + .card { flex-direction: row; } + .card img { inline-size: 150px; } +} +``` + +### Typed custom property with @property +```css +@property --hue { + syntax: ""; + initial-value: 260; + inherits: true; +} +/* Now --hue can be animated/transitioned */ +.el { --hue: 260; transition: --hue 0.3s; } +.el:hover { --hue: 145; } +.el { color: oklch(0.6 0.2 var(--hue)); } +``` + +--- + +## Progressive Enhancement Template + +```css +/* Base: works everywhere */ +.layout { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +/* Enhancement: container queries */ +@supports (container-type: inline-size) { + .wrapper { container-type: inline-size; } + @container (inline-size >= 600px) { + .layout { display: grid; grid-template-columns: 1fr 1fr; } + } +} + +/* Enhancement: :has() */ +@supports selector(:has(*)) { + .card:has(img) { grid-template-rows: 200px 1fr; } +} + +/* Enhancement: anchor positioning (newly Baseline Jan 2026) */ +@supports (anchor-name: --a) { + .trigger { anchor-name: --trigger; } + .tooltip { + position: fixed; + position-anchor: --trigger; + position-area: block-start; + } +} + +/* Enhancement: Chromium-only features (as of mid-2026) */ +@supports (interpolate-size: allow-keywords) { + :root { interpolate-size: allow-keywords; } +} + +@supports (appearance: base-select) { + select { appearance: base-select; } +} + +/* Enhancement: newly Baseline Jun 2026 — older browsers keep fixed sizing */ +@supports (field-sizing: content) { + textarea { field-sizing: content; } +} +``` + +--- + +## Anti-Patterns Recap + +| # | Anti-Pattern | Fix | +|---|---|---| +| 1 | `!important` for overrides | `@layer` cascade control | +| 2 | Deep nesting `.a .b .c .d` | Flat selectors, `@scope` | +| 3 | IDs for styling `#header` | Classes `.header` | +| 4 | `@media` for component sizing | Container queries | +| 5 | JS scroll listeners | Scroll-driven animations | +| 6 | JS tooltip positioning | Anchor positioning | +| 7 | `max-height: 9999px` | `interpolate-size: allow-keywords` | +| 8 | `margin-left` / `padding-right` | Logical properties (`margin-inline-start`) | +| 9 | `rgba()` with commas | `rgb(r g b / a)` space-separated | +| 10 | `text-wrap: balance` on paragraphs | Only headings / short text (perf) | diff --git a/.agents/skills/modern-css/references/COLOR.md b/.agents/skills/modern-css/references/COLOR.md new file mode 100644 index 0000000..202f96a --- /dev/null +++ b/.agents/skills/modern-css/references/COLOR.md @@ -0,0 +1,541 @@ +# Color — Building Perceptually Uniform Color Systems + +> Sources: +> - [CSS Color Level 4](https://www.w3.org/TR/css-color-4/) — W3C Specification +> - [CSS Color Level 5](https://www.w3.org/TR/css-color-5/) — `color-mix()`, relative color syntax +> - [OKLCH in CSS: why we moved from RGB and HSL](https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl) — Evil Martians +> - [MDN: CSS Color](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_colors) — Mozilla + +Everything in this file — `oklch()`, `oklab()`, `color-mix()`, relative color syntax, `light-dark()`, `color()` — is Baseline as of mid-2026. Use freely. + +## Contents + +- [Why Modern Color Spaces Matter](#why-modern-color-spaces-matter) +- [Modern Syntax: Space-Separated Values](#modern-syntax-space-separated-values) +- [OKLCH — The Recommended Color Space](#oklch--the-recommended-color-space) +- [OKLab — Best for Gradient Interpolation](#oklab--best-for-gradient-interpolation) +- [Display-P3 — Wide-Gamut Colors](#display-p3--wide-gamut-colors) +- [`color-mix()` — Blending Colors](#color-mix--blending-colors) +- [Relative Color Syntax — Deriving Colors from Tokens](#relative-color-syntax--deriving-colors-from-tokens) +- [`light-dark()` and `color-scheme` — Theming](#light-dark-and-color-scheme--theming) +- [Putting It All Together](#putting-it-all-together) +- [Anti-Patterns](#anti-patterns) + +--- + +## Why Modern Color Spaces Matter + +sRGB represents roughly 35% of the colors the human eye can see. Every hex value, `rgb()`, and `hsl()` color is confined to this gamut. Modern displays (P3 on Apple devices, Rec. 2020 on HDR) can render far more — Display-P3 covers ~50% more colors than sRGB. + +The bigger problem is perceptual uniformity. HSL claims to separate lightness from hue, but it lies: `hsl(60, 100%, 50%)` (yellow) appears far brighter than `hsl(240, 100%, 50%)` (blue) despite identical `L` values. This makes programmatic palette generation unreliable. OKLCH fixes this — equal lightness values produce equal perceived brightness across all hues. + +```mermaid +flowchart TB + subgraph Gamut["Color Gamut Size"] + direction TB + SRGB["sRGB
~35% of visible spectrum
hex, rgb(), hsl()"] + P3["Display-P3
~50% larger than sRGB
color(display-p3 ...)"] + LAB["Lab / LCH
Device-independent
Approximates human vision"] + end + + SRGB -->|"subset of"| P3 + P3 -->|"subset of"| LAB + + subgraph Working["Working Spaces (use these)"] + OKLCH["OKLCH
Perceptually uniform
Polar: L, C, H
RECOMMENDED"] + OKLAB["OKLab
Perceptually uniform
Cartesian: L, a, b
Best for gradients"] + end + + LAB -.->|"corrected version"| OKLCH + LAB -.->|"corrected version"| OKLAB + + style OKLCH fill:#16a34a,color:#fff,stroke:#15803d + style OKLAB fill:#2563eb,color:#fff,stroke:#1d4ed8 + style SRGB fill:#94a3b8,color:#1e293b,stroke:#64748b + style P3 fill:#f59e0b,color:#1e293b,stroke:#d97706 + style LAB fill:#8b5cf6,color:#fff,stroke:#7c3aed +``` + +**Key takeaway:** Use OKLCH as your default color space. Fall back to sRGB only for legacy compatibility. Use `color-mix()` in oklch for blending. Use OKLab for gradient interpolation. + +--- + +## Modern Syntax: Space-Separated Values + +All modern color functions use space-separated syntax with `/` for alpha. The comma-separated form and the `a` suffix (`rgba`, `hsla`) are legacy. + +```css +/* ❌ Legacy syntax — do not use in new code */ +.legacy { + color: rgba(31, 41, 59, 0.26); + background: hsla(220, 14%, 96%, 0.5); +} + +/* ✅ Modern syntax — space-separated, slash for alpha */ +.modern { + color: rgb(31 41 59 / 0.26); + background: hsl(220 14% 96% / 0.5); + border-color: oklch(0.5 0.15 240 / 0.8); +} +``` + +Consistent across `rgb()`, `hsl()`, `oklch()`, `oklab()`, `lab()`, `lch()`, and `color()`. Prefer this syntax for new styles when compatible with the project's targets and conventions. + +--- + +## OKLCH — The Recommended Color Space + +Use OKLCH as the default for all new projects, design systems, and palette generation. + +### Parameters + +| Parameter | Range | Description | +|-----------|-------|-------------| +| **L** (Lightness) | `0` to `1` | `0` = black, `1` = white. Perceptually linear. | +| **C** (Chroma) | `0` to `~0.4` | Color intensity. `0` = gray. Most usable values `0.01`-`0.3`. | +| **H** (Hue) | `0` to `360` | `0`=pink, `30`=red, `70`=orange, `90`=yellow, `145`=green, `240`=blue, `300`=purple. | + +### When to Use + +- **Design systems** — Palette scales where each step has equal visual weight +- **Programmatic palette generation** — Sweep hue at constant L and C for consistent vibrancy +- **Accessible color pairs** — Predictable contrast ratios because L is perceptually linear +- **Any new project** — There is no reason to start with HSL anymore + +### OKLCH in Practice + +```css +:root { + /* Constant lightness + chroma, varying hue = equally vibrant colors */ + --color-blue: oklch(0.6 0.2 240); + --color-green: oklch(0.6 0.2 145); + --color-red: oklch(0.6 0.2 30); + --color-purple: oklch(0.6 0.2 300); + + /* Lightness scale — same hue/chroma, varying lightness */ + --blue-100: oklch(0.93 0.04 240); + --blue-300: oklch(0.75 0.13 240); + --blue-500: oklch(0.55 0.22 240); + --blue-700: oklch(0.35 0.17 240); + --blue-900: oklch(0.20 0.08 240); +} +``` + +### Why Not HSL + +```css +/* ❌ HSL: Same L=50%, wildly different perceived brightness */ +.hsl-problem { + --yellow: hsl(60, 100%, 50%); /* Appears very bright */ + --blue: hsl(240, 100%, 50%); /* Appears very dark */ +} + +/* ✅ OKLCH: Same L=0.7, same perceived brightness */ +.oklch-solution { + --yellow: oklch(0.7 0.15 90); /* Looks equally bright */ + --blue: oklch(0.7 0.15 240); /* Looks equally bright */ +} +``` + +--- + +## OKLab — Best for Gradient Interpolation + +OKLab uses Cartesian coordinates (`L`, `a`, `b`) instead of polar (`L`, `C`, `H`). Use OKLab when interpolating between colors — it avoids the "muddy middle" and hue banding that sRGB produces, and avoids the hue-angle ambiguity of polar spaces. + +```css +/* ❌ sRGB interpolation — muddy gray in the middle */ +.gradient-bad { + background: linear-gradient(in srgb, oklch(0.7 0.25 145), oklch(0.7 0.25 30)); +} + +/* ✅ OKLab interpolation — vibrant, no dead zone */ +.gradient-good { + background: linear-gradient(in oklab, oklch(0.7 0.25 145), oklch(0.7 0.25 30)); +} + +/* Also good: OKLCH with explicit hue direction */ +.gradient-oklch { + background: linear-gradient(in oklch shorter hue, oklch(0.7 0.25 145), oklch(0.7 0.25 30)); +} +``` + +Use `in oklab` on gradients by default. Use `in oklch shorter hue` when you need to control hue direction (e.g., rainbow effects with `longer hue`). + +--- + +## Display-P3 — Wide-Gamut Colors + +Display-P3 covers vivid reds, greens, and oranges that sRGB cannot represent. Always declare an sRGB fallback first — browsers that do not support `color()` ignore it. + +```css +.vivid-button { + /* sRGB fallback */ + background-color: oklch(0.65 0.25 145); + /* P3 override — only applied on wide-gamut displays */ + background-color: color(display-p3 0.2 0.8 0.3); +} +``` + +### Detecting P3 Support + +```css +@media (color-gamut: p3) { + :root { + --brand-green: color(display-p3 0.2 0.85 0.3); + --brand-red: color(display-p3 0.95 0.2 0.15); + } +} + +@supports (color: color(display-p3 1 1 1)) { + .accent { color: color(display-p3 0.9 0.3 0.2); } +} +``` + +### OKLCH Often Replaces P3 + +OKLCH with high chroma values exceeds sRGB — browsers automatically map to the widest available gamut. **OKLCH with high chroma is often simpler than explicit P3 declarations.** + +```css +/* Equivalent on a P3 display: */ +.option-a { color: color(display-p3 0.2 0.8 0.3); } +.option-b { color: oklch(0.72 0.3 145); } /* Auto-mapped to P3 */ +``` + +Prefer OKLCH unless you need exact Display-P3 coordinates from a design tool. + +--- + +## `color-mix()` — Blending Colors + +`color-mix()` blends two colors in a specified color space. The interpolation space matters enormously. + +### CRITICAL: Interpolation Space Matters + +```css +/* ❌ Mixing in sRGB — produces muddy, desaturated result */ +.muddy { background: color-mix(in srgb, blue, yellow); } + +/* ✅ Mixing in OKLCH — preserves vibrancy */ +.vibrant { background: color-mix(in oklch, blue, yellow); } +``` + +Consider `in oklch` for perceptual color adjustments; preserve another space +when it matches the intended interpolation or the project's token system. + +### Common Patterns + +```css +:root { + --brand: oklch(0.55 0.22 240); + + /* Tinting — mix with white */ + --brand-light: color-mix(in oklch, var(--brand) 30%, white); + --brand-lighter: color-mix(in oklch, var(--brand) 15%, white); + + /* Shading — mix with black */ + --brand-dark: color-mix(in oklch, var(--brand) 70%, black); + --brand-darker: color-mix(in oklch, var(--brand) 50%, black); + + /* Semi-transparent — mix with transparent */ + --brand-hover: color-mix(in oklch, var(--brand) 80%, transparent); + --brand-ghost: color-mix(in oklch, var(--brand) 10%, transparent); + + /* Muted — mix with same-lightness gray */ + --brand-muted: color-mix(in oklch, var(--brand) 60%, oklch(0.55 0 0)); + + /* Blend two theme colors */ + --secondary: oklch(0.6 0.2 145); + --accent: color-mix(in oklch, var(--brand), var(--secondary)); +} +``` + +--- + +## Relative Color Syntax — Deriving Colors from Tokens + +Relative color syntax creates new colors by transforming an existing color's channels. This replaces Sass `darken()`, `lighten()`, `adjust-hue()`, and similar functions. + +### Syntax + +```css +oklch(from var(--base) calc(l - 0.1) c h) +``` + +Channel names (`l`, `c`, `h` for OKLCH) become variables usable in `calc()`. + +### Transformations + +```css +:root { + --base: oklch(0.6 0.2 240); + + /* Darken / Lighten */ + --darker: oklch(from var(--base) calc(l - 0.15) c h); + --lighter: oklch(from var(--base) calc(l + 0.15) c h); + + /* Desaturate / Saturate */ + --muted: oklch(from var(--base) l calc(c - 0.1) h); + --vivid: oklch(from var(--base) l calc(c + 0.1) h); + --gray: oklch(from var(--base) l 0 h); + + /* Complement (opposite hue) */ + --complement: oklch(from var(--base) l c calc(h + 180)); + + /* Analogous (adjacent hues) */ + --analog-left: oklch(from var(--base) l c calc(h - 30)); + --analog-right: oklch(from var(--base) l c calc(h + 30)); + + /* Add alpha */ + --semi: oklch(from var(--base) l c h / 0.5); + --ghost: oklch(from var(--base) l c h / 0.1); +} +``` + +### Cross-Space Conversion + +Input any format, transform in OKLCH. The browser converts automatically: + +```css +:root { + --legacy-brand: #1e40af; + --brand-dark: oklch(from var(--legacy-brand) calc(l - 0.1) c h); +} +``` + +### Complete Token System + +Derive an entire palette from a single base value: + +```css +:root { + --brand: oklch(0.55 0.22 240); + + /* Auto-generated scale */ + --brand-50: oklch(from var(--brand) 0.97 calc(c * 0.1) h); + --brand-100: oklch(from var(--brand) 0.93 calc(c * 0.2) h); + --brand-200: oklch(from var(--brand) 0.85 calc(c * 0.4) h); + --brand-300: oklch(from var(--brand) 0.75 calc(c * 0.6) h); + --brand-400: oklch(from var(--brand) 0.65 calc(c * 0.8) h); + --brand-500: oklch(from var(--brand) l c h); + --brand-600: oklch(from var(--brand) calc(l - 0.08) c h); + --brand-700: oklch(from var(--brand) calc(l - 0.16) c h); + --brand-800: oklch(from var(--brand) calc(l - 0.24) calc(c * 0.8) h); + --brand-900: oklch(from var(--brand) calc(l - 0.32) calc(c * 0.6) h); + + /* Semantic tokens from the same base */ + --brand-hover: oklch(from var(--brand) calc(l - 0.05) c h); + --brand-active: oklch(from var(--brand) calc(l - 0.1) c h); + --brand-disabled: oklch(from var(--brand) l calc(c * 0.3) h / 0.5); + --brand-ring: oklch(from var(--brand) l c h / 0.3); + --brand-surface: oklch(from var(--brand) 0.97 calc(c * 0.1) h); +} +``` + +--- + +## `light-dark()` and `color-scheme` — Theming + +`light-dark()` returns one of two colors depending on the active color scheme. It requires `color-scheme` to be set. + +### Setup (REQUIRED) + +```css +:root { + color-scheme: light dark; /* Without this, light-dark() always returns the light value */ +} +``` + +### Basic Usage + +```css +:root { + color-scheme: light dark; + + --text: light-dark(oklch(0.2 0 0), oklch(0.9 0 0)); + --surface: light-dark(oklch(0.99 0 0), oklch(0.15 0 0)); + --border: light-dark(oklch(0.85 0 0), oklch(0.3 0 0)); + --brand: light-dark(oklch(0.5 0.2 240), oklch(0.7 0.2 240)); +} +``` + +### `color-scheme` vs `prefers-color-scheme` + +| Feature | `color-scheme` | `prefers-color-scheme` | +|---------|---------------|----------------------| +| **What** | CSS property on elements | Media query | +| **Scope** | Per-element (inherited) | Whole page (OS-level) | +| **Controls** | UA defaults + `light-dark()` | Conditional `@media` blocks | + +`light-dark()` reads the **computed** `color-scheme`, not the media query. If the OS is in dark mode but `color-scheme: light` is forced on an element, `light-dark()` returns the light value inside that element. + +### Per-Component Theme Overrides + +`color-scheme` is inherited but can be overridden per element — enabling "island" theming: + +```css +/* Force sidebar to always be dark, regardless of OS setting */ +.sidebar { + color-scheme: dark; + background: var(--surface); /* Resolves to dark variant */ + color: var(--text); /* Resolves to dark variant */ +} + +/* Force modal to always be light */ +.modal { + color-scheme: light; + background: var(--surface); /* Resolves to light variant */ +} +``` + +### Complete Theming Pattern + +```css +:root { + color-scheme: light dark; + + --surface-0: light-dark(oklch(1 0 0), oklch(0.13 0 0)); + --surface-1: light-dark(oklch(0.97 0 0), oklch(0.18 0 0)); + --surface-2: light-dark(oklch(0.94 0 0), oklch(0.23 0 0)); + + --text-primary: light-dark(oklch(0.15 0 0), oklch(0.93 0 0)); + --text-secondary: light-dark(oklch(0.4 0 0), oklch(0.7 0 0)); + --text-muted: light-dark(oklch(0.6 0 0), oklch(0.5 0 0)); + + --brand: light-dark(oklch(0.5 0.2 240), oklch(0.7 0.18 240)); + --border: light-dark(oklch(0.87 0 0), oklch(0.3 0 0)); + --shadow: light-dark(oklch(0 0 0 / 0.1), oklch(0 0 0 / 0.4)); +} + +/* Scheme-specific assets still need the media query */ +@media (prefers-color-scheme: dark) { + .logo { content: url('/logo-dark.svg'); } +} +``` + +--- + +## Putting It All Together + +Combines OKLCH, relative color syntax, `color-mix()`, and `light-dark()`: + +```css +:root { + color-scheme: light dark; + + /* Primitive hues — single source of truth */ + --hue-primary: 240; + --hue-success: 145; + --hue-danger: 25; + + /* Base colors */ + --primary: oklch(0.55 0.22 var(--hue-primary)); + --success: oklch(0.6 0.2 var(--hue-success)); + --danger: oklch(0.6 0.22 var(--hue-danger)); + + /* Semantic tokens */ + --bg: light-dark(oklch(0.99 0 0), oklch(0.13 0 0)); + --text: light-dark(oklch(0.15 0 0), oklch(0.93 0 0)); + + /* Interactive states via relative color */ + --primary-hover: oklch(from var(--primary) calc(l - 0.05) c h); + --primary-active: oklch(from var(--primary) calc(l - 0.1) c h); + --primary-ring: oklch(from var(--primary) l c h / 0.3); + + /* Surface tints via color-mix() */ + --primary-surface: color-mix(in oklch, var(--primary) 8%, var(--bg)); + --danger-surface: color-mix(in oklch, var(--danger) 8%, var(--bg)); +} + +.btn-primary { + background: var(--primary); + color: oklch(from var(--primary) 0.98 0 h); + &:hover { background: var(--primary-hover); } + &:active { background: var(--primary-active); } + &:focus-visible { outline: 2px solid var(--primary-ring); } +} + +.alert-danger { + background: var(--danger-surface); + border-left: 3px solid var(--danger); + color: oklch(from var(--danger) calc(l - 0.15) c h); +} +``` + +--- + +## Anti-Patterns + +### Hardcoded Palettes + +```css +/* ❌ Manually defining every shade — unmaintainable */ +:root { + --blue-100: #dbeafe; + --blue-300: #93c5fd; + --blue-500: #3b82f6; + --blue-700: #1d4ed8; + --blue-900: #1e3a8a; +} + +/* ✅ Derive from a single base */ +:root { + --blue: oklch(0.6 0.22 240); + --blue-100: oklch(from var(--blue) 0.93 calc(c * 0.2) h); + --blue-500: oklch(from var(--blue) l c h); + --blue-900: oklch(from var(--blue) 0.2 calc(c * 0.6) h); +} +``` + +### Legacy `rgba()` / `hsla()` Syntax + +```css +/* ❌ Comma-separated, function name with 'a' suffix */ +.legacy { background: rgba(59, 130, 246, 0.5); } + +/* ✅ Space-separated, slash for alpha */ +.modern { background: oklch(0.6 0.22 240 / 0.5); } +``` + +### Missing `color-scheme` Declaration + +```css +/* ❌ light-dark() always returns the first (light) value */ +:root { --bg: light-dark(white, #111); } + +/* ✅ Must declare color-scheme first */ +:root { + color-scheme: light dark; + --bg: light-dark(white, oklch(0.13 0 0)); +} +``` + +### Mixing in sRGB + +```css +/* ❌ sRGB interpolation produces muddy, desaturated mixes */ +.bad { color: color-mix(in srgb, red, blue); } + +/* ✅ OKLCH preserves chroma */ +.good { color: color-mix(in oklch, red, blue); } +``` + +### Using HSL for Programmatic Palettes + +```css +/* ❌ HSL lightness is not perceptually uniform */ +.palette { + --step-1: hsl(220, 80%, 90%); + --step-2: hsl(220, 80%, 70%); + --step-3: hsl(220, 80%, 50%); /* perceived jump is uneven */ +} + +/* ✅ OKLCH lightness is perceptually uniform */ +.palette { + --step-1: oklch(0.9 0.08 240); + --step-2: oklch(0.7 0.16 240); + --step-3: oklch(0.5 0.22 240); /* equal perceptual steps */ +} +``` + +> For non-Baseline features, always feature-detect with `@supports` or use progressive enhancement. Check [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS) or [Baseline](https://web.dev/baseline) for current browser support. diff --git a/.agents/skills/modern-css/references/COMPONENTS.md b/.agents/skills/modern-css/references/COMPONENTS.md new file mode 100644 index 0000000..3823328 --- /dev/null +++ b/.agents/skills/modern-css/references/COMPONENTS.md @@ -0,0 +1,554 @@ +# Components — Interactive UI Without JavaScript + +> Sources: [Open UI](https://open-ui.org/), [Popover API](https://html.spec.whatwg.org/multipage/popover.html), [CSS Anchor Positioning](https://www.w3.org/TR/css-anchor-position-1/), [Invoker Commands](https://open-ui.org/components/invokers.explainer/), [Interop 2025](https://webkit.org/blog/17808/interop-2025-review/). These APIs eliminate entire categories of JavaScript — tooltips, dropdowns, modals, popovers, auto-sizing inputs — with declarative HTML attributes and CSS properties. + +## Contents + +- [Customizable `` — `appearance: base-select` + +Before this, styling a dropdown with images or icons meant rebuilding the widget in JavaScript — destroying native accessibility, keyboard navigation, and form integration. Opt in with CSS; unsupported browsers render a standard ``. + +### Opt-In and Pseudo-Elements + +```css +select, +::picker(select) { + appearance: base-select; +} +``` + +| Pseudo-Element | Targets | Purpose | +|---|---|---| +| `::picker(select)` | Dropdown popover on open | Style dropdown container | +| `::picker-icon` | Arrow/chevron indicator | Replace or animate the arrow | +| `option::checkmark` | Selected-item indicator | Style or hide the checkmark | +| `` | Reflected chosen option | Display rich content in collapsed state | + +Use when options need rich content — flags, avatars, color swatches. Do NOT use for plain text lists. + +### Complete Example + +```html + +``` + +```css +/* ❌ JavaScript approach — custom dropdown widget */ +``` +```javascript +// 200+ lines: keyboard handling, ARIA roles, click-outside, +// scroll locking, focus trapping... +class CustomSelect extends HTMLElement { /* ... */ } +``` +```css +/* ✅ Pure CSS — native `. Feature-detect with `@supports (appearance: base-select)`. + +--- + +## Popover API + +Replaces custom JavaScript tooltip/dropdown/notification implementations. Popovers get automatic top-layer promotion, focus management, and light dismiss. `auto` and `manual` are Baseline (since April 2024); `hint` is Chromium-only (Chrome/Edge 133+) as of mid-2026 — unsupported browsers treat it as `auto`. + +### Three Types + +| Type | Attribute | Light Dismiss | Closes Others | Use When | +|---|---|---|---|---| +| `auto` | `popover` or `popover="auto"` | Yes (click outside, ESC) | Yes | Menus, dropdowns, action sheets | +| `manual` | `popover="manual"` | No | No | Notifications, toasts, persistent panels | +| `hint` | `popover="hint"` (Chromium-only) | Yes | Only other hints | Tooltips, ephemeral help text | + +### Basic Usage + +```html + +
+

Menu content here

+
+``` + +### Styling + +```css +/* ❌ JavaScript approach — manually toggling visibility */ +``` +```javascript +const btn = document.querySelector('#toggle'); +const menu = document.querySelector('#menu'); +btn.addEventListener('click', () => menu.classList.toggle('open')); +document.addEventListener('click', (e) => { + if (!menu.contains(e.target) && e.target !== btn) + menu.classList.remove('open'); +}); +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') menu.classList.remove('open'); +}); +``` +```css +/* ✅ Pure CSS — popover with enter/exit transitions */ +[popover] { + opacity: 0; + scale: 0.95; + transition: opacity 0.2s, scale 0.2s, + display 0.2s allow-discrete, + overlay 0.2s allow-discrete; +} + +[popover]:popover-open { + opacity: 1; + scale: 1; +} + +@starting-style { + [popover]:popover-open { + opacity: 0; + scale: 0.95; + } +} + +[popover]::backdrop { + background: oklch(0 0 0 / 0.25); + backdrop-filter: blur(2px); +} +``` + +### `hint` Type — Ephemeral Tooltips + +Unlike `auto`, opening a `hint` does not close other open `auto` popovers. A user can have a dropdown open and still see a tooltip on a menu item. + +```html + +
+ +
Switches to dark theme
+
+``` + +--- + +## Invoker Commands — Declarative Button Actions + +Replace JavaScript event listeners with `commandfor` and `command` attributes. **Baseline Newly Available since December 2025** (Chrome/Edge 135+, Firefox 144+, Safari 26.2+). + +Gotcha: always add `type="button"` to invoker buttons inside a `
` — buttons default to `type="submit"` and will submit the form. + +### Built-In Commands + +| Command | Action | Target | +|---|---|---| +| `toggle-popover` | Toggle popover open/closed | `[popover]` | +| `show-popover` | Show popover | `[popover]` | +| `hide-popover` | Hide popover | `[popover]` | +| `show-modal` | Open dialog as modal | `` | +| `close` | Close dialog | `` | + +```html + + + +

Dialog content

+ +
+ +``` + +```html + + + +

Dialog content

+ +
+``` + +### Custom Commands + +Prefix with `--` for application-specific actions. Custom commands fire a `command` event on the target. + +```html + + +
...
+ + +``` + +Use invokers whenever a button's sole purpose is to trigger an action on another element. + +--- + +## Dialog Light Dismiss — `closedby` Attribute + +Controls how a `` can be dismissed — click-outside-to-close previously required JavaScript. + +**Support (mid-2026): not Baseline.** Chrome/Edge 134+ and Firefox 141+ support it; Safari does not yet (an Interop 2026 focus area). Unsupported browsers keep default dialog behavior (ESC closes modals) — keep a JS click-outside fallback if light dismiss is essential. + +| Value | ESC Key | Click Outside | Programmatic `.close()` | +|---|---|---|---| +| `none` | No | No | Yes | +| `closerequest` (modal default) | Yes | No | Yes | +| `any` | Yes | Yes | Yes | + +```html + +

Content

+ +``` + +```html + + +

Content

+ +
+ +``` + +```css +dialog { + border: none; + border-radius: 1rem; + padding: 2rem; + max-inline-size: min(90vw, 500px); + box-shadow: 0 12px 40px oklch(0 0 0 / 0.2); +} + +dialog::backdrop { + background: oklch(0 0 0 / 0.4); + backdrop-filter: blur(4px); +} +``` + +--- + +## Interest Invokers — Hover/Focus-Triggered UI + +The `interestfor` attribute triggers popovers on hover or focus rather than click. Default delay: 0.5s show/hide. Works on buttons and links. + +**Support (mid-2026): Chromium-only (Chrome/Edge 142+), still experimental** — the attribute was renamed from `interesttarget` to `interestfor` during standardization, and the touch-device interaction model is still in flux. Provide a `title` attribute fallback on the trigger for other browsers. + +```html + +
This appears on hover/focus
+``` + +```css +/* ❌ JavaScript approach — hover intent detection */ +``` +```javascript +let timer; +trigger.addEventListener('mouseenter', () => { + timer = setTimeout(() => tooltip.showPopover(), 300); +}); +trigger.addEventListener('mouseleave', () => { + clearTimeout(timer); + setTimeout(() => tooltip.hidePopover(), 200); +}); +``` +```css +/* ✅ CSS interest-delay — declarative timing */ +[interestfor] { + interest-delay: 300ms; +} +``` + +Replaces `mouseenter`/`mouseleave`/`focusin`/`focusout` JavaScript. Keyboard focus triggers the popover automatically. + +--- + +## Anchor Positioning + +Replaces Floating UI, Popper.js, and Tether. An element declares itself as an anchor; another positions relative to it. The browser handles viewport collision, auto-flipping, and scroll-aware repositioning. + +**Support: Baseline Newly Available since January 2026** (Chrome/Edge 125+, Safari 26+, Firefox 147+). Coverage of older browsers is still limited (Safari 26 requires recent OS versions) — use the [OddBird polyfill](https://github.com/oddbird/css-anchor-positioning) or a JS fallback where positioning is critical. Note the property renames: `inset-area` → `position-area` and `position-try-options` → `position-try-fallbacks` (old names are dead — never emit them). + +### Core Properties + +| Property | Purpose | Example | +|---|---|---| +| `anchor-name` | Declare an anchor | `anchor-name: --trigger` | +| `position-anchor` | Connect to an anchor | `position-anchor: --trigger` | +| `position-area` | Place relative to anchor | `position-area: block-end` | +| `position-try-fallbacks` | Fallbacks on overflow | `position-try-fallbacks: flip-block` | + +### Basic Positioning + +```css +/* ❌ JavaScript approach — Floating UI / Popper.js */ +``` +```javascript +import { computePosition, flip, offset } from '@floating-ui/dom'; +computePosition(trigger, tooltip, { + placement: 'bottom', + middleware: [offset(8), flip()], +}).then(({ x, y }) => { + tooltip.style.left = `${x}px`; + tooltip.style.top = `${y}px`; +}); +``` +```css +/* ✅ Pure CSS — anchor positioning */ +.trigger { + anchor-name: --trigger; +} + +.tooltip { + position: fixed; + position-anchor: --trigger; + position-area: block-end; + margin-block-start: 8px; +} +``` + +### Auto-Flip on Overflow + +```css +.tooltip { + position: fixed; + position-anchor: --trigger; + position-area: block-end; + position-try-fallbacks: flip-block, flip-inline; +} +``` + +| Keyword | Behavior | +|---|---| +| `flip-block` | Flip bottom to top (or top to bottom) | +| `flip-inline` | Flip right to left (or left to right) | +| `flip-block flip-inline` | Flip both axes | + +### Custom Fallback Positions + +```css +.dropdown-menu { + position: fixed; + position-anchor: --menu-trigger; + position-area: block-end span-inline-end; + position-try-fallbacks: --above, --left; +} + +@position-try --above { + position-area: block-start span-inline-end; +} + +@position-try --left { + position-area: inline-start; +} +``` + +### Anchored Container Queries — Arrow Direction + +When a tooltip flips, the arrow must point the other way. Detect which fallback was applied (`container-type: anchored` is Chromium-only and experimental as of mid-2026): + +```css +.tooltip-wrapper { + container-type: anchored; +} + +.tooltip-arrow { + rotate: 0deg; /* default: arrow up, tooltip below */ +} + +@container anchored(fallback: flip-block) { + .tooltip-arrow { + rotate: 180deg; /* flipped: arrow down, tooltip above */ + } +} +``` + +### Complete Tooltip — Zero JavaScript + +Combines anchor positioning, popover `hint`, and interest invokers into a fully declarative tooltip: + +```html + +
+ Tooltip content +
+``` + +```css +[popover="hint"] { + position: fixed; + position-area: block-start; + margin-block-end: 6px; + position-try-fallbacks: flip-block; + + background: oklch(0.2 0 0); + color: oklch(0.95 0 0); + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.875rem; + border: none; + + opacity: 0; + transition: opacity 0.15s; +} + +[popover="hint"]:popover-open { + opacity: 1; +} + +@starting-style { + [popover="hint"]:popover-open { + opacity: 0; + } +} +``` + +--- + +## `field-sizing: content` — Auto-Sizing Form Fields + +Auto-resizes inputs, textareas, and selects to fit content. **Baseline Newly Available since June 2026** (Chrome/Edge 123+, Safari 26.2+, Firefox 152+). Unsupported browsers keep fixed-size fields — safe progressive enhancement. + +```css +/* ❌ JavaScript approach — auto-expanding textarea */ +``` +```javascript +const textarea = document.querySelector('textarea'); +textarea.addEventListener('input', () => { + textarea.style.height = 'auto'; + textarea.style.height = textarea.scrollHeight + 'px'; +}); +// Bugs: flickers on fast typing, breaks on box-sizing changes +``` +```css +/* ✅ Pure CSS — auto-sizing */ +textarea { + field-sizing: content; +} +``` + +### Constraints and Progressive Enhancement + +Always set bounds — without them, the element grows infinitely. Works on `input[type="text"]`, `textarea`, and `select`. + +```css +textarea { + min-block-size: 100px; + resize: vertical; +} + +@supports (field-sizing: content) { + textarea { + field-sizing: content; + min-block-size: 3lh; + max-block-size: 50vh; + resize: none; + } +} +``` + +--- + +## Decision Guide + +| Need | Solution | JS Required | Cross-Browser (mid-2026) | +|---|---|---|---| +| Styled dropdown with images | `appearance: base-select` | No | Chromium only (native fallback) | +| Tooltip on hover | `interestfor` + `popover="hint"` + anchor positioning | No | Chromium only (`title` fallback) | +| Dropdown menu on click | `popovertarget` + `popover` + anchor positioning | No | Yes (anchor: newly Baseline) | +| Modal (ESC + click outside) | `` + `command="show-modal"` | No | `closedby`: not in Safari | +| Toast / notification | `popover="manual"` + `command="show-popover"` | Minimal (timer) | Yes | +| Auto-expanding textarea | `field-sizing: content` | No | Yes (newly Baseline) | +| Positioned element that flips | `position-try-fallbacks: flip-block` | No | Yes (newly Baseline) | +| Button opens/closes a panel | `commandfor` + `command="toggle-popover"` | No | Yes (newly Baseline) | + +> For non-Baseline features, always feature-detect with `@supports` or use progressive enhancement. Check [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS) or [Baseline](https://web.dev/baseline) for current browser support. diff --git a/.agents/skills/modern-css/references/LAYOUT.md b/.agents/skills/modern-css/references/LAYOUT.md new file mode 100644 index 0000000..8b374b4 --- /dev/null +++ b/.agents/skills/modern-css/references/LAYOUT.md @@ -0,0 +1,585 @@ +# Layout — Choosing the Right Layout System + +> Sources: +> - [CSS Grid Layout Level 2 (Subgrid)](https://www.w3.org/TR/css-grid-2/) — W3C +> - [CSS Grid Layout Level 3 (Grid Lanes)](https://www.w3.org/TR/css-grid-3/) — W3C Editor's Draft +> - [CSS Containment Level 3 (Container Queries)](https://www.w3.org/TR/css-contain-3/) — W3C +> - [CSS Box Sizing Level 4 (Intrinsic Sizing)](https://www.w3.org/TR/css-sizing-4/) — W3C +> - [MDN: CSS Grid](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout) — Mozilla +> - [MDN: Container Queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_queries) — Mozilla + +Modern CSS provides four layout primitives: **Grid** for 2D structure, **Flexbox** for 1D alignment, **Container Queries** for component-level responsiveness, and **intrinsic sizing** for content-driven dimensions. Every layout decision starts by identifying which axis matters, whether the component owns its sizing or its container does, and whether children need cross-alignment. + +## Contents + +- [Layout Decision Flowchart](#layout-decision-flowchart) +- [1. CSS Grid](#1-css-grid) +- [2. Subgrid](#2-subgrid) +- [3. Grid Lanes (Masonry)](#3-grid-lanes-masonry) +- [4. Flexbox](#4-flexbox) +- [5. Container Queries](#5-container-queries) +- [6. Intrinsic Sizing](#6-intrinsic-sizing) +- [7. Aspect Ratio](#7-aspect-ratio) +- [Anti-Patterns](#anti-patterns) + +--- + +## Layout Decision Flowchart + +```mermaid +flowchart TD + START["What are you laying out?"] --> DIM{"How many axes
matter?"} + DIM -->|"Both rows + columns"| GRID["Use CSS Grid"] + DIM -->|"Single axis only"| FLEX["Use Flexbox"] + GRID --> CHILD{"Do child internals need
to align across siblings?"} + CHILD -->|"Yes — card headers,
footers must line up"| SUBGRID["Grid + Subgrid"] + CHILD -->|"No"| GRIDTYPE{"Fixed row heights
or variable?"} + GRIDTYPE -->|"Uniform rows"| STDGRID["Standard Grid
repeat(auto-fill, minmax())"] + GRIDTYPE -->|"Variable heights,
waterfall style"| LANES{"Browser support
acceptable?"} + LANES -->|"Yes (Safari 26.4+;
flags elsewhere)"| GRIDLANES["display: grid-lanes"] + LANES -->|"No — need production"| FALLBACK["Grid + JS library
or CSS columns fallback"] + FLEX --> FLEXDIR{"Wrapping needed?"} + FLEXDIR -->|"No — single line"| FLEXLINE["Flexbox
(nav, toolbar, centering)"] + FLEXDIR -->|"Yes — wraps to
multiple lines"| CONSIDER["Consider Grid instead
(better 2D control)"] + style GRID fill:#3b82f6,stroke:#2563eb,color:white + style SUBGRID fill:#6366f1,stroke:#4f46e5,color:white + style STDGRID fill:#3b82f6,stroke:#2563eb,color:white + style GRIDLANES fill:#f59e0b,stroke:#d97706,color:white + style FLEX fill:#10b981,stroke:#059669,color:white + style FLEXLINE fill:#10b981,stroke:#059669,color:white + style CONSIDER fill:#ef4444,stroke:#dc2626,color:white + style FALLBACK fill:#6b7280,stroke:#4b5563,color:white +``` + +--- + +## 1. CSS Grid + +Use Grid when layout involves **both rows and columns** — page structure, card grids, dashboard panels, form layouts. Grid is the default choice for any 2D arrangement. + +**When to use:** Page-level structure, card grids, spanning items across rows/columns, responsive grids that reflow without media queries. +**When NOT to use:** Single-axis alignment (use Flexbox), content that flows like text (use normal flow). + +### Responsive Grid with `auto-fill` / `auto-fit` + +```css +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.5rem; +} +``` + +`auto-fill` keeps empty tracks (items stay at minmax size). `auto-fit` collapses empty tracks (items stretch to fill). Use `auto-fill` for consistent widths; use `auto-fit` when fewer items should expand. + +### Named Grid Areas + +```css +.page { + display: grid; + grid-template-areas: + "header header" + "sidebar main" + "footer footer"; + grid-template-columns: 260px 1fr; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; +} +.page-header { grid-area: header; } +.page-sidebar { grid-area: sidebar; } +.page-main { grid-area: main; } +.page-footer { grid-area: footer; } + +@media (width < 768px) { + .page { + grid-template-areas: "header" "main" "sidebar" "footer"; + grid-template-columns: 1fr; + } +} +``` + +### Named Lines + +Semantic anchors for item placement — no counting track numbers. + +```css +.layout { + display: grid; + grid-template-columns: + [full-start] 1fr + [content-start] minmax(0, 960px) + [content-end] 1fr + [full-end]; +} +.layout > * { grid-column: content; } +.layout > .full-bleed { grid-column: full; } +``` + +### Grid Alignment + +```css +.grid-container { + place-items: center; /* align all items: block + inline */ + place-content: space-between; /* distribute tracks within container */ +} +.grid-item { + place-self: center end; /* override for a single item */ +} +``` + +--- + +## 2. Subgrid + +Use Subgrid when **child elements inside grid items must align across sibling items**. The canonical example: a row of cards where every heading, body, and footer lines up, regardless of content length. + +**Browser support:** Baseline since September 2023. Production-ready; keep the `@supports` fallback below only if you must support pre-2023 browsers. + +```css +/* ❌ Without subgrid: each card's rows are independent */ +.card { + display: grid; + grid-template-rows: auto 1fr auto; /* isolated — won't align across cards */ +} +``` + +```css +/* ✅ With subgrid: card rows inherit parent grid tracks */ +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + grid-auto-rows: auto; + gap: 1rem 1.5rem; +} +.card { + display: grid; + grid-row: span 3; + grid-template-rows: subgrid; +} +.card-heading { grid-row: 1; } +.card-body { grid-row: 2; } +.card-footer { grid-row: 3; } +``` + +Subgrid can apply to one axis independently. Use `grid-template-rows: subgrid` with custom columns, or `grid-template-columns: subgrid` with custom rows. + +### Fallback for Legacy Browsers + +```css +.card { + display: grid; + grid-template-rows: auto 1fr auto; +} +@supports (grid-template-rows: subgrid) { + .card { + grid-row: span 3; + grid-template-rows: subgrid; + } +} +``` + +--- + +## 3. Grid Lanes (Masonry) + +Use Grid Lanes when content has **variable heights and must pack tightly** — image galleries, Pinterest-style feeds, mixed-content cards. + +**Status (mid-2026): not yet cross-browser.** After a multi-year syntax debate (`display: masonry` vs `grid-template-rows: masonry`), the CSSWG settled on `display: grid-lanes` in CSS Grid Level 3. Safari 26.4 is the first stable browser to ship it; Chrome and Firefox have implementations behind flags and are expected to ship later in 2026. Always ship a fallback. + +Grid Lanes defines strict lanes (columns) via `grid-template-columns` but lets items flow freely in the stacking axis. Items pack into whichever lane gets them closest to the top. + +```css +.gallery { + display: grid-lanes; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1rem; +} +``` + +The `flow-tolerance` property (renamed from `item-tolerance` by the CSSWG in January 2026) relaxes strict shortest-lane placement — items can go into a slightly taller lane to stay closer to source order: + +```css +.gallery { + display: grid-lanes; + grid-template-columns: repeat(3, 1fr); + flow-tolerance: 50px; + gap: 1rem; +} +``` + +### Progressive Enhancement + +```css +/* ❌ No fallback — broken in most browsers */ +.gallery { display: grid-lanes; } +``` + +```css +/* ✅ Progressive enhancement */ +.gallery { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1rem; +} +@supports (display: grid-lanes) { + .gallery { display: grid-lanes; } +} +``` + +CSS columns as an alternative fallback (closer visual approximation): + +```css +.gallery { + columns: 250px; + column-gap: 1rem; +} +.gallery > * { break-inside: avoid; margin-bottom: 1rem; } + +@supports (display: grid-lanes) { + .gallery { + columns: unset; + display: grid-lanes; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1rem; + } + .gallery > * { break-inside: unset; margin-bottom: unset; } +} +``` + +| Feature | Standard Grid | Grid Lanes | Flexbox Wrap | +|---------|--------------|------------|--------------| +| Axis control | 2D (rows + columns) | Lanes + free stacking | 1D + wrap | +| Variable heights | Gaps between items | Tight packing | Uneven rows | +| Named areas/lines | Yes | Yes (lane axis only) | No | +| Browser support | Baseline | Safari 26.4+ only | Baseline | + +--- + +## 4. Flexbox + +Use Flexbox for **single-axis layout** — distributing space, centering, navigation bars, toolbars, inline controls. + +**When to use:** Centering, nav bars, toolbars, button groups, distributing space in a row/column. +**When NOT to use:** 2D layouts (use Grid), wrapping card grids (use Grid with `auto-fill`), layouts where items in different rows must align vertically. + +### Centering + +```css +.center { + display: flex; + place-content: center; + place-items: center; +} +``` + +### Space Distribution + +```css +.toolbar { + display: flex; + gap: 0.5rem; +} +.toolbar .push-right { margin-inline-start: auto; } + +.nav { display: flex; justify-content: space-between; } +.tabs { display: flex; justify-content: space-evenly; } +``` + +### Flex Sizing + +```css +/* Equal columns */ +.equal { display: flex; & > * { flex: 1; } } + +/* Fixed sidebar + fluid main */ +.layout { + display: flex; + .sidebar { flex: 0 0 260px; } + .main { flex: 1; } +} +``` + +### Anti-Pattern: Flexbox for Card Grids + +```css +/* ❌ Last row items stretch unevenly */ +.card-grid { display: flex; flex-wrap: wrap; gap: 1rem; } +.card { flex: 1 1 300px; } +``` + +```css +/* ✅ Grid guarantees consistent columns */ +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} +``` + +--- + +## 5. Container Queries + +Container queries shift responsive design from **viewport-based to component-based**. The core principle: **components should respond to their container, not the viewport.** A card in a wide main column should look different from the same card in a narrow sidebar — and that logic belongs to the card, not a media query that knows the page layout. + +### Container vs Media Query Mental Model + +```mermaid +flowchart LR + subgraph MEDIA["@media — viewport-driven"] + direction TB + VP["Viewport Width"] --> MQ["@media (width >= 768px)"] + MQ --> PAGE["Page-level layout changes"] + PAGE --> NOTE1["Components coupled
to page structure"] + end + subgraph CONTAINER["@container — component-driven"] + direction TB + CT["Container Width"] --> CQ["@container (inline-size >= 400px)"] + CQ --> COMP["Component adapts
to its own space"] + COMP --> NOTE2["Components are
portable and reusable"] + end + MEDIA -.->|"Shift from viewport
thinking to container
thinking"| CONTAINER + style MEDIA fill:#ef4444,stroke:#dc2626,color:white + style CONTAINER fill:#10b981,stroke:#059669,color:white +``` + +Use `@media` for page-level layout (sidebar collapses, nav changes). Use `@container` for component-level adaptation. + +### Setting Up Containment + +```css +.card-wrapper { + container: card / inline-size; /* name / type shorthand */ +} +``` + +**Container types:** `inline-size` (query width — use almost always), `size` (query both axes — needs defined height), `normal` (default, cannot query). + +### Size Queries + +```css +.card { display: grid; gap: 1rem; } + +@container card (inline-size >= 400px) { + .card { grid-template-columns: 200px 1fr; } +} +@container card (inline-size >= 700px) { + .card { grid-template-columns: 300px 1fr 200px; } + .card-metadata { display: block; } +} +``` + +Use range syntax. Avoid legacy `min-width`/`max-width`. + +```css +/* ❌ Legacy */ @container card (min-width: 400px) { } +/* ✅ Modern */ @container card (inline-size >= 400px) { } +/* ✅ Range */ @container card (400px <= inline-size <= 800px) { } +``` + +### Style Queries + +Respond to custom property values on the container — conditional styling based on context, not size. + +```css +@container card style(--variant: featured) { + .card { border: 2px solid oklch(0.7 0.15 250); } +} +@container card style(--variant: compact) { + .card { padding: 0.5rem; font-size: 0.875rem; } +} +``` + +**Range syntax for style queries**: compare numeric custom property values. + +```css +@container card style(--priority >= 3) { + .card { border-inline-start: 4px solid oklch(0.6 0.2 30); } +} +``` + +**Browser support:** Style queries on custom properties became Baseline Newly Available in May 2026 (Chrome 111+, Safari 18+, Firefox 151+). Range syntax for style queries is newer still — verify on MDN. Unsupported browsers skip the `@container style()` block, so structure defaults outside it and treat the query as an enhancement. + +### Container Query Units + +| Unit | Relative To | +|------|-------------| +| `cqw` | 1% of container width | +| `cqh` | 1% of container height (needs `container-type: size`) | +| `cqi` | 1% of container inline size (prefer over `cqw`) | +| `cqb` | 1% of container block size (needs `container-type: size`) | +| `cqmin` | Smaller of `cqi` or `cqb` | +| `cqmax` | Larger of `cqi` or `cqb` | + +Prefer `cqi` over `cqw` — it respects writing direction. + +```css +.card-wrapper { container-type: inline-size; } +.card-title { font-size: clamp(1rem, 3cqi, 1.75rem); } +.card-body { padding: clamp(0.75rem, 2cqi, 2rem); } +``` + +### When to Use Each + +| Concern | `@container` | `@media` | +|---------|-------------|----------| +| Component adapts to available space | Yes | No | +| Page structure changes | No | Yes | +| Component reused in multiple contexts | Yes | No | +| User preferences (dark mode, motion) | No | Yes (`prefers-*`) | + +### Practical Example: Reusable Article Card + +```css +.article-container { container: article / inline-size; } +.article-card { + display: grid; + gap: 0.75rem; + padding: clamp(0.75rem, 2cqi, 1.5rem); +} +.article-card .thumbnail { aspect-ratio: 16 / 9; object-fit: cover; } + +@container article (inline-size < 400px) { + .article-card { grid-template-columns: 1fr; } + .article-card .metadata { display: none; } +} +@container article (400px <= inline-size < 700px) { + .article-card { grid-template-columns: 160px 1fr; } +} +@container article (inline-size >= 700px) { + .article-card { grid-template-columns: 240px 1fr auto; } +} +``` + +This card works in a full-width column, a 300px sidebar, a modal, or a dashboard widget without changing any CSS. + +--- + +## 6. Intrinsic Sizing + +Intrinsic sizing keywords let elements size themselves based on content or container, replacing fixed widths. + +| Keyword | Behavior | Use When | +|---------|----------|----------| +| `min-content` | Shrinks to narrowest without overflow (longest word) | Collapsible sidebars, tight table columns | +| `max-content` | Expands to fit all content, no wrapping | Tags, badges, inline labels | +| `fit-content` | Grows with content up to available space, then wraps | Dialogs, tooltips, captions | +| `stretch` | Fills available space (margin box). Replaces `-webkit-fill-available` | Full-width buttons, full-height apps | + +The `stretch` keyword is Chromium-only (Chrome 138+) as of mid-2026 — provide a fallback (`100%` or `-webkit-fill-available`) before it. The other three keywords are Baseline. + +```css +.dialog { width: fit-content; max-width: 90vw; min-width: 320px; } +.tag { width: max-content; padding-inline: 0.75em; } +.app { min-height: 100%; min-height: stretch; } +``` + +### In Grid Tracks + +```css +.layout { + display: grid; + grid-template-columns: + min-content /* sidebar: as narrow as content allows */ + 1fr /* main: remaining space */ + max-content; /* aside: as wide as content needs */ +} +``` + +### `fit-content()` Function + +Differs from the `fit-content` keyword — accepts a maximum size argument: + +```css +.page { + display: grid; + grid-template-columns: fit-content(200px) 1fr fit-content(300px); +} +``` + +### Extrinsic vs Intrinsic + +```css +/* ❌ Fragile fixed widths */ +.sidebar { width: 250px; } +.dialog { width: 500px; } +``` + +```css +/* ✅ Content-driven sizing */ +.sidebar { width: fit-content; min-width: 200px; max-width: 350px; } +.dialog { width: fit-content; max-width: min(600px, 90vw); } +``` + +--- + +## 7. Aspect Ratio + +The `aspect-ratio` property declares a preferred ratio, replacing the padding-top percentage hack. + +```css +.video { aspect-ratio: 16 / 9; width: 100%; } +.avatar { aspect-ratio: 1; width: 4rem; border-radius: 50%; } +.card { aspect-ratio: 3 / 4; } +``` + +### Legacy vs Modern + +```css +/* ❌ The padding-top hack */ +.video-wrapper { + position: relative; padding-top: 56.25%; height: 0; +} +.video-wrapper iframe { + position: absolute; inset: 0; width: 100%; height: 100%; +} +``` + +```css +/* ✅ Declarative aspect ratio */ +.video-wrapper { aspect-ratio: 16 / 9; width: 100%; } +.video-wrapper iframe { width: 100%; height: 100%; } +``` + +### Combining with `object-fit` + +```css +.thumbnail { aspect-ratio: 4 / 3; object-fit: cover; object-position: center; } +.product-image { aspect-ratio: 1; object-fit: contain; background: oklch(0.97 0 0); } +``` + +| `object-fit` | Behavior | +|--------------|----------| +| `cover` | Fills box, crops overflow. Hero images, thumbnails. | +| `contain` | Fits inside box, may letterbox. Product images, logos. | +| `fill` | Stretches to fill (distorts). Rarely useful. | +| `none` | Natural size, no scaling. Crops if larger than box. | +| `scale-down` | Like `contain`, never scales up. | + +### Preventing Layout Shift + +Combine `auto` with a fallback ratio for images not yet loaded: + +```css +img { + aspect-ratio: auto 4 / 3; + /* Natural ratio once loaded; 4/3 before load to prevent CLS */ +} +``` + +> For non-Baseline features, always feature-detect with `@supports` or use progressive enhancement. Check [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS) or [Baseline](https://web.dev/baseline) for current browser support. + +--- + +## Anti-Patterns + +| Anti-Pattern | Problem | Fix | +|---|---|---| +| `flex-wrap` for card grids | Last row stretches unevenly | `display: grid` with `auto-fill` | +| `@media` for component layout | Couples component to viewport | `@container` queries | +| Padding-top hack for ratio | Fragile, extra wrapper | `aspect-ratio` property | +| Fixed widths on fluid elements | Breaks on resize | Intrinsic sizing or `minmax()` | +| Nested grids without subgrid | Child rows misalign | `grid-template-rows: subgrid` | +| JS masonry libraries | Heavy, layout thrashing | `display: grid-lanes` (when ready) | +| `height: 100vh` on mobile | Ignores mobile browser chrome | `100dvh` | +| `float` for layout | Legacy, fragile | Grid or Flexbox | diff --git a/.agents/skills/modern-css/references/PERFORMANCE.md b/.agents/skills/modern-css/references/PERFORMANCE.md new file mode 100644 index 0000000..7f85ce5 --- /dev/null +++ b/.agents/skills/modern-css/references/PERFORMANCE.md @@ -0,0 +1,614 @@ +# Performance — Rendering, Typography, and Accessibility + +> Sources: [CSS Containment Level 2](https://www.w3.org/TR/css-contain-2/), [CSS Logical Properties Level 1](https://www.w3.org/TR/css-logical-1/), [CSS Text Level 4](https://www.w3.org/TR/css-text-4/), [MDN Web Docs: CSS](https://developer.mozilla.org/en-US/docs/Web/CSS). All features below are Baseline unless noted otherwise. + +## Contents + +- [`content-visibility: auto` — Skip Off-Screen Rendering](#content-visibility-auto--skip-off-screen-rendering) +- [CSS Containment — The `contain` Property](#css-containment--the-contain-property) +- [`will-change` — Compositor Hints](#will-change--compositor-hints) +- [Typography](#typography) +- [Logical Properties — Writing-Direction-Aware CSS](#logical-properties--writing-direction-aware-css) +- [Accessibility Media Queries](#accessibility-media-queries) +- [Progressive Enhancement Meta-Pattern](#progressive-enhancement-meta-pattern) +- [Modern Viewport Units](#modern-viewport-units) +- [Performance Checklist](#performance-checklist) + +--- + +## `content-visibility: auto` — Skip Off-Screen Rendering + +The single most impactful CSS performance property. When applied to off-screen sections, the browser skips layout, paint, and style computation entirely. Chrome's own documentation reports up to **7x rendering improvement** on long pages. The browser lazily renders content only when it approaches the viewport — CSS-level virtualization without JavaScript. + +Use on discrete page sections that start off-screen: article cards below the fold, long lists, tab panels, accordion bodies, comment threads, footer regions. + +**CRITICAL: Never apply to above-the-fold content.** The browser delays rendering of `content-visibility: auto` elements, which directly delays Largest Contentful Paint (LCP). If your hero section, primary heading, or first visible card has this property, you have made performance worse. + +**Do not apply to tiny elements.** The overhead of containment tracking per element exceeds the rendering savings for small items. Apply to section-level containers, not to each individual child. + +**Always measure before and after.** Use Chrome DevTools Performance panel or Lighthouse. If you cannot measure a difference, remove it. + +### Required: `contain-intrinsic-size` + +When the browser skips rendering, it does not know the element's height. Without a size hint, the scrollbar jumps as elements render. You **must** pair with `contain-intrinsic-size`. The `auto` keyword tells the browser to remember the actual size after first render. + +```css +/* ❌ Missing size hint — scrollbar jumps, layout shifts */ +.article-section { + content-visibility: auto; +} + +/* ✅ Proper implementation with size estimate */ +.article-section { + content-visibility: auto; + contain-intrinsic-size: auto 500px; +} +``` + +### Pattern: Long Article Page + +```css +/* Above the fold — NO content-visibility */ +.hero, +.article-intro { + /* Renders immediately */ +} + +/* Below-fold sections skip rendering until needed */ +.article-body, +.comments-section, +.related-articles, +.site-footer { + content-visibility: auto; + contain-intrinsic-size: auto 800px; +} +``` + +Baseline. Feature-detect with `@supports (content-visibility: auto)`. + +--- + +## CSS Containment — The `contain` Property + +`contain` tells the browser that an element's internals are independent from the rest of the page. The rendering engine can skip recalculating layout, paint, or style for the entire document when something inside that element changes. `content-visibility: auto` implicitly applies containment — `contain` is the manual, granular version. + +| Value | What It Isolates | Effect | +|---|---|---| +| `size` | Element's size from children | Element does not resize based on content. You must set explicit dimensions. | +| `layout` | Internal layout from external | Floats, counters, layout changes inside do not affect outside. | +| `paint` | Painting boundary | Content that overflows is clipped. No visible overflow. | +| `style` | Counter and `content` scoping | CSS counters and `quotes` do not leak out. | +| `strict` | All of the above | `size layout paint style`. Maximum isolation. | +| `content` | Layout + paint + style | Like `strict` but without `size` — element can still auto-size. | + +Use `contain: content` on independent UI components — cards, widgets, modals, sidebar panels — especially inside container queries. Use `contain: strict` when you know exact dimensions (ad slots, map containers, iframes). + +**Do not use `contain: size` without explicit dimensions** — the element collapses to zero. **Do not use `contain: paint` on elements that intentionally overflow** — tooltips and dropdowns will be clipped. + +```css +/* ❌ Size containment without dimensions — collapses to 0x0 */ +.widget { + contain: strict; +} + +/* ✅ Size containment with explicit dimensions */ +.widget { + contain: strict; + inline-size: 300px; + block-size: 200px; +} + +/* ✅ Content containment — auto-sizes but isolates layout/paint */ +.card { + contain: content; +} +``` + +Container queries require containment. When you declare `container-type: inline-size`, the browser implicitly applies `contain: inline-size layout style`. No need for explicit `contain` on container query containers. + +--- + +## `will-change` — Compositor Hints + +`will-change` tells the browser which properties will animate, allowing it to create a GPU layer in advance. It is an optimization hint, not a directive. Ideally, add it dynamically just before animation starts, then remove it after — GPU layers consume memory for the entire time they exist. + +**Do not apply to everything.** Every `will-change` creates a GPU compositor layer consuming video memory. A page with `* { will-change: transform; }` can cause the browser to fall back to software rendering. + +**Do not use on elements that never animate.** It wastes GPU memory with zero benefit. + +**Do not use as a "performance sprinkle."** If animation is janky, profile first. The cause is usually layout thrashing, not missing compositor hints. + +```css +/* ❌ Wasteful — GPU layers for everything */ +* { + will-change: transform, opacity; +} + +/* ✅ Targeted — only the property that animates */ +.modal-overlay { + will-change: opacity; + transition: opacity 0.3s ease; +} + +/* ✅ Dynamic via CSS — prime layer on parent hover */ +.card-list:hover .card { + will-change: transform; +} + +.card:hover { + transform: translateY(-4px); +} +``` + +--- + +## Typography + +### `text-wrap: balance` — Equalized Line Lengths + +Distributes text across lines so each line is approximately the same width. Eliminates the problem of headings with one or two orphaned words on the last line. + +**Use only on headings and short text.** Browsers cap the balancing algorithm at 6 lines (Chrome) to 10 lines (spec). Beyond that, balancing is silently ignored. The algorithm is computationally expensive — it tries multiple line-breaking solutions. + +```css +/* ❌ Applying to all text — performance-heavy, ignored on long content */ +body { + text-wrap: balance; +} + +/* ✅ Headings only */ +:is(h1, h2, h3, h4, h5, h6) { + text-wrap: balance; +} +``` + +### `text-wrap: pretty` — Better Paragraph Rag + +Adjusts line breaks to avoid orphans and improves the right-edge "rag" of left-aligned text. Designed for paragraphs and long-form content. Progressive enhancement — Firefox does not support it yet. Degrades gracefully. + +```css +/* Headings balanced, body text pretty */ +:is(h1, h2, h3, h4, h5, h6) { + text-wrap: balance; +} + +:is(p, li, dd, blockquote, figcaption) { + text-wrap: pretty; +} +``` + +### Fluid Typography with `clamp()` + +Creates font sizes that scale smoothly between a minimum and maximum, eliminating breakpoint-based jumps. Always use `rem` for min/max to respect user font-size preferences. + +```css +/* ❌ Breakpoint-based — jumpy */ +h1 { font-size: 1.5rem; } +@media (min-width: 768px) { h1 { font-size: 2rem; } } +@media (min-width: 1200px) { h1 { font-size: 3rem; } } + +/* ✅ Fluid — smooth scaling */ +h1 { + font-size: clamp(1.5rem, 1rem + 2vw, 3rem); +} +``` + +#### Common Fluid Scale + +```css +:root { + --text-sm: clamp(0.875rem, 0.8rem + 0.3vw, 1rem); + --text-base: clamp(1rem, 0.875rem + 0.5vw, 1.25rem); + --text-2xl: clamp(1.25rem, 1rem + 1vw, 1.75rem); + --text-3xl: clamp(1.5rem, 1rem + 2vw, 2.5rem); + --text-4xl: clamp(2rem, 1.2rem + 3.2vw, 3.5rem); +} + +body { font-size: var(--text-base); } +h1 { font-size: var(--text-4xl); } +h2 { font-size: var(--text-3xl); } +h3 { font-size: var(--text-2xl); } +``` + +### `text-box` — Optical Vertical Centering + +`text-box` (shorthand for `text-box-trim` and `text-box-edge`) trims extra space above and below text from the font's line-height metrics. This invisible space causes text to appear off-center in buttons, badges, and tight containers. + +**Not Baseline as of mid-2026:** Chrome/Edge 133+ and Safari 18.2+ support it; Firefox does not (implementation in progress). Degrades gracefully — untrimmed text just keeps its default metrics. Feature-detect with `@supports (text-box: trim-both cap alphabetic)`. + +```css +/* ❌ Padding hacks to compensate for font metrics */ +.badge { + padding: 0.15em 0.5em 0.25em; +} + +/* ✅ Trim to cap height and alphabetic baseline */ +.badge { + text-box: trim-both cap alphabetic; + padding: 0.25em 0.5em; +} +``` + +| Value | Trims | +|---|---| +| `trim-both` | Above and below | +| `trim-start` / `trim-end` | Above only / below only | +| `cap alphabetic` | To cap height and alphabetic baseline | +| `ex alphabetic` | To x-height and alphabetic baseline | + +Use on buttons, badges, pills, tags. Do not apply globally — it changes effective line-height and breaks paragraph spacing. + +### Line-Height Units: `lh` and `rlh` + +`lh` equals the computed `line-height` of the current element. `rlh` equals the root element's `line-height`. These keep spacing proportional to the text rhythm. + +```css +p { + margin-block-end: 1lh; /* Exactly one line of text */ +} + +.section-divider { + block-size: 3rlh; /* Three root line-heights */ +} + +.drop-cap::first-letter { + font-size: 3lh; + float: inline-start; + line-height: 1; +} +``` + +--- + +## Logical Properties — Writing-Direction-Aware CSS + +Logical properties replace physical direction properties (`left`, `right`, `top`, `bottom`) with flow-relative equivalents. In LTR, `margin-inline-start` maps to `margin-left`. In RTL, it maps to `margin-right`. In vertical writing modes, the mapping rotates accordingly. + +### The Rule + +Prefer logical properties for writing-direction-relative layout. Keep physical +properties when the intended behavior is tied to a physical edge; changing them +mechanically can change vertical-writing or mixed-direction layouts. + +### Full Mapping Table + +| Physical | Logical | +|---|---| +| `margin-top` / `margin-bottom` | `margin-block-start` / `margin-block-end` | +| `margin-left` / `margin-right` | `margin-inline-start` / `margin-inline-end` | +| `margin-top` + `bottom` / `left` + `right` | `margin-block` / `margin-inline` | +| `padding-top` / `padding-bottom` | `padding-block-start` / `padding-block-end` | +| `padding-left` / `padding-right` | `padding-inline-start` / `padding-inline-end` | +| `padding-top` + `bottom` / `left` + `right` | `padding-block` / `padding-inline` | +| `border-top` / `border-bottom` | `border-block-start` / `border-block-end` | +| `border-left` / `border-right` | `border-inline-start` / `border-inline-end` | +| `border-top-left-radius` | `border-start-start-radius` | +| `border-top-right-radius` | `border-start-end-radius` | +| `border-bottom-left-radius` | `border-end-start-radius` | +| `border-bottom-right-radius` | `border-end-end-radius` | +| `width` / `height` | `inline-size` / `block-size` | +| `min-width` / `min-height` | `min-inline-size` / `min-block-size` | +| `max-width` / `max-height` | `max-inline-size` / `max-block-size` | +| `top` / `bottom` | `inset-block-start` / `inset-block-end` | +| `left` / `right` | `inset-inline-start` / `inset-inline-end` | +| `top` + `bottom` / `left` + `right` | `inset-block` / `inset-inline` | +| `text-align: left` / `right` | `text-align: start` / `end` | +| `float: left` / `right` | `float: inline-start` / `inline-end` | +| `clear: left` / `right` | `clear: inline-start` / `inline-end` | +| `overflow-x` / `overflow-y` | `overflow-inline` / `overflow-block` | +| `resize: horizontal` / `vertical` | `resize: inline` / `block` | + +### Before/After + +```css +/* ❌ Physical — breaks in RTL */ +.sidebar { + margin-left: 2rem; + padding-right: 1rem; + border-bottom: 1px solid oklch(0.85 0 0); + width: 300px; + top: 0; + left: 0; + text-align: left; +} + +/* ✅ Logical — works in LTR, RTL, and vertical */ +.sidebar { + margin-inline-start: 2rem; + padding-inline-end: 1rem; + border-block-end: 1px solid oklch(0.85 0 0); + inline-size: 300px; + inset-block-start: 0; + inset-inline-start: 0; + text-align: start; +} +``` + +--- + +## Accessibility Media Queries + +These media queries detect user preferences at the OS level. Respecting them is not optional — it is a core accessibility requirement. + +### `prefers-reduced-motion: reduce` + +The user has requested reduced motion. Affects users with vestibular disorders, motion sensitivity, or those who find animation distracting. + +#### Universal Reset Pattern + +Apply once, globally. Removes all animations and transitions unless explicitly overridden for essential motion (loading spinners, progress bars). + +```css +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} +``` + +Why `0.01ms` instead of `0s`? Setting `0s` prevents `animationend` and `transitionend` events from firing, breaking JavaScript listeners. Near-zero fires the events while being visually instant. + +```css +/* Selective override for essential animation */ +@media (prefers-reduced-motion: reduce) { + .loading-spinner { + animation-duration: 1.5s !important; + animation-iteration-count: infinite !important; + } +} +``` + +### `prefers-contrast: more` / `less` + +The user wants higher or lower contrast. `more` is the common case. + +```css +@media (prefers-contrast: more) { + :root { + --border-color: oklch(0 0 0); + --text-secondary: oklch(0.25 0 0); + } + + button, input, select, textarea { + border: 2px solid var(--border-color); + } + + :focus-visible { + outline-width: 3px; + } +} +``` + +### `forced-colors: active` — Windows High Contrast Mode + +The OS overrides all colors with a user-defined palette. Most CSS colors are ignored. Use system color keywords to work with the forced palette, not against it. + +| System Color | Maps To | +|---|---| +| `CanvasText` / `Canvas` | Text / background | +| `LinkText` | Link color | +| `ButtonText` / `ButtonFace` | Button text / background | +| `Highlight` / `HighlightText` | Selection background / text | + +```css +@media (forced-colors: active) { + .btn { + border: 2px solid ButtonText; + background: ButtonFace; + color: ButtonText; + } + + .icon-status { + forced-color-adjust: none; + border: 2px solid currentColor; + } + + :focus-visible { + outline: 2px solid Highlight; + outline-offset: 2px; + } +} +``` + +### `prefers-color-scheme: light` / `dark` + +Detects OS-level color scheme preference. Always pair with the `color-scheme` property — without it, browser chrome (scrollbars, form controls) stays light even when your page is dark. + +```css +/* ❌ No color-scheme — browser UI stays light */ +@media (prefers-color-scheme: dark) { + body { + background: oklch(0.15 0 0); + color: oklch(0.9 0 0); + } +} + +/* ✅ Full dark adaptation */ +:root { + color-scheme: light dark; +} + +@media (prefers-color-scheme: dark) { + :root { + --surface: oklch(0.15 0 0); + --text: oklch(0.9 0 0); + } +} +``` + +Use `light-dark()` for inline values without repeating media queries: + +```css +:root { + color-scheme: light dark; + --surface: light-dark(oklch(0.98 0 0), oklch(0.15 0 0)); + --text: light-dark(oklch(0.15 0 0), oklch(0.9 0 0)); + --border: light-dark(oklch(0.85 0 0), oklch(0.3 0 0)); +} +``` + +--- + +## Progressive Enhancement Meta-Pattern + +Build the baseline first, then enhance with `@supports`. Users on older browsers get a functional page, not a broken one. + +### Template + +```css +/* Baseline — works everywhere */ +.component { + /* Functional styles */ +} + +/* Enhancement — better where supported */ +@supports (feature: value) { + .component { + /* Modern feature */ + } +} +``` + +### Practical Examples + +```css +/* Content visibility — baseline renders normally, enhancement skips off-screen */ +.article-section { + margin-block-end: 2rem; +} + +@supports (content-visibility: auto) { + .article-section:not(:first-child) { + content-visibility: auto; + contain-intrinsic-size: auto 600px; + } +} + +/* Text wrapping — baseline wraps normally, enhancements improve typography */ +@supports (text-wrap: balance) { + h2 { text-wrap: balance; } +} + +@supports (text-wrap: pretty) { + p { text-wrap: pretty; } +} + +/* Layered enhancements — each independent */ +.card { + padding: 1rem; + border: 1px solid oklch(0.85 0 0); +} + +@supports (container-type: inline-size) { + .card-wrapper { container-type: inline-size; } + + @container (inline-size > 400px) { + .card { padding: 2rem; } + } +} + +@supports (text-box: trim-both cap alphabetic) { + .card-title { text-box: trim-both cap alphabetic; } +} +``` + +--- + +## Modern Viewport Units + +The traditional `vh` is ambiguous on mobile — the viewport height changes when the address bar shows or hides, causing `100vh` to overflow the visible area. Modern units resolve this. + +| Unit | Name | Meaning | +|---|---|---| +| `svh` | Small viewport height | Address bar **visible** (smallest viewport) | +| `lvh` | Large viewport height | Address bar **hidden** (largest viewport) | +| `dvh` | Dynamic viewport height | Tracks **current** height as it changes | +| `svw` / `lvw` / `dvw` | Width variants | Rarely differ from `vw` | +| `dvmin` / `dvmax` | Dynamic min/max | Smaller/larger of `dvw` and `dvh` | + +Use `dvh` for elements that must fill the visible viewport exactly. Use `svh` for elements that must never overflow. Use `lvh` when you want maximum space and accept brief overflow during address bar transitions. + +**Do not use `dvh` with transitions on height** — `dvh` changes continuously as the address bar animates, causing jank. **Do not replace all `vh` with `dvh` blindly** — on desktop they are identical. + +```css +/* ❌ Legacy — overflows on mobile */ +.hero { + height: 100vh; +} + +/* ✅ Dynamic — fills visible viewport */ +.hero { + height: 100dvh; +} + +/* ✅ Safe minimum — never overflows */ +.full-screen-modal { + min-height: 100svh; +} +``` + +### Pattern: Mobile App Shell + +```css +.app-shell { + display: grid; + grid-template-rows: auto 1fr auto; + block-size: 100dvh; +} + +.app-header { + position: sticky; + inset-block-start: 0; +} + +.app-content { + overflow-y: auto; +} +``` + +### Fallback + +```css +.hero { + height: 100vh; /* Fallback */ + height: 100dvh; /* Override in supporting browsers */ +} +``` + +--- + +## Performance Checklist + +### Rendering +- [ ] Apply `content-visibility: auto` to below-fold sections with `contain-intrinsic-size` +- [ ] Verify `content-visibility` is NOT on above-fold content (check LCP) +- [ ] Use `contain: content` on independent component containers +- [ ] Apply `will-change` only to actively animating elements + +### Typography +- [ ] Use `text-wrap: balance` on headings, not paragraphs +- [ ] Use `text-wrap: pretty` on paragraphs as progressive enhancement +- [ ] Use `clamp()` with `rem` min/max for fluid typography +- [ ] Use `text-box: trim-both cap alphabetic` for optical centering in buttons/badges + +### Logical Properties +- [ ] Use logical properties in all new code +- [ ] Use `inset-block` / `inset-inline` for positioned elements +- [ ] Use `text-align: start` / `end` instead of `left` / `right` + +### Accessibility +- [ ] Include `prefers-reduced-motion: reduce` universal reset +- [ ] Adapt UI for `prefers-contrast: more` +- [ ] Test with `forced-colors: active` +- [ ] Set `color-scheme: light dark` on `:root` +- [ ] Use `light-dark()` for scheme-adaptive colors + +### Viewport +- [ ] Use `dvh` for mobile full-viewport layouts instead of `vh` +- [ ] Use `svh` for fixed layouts that must never overflow +- [ ] Provide `vh` fallback before `dvh` for legacy browsers diff --git a/.agents/skills/modern-css/references/SCROLL.md b/.agents/skills/modern-css/references/SCROLL.md new file mode 100644 index 0000000..335494c --- /dev/null +++ b/.agents/skills/modern-css/references/SCROLL.md @@ -0,0 +1,597 @@ +# Scroll — Replacing JavaScript Scroll Listeners + +> Sources: [Scroll-Driven Animations Level 1](https://www.w3.org/TR/scroll-animations-1/), [CSS Conditional Rules Level 5](https://www.w3.org/TR/css-conditional-5/) (scroll-state queries), [CSS Overflow Level 5](https://www.w3.org/TR/css-overflow-5/) (scroll markers/buttons), [MDN: Scroll-Driven Animations](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll-driven_animations), [CSS Wrapped 2025](https://chrome.dev/css-wrapped-2025/). + +## Contents + +- [Why This Exists](#why-this-exists) +- [`scroll()` — Scroll Progress Timeline](#scroll--scroll-progress-timeline) +- [`view()` — View Progress Timeline](#view--view-progress-timeline) +- [Scroll-State Container Queries](#scroll-state-container-queries) +- [Native CSS Carousels](#native-css-carousels) +- [`scrollend` Event](#scrollend-event) +- [Accessibility](#accessibility) + +--- + +## Why This Exists + +Every `addEventListener('scroll', ...)` creates a main-thread bottleneck. Scroll listeners fire at 60-120Hz, force layout recalculations, and cause jank. CSS scroll-driven animations run on the compositor thread at zero JavaScript cost. + +Two timeline types map animation progress to a physical scroll property instead of wall-clock time: + +- **`scroll()`** — progress maps to the scroll position of a container (0% at top, 100% at bottom). +- **`view()`** — progress maps to the visibility of an element within a scrollport (0% when entering, 100% when exiting). + +```mermaid +flowchart LR + A["animation-timeline"] --> B["scroll()"] + A --> C["view()"] + + B --> B1["Tied to scroll position"] + B1 --> B2["0% = scroll start"] + B2 --> B3["100% = scroll end"] + B3 --> B4["Use: progress bars, parallax, shrinking headers"] + + C --> C1["Tied to element visibility"] + C1 --> C2["entry: element enters scrollport"] + C2 --> C3["contain: element fully visible"] + C3 --> C4["exit: element leaves scrollport"] + C4 --> C5["Use: fade-in on scroll, reveal animations"] +``` + +**Support (mid-2026): NOT Baseline.** Chrome/Edge 115+ and Safari 26+ support scroll-driven animations; Firefox has a complete implementation behind a flag (`layout.css.scroll-driven-animations.enabled`, on by default in Nightly — expected to ship soon). Design them as progressive enhancement: browsers that don't understand `animation-timeline` simply run the animation on the normal clock (instantly, with default `0s` duration) and settle on the final state — content stays visible, the scroll effect just doesn't happen. Use `@supports (animation-timeline: view())` when you want cleanly separated fallback styling. + +--- + +## `scroll()` — Scroll Progress Timeline + +Maps animation progress to the scroll position of a container. At scroll start, progress is 0%. At scroll end, 100%. + +### Core Pattern: Reading Progress Bar + +```css +/* ❌ JavaScript — main-thread scroll listener */ +``` +```javascript +const bar = document.querySelector('.progress-bar'); +window.addEventListener('scroll', () => { + const pct = window.scrollY / (document.body.scrollHeight - window.innerHeight); + bar.style.width = `${pct * 100}%`; +}); +``` +```css +/* ✅ CSS — compositor-thread, zero JavaScript */ +.progress-bar { + position: fixed; + inset-block-start: 0; + inset-inline-start: 0; + block-size: 3px; + background: oklch(0.55 0.2 260); + transform-origin: left; + animation: grow-progress linear both; + animation-timeline: scroll(); +} + +@keyframes grow-progress { + from { scale: 0 1; } + to { scale: 1 1; } +} +``` + +### `scroll()` Parameters + +```css +animation-timeline: scroll(); /* nearest ancestor, block axis */ +animation-timeline: scroll(root); /* document viewport */ +animation-timeline: scroll(root inline); /* document, horizontal axis */ +animation-timeline: scroll(self); /* element itself as scroller */ +``` + +Parameters: **scroller** (`nearest`, `root`, `self`) and **axis** (`block`, `inline`, `x`, `y`). Both optional. Order does not matter. + +### Named Scroll Timelines + +When the animated element is not inside the scroll container, use a named timeline. + +```css +.scroll-container { + overflow-y: auto; + scroll-timeline: --page-scroll block; +} + +.floating-indicator { + position: fixed; + animation: slide-in linear both; + animation-timeline: --page-scroll; +} + +@keyframes slide-in { + from { translate: -100% 0; } + to { translate: 0 0; } +} +``` + +Named timeline names must start with `--` (like custom properties). + +### Scenario: Parallax + +```css +/* ❌ JavaScript — jank, layout thrashing */ +``` +```javascript +window.addEventListener('scroll', () => { + const y = window.scrollY; + bg.style.transform = `translateY(${y * 0.3}px)`; +}); +``` +```css +/* ✅ CSS — smooth, compositor-only */ +.parallax-layer { + animation: parallax-shift linear both; + animation-timeline: scroll(root); +} + +@keyframes parallax-shift { + from { translate: 0 0; } + to { translate: 0 -30%; } +} +``` + +### Scenario: Shrinking Header on Scroll + +```css +.site-header { + position: sticky; + inset-block-start: 0; + animation: shrink-header linear both; + animation-timeline: scroll(root); + animation-range: 0px 200px; /* constrain to first 200px of scroll */ +} + +@keyframes shrink-header { + from { padding-block: 2rem; font-size: 1.5rem; } + to { padding-block: 0.5rem; font-size: 1rem; } +} +``` + +### Critical: Firefox Duration Requirement + +`animation-duration` is ignored with scroll timelines — scroll position controls progress. But **Firefox requires a non-zero value** for the animation to apply: + +```css +.element { + animation: my-anim linear both; + animation-timeline: scroll(); + animation-duration: 1ms; /* Required by Firefox */ +} +``` + +--- + +## `view()` — View Progress Timeline + +Tracks an element's visibility within a scrollport. Replaces `IntersectionObserver` for visual effects. + +### Core Pattern: Fade-In on Scroll + +```css +/* ❌ JavaScript — IntersectionObserver setup */ +``` +```javascript +const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + observer.unobserve(entry.target); + } + }); +}, { threshold: 0.1 }); +document.querySelectorAll('.reveal').forEach(el => observer.observe(el)); +``` +```css +/* ✅ CSS — declarative, no observer, no class toggling */ +.reveal { + animation: fade-in linear both; + animation-timeline: view(); + animation-range: entry 0% cover 40%; +} + +@keyframes fade-in { + from { opacity: 0; translate: 0 50px; } + to { opacity: 1; translate: 0 0; } +} +``` + +### `view()` Parameters + +```css +animation-timeline: view(); /* block axis (vertical) */ +animation-timeline: view(inline); /* inline axis (horizontal) */ +animation-timeline: view(block 20px); /* with inset — shrink detection area */ +``` + +Parameters: **axis** (`block`, `inline`, `x`, `y`) and **inset** (shrinks scrollport for detection). Both optional. + +### `animation-range` — When the Animation Plays + +This is the most important property for `view()` timelines. It defines which portion of the timeline drives animation progress. + +``` +Timeline phases for view(): +┌──────────┬──────────┬──────────┬──────────┬──────────┐ +│ entry │ entry- │ contain │ exit- │ exit │ +│ │ crossing │ │ crossing │ │ +│ Starts │ Crosses │ Fully │ Crosses │ Fully │ +│ entering │ into │ visible │ out of │ exited │ +└──────────┴──────────┴──────────┴──────────┴──────────┘ + 0% 50% 100% +``` + +```css +animation-range: entry 0% cover 50%; /* first pixel to fully inside */ +animation-range: entry; /* entry phase only */ +animation-range: exit; /* exit phase only */ +animation-range: entry 25% exit 75%; /* 25% entered to 75% exited */ +``` + +### Named View Timelines + +When the animated element differs from the tracked element, use a named timeline. + +```css +.tracked-section { + view-timeline: --section-reveal block; +} + +.tracked-section .content { + animation: slide-up linear both; + animation-timeline: --section-reveal; + animation-range: entry 0% contain 30%; +} + +@keyframes slide-up { + from { opacity: 0; translate: 0 40px; } + to { opacity: 1; translate: 0 0; } +} +``` + +### Scenario: Image Scale-Up on Scroll + +```css +.hero-image { + animation: scale-in linear both; + animation-timeline: view(); + animation-range: entry 0% contain 50%; +} + +@keyframes scale-in { + from { scale: 0.8; opacity: 0.5; } + to { scale: 1; opacity: 1; } +} +``` + +--- + +## Scroll-State Container Queries + +Boolean state detection for scroll-related properties. Unlike scroll-driven animations (which map progress to a timeline), these toggle discrete states. + +**Support: Chromium-only (Chrome/Edge 133+) as of mid-2026. Feature-detect with `@supports (container-type: scroll-state)`.** + +**CRITICAL RULE**: The styled element must be a **child** of the scroll-state container. You cannot style the container itself. This is the same child-only rule as size container queries. + +```css +/* WRONG — styles the container itself */ +.header { container-type: scroll-state; } +@container scroll-state(stuck: top) { + .header { box-shadow: ...; } /* FAILS */ +} + +/* RIGHT — styles a child */ +.header { container-type: scroll-state; } +@container scroll-state(stuck: top) { + .header-inner { box-shadow: ...; } /* WORKS */ +} +``` + +### `stuck` — Sticky Header Shadow + +Detects when `position: sticky` is stuck to a scroll edge. Replaces sentinel-based `IntersectionObserver` hacks. + +```css +/* ❌ JavaScript — sentinel element + IntersectionObserver */ +``` +```javascript +const sentinel = document.createElement('div'); +header.parentNode.insertBefore(sentinel, header); +const observer = new IntersectionObserver(([e]) => { + header.classList.toggle('stuck', !e.isIntersecting); +}, { threshold: [1] }); +observer.observe(sentinel); +``` +```css +/* ✅ CSS — no sentinel, no observer */ +.sticky-header { + position: sticky; + inset-block-start: 0; + container-type: scroll-state; +} + +@container scroll-state(stuck: top) { + .sticky-header-content { + box-shadow: 0 2px 8px oklch(0 0 0 / 0.15); + border-block-end: 1px solid oklch(0.85 0 0); + backdrop-filter: blur(8px); + } +} +``` + +Values: `top`, `bottom`, `inset-block-start`, `inset-block-end`, `inset-inline-start`, `inset-inline-end`, `none`. + +### `snapped` — Carousel Item Highlighting + +Detects the current snap target. Use `not scroll-state(snapped: ...)` to dim non-active items. + +```css +.carousel { + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; +} + +.carousel-item { + scroll-snap-align: center; + container-type: scroll-state; +} + +@container not scroll-state(snapped: x) { + .carousel-item-content { + opacity: 0.4; + scale: 0.95; + transition: opacity 0.3s, scale 0.3s; + } +} + +@container scroll-state(snapped: x) { + .carousel-item-content { + opacity: 1; + scale: 1; + } +} +``` + +Values: `x`, `y`, `block`, `inline`, `none`. + +### `scrollable` — Overflow Indicators + +Show/hide scroll affordances based on whether content overflows. + +```css +.scrollable-area { + overflow-x: auto; + container-type: scroll-state; +} + +@container scroll-state(scrollable: right) { + .scroll-hint-end { + display: block; + background: linear-gradient(to left, oklch(0.98 0 0), transparent); + } +} + +@container not scroll-state(scrollable: right) { + .scroll-hint-end { display: none; } +} +``` + +Values: `top`, `bottom`, `left`, `right`, `inset-block-start`, `inset-block-end`, `inset-inline-start`, `inset-inline-end`, `none`. + +### `scrolled` — Hide-on-Scroll Headers + +Detects whether a container has been scrolled from its initial position. + +```css +.page-wrapper { + container-type: scroll-state; +} + +@container scroll-state(scrolled: top) { + .auto-hide-header { + translate: 0 -100%; + transition: translate 0.3s ease; + } + .back-to-top { + opacity: 1; + pointer-events: auto; + } +} + +@container not scroll-state(scrolled: top) { + .auto-hide-header { + translate: 0 0; + transition: translate 0.3s ease; + } +} +``` + +Values: `top`, `bottom`, `left`, `right`, `inset-block-start`, `inset-block-end`, `inset-inline-start`, `inset-inline-end`, `none`. + +--- + +## Native CSS Carousels + +Built-in pseudo-elements for carousel controls. The browser generates accessible prev/next buttons and pagination markers natively — no JavaScript, no ARIA wiring. Keyboard navigation, screen reader announcements, and focus management are automatic. + +**Support: Chromium-only (Chrome/Edge 135+) as of mid-2026. Feature-detect with `@supports selector(::scroll-button(inline-start))`.** In other browsers the carousel remains a plain scroll-snap container — usable, just without generated controls. + +### `::scroll-button()` — Prev/Next Navigation + +Generates buttons that scroll the container by one page. Only created when `content` is set to a non-`none` value. Auto-disabled at scroll boundaries. + +```css +.carousel { + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; +} + +.carousel > * { + scroll-snap-align: start; + flex: 0 0 100%; +} + +.carousel::scroll-button(inline-start) { content: '\2190'; } +.carousel::scroll-button(inline-end) { content: '\2192'; } + +.carousel::scroll-button(*) { + background: oklch(0.2 0 0 / 0.7); + color: oklch(0.98 0 0); + border: none; + border-radius: 50%; + inline-size: 3rem; + block-size: 3rem; + cursor: pointer; +} + +.carousel::scroll-button(*):disabled { opacity: 0.3; cursor: default; } +.carousel::scroll-button(*):focus-visible { + outline: 2px solid oklch(0.55 0.2 260); + outline-offset: 2px; +} +``` + +Direction arguments: `inline-start`, `inline-end`, `block-start`, `block-end`, `up`, `down`, `left`, `right`, `*` (all). Buttons automatically get `role="button"` and accessible names ("scroll left", "scroll right"). + +### `::scroll-marker` — Pagination Dots + +Generated on each child of the scroll container. Creates clickable indicators that scroll to the associated item. `:target-current` matches the currently scrolled-to item's marker. + +```css +.carousel { + overflow-x: auto; + scroll-snap-type: x mandatory; + scroll-marker-group: after; /* markers after content */ +} + +.carousel::scroll-marker-group { + display: flex; + gap: 0.5rem; + justify-content: center; + padding-block: 1rem; +} + +.carousel > *::scroll-marker { + content: ''; + inline-size: 12px; + block-size: 12px; + border-radius: 50%; + background: oklch(0.75 0 0); + cursor: pointer; +} + +.carousel > *::scroll-marker:target-current { + background: oklch(0.45 0.2 260); +} +``` + +`scroll-marker-group` values: `after` (below/after content), `before` (above/before). + +### Scroll-Spy with `scroll-marker-group: before` + +Use markers as a table-of-contents. The marker for the currently visible section highlights automatically. + +```css +.page-content { + overflow-y: auto; + scroll-snap-type: y proximity; + scroll-marker-group: before; +} + +.page-content::scroll-marker-group { + display: flex; + gap: 1rem; + padding: 1rem; + position: sticky; + inset-block-start: 0; + background: oklch(0.98 0 0); + z-index: 10; +} + +.page-content > section { scroll-snap-align: start; } + +.page-content > section::scroll-marker { + content: attr(data-label); + padding: 0.5rem 1rem; + border-radius: 2rem; + background: oklch(0.95 0 0); +} + +.page-content > section::scroll-marker:target-current { + background: oklch(0.45 0.2 260); + color: oklch(0.98 0 0); +} +``` + +--- + +## `scrollend` Event + +Native event when scrolling completes. Fires once, after all momentum and snap settling. Replaces debounced scroll listeners. **Baseline since September 2025** (Chrome 114+, Firefox 109+, Safari 26+). + +```javascript +// ❌ Old: debounced scroll listener +let timeout; +container.addEventListener('scroll', () => { + clearTimeout(timeout); + timeout = setTimeout(() => { /* ... */ }, 150); +}); + +// ✅ New: native scrollend +container.addEventListener('scrollend', () => { + // fires once, after snap settling +}); +``` + +Fires on: finger lift, mousewheel stop, keyboard scroll end, `scrollTo()` completion, fling momentum end. Does NOT fire when scroll position is unchanged or no overflow exists. + +Related: `scrollsnapchange` (after snap target changes) and `scrollsnapchanging` (during snap transition) — limited cross-browser support. + +--- + +## Accessibility + +### The Universal Opt-Out Pattern + +Wrap ALL scroll-driven animations in a motion preference query. No exceptions. + +```css +@media (prefers-reduced-motion: no-preference) { + .reveal { + animation: fade-in linear both; + animation-timeline: view(); + animation-range: entry 0% cover 40%; + } + + .parallax-layer { + animation: parallax linear both; + animation-timeline: scroll(root); + } +} + +@media (prefers-reduced-motion: reduce) { + .reveal { opacity: 1; } /* ensure content visible without animation */ +} +``` + +### What Does NOT Need Motion Wrapping + +- Scroll-state queries (`stuck`, `snapped`, `scrollable`, `scrolled`) -- toggle static visual states, not animations. +- `::scroll-button()` and `::scroll-marker` -- navigation controls, not motion effects. +- `scrollend` handlers -- respond after scrolling, not during. + +### Native Carousel Accessibility + +`::scroll-button()` automatically receives: implicit `role="button"`, accessible direction names, keyboard focusability, auto-disable at boundaries. `::scroll-marker` automatically receives: grouping semantics, active state announcement, keyboard navigation between markers. Do NOT manually add ARIA attributes to these pseudo-elements. + +> For non-Baseline features, always feature-detect with `@supports` or use progressive enhancement. Check [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS) or [Baseline](https://web.dev/baseline) for current browser support. diff --git a/.agents/skills/modern-css/references/SELECTORS.md b/.agents/skills/modern-css/references/SELECTORS.md new file mode 100644 index 0000000..6c935c5 --- /dev/null +++ b/.agents/skills/modern-css/references/SELECTORS.md @@ -0,0 +1,436 @@ +# Selectors — Targeting Elements Precisely + +> Sources: [CSS Selectors Level 4](https://www.w3.org/TR/selectors-4/), [MDN CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors), [CSS Wrapped 2025](https://chrome.dev/css-wrapped-2025/). All selectors below are Baseline unless noted. + +## Contents + +- [`:has()` — The Parent Selector](#has--the-parent-selector) +- [`:is()` vs `:where()` — Selector List Matching](#is-vs-where--selector-list-matching) +- [`:not()` Enhanced — Complex Negation](#not-enhanced--complex-negation) +- [`:focus-visible` — Accessible Focus Styling](#focus-visible--accessible-focus-styling) +- [Scroll-Driven State: `:target` and Scroll-State Queries](#scroll-driven-state-target-and-scroll-state-queries) +- [Modern Pseudo-Elements Reference](#modern-pseudo-elements-reference) +- [Combining Modern Selectors](#combining-modern-selectors) + +--- + +## `:has()` — The Parent Selector + +The most impactful CSS selector ever added. `:has()` selects an element based on what it **contains** or what **follows** it. Before `:has()`, styling a parent based on its children required JavaScript. That era is over. Baseline in all modern browsers. The #1 most-loved CSS feature in developer surveys since its release. + +`:has()` does not select the argument — it selects the element the `:has()` is attached to, if the argument matches somewhere inside (or after) it. + +### Card With vs Without Image + +```css +/* ❌ JavaScript approach — classList toggling */ +``` +```javascript +document.querySelectorAll('.card').forEach(card => { + if (card.querySelector('img')) card.classList.add('card--has-image'); +}); +``` +```css +/* ✅ Pure CSS — no JS, no extra classes */ +.card:has(img) { grid-template-rows: 200px 1fr; } +.card:not(:has(img)) { grid-template-rows: 1fr; padding-block-start: 2rem; } +``` + +### Form Validation Styling + +```css +/* ❌ JavaScript approach */ +``` +```javascript +form.addEventListener('input', () => { + form.classList.toggle('form--error', !!form.querySelector(':invalid')); +}); +``` +```css +/* ✅ Pure CSS — reacts instantly, no event listeners */ +form:has(:invalid) { border-color: oklch(0.65 0.25 25); } +form:has(:invalid) .submit-btn { opacity: 0.5; pointer-events: none; } +form:not(:has(:invalid)) .submit-btn { background: oklch(0.65 0.2 145); } +``` + +### Label Reacting to Sibling Input State + +Use the adjacent sibling combinator inside `:has()` to select a label based on its neighboring input. + +```css +label:has(+ input:focus) { color: oklch(0.55 0.2 260); font-weight: 700; } +label:has(+ input:placeholder-shown) { opacity: 0.6; } +label:has(+ input:not(:placeholder-shown):invalid) { color: oklch(0.65 0.25 25); } +``` + +### Quantity Queries + +Style containers differently based on how many children they contain. + +```css +/* 5+ items — compact layout */ +ul:has(li:nth-child(5)) { font-size: 0.875rem; gap: 0.25rem; } + +/* 10+ items — multi-column */ +ul:has(li:nth-child(10)) { columns: 2; } + +/* Exactly 1 item */ +ul:has(> li:only-child) { list-style: none; padding: 0; } + +/* 4+ cards — switch to grid */ +.card-grid:has(.card:nth-child(4)) { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); +} +``` + +### Global Page State + +```css +body:has(dialog[open]) { overflow: hidden; } + +/* Dark mode toggle without JS (checkbox hack + :has) */ +html:has(#dark-mode-toggle:checked) { color-scheme: dark; } + +.sidebar:has(nav li:nth-child(8)) { overflow-y: auto; max-block-size: 100dvh; } +``` + +### Feature Detection and Performance + +`:has()` is Baseline, but for defensive coding: `@supports selector(:has(*)) { ... }`. + +Scope `:has()` to specific elements. `.card:has(img)` is fast. `*:has(*)` is not. Avoid overly broad `:has()` in high-frequency selectors. + +--- + +## `:is()` vs `:where()` — Selector List Matching + +Both accept a forgiving selector list and match if **any** selector in the list matches. Same purpose: reducing repetition. The difference is specificity. + +| Selector | Specificity | Use When | +|----------|-------------|----------| +| `:is()` | Takes the **highest** specificity in its list | Normal component styling | +| `:where()` | Always **zero** specificity | Resets, defaults, layers that should be easy to override | + +```css +/* :is() — specificity of its most specific argument */ +:is(#header, .nav, footer) a { + /* Specificity: (1,0,1) — takes #header's ID specificity */ + color: oklch(0.55 0.2 260); +} + +/* :where() — zero specificity regardless of arguments */ +:where(#header, .nav, footer) a { + /* Specificity: (0,0,1) — only the `a` counts */ + color: oklch(0.55 0.2 260); +} +``` + +### Use `:is()` for Normal Styling + +```css +/* ❌ Repetitive */ +header a:hover, nav a:hover, footer a:hover { text-decoration: underline; } + +/* ✅ Grouped with :is() */ +:is(header, nav, footer) a:hover { text-decoration: underline; } + +.btn:is(:hover, :focus-visible) { background: oklch(0.5 0.2 260); } +:is(h1, h2, h3, h4):is(:first-child) { margin-block-start: 0; } +``` + +### Use `:where()` for Overridable Defaults + +```css +/* Reset — any author style should win without specificity fights */ +:where(ul, ol) { list-style: none; padding: 0; margin: 0; } +:where(a) { color: inherit; text-decoration: none; } + +/* Base layer defaults */ +:where(.prose) :where(h1, h2, h3) { font-weight: 700; line-height: 1.2; } + +/* Override with zero effort — any class selector wins over :where() */ +.custom-heading { font-weight: 400; } +``` + +Pairs well with `@layer`: + +```css +@layer reset { + :where(*) { margin: 0; padding: 0; box-sizing: border-box; } +} +``` + +### Forgiving Selector Lists + +Both use **forgiving** parsing. If one selector in the list is invalid, the rest still work. + +```css +/* Regular list: if :unknown-pseudo is invalid, ENTIRE rule fails */ +.a, :unknown-pseudo, .b { color: red; } + +/* :is() / :where(): invalid selectors skipped, .a and .b still match */ +:is(.a, :unknown-pseudo, .b) { color: red; } +``` + +--- + +## `:not()` Enhanced — Complex Negation + +`:not()` now accepts complex selector lists (Selectors Level 4). Previously limited to a single simple selector. + +```css +/* ❌ Old: chained single :not() calls */ +li:not(:first-child):not(:last-child) { border-block-start: 1px solid oklch(0.8 0 0); } + +/* ✅ New: selector list in one :not() */ +li:not(:first-child, :last-child) { border-block-start: 1px solid oklch(0.8 0 0); } +``` + +### Practical Patterns + +```css +form :not([type="submit"], [type="hidden"]) { margin-block-end: 1rem; } +:not(nav, footer) > a { text-decoration: underline; } +:is(button, input, select, textarea):not(:disabled) { cursor: pointer; } +.card:not(:has(img), .featured) { background: oklch(0.97 0 0); } +``` + +Specificity of `:not()` is taken from its most specific argument, same as `:is()`. + +--- + +## `:focus-visible` — Accessible Focus Styling + +Use `:focus-visible` instead of `:focus` for visible focus indicators. The browser shows `:focus-visible` styles only on **keyboard** navigation (Tab, arrow keys) or assistive technology — not on mouse clicks. This is the accessibility-correct approach. + +`:focus` applies on **every** focus event including clicks, causing unwanted focus rings that lead developers to remove outlines entirely — breaking keyboard accessibility. + +```css +/* ❌ Anti-pattern: removing all outlines */ +*:focus { outline: none; } + +/* ❌ Shows focus ring on mouse click */ +.btn:focus { outline: 3px solid oklch(0.55 0.2 260); } + +/* ✅ Shows focus ring only on keyboard navigation */ +.btn:focus-visible { + outline: 3px solid oklch(0.55 0.2 260); + outline-offset: 2px; +} +``` + +### Standard Pattern + +```css +:focus { outline: none; } +:focus-visible { outline: 2px solid oklch(0.55 0.2 260); outline-offset: 2px; } +``` + +### Combining with Other Selectors + +```css +.btn:hover { background: oklch(0.45 0.2 260); } +.btn:focus-visible { outline: 3px solid oklch(0.7 0.2 260); outline-offset: 2px; } + +/* Highlight parent on keyboard focus */ +.input-group:has(:focus-visible) { + box-shadow: 0 0 0 3px oklch(0.7 0.15 260 / 0.3); +} + +/* Only show ring when internal focus is from keyboard */ +.dropdown:focus-within:has(:focus-visible) { + outline: 2px solid oklch(0.55 0.2 260); +} +``` + +--- + +## Scroll-Driven State: `:target` and Scroll-State Queries + +### `:target` — Fragment-Based Styling + +Matches the element whose `id` matches the URL fragment (`#hash`). Use for scroll-to-section highlighting and in-page navigation. + +```css +section:target { + background: oklch(0.95 0.05 80); + border-inline-start: 4px solid oklch(0.55 0.2 260); +} + +html { scroll-behavior: smooth; } +section:target { animation: flash 1s ease-out; } + +@keyframes flash { + from { background: oklch(0.9 0.1 80); } + to { background: transparent; } +} +``` + +### Scroll-State Container Queries + +For true scroll-spy behavior (styling based on scroll position), use scroll-state container queries. These track stuck (sticky) and snapped (scroll-snap) states. + +As with all container queries, rules inside `@container` can only style **descendants** of the container — never the container itself. Put the visual styles on an inner wrapper element. See [SCROLL.md](SCROLL.md#scroll-state-container-queries) for the full pattern. + +```css +/* Stuck state — detect when sticky element is stuck. + Styles apply to .sticky-header-inner, a child of the container. */ +.sticky-header { + position: sticky; + top: 0; + container-type: scroll-state; +} + +@container scroll-state(stuck: top) { + .sticky-header-inner { + box-shadow: 0 2px 8px oklch(0 0 0 / 0.15); + border-block-end: 1px solid oklch(0.85 0 0); + } +} + +/* Snapped state — detect when element is snapped */ +.carousel-item { + scroll-snap-align: center; + container-type: scroll-state; +} + +@container scroll-state(snapped: x) { + .carousel-item-content { scale: 1.05; opacity: 1; } +} +``` + +**Support:** Chromium-only (Chrome/Edge 133+) as of mid-2026. Feature-detect: `@supports (container-type: scroll-state) { ... }` + +--- + +## Modern Pseudo-Elements Reference + +### Baseline (Use Freely) + +| Pseudo-Element | What It Targets | Use When | +|---|---|---| +| `::marker` | Bullet/number of list items | Custom list styling (color, size, content) | +| `::backdrop` | Overlay behind `` or fullscreen | Dimming/blurring behind modals | +| `::placeholder` | Placeholder text in inputs | Styling hint text (color, font, opacity) | +| `::selection` | User-highlighted text | Brand-colored text selection | +| `::details-content` | Content area of `
` | Animating accordions (Baseline since Sep 2025: Chrome 131+, Firefox 143+, Safari 18.4+) | + +```css +/* Animate
open/close — no JS accordion needed */ +details::details-content { + block-size: 0; + overflow: clip; + interpolate-size: allow-keywords; /* Chromium-only: enables 0 → auto interpolation */ + transition: block-size 0.3s ease, content-visibility 0.3s ease allow-discrete; +} +details[open]::details-content { + block-size: auto; +} +``` + +```css +li::marker { color: oklch(0.55 0.2 260); font-size: 1.2em; } +li::marker { content: '-- '; } + +dialog::backdrop { background: oklch(0.15 0 0 / 0.6); backdrop-filter: blur(4px); } +input::placeholder { color: oklch(0.6 0 0); font-style: italic; } +::selection { background: oklch(0.55 0.2 260 / 0.3); color: oklch(0.2 0 0); } +``` + +### View Transition Pseudo-Elements (Baseline Newly Available Oct 2025) + +Exist only during a same-document view transition (Baseline since Firefox 144, October 2025). Customize animation between old and new states. + +| Pseudo-Element | What It Targets | +|---|---| +| `::view-transition` | Root overlay for the entire transition | +| `::view-transition-group(name)` | Container animating between old/new | +| `::view-transition-old(name)` | Snapshot of the outgoing state | +| `::view-transition-new(name)` | Snapshot of the incoming state | +| `::view-transition-image-pair(name)` | Container holding old and new snapshots | + +```css +.hero-image { view-transition-name: hero; } +::view-transition-old(hero) { animation: fade-out 0.3s ease-out; } +::view-transition-new(hero) { animation: fade-in 0.3s ease-in; } +::view-transition-old(root) { animation-duration: 0.2s; } +``` + +### Chromium-Only / Emerging (Feature-Detect Required) + +As of mid-2026 these ship only in Chromium (Safari 27 has announced customizable `` | Custom dropdown widgets | +| `::picker-icon` | Dropdown arrow on `` | JS-based display sync | +| `::scroll-button(*)` | Prev/next buttons for overflow containers | JS carousel controls | +| `::scroll-marker` | Individual scroll indicator (dot) | JS scroll indicator dots | +| `::scroll-marker-group` | Container for scroll markers | JS carousel dot container | + +```css +/* Native carousel controls */ +.carousel { overflow-x: auto; scroll-snap-type: x mandatory; } +.carousel::scroll-button(inline-start) { content: '<'; } +.carousel::scroll-button(inline-end) { content: '>'; } + +.carousel::scroll-marker-group { + display: flex; gap: 0.5rem; justify-content: center; padding-block: 0.5rem; +} +.carousel > *::scroll-marker { + content: ''; inline-size: 12px; block-size: 12px; + border-radius: 50%; background: oklch(0.75 0 0); +} +.carousel > *::scroll-marker:target-current { background: oklch(0.45 0.2 260); } +``` + +### Customizable `` sits inline on the baseline: a filled block of the one-step tone, exactly one line-height tall and about 7em wide, with the placeholder in the same face and size in the softer ink, no border, and the page's radius or motif corner. The sentence's last word is the submit ` +``` + +## Dynamic Shortcuts + +Use RegExp matcher with function, similar to dynamic rules: + +```ts +shortcuts: [ + // Static shortcuts can be in array too + { + btn: 'py-2 px-4 font-semibold rounded-lg shadow-md', + }, + // Dynamic shortcut + [/^btn-(.*)$/, ([, c]) => `bg-${c}-400 text-${c}-100 py-2 px-4 rounded-lg`], +] +``` + +Now `btn-green` and `btn-red` generate: + +```css +.btn-green { + padding: 0.5rem 1rem; + --un-bg-opacity: 1; + background-color: rgb(74 222 128 / var(--un-bg-opacity)); + border-radius: 0.5rem; + --un-text-opacity: 1; + color: rgb(220 252 231 / var(--un-text-opacity)); +} +``` + +## Accessing Theme in Shortcuts + +Dynamic shortcuts receive context with theme access: + +```ts +shortcuts: [ + [/^badge-(.*)$/, ([, c], { theme }) => { + if (Object.keys(theme.colors).includes(c)) + return `bg-${c}4:10 text-${c}5 rounded` + }], +] +``` + +## Shortcuts Layer + +Shortcuts are output to the `shortcuts` layer by default. Configure with: + +```ts +shortcutsLayer: 'my-shortcuts-layer' +``` + +## Key Points + +- Later shortcuts have higher priority +- Shortcuts can reference other shortcuts +- Dynamic shortcuts work like dynamic rules +- Shortcuts are expanded at build time, not runtime +- All variants work with shortcuts (`hover:btn`, `dark:btn`, etc.) + + diff --git a/.agents/skills/unocss/references/core-theme.md b/.agents/skills/unocss/references/core-theme.md new file mode 100644 index 0000000..ba28098 --- /dev/null +++ b/.agents/skills/unocss/references/core-theme.md @@ -0,0 +1,172 @@ +--- +name: unocss-theme +description: Theming system for colors, breakpoints, and design tokens +--- + +# UnoCSS Theme + +UnoCSS supports theming similar to Tailwind CSS / Windi CSS. The `theme` property is deep-merged with the default theme. + +## Basic Usage + +```ts +theme: { + colors: { + veryCool: '#0000ff', // class="text-very-cool" + brand: { + primary: 'hsl(var(--hue, 217) 78% 51%)', // class="bg-brand-primary" + DEFAULT: '#942192' // class="bg-brand" + }, + }, +} +``` + +## Using Theme in Rules + +Access theme values in dynamic rules: + +```ts +rules: [ + [/^text-(.*)$/, ([, c], { theme }) => { + if (theme.colors[c]) + return { color: theme.colors[c] } + }], +] +``` + +## Using Theme in Variants + +```ts +variants: [ + { + name: 'variant-name', + match(matcher, { theme }) { + // Access theme.breakpoints, theme.colors, etc. + }, + }, +] +``` + +## Using Theme in Shortcuts + +```ts +shortcuts: [ + [/^badge-(.*)$/, ([, c], { theme }) => { + if (Object.keys(theme.colors).includes(c)) + return `bg-${c}4:10 text-${c}5 rounded` + }], +] +``` + +## Breakpoints + +**Warning:** Custom `breakpoints` object **overrides** the default, not merges. + +```ts +theme: { + breakpoints: { + sm: '320px', + md: '640px', + }, +} +``` + +Only `sm:` and `md:` variants will be available. + +### Inherit Default Breakpoints + +Use `extendTheme` to merge with defaults: + +```ts +extendTheme: (theme) => { + return { + ...theme, + breakpoints: { + ...theme.breakpoints, + sm: '320px', + md: '640px', + }, + } +} +``` + +**Note:** `verticalBreakpoints` works the same for vertical layout. + +### Breakpoint Sorting + +Breakpoints are sorted by size. Use consistent units to avoid errors: + +```ts +theme: { + breakpoints: { + sm: '320px', + // Don't mix units - convert rem to px + // md: '40rem', // Bad + md: `${40 * 16}px`, // Good + lg: '960px', + }, +} +``` + +## ExtendTheme + +`extendTheme` lets you modify the merged theme object: + +### Mutate Theme + +```ts +extendTheme: (theme) => { + theme.colors.veryCool = '#0000ff' + theme.colors.brand = { + primary: 'hsl(var(--hue, 217) 78% 51%)', + } +} +``` + +### Replace Theme + +Return a new object to completely replace: + +```ts +extendTheme: (theme) => { + return { + ...theme, + colors: { + ...theme.colors, + veryCool: '#0000ff', + }, + } +} +``` + +## Theme Differences in Presets + +### preset-wind3 vs preset-wind4 + +| preset-wind3 | preset-wind4 | +|--------------|--------------| +| `fontFamily` | `font` | +| `fontSize` | `text.fontSize` | +| `lineHeight` | `text.lineHeight` or `leading` | +| `letterSpacing` | `text.letterSpacing` or `tracking` | +| `borderRadius` | `radius` | +| `easing` | `ease` | +| `breakpoints` | `breakpoint` | +| `boxShadow` | `shadow` | +| `transitionProperty` | `property` | + +## Common Theme Keys + +- `colors` - Color palette +- `breakpoints` - Responsive breakpoints +- `fontFamily` - Font stacks +- `fontSize` - Text sizes +- `spacing` - Spacing scale +- `borderRadius` - Border radius values +- `boxShadow` - Shadow definitions +- `animation` - Animation keyframes and timing + + diff --git a/.agents/skills/unocss/references/core-variants.md b/.agents/skills/unocss/references/core-variants.md new file mode 100644 index 0000000..b6a2923 --- /dev/null +++ b/.agents/skills/unocss/references/core-variants.md @@ -0,0 +1,175 @@ +--- +name: unocss-variants +description: Apply variations like hover:, dark:, responsive to rules +--- + +# UnoCSS Variants + +Variants apply modifications to utility rules, like `hover:`, `dark:`, or responsive prefixes. + +## How Variants Work + +When matching `hover:m-2`: + +1. `hover:m-2` is extracted from source +2. Sent to all variants for matching +3. `hover:` variant matches and returns `m-2` +4. Result `m-2` continues to next variants +5. Finally matches the rule `.m-2 { margin: 0.5rem; }` +6. Variant transformation applied: `.hover\:m-2:hover { margin: 0.5rem; }` + +## Creating Custom Variants + +```ts +variants: [ + // hover: variant + (matcher) => { + if (!matcher.startsWith('hover:')) + return matcher + return { + // Remove prefix, pass to next variants/rules + matcher: matcher.slice(6), + // Modify the selector + selector: s => `${s}:hover`, + } + }, +], +rules: [ + [/^m-(\d)$/, ([, d]) => ({ margin: `${d / 4}rem` })], +] +``` + +## Variant Return Object + +- `matcher` - The processed class name to pass forward +- `selector` - Function to customize the CSS selector +- `parent` - Wrapper like `@media`, `@supports` +- `layer` - Specify output layer +- `sort` - Control ordering + +## Built-in Variants (preset-wind3) + +### Pseudo-classes +- `hover:`, `focus:`, `active:`, `visited:` +- `first:`, `last:`, `odd:`, `even:` +- `disabled:`, `checked:`, `required:` +- `focus-within:`, `focus-visible:` + +### Pseudo-elements +- `before:`, `after:` +- `placeholder:`, `selection:` +- `marker:`, `file:` + +### Responsive +- `sm:`, `md:`, `lg:`, `xl:`, `2xl:` +- `lt-sm:` (less than sm) +- `at-lg:` (at lg only) + +### Dark Mode +- `dark:` - Class-based dark mode (default) +- `@dark:` - Media query dark mode + +### Group/Peer +- `group-hover:`, `group-focus:` +- `peer-checked:`, `peer-focus:` + +### Container Queries +- `@container`, `@sm:`, `@md:` + +### Print +- `print:` + +### Supports +- `supports-[display:grid]:` + +### Aria +- `aria-checked:`, `aria-disabled:` + +### Data Attributes +- `data-[state=open]:` + +## Dark Mode Configuration + +### Class-based (default) +```ts +presetWind3({ + dark: 'class' +}) +``` + +```html +
+``` + +Generates: `.dark .dark\:bg-gray-800 { ... }` + +### Media Query +```ts +presetWind3({ + dark: 'media' +}) +``` + +Generates: `@media (prefers-color-scheme: dark) { ... }` + +### Opt-in Media Query +Use `@dark:` regardless of config: + +```html +
+``` + +### Custom Selectors +```ts +presetWind3({ + dark: { + light: '.light-mode', + dark: '.dark-mode', + } +}) +``` + +## CSS @layer Variant + +Native CSS `@layer` support: + +```html +
+``` + +Generates: +```css +@layer foo { + .layer-foo\:p-4 { padding: 1rem; } +} +``` + +## Breakpoint Differences from Windi CSS + +| Windi CSS | UnoCSS | +|-----------|--------| +| `xl:p-1` | `xl:p-1` | + +## Media Hover (Experimental) + +Addresses sticky hover on touch devices: + +```html +
+``` + +Generates: +```css +@media (hover: hover) and (pointer: fine) { + .\@hover-text-red:hover { color: rgb(248 113 113); } +} +``` + + diff --git a/.agents/skills/unocss/references/integrations-nuxt.md b/.agents/skills/unocss/references/integrations-nuxt.md new file mode 100644 index 0000000..af53921 --- /dev/null +++ b/.agents/skills/unocss/references/integrations-nuxt.md @@ -0,0 +1,199 @@ +--- +name: unocss-nuxt-integration +description: UnoCSS module for Nuxt applications +--- + +# UnoCSS Nuxt Integration + +The official Nuxt module for UnoCSS. + +## Installation + +```bash +pnpm add -D unocss @unocss/nuxt +``` + +Add to Nuxt config: + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + modules: [ + '@unocss/nuxt', + ], +}) +``` + +Create config file: + +```ts +// uno.config.ts +import { defineConfig, presetWind3 } from 'unocss' + +export default defineConfig({ + presets: [ + presetWind3(), + ], +}) +``` + +**Note:** The `uno.css` entry is automatically injected by the module. + +## Support Status + +| Build Tool | Nuxt 2 | Nuxt Bridge | Nuxt 3 | +|------------|--------|-------------|--------| +| Webpack Dev | ✅ | ✅ | 🚧 | +| Webpack Build | ✅ | ✅ | ✅ | +| Vite Dev | - | ✅ | ✅ | +| Vite Build | - | ✅ | ✅ | + +## Configuration + +### Using uno.config.ts (Recommended) + +Use a dedicated config file for best IDE support: + +```ts +// uno.config.ts +import { defineConfig, presetWind3, presetIcons } from 'unocss' + +export default defineConfig({ + presets: [ + presetWind3(), + presetIcons(), + ], + shortcuts: { + 'btn': 'py-2 px-4 font-semibold rounded-lg', + }, +}) +``` + +### Nuxt Layers Support + +Enable automatic config merging from Nuxt layers: + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + unocss: { + nuxtLayers: true, + }, +}) +``` + +Then in your root config: + +```ts +// uno.config.ts +import config from './.nuxt/uno.config.mjs' + +export default config +``` + +Or extend the merged config: + +```ts +// uno.config.ts +import { mergeConfigs } from '@unocss/core' +import config from './.nuxt/uno.config.mjs' + +export default mergeConfigs([config, { + // Your overrides + shortcuts: { + 'custom': 'text-red-500', + }, +}]) +``` + +## Common Setup Example + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + modules: [ + '@unocss/nuxt', + ], +}) +``` + +```ts +// uno.config.ts +import { + defineConfig, + presetAttributify, + presetIcons, + presetTypography, + presetWebFonts, + presetWind3, + transformerDirectives, + transformerVariantGroup, +} from 'unocss' + +export default defineConfig({ + presets: [ + presetWind3(), + presetAttributify(), + presetIcons({ + scale: 1.2, + }), + presetTypography(), + presetWebFonts({ + fonts: { + sans: 'DM Sans', + mono: 'DM Mono', + }, + }), + ], + transformers: [ + transformerDirectives(), + transformerVariantGroup(), + ], + shortcuts: [ + ['btn', 'px-4 py-1 rounded inline-block bg-teal-600 text-white cursor-pointer hover:bg-teal-700 disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50'], + ], +}) +``` + +## Usage in Components + +```vue + +``` + +With attributify mode: + +```vue + +``` + +## Inspector + +In development, visit `/_nuxt/__unocss` to access the UnoCSS inspector. + +## Key Differences from Vite + +- No need to import `virtual:uno.css` - automatically injected +- Config file discovery works the same +- All Vite plugin features available +- Nuxt layers config merging available + + diff --git a/.agents/skills/unocss/references/integrations-vite.md b/.agents/skills/unocss/references/integrations-vite.md new file mode 100644 index 0000000..18bee43 --- /dev/null +++ b/.agents/skills/unocss/references/integrations-vite.md @@ -0,0 +1,290 @@ +--- +name: unocss-vite-integration +description: Setting up UnoCSS with Vite and framework-specific tips +--- + +# UnoCSS Vite Integration + +The Vite plugin is the most common way to use UnoCSS. + +## Installation + +```bash +pnpm add -D unocss +``` + +```ts +// vite.config.ts +import UnoCSS from 'unocss/vite' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + UnoCSS(), + ], +}) +``` + +Create config file: + +```ts +// uno.config.ts +import { defineConfig, presetWind3 } from 'unocss' + +export default defineConfig({ + presets: [ + presetWind3(), + ], +}) +``` + +Add to entry: + +```ts +// main.ts +import 'virtual:uno.css' +``` + +## Modes + +### global (default) + +Standard mode - generates global CSS injected via `uno.css` import. + +```ts +import 'virtual:uno.css' +``` + +### vue-scoped + +Injects generated CSS into Vue SFC ` +
...
+` +``` + +### per-module (experimental) + +Generates CSS per module with optional scoping. + +### dist-chunk (experimental) + +Generates CSS per chunk on build for MPA. + +## DevTools Support + +Edit classes directly in browser DevTools: + +```ts +import 'virtual:uno.css' +import 'virtual:unocss-devtools' +``` + +**Warning:** Uses MutationObserver to detect changes. Dynamic classes from scripts will also be included. + +## Framework-Specific Setup + +### React + +```ts +// vite.config.ts +import React from '@vitejs/plugin-react' +import UnoCSS from 'unocss/vite' + +export default { + plugins: [ + UnoCSS(), // Must be before React when using attributify + React(), + ], +} +``` + +**Note:** Remove `tsc` from build script if using `@unocss/preset-attributify`. + +### Vue + +Works out of the box with `@vitejs/plugin-vue`. + +### Svelte + +```ts +import { svelte } from '@sveltejs/vite-plugin-svelte' +import extractorSvelte from '@unocss/extractor-svelte' +import UnoCSS from 'unocss/vite' + +export default { + plugins: [ + UnoCSS({ + extractors: [extractorSvelte()], + }), + svelte(), + ], +} +``` + +Supports `class:foo` and `class:foo={bar}` syntax. + +### SvelteKit + +Same as Svelte, use `sveltekit()` from `@sveltejs/kit/vite`. In the default global mode there is no `main.ts` entry, so import the stylesheet from the root layout: + +```svelte + + +``` + +### Solid + +```ts +import UnoCSS from 'unocss/vite' +import solidPlugin from 'vite-plugin-solid' + +export default { + plugins: [ + UnoCSS(), + solidPlugin(), + ], +} +``` + +### Preact + +```ts +import Preact from '@preact/preset-vite' +import UnoCSS from 'unocss/vite' + +export default { + plugins: [ + UnoCSS(), + Preact(), + ], +} +``` + +### Elm + +```ts +import Elm from 'vite-plugin-elm' +import UnoCSS from 'unocss/vite' + +export default { + plugins: [ + Elm(), + UnoCSS(), + ], +} +``` + +### Web Components (Lit) + +```ts +UnoCSS({ + mode: 'shadow-dom', + shortcuts: [ + { 'cool-blue': 'bg-blue-500 text-white' }, + ], +}) +``` + +```ts +// my-element.ts +@customElement('my-element') +export class MyElement extends LitElement { + static styles = css` + :host { ... } + @unocss-placeholder + ` +} +``` + +Supports `part-[]:` for `::part` styling. + +## Inspector + +Visit `http://localhost:5173/__unocss` in dev mode to: + +- Inspect generated CSS rules +- See applied classes per file +- Test utilities in REPL + +## Legacy Browser Support + +With `@vitejs/plugin-legacy`: + +```ts +import legacy from '@vitejs/plugin-legacy' +import UnoCSS from 'unocss/vite' + +export default { + plugins: [ + UnoCSS({ + legacy: { + renderModernChunks: false, + }, + }), + legacy({ + targets: ['defaults', 'not IE 11'], + renderModernChunks: false, + }), + ], +} +``` + +## VanillaJS / TypeScript + +By default, `.js` and `.ts` files are not extracted. Configure to include: + +```ts +// uno.config.ts +export default defineConfig({ + content: { + pipeline: { + include: [ + /\.(vue|svelte|[jt]sx|html)($|\?)/, + 'src/**/*.{js,ts}', + ], + }, + }, +}) +``` + +Or use magic comment in files: + +```ts +// @unocss-include +export const classes = { + active: 'bg-primary text-white', +} +``` + + diff --git a/.agents/skills/unocss/references/preset-attributify.md b/.agents/skills/unocss/references/preset-attributify.md new file mode 100644 index 0000000..4b41e14 --- /dev/null +++ b/.agents/skills/unocss/references/preset-attributify.md @@ -0,0 +1,142 @@ +--- +name: preset-attributify +description: Group utilities in HTML attributes instead of class +--- + +# Preset Attributify + +Group utilities in HTML attributes for better readability. + +## Installation + +```ts +import { defineConfig, presetAttributify, presetWind3 } from 'unocss' + +export default defineConfig({ + presets: [ + presetWind3(), + presetAttributify(), + ], +}) +``` + +## Basic Usage + +Instead of long class strings: + +```html + +``` + +Group by prefix in attributes: + +```html + +``` + +## Prefix Self-Referencing + +For utilities matching their prefix (`flex`, `grid`, `border`), use `~`: + +```html + + + + + +``` + +## Valueless Attributify + +Use utilities as boolean attributes: + +```html +
+``` + +## Handling Property Conflicts + +When attribute names conflict with HTML properties: + +```html + +Text color to red +``` + +### Enforce Prefix + +```ts +presetAttributify({ + prefix: 'un-', + prefixedOnly: true, +}) +``` + +## Options + +```ts +presetAttributify({ + strict: false, // Only generate CSS for attributify + prefix: 'un-', // Attribute prefix + prefixedOnly: false, // Require prefix for all + nonValuedAttribute: true, // Support valueless attributes + ignoreAttributes: [], // Attributes to ignore + trueToNonValued: false, // Treat value="true" as valueless +}) +``` + +## TypeScript Support + +### Vue 3 + +```ts +// html.d.ts +declare module '@vue/runtime-dom' { + interface HTMLAttributes { [key: string]: any } +} +declare module '@vue/runtime-core' { + interface AllowedComponentProps { [key: string]: any } +} +export {} +``` + +### React + +```ts +import type { AttributifyAttributes } from '@unocss/preset-attributify' + +declare module 'react' { + interface HTMLAttributes extends AttributifyAttributes {} +} +``` + +## JSX Support + +For JSX where `
` becomes `
`: + +```ts +import { transformerAttributifyJsx } from 'unocss' + +export default defineConfig({ + transformers: [ + transformerAttributifyJsx(), + ], +}) +``` + +**Important:** Only use attributify if `uno.config.*` shows `presetAttributify()` is enabled. + + diff --git a/.agents/skills/unocss/references/preset-icons.md b/.agents/skills/unocss/references/preset-icons.md new file mode 100644 index 0000000..a668297 --- /dev/null +++ b/.agents/skills/unocss/references/preset-icons.md @@ -0,0 +1,184 @@ +--- +name: preset-icons +description: Pure CSS icons using Iconify with any icon set +--- + +# Preset Icons + +Use any icon as a pure CSS class, powered by Iconify. + +## Installation + +```bash +pnpm add -D @unocss/preset-icons @iconify-json/[collection-name] +``` + +Example: `@iconify-json/mdi` for Material Design Icons, `@iconify-json/carbon` for Carbon icons. + +```ts +import { defineConfig, presetIcons } from 'unocss' + +export default defineConfig({ + presets: [ + presetIcons(), + ], +}) +``` + +## Usage + +Two naming conventions: +- `-` → `i-ph-anchor-simple-thin` +- `:` → `i-ph:anchor-simple-thin` + +```html + +
+ + +
+ + +
+ + + +``` + +Expands to: `hover:bg-blue-600 hover:text-white hover:scale-105` + +### Dark Mode + +```html +
+ Dark content +
+``` + +Expands to: `dark:bg-gray-800 dark:text-white` + +### Responsive + +```html +
+ Responsive flex +
+``` + +Expands to: `md:flex md:items-center md:gap-4` + +### Nested Groups + +```html +
+ Large screen hover +
+``` + +Expands to: `lg:hover:bg-blue-500 lg:hover:text-white` + +### Multiple Prefixes + +```html +
+ Styled text +
+``` + +Expands to: `text-sm text-gray-600 font-medium font-mono` + +## Key Points + +- Use parentheses `()` to group utilities +- The prefix applies to all utilities inside the group +- Can be combined with any variant (hover, dark, responsive, etc.) +- Nesting is supported +- Works in class attributes and other extraction sources + + diff --git a/.agents/skills/vitest/GENERATION.md b/.agents/skills/vitest/GENERATION.md new file mode 100644 index 0000000..a417918 --- /dev/null +++ b/.agents/skills/vitest/GENERATION.md @@ -0,0 +1,5 @@ +# Generation Info + +- **Source:** `sources/vitest` +- **Git SHA:** `ea1c44fb581978421f9f0901ce8c635970ea72fb` +- **Generated:** 2026-09-25 diff --git a/.agents/skills/vitest/SKILL.md b/.agents/skills/vitest/SKILL.md new file mode 100644 index 0000000..642bca6 --- /dev/null +++ b/.agents/skills/vitest/SKILL.md @@ -0,0 +1,55 @@ +--- +name: vitest +description: Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures. +metadata: + author: Anthony Fu + version: "2026.9.25" + source: Generated from https://github.com/vitest-dev/vitest, scripts located at https://github.com/antfu/skills +--- + +Vitest is a next-generation testing framework powered by Vite. It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box. Vitest shares the same config, transformers, resolvers, and plugins with your Vite app. + +**Key Features:** +- Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates +- Jest-compatible: Drop-in replacement for most Jest test suites +- Smart watch mode: Only reruns affected tests based on module graph +- Native ESM, TypeScript, JSX support without configuration +- Multi-threaded workers for parallel test execution +- Built-in coverage via V8 or Istanbul +- Snapshot testing, mocking, and spy utilities + +> The skill is based on Vitest 5.0.1, generated at 2026-09-25. + +## Core + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) | +| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) | +| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) | +| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) | +| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) | +| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) | + +## Features + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) | +| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) | +| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) | +| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) | +| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) | +| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) | +| Test Tags | Label tests with tags to filter runs and apply shared options | [features-test-tags](references/features-test-tags.md) | +| Reporters | Built-in reporters, default selection, CI/output config | [features-reporters](references/features-reporters.md) | +| Benchmarking | Write benchmarks with the bench fixture (Tinybench) | [features-benchmarking](references/features-benchmarking.md) | + +## Advanced + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) | +| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) | +| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) | +| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) | diff --git a/.agents/skills/vitest/references/advanced-environments.md b/.agents/skills/vitest/references/advanced-environments.md new file mode 100644 index 0000000..ded10d3 --- /dev/null +++ b/.agents/skills/vitest/references/advanced-environments.md @@ -0,0 +1,272 @@ +--- +name: test-environments +description: Configure environments like jsdom, happy-dom for browser APIs +--- + +# Test Environments + +## Available Environments + +- `node` (default) - Node.js environment +- `jsdom` - Browser-like with DOM APIs +- `happy-dom` - Faster alternative to jsdom +- `edge-runtime` - Vercel Edge Runtime + +## Configuration + +```ts +// vitest.config.ts +defineConfig({ + test: { + environment: 'jsdom', + + // Environment-specific options + environmentOptions: { + jsdom: { + url: 'http://localhost', + }, + }, + }, +}) +``` + +## Installing Environment Packages + +```bash +# jsdom +npm i -D jsdom + +# happy-dom (faster, fewer APIs) +npm i -D happy-dom +``` + +## Per-File Environment + +Use magic comment at top of file: + +```ts +// @vitest-environment jsdom + +import { expect, test } from 'vitest' + +test('DOM test', () => { + const div = document.createElement('div') + expect(div).toBeInstanceOf(HTMLDivElement) +}) +``` + +## jsdom Environment + +Full browser environment simulation: + +```ts +// @vitest-environment jsdom + +test('DOM manipulation', () => { + document.body.innerHTML = '
' + + const app = document.getElementById('app') + app.textContent = 'Hello' + + expect(app.textContent).toBe('Hello') +}) + +test('window APIs', () => { + expect(window.location.href).toBeDefined() + expect(localStorage).toBeDefined() +}) +``` + +### jsdom Options + +```ts +defineConfig({ + test: { + environmentOptions: { + jsdom: { + url: 'http://localhost:3000', + html: '', + userAgent: 'custom-agent', + resources: 'usable', + }, + }, + }, +}) +``` + +## happy-dom Environment + +Faster but fewer APIs: + +```ts +// @vitest-environment happy-dom + +test('basic DOM', () => { + const el = document.createElement('div') + el.className = 'test' + expect(el.className).toBe('test') +}) +``` + +## Multiple Environments per Project + +Use projects for different environments: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'dom', + include: ['tests/dom/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Custom Environment + +Create custom environment package: + +```ts +// vitest-environment-custom/index.ts +import type { Environment } from 'vitest/runtime' + +export default { + name: 'custom', + viteEnvironment: 'ssr', // or 'client' + + setup() { + // Setup global state + globalThis.myGlobal = 'value' + + return { + teardown() { + delete globalThis.myGlobal + }, + } + }, +} +``` + +Use with: + +```ts +defineConfig({ + test: { + environment: 'custom', + }, +}) +``` + +## Environment with VM + +For full isolation: + +```ts +export default { + name: 'isolated', + viteEnvironment: 'ssr', + + async setupVM() { + const vm = await import('node:vm') + const context = vm.createContext() + + return { + getVmContext() { + return context + }, + teardown() {}, + } + }, + + setup() { + return { teardown() {} } + }, +} +``` + +## Browser Mode (Separate from Environments) + +For real browser testing, use Vitest Browser Mode. In **v4 the provider is an object** (not a string), and the context imports from `vitest/browser`: + +```ts +import { playwright } from '@vitest/browser-playwright' + +defineConfig({ + test: { + browser: { + enabled: true, + provider: playwright({ launchOptions: { slowMo: 100 } }), + instances: [{ browser: 'chromium' }], // or 'firefox', 'webkit' + }, + }, +}) +``` + +```ts +import { page } from 'vitest/browser' // v4: was '@vitest/browser/context' +``` + +> v5: DOM-environment global assignments (e.g. `window.innerWidth`) now propagate to the underlying jsdom/happy-dom implementation. Locators are exact/strict by default (`getByText('Item')` no longer matches `Item 1`). `browser.api` is deprecated — move it to the top-level `api` option. Browser mode adds a built-in Trace View (`browser.traceView: true`). + +## CSS and Assets + +In jsdom/happy-dom, configure CSS handling: + +```ts +defineConfig({ + test: { + css: true, // Process CSS + + // Or with options + css: { + include: /\.module\.css$/, + modules: { + classNameStrategy: 'non-scoped', + }, + }, + }, +}) +``` + +## Fixing External Dependencies + +If external deps fail with CSS/asset errors: + +```ts +defineConfig({ + test: { + server: { + deps: { + inline: ['problematic-package'], + }, + }, + }, +}) +``` + +## Key Points + +- Default is `node` - no browser APIs +- Use `jsdom` for full browser simulation +- Use `happy-dom` for faster tests with basic DOM +- Per-file environment via `// @vitest-environment` comment +- Use projects for multiple environment configurations +- Browser Mode is for real browser testing, not environment + + diff --git a/.agents/skills/vitest/references/advanced-projects.md b/.agents/skills/vitest/references/advanced-projects.md new file mode 100644 index 0000000..b9534f5 --- /dev/null +++ b/.agents/skills/vitest/references/advanced-projects.md @@ -0,0 +1,341 @@ +--- +name: projects-workspaces +description: Multi-project configuration for monorepos and different test types +--- + +# Projects + +Run different test configurations in the same Vitest process. + +## v5 Config Inheritance & Nested Projects + +- **Inline projects inherit the root config by default** — `extends` now defaults to `true`, so root Vite options (`plugins`, `resolve.alias`) and test options are inherited. Arrays like `setupFiles` are appended, not replaced. Opt out with `extends: false`, or inherit from another file with `extends: './vitest.shared.ts'`. Projects referenced as config files/directories still don't inherit the root. +- **Referenced config files can declare their own `projects`** — such a config acts as a container providing *nested projects* named `app (unit)`, `app (e2e)`, etc. In v4 a referenced config's `projects` field was silently ignored, so audit merged configs that pull one in. +- **Inline projects share the declaring config's Vite server by default** ([`sharedViteServer`](core-config.md)) — the declaring config runs once, so plugin `config` hooks no longer run per project. A project gets its own server only when it changes the Vite config (`plugins`, `alias`, `css`, `deps.optimizer`, `root`, `browser`, `mode`). Set `sharedViteServer: false` if a plugin must be re-instantiated per project. + +```ts +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], // inherited by every inline project (v4 needed extends: true) + test: { + projects: [ + { test: { name: 'unit', include: ['**/*.unit.test.ts'] } }, + // a package that declares its own projects becomes a nested container: + './packages/app/vitest.config.ts', // -> "app (unit)", "app (e2e)", ... + ], + }, +}) +``` + +## Basic Projects Setup + +```ts +// vitest.config.ts +defineConfig({ + test: { + projects: [ + // Glob patterns for config files + 'packages/*', + + // Inline config + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'integration', + include: ['tests/integration/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Monorepo Pattern + +```ts +defineConfig({ + test: { + projects: [ + // Each package has its own vitest.config.ts + 'packages/core', + 'packages/cli', + 'packages/utils', + ], + }, +}) +``` + +Package config: + +```ts +// packages/core/vitest.config.ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'core', + include: ['src/**/*.test.ts'], + environment: 'node', + }, +}) +``` + +## Different Environments + +Run same tests in different environments: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'happy-dom', + root: './shared-tests', + environment: 'happy-dom', + setupFiles: ['./setup.happy-dom.ts'], + }, + }, + { + test: { + name: 'node', + root: './shared-tests', + environment: 'node', + setupFiles: ['./setup.node.ts'], + }, + }, + ], + }, +}) +``` + +## Browser + Node Projects + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'browser', + include: ['tests/browser/**/*.test.ts'], + browser: { + enabled: true, + name: 'chromium', + provider: 'playwright', + }, + }, + }, + ], + }, +}) +``` + +## Shared Configuration + +```ts +// vitest.shared.ts +export const sharedConfig = { + testTimeout: 10000, + setupFiles: ['./tests/setup.ts'], +} + +// vitest.config.ts +import { sharedConfig } from './vitest.shared' + +defineConfig({ + test: { + projects: [ + { + test: { + ...sharedConfig, + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + }, + }, + { + test: { + ...sharedConfig, + name: 'e2e', + include: ['tests/e2e/**/*.test.ts'], + }, + }, + ], + }, +}) +``` + +## Project-Specific Dependencies + +Each project can have different dependencies inlined: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'project-a', + server: { + deps: { + inline: ['package-a'], + }, + }, + }, + }, + ], + }, +}) +``` + +## Running Specific Projects + +```bash +# Run specific project (v5 adds the -p shorthand) +vitest --project unit +vitest -p integration + +# Multiple projects / wildcards +vitest --project unit --project e2e +vitest --project="packages*" + +# Exclude a project +vitest --project="!browser" + +# Nested projects: --project matches the prefix +vitest -p app # every project of the "app" config +vitest -p "app (unit)" # just one nested project +``` + +## Providing Values to Projects + +Share values from config to tests: + +```ts +// vitest.config.ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'staging', + provide: { + apiUrl: 'https://staging.api.com', + debug: true, + }, + }, + }, + { + test: { + name: 'production', + provide: { + apiUrl: 'https://api.com', + debug: false, + }, + }, + }, + ], + }, +}) + +// In tests, use inject +import { inject } from 'vitest' + +test('uses correct api', () => { + const url = inject('apiUrl') + expect(url).toContain('api.com') +}) +``` + +## With Fixtures + +```ts +const test = base.extend({ + apiUrl: ['/default', { injected: true }], +}) + +test('uses injected url', ({ apiUrl }) => { + // apiUrl comes from project's provide config +}) +``` + +## Per-Project Pool & Isolation (v4) + +Since the v4 pool rework, isolation, parallelism, and Node CLI options can be set **per project**: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + isolate: false, // fast, non-isolated unit tests + exclude: ['**/*.integration.test.ts'], + }, + }, + { + test: { + name: 'sequential', + include: ['**/*.sequential.test.ts'], + fileParallelism: false, // run these files one at a time + }, + }, + { + test: { + name: 'staging', + execArgv: ['--env-file=.env.staging'], // per-project Node flags + }, + }, + ], + }, +}) +``` + +## Global Setup per Project + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'with-db', + globalSetup: ['./tests/db-setup.ts'], + }, + }, + ], + }, +}) +``` + +## Key Points + +- Projects run in same Vitest process (replaces the removed `workspace` option) +- Each project can have different environment, pool, isolation, and config +- Use glob patterns for monorepo packages +- Run specific projects with `--project` (supports wildcards and `!` exclusion) +- Use `provide` to inject config values into tests +- Inline projects inherit root config by default (v5 `extends: true`); set `extends: false` to opt out +- Referenced configs that declare `projects` provide nested projects (`name (child)`) + + diff --git a/.agents/skills/vitest/references/advanced-type-testing.md b/.agents/skills/vitest/references/advanced-type-testing.md new file mode 100644 index 0000000..3b583d1 --- /dev/null +++ b/.agents/skills/vitest/references/advanced-type-testing.md @@ -0,0 +1,239 @@ +--- +name: type-testing +description: Test TypeScript types with expectTypeOf and assertType +--- + +# Type Testing + +Test TypeScript types without runtime execution. + +## Setup + +Type tests use `.test-d.ts` extension: + +```ts +// math.test-d.ts +import { expectTypeOf } from 'vitest' +import { add } from './math' + +test('add returns number', () => { + expectTypeOf(add).returns.toBeNumber() +}) +``` + +## Configuration + +```ts +defineConfig({ + test: { + typecheck: { + enabled: true, + + // Only type check + only: false, + + // Checker: 'tsc' or 'vue-tsc' + checker: 'tsc', + + // Include patterns + include: ['**/*.test-d.ts'], + + // tsconfig to use + tsconfig: './tsconfig.json', + }, + }, +}) +``` + +## expectTypeOf API + +```ts +import { expectTypeOf } from 'vitest' + +// Basic type checks +expectTypeOf().toBeString() +expectTypeOf().toBeNumber() +expectTypeOf().toBeBoolean() +expectTypeOf().toBeNull() +expectTypeOf().toBeUndefined() +expectTypeOf().toBeVoid() +expectTypeOf().toBeNever() +expectTypeOf().toBeAny() +expectTypeOf().toBeUnknown() +expectTypeOf().toBeObject() +expectTypeOf().toBeFunction() +expectTypeOf<[]>().toBeArray() +expectTypeOf().toBeSymbol() +``` + +## Value Type Checking + +```ts +const value = 'hello' +expectTypeOf(value).toBeString() + +const obj = { name: 'test', count: 42 } +expectTypeOf(obj).toExtend<{ name: string }>() +expectTypeOf(obj).toHaveProperty('name') +``` + +## Function Types + +```ts +function greet(name: string): string { + return `Hello, ${name}` +} + +expectTypeOf(greet).toBeFunction() +expectTypeOf(greet).parameters.toEqualTypeOf<[string]>() +expectTypeOf(greet).returns.toBeString() + +// Parameter checking +expectTypeOf(greet).parameter(0).toBeString() +``` + +## Object Types + +```ts +interface User { + id: number + name: string + email?: string +} + +expectTypeOf().toHaveProperty('id') +expectTypeOf().toHaveProperty('name').toBeString() + +// Check shape +expectTypeOf({ id: 1, name: 'test' }).toExtend() +``` + +## Equality vs Matching + +`toMatchTypeOf` is **deprecated** (expect-type v1.2+) — use `toExtend` for subset matching: + +```ts +interface A { x: number } +interface B { x: number; y: string } + +// toExtend - subset matching (replaces toMatchTypeOf) +expectTypeOf().toExtend() // B extends A + +// toEqualTypeOf - exact match +expectTypeOf().not.toEqualTypeOf() // Not exact match +expectTypeOf().toEqualTypeOf<{ x: number }>() // Exact match +``` + +## Branded Types + +```ts +type UserId = number & { __brand: 'UserId' } +type PostId = number & { __brand: 'PostId' } + +expectTypeOf().not.toEqualTypeOf() +expectTypeOf().not.toEqualTypeOf() +``` + +## Generic Types + +```ts +function identity(value: T): T { + return value +} + +expectTypeOf(identity).returns.toBeString() +expectTypeOf(identity).returns.toBeNumber() +``` + +## Nullable Types + +```ts +type MaybeString = string | null | undefined + +expectTypeOf().toBeNullable() +expectTypeOf().not.toBeNullable() +``` + +## assertType + +Assert a value matches a type (no assertion at runtime): + +```ts +import { assertType } from 'vitest' + +function getUser(): User | null { + return { id: 1, name: 'test' } +} + +test('returns user', () => { + const result = getUser() + + // @ts-expect-error - should fail type check + assertType(result) + + // Correct type + assertType(result) +}) +``` + +## Using @ts-expect-error + +Test that code produces type error: + +```ts +test('rejects wrong types', () => { + function requireString(s: string) {} + + // @ts-expect-error - number not assignable to string + requireString(123) +}) +``` + +## Running Type Tests + +```bash +# Run type tests +vitest typecheck + +# Run alongside unit tests +vitest --typecheck + +# Type tests only +vitest --typecheck.only +``` + +## Mixed Test Files + +Combine runtime and type tests: + +```ts +// user.test.ts +import { describe, expect, expectTypeOf, test } from 'vitest' +import { createUser } from './user' + +describe('createUser', () => { + test('runtime: creates user', () => { + const user = createUser('John') + expect(user.name).toBe('John') + }) + + test('types: returns User type', () => { + expectTypeOf(createUser).returns.toExtend<{ name: string }>() + }) +}) +``` + +## Key Points + +- Use `.test-d.ts` for type-only tests +- `expectTypeOf` for type assertions +- `toExtend` for subset matching (`toMatchTypeOf` is deprecated) +- `toEqualTypeOf` for exact type matching +- Use `@ts-expect-error` to test type errors +- Run with `vitest typecheck` or `--typecheck` + + diff --git a/.agents/skills/vitest/references/advanced-vi.md b/.agents/skills/vitest/references/advanced-vi.md new file mode 100644 index 0000000..bcd38e8 --- /dev/null +++ b/.agents/skills/vitest/references/advanced-vi.md @@ -0,0 +1,290 @@ +--- +name: vi-utilities +description: vi helper for mocking, timers, utilities +--- + +# Vi Utilities + +The `vi` helper provides mocking and utility functions. + +```ts +import { vi } from 'vitest' +``` + +## Mock Functions + +```ts +// Create mock +const fn = vi.fn() +const fnWithImpl = vi.fn((x) => x * 2) + +// Check if mock +vi.isMockFunction(fn) // true + +// Mock methods +fn.mockReturnValue(42) +fn.mockReturnValueOnce(1) +fn.mockResolvedValue(data) +fn.mockRejectedValue(error) +fn.mockImplementation(() => 'result') +fn.mockImplementationOnce(() => 'once') + +// Clear/reset +fn.mockClear() // Clear call history +fn.mockReset() // Clear history + implementation +fn.mockRestore() // Restore original (for spies) +``` + +## Spying + +```ts +const obj = { method: () => 'original' } + +const spy = vi.spyOn(obj, 'method') +obj.method() + +expect(spy).toHaveBeenCalled() + +// Mock implementation +spy.mockReturnValue('mocked') + +// Spy on getter/setter +vi.spyOn(obj, 'prop', 'get').mockReturnValue('value') +``` + +## Module Mocking + +```ts +// Hoisted to top of file +vi.mock('./module', () => ({ + fn: vi.fn(), +})) + +// Partial mock +vi.mock('./module', async (importOriginal) => ({ + ...(await importOriginal()), + specificFn: vi.fn(), +})) + +// Spy mode - keep implementation +vi.mock('./module', { spy: true }) + +// Import actual module inside mock +const actual = await vi.importActual('./module') + +// Import as mock +const mocked = await vi.importMock('./module') +``` + +## Dynamic Mocking + +```ts +// Not hoisted - use with dynamic imports +vi.doMock('./config', () => ({ key: 'value' })) +const config = await import('./config') + +// Unmock +vi.doUnmock('./config') +vi.unmock('./module') // Hoisted +``` + +## Conditional Mocking — vi.when (v5) + +Argument-specific spy behaviors: + +```ts +vi.when(spy) + .calledWith(1).thenReturn('one') + .calledWith(2).thenReturn('two') + +// then* actions: thenReturn / thenThrow / thenResolve / thenReject (+ *Once) +// options: { times }, second arg { onUnmatched: 'throw' | 'passthrough' | fn } + +vi.isWhenChain(w) // type guard for a When chain +``` + +See [features-mocking](features-mocking.md) for FIFO/LIFO matching and `toHaveBeenExhausted`. + +## Assertion Helpers — vi.defineHelper (4.1+) + +Wrap reusable assertion functions so failures point at the **call site**, not inside the helper: + +```ts +const expectValidUser = vi.defineHelper((user: unknown) => { + expect(user).toHaveProperty('id') + expect(user).toHaveProperty('email') +}) + +test('returns a valid user', async () => { + expectValidUser(await fetchUser('alice')) // failures reported here +}) +``` + +## Reset Modules + +```ts +// Clear module cache +vi.resetModules() + +// Wait for dynamic imports +await vi.dynamicImportSettled() +``` + +## Fake Timers + +```ts +vi.useFakeTimers() + +// Choose which timers to fake (toFake and toNotFake are mutually exclusive) +vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) +vi.useFakeTimers({ toNotFake: ['setInterval'] }) + +// v5: Temporal is faked alongside Date when present on globalThis; +// exclude it with toNotFake: ['Temporal'] + +setTimeout(() => console.log('done'), 1000) + +// Advance time +vi.advanceTimersByTime(1000) +vi.advanceTimersByTimeAsync(1000) // For async callbacks +vi.advanceTimersToNextTimer() +vi.advanceTimersToNextFrame() // requestAnimationFrame + +// Run all timers +vi.runAllTimers() +vi.runAllTimersAsync() +vi.runOnlyPendingTimers() + +// Clear timers +vi.clearAllTimers() + +// Check state +vi.getTimerCount() +vi.isFakeTimers() + +// Restore +vi.useRealTimers() +``` + +## Mock Date/Time + +```ts +vi.setSystemTime(new Date('2024-01-01')) +expect(new Date().getFullYear()).toBe(2024) +// v5: also drives Temporal.Now when Temporal is available + +vi.getMockedSystemTime() // Get mocked date +vi.getRealSystemTime() // Get real time (ms) +``` + +## Global/Env Mocking + +```ts +// Stub global +vi.stubGlobal('fetch', vi.fn()) +vi.unstubAllGlobals() + +// Stub environment +vi.stubEnv('API_KEY', 'test') +vi.stubEnv('NODE_ENV', 'test') +vi.unstubAllEnvs() +``` + +## Hoisted Code + +Run code before imports: + +```ts +const mock = vi.hoisted(() => vi.fn()) + +vi.mock('./module', () => ({ + fn: mock, // Can reference hoisted variable +})) +``` + +## Waiting Utilities + +```ts +// Wait for callback to succeed +await vi.waitFor(async () => { + const el = document.querySelector('.loaded') + expect(el).toBeTruthy() +}, { timeout: 5000, interval: 100 }) + +// Wait for truthy value +const element = await vi.waitUntil( + () => document.querySelector('.loaded'), + { timeout: 5000 } +) +``` + +## Mock Object + +Mock all methods of an object: + +```ts +const original = { + method: () => 'real', + nested: { fn: () => 'nested' }, +} + +const mocked = vi.mockObject(original) +mocked.method() // undefined (mocked) +mocked.method.mockReturnValue('mocked') + +// Spy mode +const spied = vi.mockObject(original, { spy: true }) +spied.method() // 'real' +expect(spied.method).toHaveBeenCalled() +``` + +## Test Configuration + +```ts +vi.setConfig({ + testTimeout: 10_000, + hookTimeout: 10_000, +}) + +vi.resetConfig() +``` + +## Global Mock Management + +```ts +vi.clearAllMocks() // Clear all mock call history +vi.resetAllMocks() // Reset + clear implementation +vi.restoreAllMocks() // Restore originals (spies) +``` + +## vi.mocked Type Helper + +TypeScript helper for mocked values: + +```ts +import { myFn } from './module' +vi.mock('./module') + +// Type as mock +vi.mocked(myFn).mockReturnValue('typed') + +// Deep mocking +vi.mocked(myModule, { deep: true }) + +// Partial mock typing +vi.mocked(fn, { partial: true }).mockResolvedValue({ ok: true }) +``` + +## Key Points + +- `vi.mock` is hoisted - use `vi.doMock` for dynamic mocking +- `vi.hoisted` lets you reference variables in mock factories +- Use `vi.spyOn` to spy on existing methods (v4: supports constructors) +- Use `vi.when` for argument-specific behaviors and `vi.defineHelper` for assertion helpers +- Fake timers require explicit setup and teardown +- `vi.waitFor` retries until assertion passes + + diff --git a/.agents/skills/vitest/references/core-cli.md b/.agents/skills/vitest/references/core-cli.md new file mode 100644 index 0000000..b5921b1 --- /dev/null +++ b/.agents/skills/vitest/references/core-cli.md @@ -0,0 +1,189 @@ +--- +name: vitest-cli +description: Command line interface commands and options +--- + +# Command Line Interface + +## Commands + +### `vitest` + +Start Vitest in watch mode (dev) or run mode (CI): + +```bash +vitest # Watch mode in dev, run mode in CI +vitest foobar # Run tests containing "foobar" in path +vitest basic/foo.test.ts:10 # Run specific test by file and line number +``` + +### `vitest run` + +Run tests once without watch mode: + +```bash +vitest run +vitest run --coverage +``` + +### `vitest watch` + +Explicitly start watch mode: + +```bash +vitest watch +``` + +### `vitest related` + +Run tests that import specific files (useful with lint-staged): + +```bash +vitest related src/index.ts src/utils.ts --run +``` + +### `vitest bench` + +Run only benchmark tests: + +```bash +vitest bench +``` + +### `vitest list` + +List all matching tests without running them: + +```bash +vitest list # List test names +vitest list --json # Output as JSON +vitest list --filesOnly # List only test files +``` + +> v5: `list` parses test files **statically** instead of running them. Pass `--no-static-parse` to run them; tune with `--static-parse-concurrency`. + +### `vitest doctor` (v5) + +Run the suite under alternative configs and recommend faster options (e.g. a different `pool`, `isolate: false`, `fsModuleCache`, lower `maxWorkers`). Needs a passing baseline; takes several times a normal run: + +```bash +vitest doctor +``` + +### `vitest init` + +Initialize project setup: + +```bash +vitest init browser # Set up browser testing +``` + +### `vitest --list-tags` + +List tags defined in config without running tests: + +```bash +vitest --list-tags # Human-readable list +vitest --list-tags=json # JSON output +``` + +## Common Options + +```bash +# Configuration +--config # Path to config file +--project, -p # Run specific project (v5 adds the -p shorthand) + +# Filtering +--testNamePattern, -t # Run tests matching pattern +--tagsFilter # Run tests by tag expression, e.g. "db && !flaky" +--changed # Run tests for changed files +--changed HEAD~1 # Tests for last commit changes +--dir # Limit test discovery to a directory + +# Reporters +--reporter # default, verbose, tree, dot, json, html, junit, minimal, blob +--reporter=json --outputFile=report.json + +# Coverage +--coverage # Enable coverage +--coverage.provider v8 # Use v8 provider +--coverage.reporter text,html + +# Execution +--shard / # Split tests across machines +--bail # Stop after n failures +--retry # Retry failed tests n times +--repeats # v5: repeat every test n times (hunt flaky tests) +--shuffle # Randomize test order +--no-file-parallelism # Run test files one at a time + +# Watch mode +--no-watch # Disable watch mode +--standalone # Start without running (v4: runs matched files if a filter is passed) + +# Environment +--environment # jsdom, happy-dom, node +--globals # Enable global APIs + +# Debugging +--inspect # Enable Node inspector +--inspect-brk # Break on start + +# Output +--silent # Suppress console output +--no-color # Disable colors +``` + +## Package.json Scripts + +```json +{ + "scripts": { + "test": "vitest", + "test:run": "vitest run", + "test:ui": "vitest --ui", + "coverage": "vitest run --coverage" + } +} +``` + +## Sharding for CI + +Split tests across multiple machines. The blob reporter writes to `.vitest/blob/` by default: + +```bash +# Machine 1 +vitest run --shard=1/3 --reporter=blob --outputFile=reports/blob-1.json + +# Machine 2 +vitest run --shard=2/3 --reporter=blob --outputFile=reports/blob-2.json + +# Merge all blobs into a final report +vitest --merge-reports=reports --reporter=junit --reporter=default +``` + +## Watch Mode Keyboard Shortcuts + +In watch mode, press: +- `a` - Run all tests +- `f` - Run only failed tests +- `u` - Update snapshots +- `p` - Filter by filename pattern +- `t` - Filter by test name pattern +- `q` - Quit + +## Key Points + +- Watch mode is default in dev, run mode in CI (when `process.env.CI` is set) +- Use `--run` flag to ensure single run (important for lint-staged) +- Both camelCase (`--testTimeout`) and kebab-case (`--test-timeout`) work +- Boolean options can be negated with `--no-` prefix +- Filter tests by tag with `--tagsFilter` (tags must be declared in config) — see [features-test-tags](features-test-tags.md) +- `--merge-reports` and `--reporter=blob` do not work in watch mode (`--merge-reports` now handles non-sharded multi-environment runs) +- v5: use `-p` as shorthand for `--project`; `vitest doctor` suggests faster config + + diff --git a/.agents/skills/vitest/references/core-config.md b/.agents/skills/vitest/references/core-config.md new file mode 100644 index 0000000..5dbb8da --- /dev/null +++ b/.agents/skills/vitest/references/core-config.md @@ -0,0 +1,216 @@ +--- +name: vitest-configuration +description: Configure Vitest with vite.config.ts or vitest.config.ts +--- + +# Configuration + +Vitest reads configuration from `vitest.config.ts` or `vite.config.ts`. It shares the same config format as Vite. + +## Basic Setup + +```ts +// vitest.config.ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + // test options + }, +}) +``` + +## Using with Existing Vite Config + +Add Vitest types reference and use the `test` property: + +```ts +// vite.config.ts +/// +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + }, +}) +``` + +## Merging Configs + +If you have separate config files, use `mergeConfig`: + +```ts +// vitest.config.ts +import { defineConfig, mergeConfig } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig(viteConfig, defineConfig({ + test: { + environment: 'jsdom', + }, +})) +``` + +## Common Options + +```ts +defineConfig({ + test: { + // Enable global APIs (describe, it, expect) without imports + globals: true, + + // Test environment: 'node', 'jsdom', 'happy-dom' + environment: 'node', + + // Setup files to run before each test file + setupFiles: ['./tests/setup.ts'], + + // Include patterns for test files + include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'], + + // Exclude patterns + exclude: ['**/node_modules/**', '**/dist/**'], + + // Limit test discovery to a directory (faster than broad excludes) + dir: './src', + + // Test timeout in ms + testTimeout: 5000, + + // Hook timeout in ms + hookTimeout: 10000, + + // Coverage configuration (v4+: define `include`, no more `all`) + coverage: { + provider: 'v8', // or 'istanbul' + reporter: ['text', 'html'], + include: ['src/**/*.ts'], + }, + + // Run each file in an isolated module graph (threads/forks pools only) + isolate: true, + + // Pool: 'forks' (default), 'threads', 'vmForks', 'vmThreads' + pool: 'forks', + + // v4+: pool options are top-level (poolOptions was removed) + maxWorkers: 4, + fileParallelism: true, + + // Clear mock call history before each test (v5: defaults to true) + clearMocks: true, + + // Restore spies created with vi.spyOn between tests + restoreMocks: true, + + // Retry failed tests + retry: 0, + + // Stop after first failure + bail: 0, + + // v5: repeat every test N times regardless of result (flaky-hunting) + repeats: 0, + }, +}) +``` + +## v5 Performance Config + +```ts +defineConfig({ + test: { + // Persist transformed modules on disk, reused across reruns AND separate + // processes (stable in v5; was experimental.fsModuleCache). Cache lives in + // node_modules by default; clear with `vitest --clearCache`. Not for browser. + fsModuleCache: true, + + // Inline projects that don't change the Vite config reuse the declaring + // config's Vite server (default true) — shared files transformed once. + // Set false if a plugin must be re-instantiated per project. + sharedViteServer: true, + + // Inject CJS globals (module/exports/require/__filename/__dirname) into + // every module (default true). Set false for stricter ESM-like scope. + injectCjsGlobals: false, + + // Detect async resources leaking past a test file (slow; debugging only) + detectAsyncLeaks: false, + }, +}) +``` + +## v4/v5 Config Changes + +- **v5 requires Vite >= 6.4.0 and Node >= 22.12.0.** `vite` is now a required peer dependency (Yarn users must `yarn add -D vite`). +- **`clearMocks` defaults to `true`** in v5 — `vi.clearAllMocks()` runs before every test (history cleared, implementations kept). Set `clearMocks: false` for the old behavior. +- **Inline projects inherit the root config by default** (`extends` defaults to `true`) — including Vite `plugins`/`resolve.alias`. Set `extends: false` to opt out. See [advanced-projects](advanced-projects.md). +- **`testNamePattern` (`-t`) matches the `>`-joined full name** (`'math > adds'`), not the space-joined Jest form. +- **`vi.mock`/`vi.unmock`/`vi.hoisted` must be at the top level** — nested calls now throw (they were only warned before). +- **Pool default is `forks`** (child processes), not `threads`. +- **`poolOptions` removed** — `maxThreads`/`maxForks` are now top-level `maxWorkers`; `singleThread`/`singleFork` become `maxWorkers: 1, isolate: false`; VM `memoryLimit` is `vmMemoryLimit`. `minWorkers` was removed. +- **`workspace` removed** — use [`projects`](advanced-projects.md). `vitest.workspace.ts` no longer supported. +- **`coverage.all` and `coverage.extensions` removed** — by default only covered files are reported; set `coverage.include` explicitly. +- **Simplified `exclude`** — only `node_modules`/`.git` excluded by default. Use `test.dir` to scope discovery, or spread `configDefaults.exclude`. +- **Config not looked up from parent dirs** — pass `--config` explicitly when running from a subdirectory. +- **`.vitest` artifact dir** — blob reports (`.vitest/blob/`), attachments (`.vitest/attachments/`), and the `html`/`json`/`junit` reporters now all live under a single `.vitest/` directory; add one entry to `.gitignore`. +- **`test.sequential`/`describe.sequential`/`sequential` removed** — use `{ concurrent: false }`. +- `deps.optimizer.web` renamed to `deps.optimizer.client`; `deps.inline`/`deps.external` moved under `server.deps`. + +## Conditional Configuration + +Use `mode` or `process.env.VITEST` for test-specific config: + +```ts +export default defineConfig(({ mode }) => ({ + plugins: mode === 'test' ? [] : [myPlugin()], + test: { + // test options + }, +})) +``` + +## Projects (Monorepos) + +Run different configurations in the same Vitest process: + +```ts +defineConfig({ + test: { + projects: [ + 'packages/*', + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'integration', + include: ['tests/integration/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Key Points + +- Vitest uses Vite's transformation pipeline - same `resolve.alias`, plugins work +- `vitest.config.ts` takes priority over `vite.config.ts` +- Use `--config` flag to specify a custom config path (required from subdirectories in v5) +- `process.env.VITEST` is set to `true` when running tests +- Test config uses `test` property, rest is Vite config +- v5 requires **Vite >= 6.4.0** and **Node >= 22.12.0**; `vite` is a required peer dependency + + diff --git a/.agents/skills/vitest/references/core-describe.md b/.agents/skills/vitest/references/core-describe.md new file mode 100644 index 0000000..d8ea086 --- /dev/null +++ b/.agents/skills/vitest/references/core-describe.md @@ -0,0 +1,195 @@ +--- +name: describe-api +description: describe/suite for grouping tests into logical blocks +--- + +# Describe API + +Group related tests into suites for organization and shared setup. + +## Basic Usage + +```ts +import { describe, expect, test } from 'vitest' + +describe('Math', () => { + test('adds numbers', () => { + expect(1 + 1).toBe(2) + }) + + test('subtracts numbers', () => { + expect(3 - 1).toBe(2) + }) +}) + +// Alias: suite +import { suite } from 'vitest' +suite('equivalent to describe', () => {}) +``` + +## Nested Suites + +```ts +describe('User', () => { + describe('when logged in', () => { + test('shows dashboard', () => {}) + test('can update profile', () => {}) + }) + + describe('when logged out', () => { + test('shows login page', () => {}) + }) +}) +``` + +## Suite Options + +```ts +// All tests inherit options +describe('slow tests', { timeout: 30_000 }, () => { + test('test 1', () => {}) // 30s timeout + test('test 2', () => {}) // 30s timeout +}) +``` + +## Suite Modifiers + +### Skip Suites + +```ts +describe.skip('skipped suite', () => { + test('wont run', () => {}) +}) + +// Conditional +describe.skipIf(process.env.CI)('not in CI', () => {}) +describe.runIf(!process.env.CI)('only local', () => {}) +``` + +### Focus Suites + +```ts +describe.only('only this suite runs', () => { + test('runs', () => {}) +}) +``` + +### Todo Suites + +```ts +describe.todo('implement later') +``` + +### Concurrent Suites + +```ts +// All tests run in parallel +describe.concurrent('parallel tests', () => { + test('test 1', async ({ expect }) => {}) + test('test 2', async ({ expect }) => {}) +}) +``` + +### Opt Out of Concurrency + +`describe.sequential` was **removed in v5**. Use `{ concurrent: false }` to opt a suite out of inherited/global concurrency: + +```ts +describe.concurrent('parallel', () => { + test('concurrent 1', async () => {}) + + describe('must be sequential', { concurrent: false }, () => { + test('step 1', async () => {}) + test('step 2', async () => {}) + }) +}) +``` + +### Shuffle Tests + +```ts +describe.shuffle('random order', () => { + test('test 1', () => {}) + test('test 2', () => {}) + test('test 3', () => {}) +}) + +// Or with option +describe('random', { shuffle: true }, () => {}) +``` + +## Parameterized Suites + +### describe.each + +```ts +describe.each([ + { name: 'Chrome', version: 100 }, + { name: 'Firefox', version: 90 }, +])('$name browser', ({ name, version }) => { + test('has version', () => { + expect(version).toBeGreaterThan(0) + }) +}) +``` + +### describe.for + +```ts +describe.for([ + ['Chrome', 100], + ['Firefox', 90], +])('%s browser', ([name, version]) => { + test('has version', () => { + expect(version).toBeGreaterThan(0) + }) +}) +``` + +## Hooks in Suites + +```ts +describe('Database', () => { + let db + + beforeAll(async () => { + db = await createDb() + }) + + afterAll(async () => { + await db.close() + }) + + beforeEach(async () => { + await db.clear() + }) + + test('insert works', async () => { + await db.insert({ name: 'test' }) + expect(await db.count()).toBe(1) + }) +}) +``` + +## Modifier Combinations + +Modifiers can be chained: + +```ts +describe.skip.concurrent('skipped concurrent', () => {}) +describe.only.shuffle('only and shuffled', () => {}) +``` + +## Key Points + +- Top-level tests belong to an implicit file suite +- Nested suites inherit parent's options (timeout, retry, concurrency, etc.) +- Hooks are scoped to their suite and nested suites +- Use `describe.concurrent` with context's `expect` for snapshots +- Shuffle order depends on `sequence.seed` config +- `describe.sequential` was removed in v5 — use `{ concurrent: false }` + + diff --git a/.agents/skills/vitest/references/core-expect.md b/.agents/skills/vitest/references/core-expect.md new file mode 100644 index 0000000..6b8d999 --- /dev/null +++ b/.agents/skills/vitest/references/core-expect.md @@ -0,0 +1,311 @@ +--- +name: expect-api +description: Assertions with matchers, asymmetric matchers, and custom matchers +--- + +# Expect API + +Vitest uses Chai assertions with Jest-compatible API. + +## Basic Assertions + +```ts +import { expect, test } from 'vitest' + +test('assertions', () => { + // Equality + expect(1 + 1).toBe(2) // Strict equality (===) + expect({ a: 1 }).toEqual({ a: 1 }) // Deep equality + + // Truthiness + expect(true).toBeTruthy() + expect(false).toBeFalsy() + expect(null).toBeNull() + expect(undefined).toBeUndefined() + expect('value').toBeDefined() + + // Numbers + expect(10).toBeGreaterThan(5) + expect(10).toBeGreaterThanOrEqual(10) + expect(5).toBeLessThan(10) + expect(0.1 + 0.2).toBeCloseTo(0.3, 5) + + // Strings + expect('hello world').toMatch(/world/) + expect('hello').toContain('ell') + + // Arrays + expect([1, 2, 3]).toContain(2) + expect([{ a: 1 }]).toContainEqual({ a: 1 }) + expect([1, 2, 3]).toHaveLength(3) + + // Objects + expect({ a: 1, b: 2 }).toHaveProperty('a') + expect({ a: 1, b: 2 }).toHaveProperty('a', 1) + expect({ a: { b: 1 } }).toHaveProperty('a.b', 1) + expect({ a: 1 }).toMatchObject({ a: 1 }) + + // Types + expect('string').toBeTypeOf('string') + expect(new Date()).toBeInstanceOf(Date) +}) +``` + +## Negation + +```ts +expect(1).not.toBe(2) +expect({ a: 1 }).not.toEqual({ a: 2 }) +``` + +## Error Assertions + +```ts +// Sync errors - wrap in function +expect(() => throwError()).toThrow() +expect(() => throwError()).toThrow('message') // string = substring of the message +expect(() => throwError()).toThrow(/pattern/) +expect(() => throwError()).toThrow(CustomError) + +// v5: toThrow('') matches ANY message (empty string is a substring of all). +// To assert an empty message, match the pattern: .toThrow(/^$/) + +// Async errors - use rejects +await expect(asyncThrow()).rejects.toThrow('error') +``` + +## Promise Assertions + +```ts +// Resolves +await expect(Promise.resolve(1)).resolves.toBe(1) +await expect(fetchData()).resolves.toEqual({ data: true }) + +// Rejects +await expect(Promise.reject('error')).rejects.toBe('error') +await expect(failingFetch()).rejects.toThrow() +``` + +> **v5:** unawaited async assertions (`resolves`, `rejects`, `toMatchFileSnapshot`) now **fail the test** — v4 only printed a warning and auto-awaited at the end. Always `await` them. + +## Spy/Mock Assertions + +```ts +const fn = vi.fn() +fn('arg1', 'arg2') +fn('arg3') + +expect(fn).toHaveBeenCalled() +expect(fn).toHaveBeenCalledTimes(2) +expect(fn).toHaveBeenCalledWith('arg1', 'arg2') +expect(fn).toHaveBeenLastCalledWith('arg3') +expect(fn).toHaveBeenNthCalledWith(1, 'arg1', 'arg2') + +expect(fn).toHaveReturned() +expect(fn).toHaveReturnedWith(value) + +// v4 additions +expect(fn).toHaveBeenCalledExactlyOnceWith('arg1', 'arg2') +expect(fnA).toHaveBeenCalledBefore(fnB) +expect(fnA).toHaveBeenCalledAfter(fnB) +``` + +### Chai-Style Spy Assertions (4.1+) + +Sinon-chai-compatible aliases, useful when migrating from Sinon: + +```ts +expect(spy).to.have.been.called +expect(spy).to.have.been.calledOnce +expect(spy).to.have.been.calledWith('arg1', 'arg2') +expect(spy).to.have.been.calledOnceWith('arg') +``` + +### Conditional Mock Exhaustion (v5) + +Assert every `vi.when` behavior was consumed: + +```ts +const w = vi.when(spy).calledWith(1).thenReturnOnce('a') +spy(1) +expect(w).toHaveBeenExhausted() +``` + +## Asymmetric Matchers + +Use inside `toEqual`, `toHaveBeenCalledWith`, etc: + +```ts +expect({ id: 1, name: 'test' }).toEqual({ + id: expect.any(Number), + name: expect.any(String), +}) + +expect({ a: 1, b: 2, c: 3 }).toEqual( + expect.objectContaining({ a: 1 }) +) + +expect([1, 2, 3, 4]).toEqual( + expect.arrayContaining([1, 3]) +) + +expect('hello world').toEqual( + expect.stringContaining('world') +) + +expect('hello world').toEqual( + expect.stringMatching(/world$/) +) + +expect({ value: null }).toEqual({ + value: expect.anything() // Matches anything except null/undefined +}) + +// Negate with expect.not +expect([1, 2]).toEqual( + expect.not.arrayContaining([3]) +) + +// toBeOneOf - value matches any option (great for optional props) +expect(user).toEqual({ + name: expect.any(String), + middleName: expect.toBeOneOf([expect.any(String), undefined]), +}) + +// schemaMatching (4.0+) - matches any Standard Schema (Zod, Valibot, ArkType) +import { z } from 'zod' +expect(payload).toEqual({ + email: expect.schemaMatching(z.string().email()), +}) +expect(repo.save).toHaveBeenCalledWith(expect.schemaMatching(UserSchema)) +``` + +## Soft Assertions + +Prefer `expect.soft` for **non-critical assertions** — it marks the test failed but continues so all failures are reported together: + +```ts +expect.soft(response.status).toBe(200) // non-critical, keeps going +expect.soft(response.headers.get('x-id')).toBeTruthy() +expect(response.body).toBeDefined() // critical: hard expect stops on failure +``` + +## Type-Narrowing Assertions (4.0+) + +`expect.assert` throws at runtime **and** narrows the TypeScript type (unlike `toBeTruthy`/`toBeDefined`, which return `void`): + +```ts +const user = cache.get('alice') // { id, name } | undefined +expect.assert(user) // throws if undefined, narrows below +expect(user.name).toBe('Alice') // no `!`, no `as` + +// Narrows typeof / instanceof too +expect.assert(typeof input === 'string') +input.toUpperCase() + +// Chai assert helpers via the same namespace +expect.assert.isDefined(maybeUser) +expect.assert.instanceOf(error, MyError) +``` + +## Poll Assertions + +Retry until passes: + +```ts +await expect.poll(() => fetchStatus()).toBe('ready') + +await expect.poll( + () => document.querySelector('.element'), + { interval: 100, timeout: 5000 } +).toBeTruthy() +``` + +> **v5:** `expect.poll` now **rejects when it times out** (v4 could still pass on a late attempt). The callback receives an `AbortSignal` that aborts on timeout so you can cancel in-flight work: + +```ts +await expect.poll(async ({ signal }) => { + const res = await fetch('/api/status', { signal }) + return res.status +}, { timeout: 1000 }).toBe(200) +``` + +## Assertion Count + +```ts +test('async assertions', async () => { + expect.assertions(2) // Exactly 2 assertions must run + + await doAsync((data) => { + expect(data).toBeDefined() + expect(data.id).toBe(1) + }) +}) + +test('at least one', () => { + expect.hasAssertions() // At least 1 assertion must run +}) +``` + +## Extending Matchers + +```ts +expect.extend({ + toBeWithinRange(received, floor, ceiling) { + const pass = received >= floor && received <= ceiling + return { + pass, + message: () => + `expected ${received} to be within range ${floor} - ${ceiling}`, + } + }, +}) + +test('custom matcher', () => { + expect(100).toBeWithinRange(90, 110) +}) +``` + +### TypeScript Declarations (v5) + +The `Matchers` interface now takes the **return type first** (`R`) and the received type second (`T`). `R` is `void` synchronously, `Promise` through `.resolves`/`.rejects`/`expect.poll`/`expect.element`: + +```ts +import 'vitest' + +declare module 'vitest' { + interface Matchers { + toBeFoo: () => R + toEqualTyped: (expected: T) => R // T mirrors the received value's type + } +} +``` + +Referencing assertion types directly also needs the return type first: `Assertion` (sync) / `Assertion, string>` (async). v5 no longer reads matchers from the global `jest.Matchers` interface — augment `vitest.Matchers` separately. + +## Snapshot Assertions + +```ts +expect(data).toMatchSnapshot() +expect(data).toMatchInlineSnapshot(`{ "id": 1 }`) +await expect(result).toMatchFileSnapshot('./expected.json') + +expect(() => throw new Error('fail')).toThrowErrorMatchingSnapshot() +``` + +## Key Points + +- Use `toBe` for primitives, `toEqual` for objects/arrays +- `toStrictEqual` checks undefined properties and array sparseness +- Always `await` async assertions (`resolves`, `rejects`, `poll`) +- Use context's `expect` in concurrent tests for correct tracking +- `toThrow` requires wrapping sync code in a function +- Use `expect.soft` for non-critical assertions; reserve hard `expect` for must-pass conditions +- Use `expect.assert` (not `toBeTruthy`) when you also need TypeScript narrowing + + diff --git a/.agents/skills/vitest/references/core-hooks.md b/.agents/skills/vitest/references/core-hooks.md new file mode 100644 index 0000000..453fa14 --- /dev/null +++ b/.agents/skills/vitest/references/core-hooks.md @@ -0,0 +1,244 @@ +--- +name: lifecycle-hooks +description: beforeEach, afterEach, beforeAll, afterAll, and around hooks +--- + +# Lifecycle Hooks + +## Basic Hooks + +```ts +import { afterAll, afterEach, beforeAll, beforeEach, test } from 'vitest' + +beforeAll(async () => { + // Runs once before all tests in file/suite + await setupDatabase() +}) + +afterAll(async () => { + // Runs once after all tests in file/suite + await teardownDatabase() +}) + +beforeEach(async () => { + // Runs before each test + await clearTestData() +}) + +afterEach(async () => { + // Runs after each test + await cleanupMocks() +}) +``` + +## Cleanup Return Pattern + +Return cleanup function from `before*` hooks: + +```ts +beforeAll(async () => { + const server = await startServer() + + // Returned function runs as afterAll + return async () => { + await server.close() + } +}) + +beforeEach(async () => { + const connection = await connect() + + // Runs as afterEach + return () => connection.close() +}) +``` + +## Scoped Hooks + +Hooks apply to current suite and nested suites: + +```ts +describe('outer', () => { + beforeEach(() => console.log('outer before')) + + test('test 1', () => {}) // outer before → test + + describe('inner', () => { + beforeEach(() => console.log('inner before')) + + test('test 2', () => {}) // outer before → inner before → test + }) +}) +``` + +## Hook Timeout + +```ts +beforeAll(async () => { + await slowSetup() +}, 30_000) // 30 second timeout +``` + +## Around Hooks + +Wrap tests with setup/teardown context: + +```ts +import { aroundEach, test } from 'vitest' + +// Wrap each test in database transaction +aroundEach(async (runTest) => { + await db.beginTransaction() + await runTest() // Must be called! + await db.rollback() +}) + +test('insert user', async () => { + await db.insert({ name: 'Alice' }) + // Automatically rolled back after test +}) +``` + +### aroundAll + +Wrap entire suite: + +```ts +import { aroundAll, test } from 'vitest' + +aroundAll(async (runSuite) => { + console.log('before all tests') + await runSuite() // Must be called! + console.log('after all tests') +}) +``` + +### Multiple Around Hooks + +Nested like onion layers: + +```ts +aroundEach(async (runTest) => { + console.log('outer before') + await runTest() + console.log('outer after') +}) + +aroundEach(async (runTest) => { + console.log('inner before') + await runTest() + console.log('inner after') +}) + +// Order: outer before → inner before → test → inner after → outer after +``` + +## Test Hooks + +Inside test body: + +```ts +import { onTestFailed, onTestFinished, test } from 'vitest' + +test('with cleanup', () => { + const db = connect() + + // Runs after test finishes (pass or fail) + onTestFinished(() => db.close()) + + // Only runs if test fails + onTestFailed(({ task }) => { + console.log('Failed:', task.result?.errors) + }) + + db.query('SELECT * FROM users') +}) +``` + +### Reusable Cleanup Pattern + +```ts +function useTestDb() { + const db = connect() + onTestFinished(() => db.close()) + return db +} + +test('query users', () => { + const db = useTestDb() + expect(db.query('SELECT * FROM users')).toBeDefined() +}) + +test('query orders', () => { + const db = useTestDb() // Fresh connection, auto-closed + expect(db.query('SELECT * FROM orders')).toBeDefined() +}) +``` + +## Concurrent Test Hooks + +For concurrent tests, use context's hooks: + +```ts +test.concurrent('concurrent', ({ onTestFinished }) => { + const resource = allocate() + onTestFinished(() => resource.release()) +}) +``` + +## Extended Test Hooks + +With `test.extend`, hooks are type-aware and must be called on the extended `test`: + +```ts +const test = base + .extend('db', { scope: 'file' }, async ({}, { onCleanup }) => { + const db = await createDb() + onCleanup(() => db.close()) + return db + }) + +// Test-level hooks see fixtures +test.beforeEach(({ db }) => db.seed()) +test.afterEach(({ db }) => db.clear()) + +// Suite-level hooks (4.1+) can access file/worker-scoped fixtures +test.beforeAll(({ db }) => db.createUsers()) +test.aroundAll(async (runSuite, { db }) => db.transaction(runSuite)) +``` + +Suite-level hooks (`beforeAll`/`afterAll`/`aroundAll`) only see **file/worker-scoped** fixtures, and the **global** `beforeAll`/`afterAll` cannot access custom fixtures — use `test.beforeAll` etc. + +## Hook Execution Order + +Default order (stack): +1. `beforeAll` (in order) +2. `beforeEach` (in order) +3. Test +4. `afterEach` (reverse order) +5. `afterAll` (reverse order) + +Configure with `sequence.hooks`: + +```ts +defineConfig({ + test: { + sequence: { + hooks: 'list', // 'stack' (default), 'list', 'parallel' + }, + }, +}) +``` + +## Key Points + +- Hooks are not called during type checking +- Return cleanup function from `before*` to avoid `after*` duplication +- `aroundEach`/`aroundAll` must call `runTest()`/`runSuite()` +- `onTestFinished` always runs, even if test fails +- Use context hooks for concurrent tests + + diff --git a/.agents/skills/vitest/references/core-test-api.md b/.agents/skills/vitest/references/core-test-api.md new file mode 100644 index 0000000..347632d --- /dev/null +++ b/.agents/skills/vitest/references/core-test-api.md @@ -0,0 +1,267 @@ +--- +name: test-api +description: test/it function for defining tests with modifiers +--- + +# Test API + +## Basic Test + +```ts +import { expect, test } from 'vitest' + +test('adds numbers', () => { + expect(1 + 1).toBe(2) +}) + +// Alias: it +import { it } from 'vitest' + +it('works the same', () => { + expect(true).toBe(true) +}) +``` + +## Async Tests + +```ts +test('async test', async () => { + const result = await fetchData() + expect(result).toBeDefined() +}) + +// Promises are automatically awaited +test('returns promise', () => { + return fetchData().then(result => { + expect(result).toBeDefined() + }) +}) +``` + +## Test Options + +```ts +// Timeout (default: 5000ms) +test('slow test', async () => { + // ... +}, 10_000) + +// Or with options object +test('with options', { timeout: 10_000, retry: 2 }, async () => { + // ... +}) +``` + +## Test Modifiers + +### Skip Tests + +```ts +test.skip('skipped test', () => { + // Won't run +}) + +// Conditional skip +test.skipIf(process.env.CI)('not in CI', () => {}) +test.runIf(process.env.CI)('only in CI', () => {}) + +// Dynamic skip via context +test('dynamic skip', ({ skip }) => { + skip(someCondition, 'reason') + // ... +}) +``` + +### Focus Tests + +```ts +test.only('only this runs', () => { + // Other tests in file are skipped +}) +``` + +### Todo Tests + +```ts +test.todo('implement later') + +test.todo('with body', () => { + // Not run, shows in report +}) +``` + +### Failing Tests + +```ts +test.fails('expected to fail', () => { + expect(1).toBe(2) // Test passes because assertion fails +}) +``` + +### Concurrent Tests + +```ts +// Run tests in parallel +test.concurrent('test 1', async ({ expect }) => { + // Use context.expect for concurrent tests + expect(await fetch1()).toBe('result') +}) + +test.concurrent('test 2', async ({ expect }) => { + expect(await fetch2()).toBe('result') +}) +``` + +### Opt Out of Concurrency + +`test.sequential` was **removed in v5**. Use `concurrent: false` to opt a test out of inherited or globally configured concurrency: + +```ts +test('must run alone', { concurrent: false }, async () => {}) +``` + +## Parameterized Tests + +### test.each + +```ts +test.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add(%i, %i) = %i', (a, b, expected) => { + expect(a + b).toBe(expected) +}) + +// With objects +test.each([ + { a: 1, b: 1, expected: 2 }, + { a: 1, b: 2, expected: 3 }, +])('add($a, $b) = $expected', ({ a, b, expected }) => { + expect(a + b).toBe(expected) +}) + +// Template literal +test.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} +`('add($a, $b) = $expected', ({ a, b, expected }) => { + expect(a + b).toBe(expected) +}) +``` + +### test.for + +Preferred over `.each` - doesn't spread arrays: + +```ts +test.for([ + [1, 1, 2], + [1, 2, 3], +])('add(%i, %i) = %i', ([a, b, expected], { expect }) => { + // Second arg is TestContext + expect(a + b).toBe(expected) +}) +``` + +> **v5:** titles are formatted with `pretty-format`, and a string interpolated through a `$` placeholder is **no longer quoted** (`case $id` → `case a1`, not `case 'a1'`). Interpolated-value length is capped by `taskTitleValueFormatTruncate` (default 40). + +## Test Context + +First argument provides context utilities: + +```ts +test('with context', ({ expect, skip, task, signal, annotate }) => { + console.log(task.name) // Test metadata + skip(someCondition, 'reason') // Skip dynamically + expect(1).toBe(1) // Context-bound expect +}) + +// signal (3.2+): AbortSignal aborted on timeout/cancel/bail +test('aborts on timeout', async ({ signal }) => { + await fetch('/resource', { signal }) +}, 2000) + +// annotate (3.2+): attach notes shown by the reporter +test('annotated', async ({ annotate }) => { + await annotate('see issue #123', 'issues') +}) +``` + +## Custom Test with Fixtures + +Prefer the **builder pattern** (4.1+) for automatic type inference: + +```ts +import { test as base } from 'vitest' + +const test = base + .extend('db', async ({}, { onCleanup }) => { + const db = await createDb() + onCleanup(() => db.close()) // runs after the test/scope + return db + }) + +test('query', async ({ db }) => { + const users = await db.query('SELECT * FROM users') + expect(users).toBeDefined() +}) +``` + +See [features-context](features-context.md) for fixture scopes, `test.override`, and the Playwright-compatible object syntax. + +## Retry Configuration + +```ts +test('flaky test', { retry: 3 }, async () => { + // Retries up to 3 times on failure +}) + +// Advanced retry options +test('with delay', { + retry: { + count: 3, + delay: 1000, + condition: /timeout/i, // Only retry on timeout errors + }, +}, async () => {}) +``` + +## Tags + +Tags must be declared in config first, then applied to tests (4.1+): + +```ts +test('database test', { tags: ['db', 'slow'] }, async () => {}) + +// Run with a tag expression: +// vitest --tagsFilter "db && !flaky" +``` + +See [features-test-tags](features-test-tags.md) for defining tags and filter syntax. + +## Benchmarks (v5) + +`bench` is no longer a top-level import — it is a [test-context fixture](features-benchmarking.md) used inside `test()`: + +```ts +// file must match benchmark.include (e.g. *.bench.ts) +test('sort', async ({ bench }) => { + await bench('Array.sort', () => [3, 1, 2].sort()).run() +}) +``` + +## Key Points + +- Pass options as the **second argument**; the 3rd-arg options object was removed in v4 (a trailing timeout number is still allowed) +- Tests with no body are marked as `todo` +- `test.only` throws in CI unless `allowOnly: true` +- Use context's `expect` for concurrent tests and snapshots +- Function name is used as test name if passed as first arg +- `test.sequential` was removed in v5 — use `{ concurrent: false }` + + diff --git a/.agents/skills/vitest/references/features-benchmarking.md b/.agents/skills/vitest/references/features-benchmarking.md new file mode 100644 index 0000000..e2074f4 --- /dev/null +++ b/.agents/skills/vitest/references/features-benchmarking.md @@ -0,0 +1,100 @@ +--- +name: benchmarking +description: Write benchmarks with the v5 bench test-context fixture (Tinybench) +--- + +# Benchmarking (v5) + +In v5 the benchmark API was rewritten: `bench` is **no longer a top-level import**. It is a [test-context fixture](features-context.md) used inside a regular `test()`, available only in files matched by `benchmark.include` (default `**/*.{bench,benchmark}.?(c|m)[jt]s?(x)`). Benchmarks are powered by [Tinybench](https://github.com/tinylibs/tinybench). + +## Defining & Running + +```ts +import { expect, test } from 'vitest' + +test('parse performance', async ({ bench }) => { + // bench() registers; .run() executes and returns the result + const result = await bench('parse', () => { + const data = JSON.parse('{"key":"value"}') + use(data) // consume the result — engines may eliminate dead code + }).run() + + expect(result.throughput.mean).toBeGreaterThan(10_000) +}) +``` + +Run benchmarks: + +```bash +vitest bench # only benchmarks (implicitly enables them) +vitest bench parser # filter by filename +vitest bench -t JSON # filter by test name +``` + +Set `benchmark: { enabled: true }` to run them alongside regular tests in a separate isolated group. + +## Comparing Implementations + +```ts +test('compare parsers', async ({ bench }) => { + const result = await bench.compare( + bench('JSON.parse', () => { JSON.parse(input) }), + bench('custom', { beforeEach: () => reset() }, () => { customParse(input) }), + { iterations: 100, time: 1000 }, // shared Tinybench options (last arg) + ) + + // Assertion matchers (delta avoids flaky failures) + expect(result.get('JSON.parse')).toBeFasterThan(result.get('custom'), { delta: 0.1 }) + expect(result.get('custom')).toBeSlowerThan(result.get('JSON.parse')) +}) +``` + +`bench.compare` interleaves iterations to reduce environmental bias and prints a comparison table after the test. + +## Storing & Replaying Baselines + +```ts +test('compare against baseline', async ({ bench }) => { + await bench.compare( + bench('current', { writeResult: './benchmarks/parse.json' }, () => parse(input)), + bench.from('previous', './benchmarks/parse.json'), // reads a stored result, no run + bench.from('remote', () => fetch(url).then(r => r.json())), + ) +}) +``` + +- `writeResult` overwrites the JSON file on every successful run (no skip-when-cached). +- `bench.from(name, source)` reads a stored result without invoking any function. +- For multi-project workspaces, pass `{ perProject: true }` and use `${projectName}` in `writeResult` paths to collect a cross-project comparison table. + +## Stability Notes + +- Benchmark files run sequentially and never in parallel; `retry` and the `delta` option reduce flakiness. +- Consume the result inside the bench fn — JS engines eliminate side-effect-free code. +- In Node mode every imported binding goes through Vite's module-runner getter; store hot references locally (`const _parse = parse`), benchmark the built package, or disable `experimental.viteModuleRunner` for the bench project. + +## Custom Provider (experimental) + +Replace the built-in Tinybench engine by pointing `benchmark.provider` at a module whose default export implements `BenchmarkProvider`: + +```ts +defineConfig({ test: { benchmark: { provider: './benchmark-provider.ts' } } }) +``` + +## v5 Migration + +- `bench` top-level import → `({ bench })` from the test context +- `bench.skip/only/todo` removed → use `test.skip/only/todo` on the surrounding test +- `benchmark.reporters`/`outputFile`/`compare`/`outputJson` and `--compare`/`--outputJson` removed → use `--reporter=json --outputFile` (JSON now has a `benchmarks` field) + +## Key Points + +- Benchmarks live in `*.bench.ts` files and run inside `test()` via `{ bench }` +- Use `bench.compare` + `toBeFasterThan`/`toBeSlowerThan` (with `delta`) for relative perf +- Persist baselines with `writeResult` and replay with `bench.from` + + diff --git a/.agents/skills/vitest/references/features-concurrency.md b/.agents/skills/vitest/references/features-concurrency.md new file mode 100644 index 0000000..cff37cc --- /dev/null +++ b/.agents/skills/vitest/references/features-concurrency.md @@ -0,0 +1,223 @@ +--- +name: concurrency-parallelism +description: Concurrent tests, parallel execution, and sharding +--- + +# Concurrency & Parallelism + +## File Parallelism + +By default, Vitest runs test files in parallel across workers: + +```ts +defineConfig({ + test: { + // Run files in parallel (default: true) + fileParallelism: true, + + // Max concurrent workers (v4: replaces maxThreads/maxForks; minWorkers removed) + maxWorkers: 4, + + // Pool type: 'forks' (default), 'threads', 'vmForks', 'vmThreads' + pool: 'forks', + }, +}) +``` + +> **v4 pool rework:** `poolOptions` was removed — all pool settings are now top-level. `singleThread`/`singleFork` become `maxWorkers: 1, isolate: false`. VM `memoryLimit` is `vmMemoryLimit`. These can now be set **per project**. + +## Concurrent Tests + +Run tests within a file in parallel: + +```ts +// Individual concurrent tests +test.concurrent('test 1', async ({ expect }) => { + expect(await fetch1()).toBe('result') +}) + +test.concurrent('test 2', async ({ expect }) => { + expect(await fetch2()).toBe('result') +}) + +// All tests in suite concurrent +describe.concurrent('parallel suite', () => { + test('test 1', async ({ expect }) => {}) + test('test 2', async ({ expect }) => {}) +}) +``` + +**Important:** Use `{ expect }` from context for concurrent tests. + +## Opting Out of Concurrency + +`test.sequential`/`describe.sequential` were **removed in v5**. Use `{ concurrent: false }`: + +```ts +describe.concurrent('mostly parallel', () => { + test('parallel 1', async () => {}) + + // Opt this test out of inherited concurrency + test('must run alone', { concurrent: false }, async () => {}) +}) + +// Or an entire suite +describe('sequential suite', { concurrent: false }, () => { + test('first', () => {}) + test('second', () => {}) +}) +``` + +Set `sequence.concurrent: true` to make all tests concurrent by default. + +## Max Concurrency + +Limit concurrent tests: + +```ts +defineConfig({ + test: { + maxConcurrency: 5, // Max concurrent tests per file + }, +}) +``` + +## Isolation + +Each file runs in isolated environment by default: + +```ts +defineConfig({ + test: { + // Disable isolation for faster runs (less safe) + isolate: false, + }, +}) +``` + +## Sharding + +Split tests across machines: + +```bash +# Machine 1 +vitest run --shard=1/3 + +# Machine 2 +vitest run --shard=2/3 + +# Machine 3 +vitest run --shard=3/3 +``` + +### CI Example (GitHub Actions) + +```yaml +jobs: + test: + strategy: + matrix: + shard: [1, 2, 3] + steps: + - run: vitest run --shard=${{ matrix.shard }}/3 --reporter=blob + + merge: + needs: test + steps: + - run: vitest --merge-reports --reporter=junit +``` + +### Merge Reports + +```bash +# Each shard outputs blob +vitest run --shard=1/3 --reporter=blob --coverage +vitest run --shard=2/3 --reporter=blob --coverage + +# Merge all blobs +vitest --merge-reports --reporter=json --coverage +``` + +## Test Sequence + +Control test order: + +```ts +defineConfig({ + test: { + sequence: { + // Run tests in random order + shuffle: true, + + // Seed for reproducible shuffle + seed: 12345, + + // Hook execution order + hooks: 'stack', // 'stack', 'list', 'parallel' + + // All tests concurrent by default + concurrent: true, + + // Order projects/groups run in (3.2+); lower runs first + groupOrder: 0, + }, + }, +}) +``` + +## Shuffle Tests + +Randomize to catch hidden dependencies: + +```ts +// Via CLI +vitest --shuffle + +// Per suite +describe.shuffle('random order', () => { + test('test 1', () => {}) + test('test 2', () => {}) + test('test 3', () => {}) +}) +``` + +## Pools (v4) + +`poolOptions` was removed; pool settings are now top-level and can be set per project: + +```ts +defineConfig({ + test: { + pool: 'forks', // 'forks' (default) | 'threads' | 'vmForks' | 'vmThreads' + maxWorkers: 8, + isolate: true, // threads/forks only; vm* pools are always isolated + vmMemoryLimit: '512MB', + }, +}) +``` + +For per-project parallelism/isolation settings, see [advanced-projects](advanced-projects.md). + +## Bail on Failure + +Stop after first failure: + +```bash +vitest --bail 1 # Stop after 1 failure +vitest --bail # Stop on first failure (same as --bail 1) +``` + +## Key Points + +- Files run in parallel by default (`pool: 'forks'`); tests within a file run sequentially unless `.concurrent` +- `concurrent` only speeds up tests that **await** (I/O, timers); pure sync tests still block the thread +- Always use context's `expect` in concurrent tests +- Use `{ concurrent: false }` (not `.sequential`) to opt out +- `maxWorkers` (not `maxThreads`/`maxForks`); `poolOptions` removed in v4 +- Sharding splits tests across CI machines; `--merge-reports` combines blob results + + diff --git a/.agents/skills/vitest/references/features-context.md b/.agents/skills/vitest/references/features-context.md new file mode 100644 index 0000000..08a0895 --- /dev/null +++ b/.agents/skills/vitest/references/features-context.md @@ -0,0 +1,163 @@ +--- +name: test-context-fixtures +description: Test context, custom fixtures with test.extend (builder pattern), scopes, and test.override +--- + +# Test Context & Fixtures + +## Built-in Context + +Every test receives context as its first argument: + +```ts +test('context', ({ task, expect, skip, signal, annotate }) => { + console.log(task.name) // Test metadata (readonly) + expect(1).toBe(1) // Expect bound to this test + skip(condition, 'reason') // Skip dynamically +}) +``` + +Properties: +- `task` — test metadata (name, file, etc.) +- `expect` — expect bound to this test (required for concurrent snapshot tests) +- `skip(condition?, message?)` — skip the test +- `signal` (3.2+) — `AbortSignal` aborted on timeout/cancel/bail +- `annotate(message, type?, attachment?)` (3.2+) — attach reporter annotations +- `onTestFinished(fn)` / `onTestFailed(fn)` — per-test cleanup/handlers +- `bench` (v5) — benchmark fixture (only in `*.bench.ts` files) + +## Custom Fixtures — Builder Pattern (4.1+, recommended) + +`.extend(name, options?, fixture)` infers types automatically. Use `onCleanup` for teardown: + +```ts +import { test as baseTest } from 'vitest' + +export const test = baseTest + // Plain value — type inferred as { port: number; host: string } + .extend('config', { port: 3000, host: 'localhost' }) + // Function fixture — can read previously defined fixtures + .extend('server', async ({ config }, { onCleanup }) => { + const server = await startServer(config) + onCleanup(() => server.close()) // runs after test/scope ends + return server + }) + +test('uses server', ({ config, server }) => { + expect(server.url).toContain(String(config.port)) +}) +``` + +> `onCleanup` can be called **once per fixture**. For multiple resources, split into separate fixtures. + +### Fixture Options + +```ts +const test = baseTest + .extend('metrics', { auto: true }, () => new Metrics()) // runs for every test + .extend('config', { scope: 'worker' }, () => loadConfig()) // once per worker + .extend('db', { scope: 'file' }, async ({ config }, { onCleanup }) => { + const db = await createDatabase(config) + onCleanup(() => db.close()) + return db + }) + .extend('baseUrl', { injected: true }, () => 'http://localhost:3000') // overridable via config +``` + +## Object Syntax (Playwright-compatible) + +Uses the `use()` callback; types must be declared manually: + +```ts +const test = baseTest.extend<{ page: Page; baseUrl: string }>({ + page: async ({}, use) => { + const page = await browser.newPage() + await use(page) // test runs here + await page.close() // cleanup after + }, + baseUrl: 'http://localhost:3000', +}) +``` + +Tuple form sets options: `fixture: [async ({}, use) => {…}, { scope: 'file' }]`. + +## Fixture Scopes (3.2+) + +| Scope | Lifetime | Can access | +|-------|----------|------------| +| `test` (default) | each test | worker + file + test fixtures + built-in context | +| `file` | once per file | worker + file fixtures | +| `worker` | once per worker process | only worker fixtures | + +Only `test`-scoped fixtures can access the built-in context (`task`, `expect`, …). In file/worker fixtures use `expect.getState().testPath` for the file path. By default every file is its own worker, so `file` and `worker` behave the same unless [isolation is disabled](features-concurrency.md). + +## Injected Fixtures (per-project values) + +```ts +// fixtures.ts +const test = baseTest.extend('url', { injected: true }, '/default') + +// vitest.config.ts — provide per project +defineConfig({ + test: { + projects: [ + { test: { name: 'prod', provide: { url: 'https://prod' } } }, + ], + }, +}) +``` + +Read raw provided values without fixtures via `import { inject } from 'vitest'`. + +## Overriding Fixtures — test.override (4.1+) + +`test.override` replaces fixture values for a suite and its children (replaces the deprecated `test.scoped`): + +```ts +describe('production', () => { + test + .override('config', { port: 8080, host: 'api.example.com' }) + .override('debug', false) // chainable + + test('uses prod config', ({ server }) => { + expect(server.url).toBe('http://api.example.com:8080') + }) +}) + +// Function override (reads other fixtures) with cleanup +test.override('db', async ({ config }, { onCleanup }) => { + const db = await createTestDatabase(config) + onCleanup(() => db.drop()) + return db +}) +``` + +You cannot introduce new fixtures or change `scope`/`auto` via `override`; use `test.extend` for new fixtures. + +## Composing & Hooks + +Extend an already-extended test, and use type-aware hooks on the extended `test`: + +```ts +import { test as dbTest } from './db-test' + +export const test = dbTest.extend('user', ({ db }) => db.createUser()) + +test.beforeEach(({ db }) => db.seed()) // sees fixtures +test.beforeAll(({ db }) => db.migrate()) // file/worker fixtures only (4.1+) +test.aroundAll(async (run, { db }) => db.tx(run)) +``` + +## Key Points + +- Prefer the **builder pattern** — types are inferred, cleanup via `onCleanup` +- Fixtures are lazy — only initialized when destructured +- Always destructure `{ db }` (not `context.db`) +- Use `{ scope: 'file' | 'worker' }` for expensive shared resources +- Use `test.override` (not `test.scoped`) to vary fixture values per suite +- Use `{ injected: true }` + project `provide` for per-project values + + diff --git a/.agents/skills/vitest/references/features-coverage.md b/.agents/skills/vitest/references/features-coverage.md new file mode 100644 index 0000000..4aef2a3 --- /dev/null +++ b/.agents/skills/vitest/references/features-coverage.md @@ -0,0 +1,226 @@ +--- +name: code-coverage +description: Code coverage with V8 or Istanbul providers +--- + +# Code Coverage + +## Setup + +```bash +# Run tests with coverage +vitest run --coverage +``` + +## Configuration + +```ts +// vitest.config.ts +defineConfig({ + test: { + coverage: { + // Provider: 'v8' (default, faster) or 'istanbul' (more compatible) + provider: 'v8', + + // Enable coverage + enabled: true, + + // Reporters + reporter: ['text', 'json', 'html'], + + // v4: define `include` to report uncovered files too. + // Without it, only files loaded during the run are reported. + include: ['src/**/*.{ts,tsx}'], + + // Exclusion is applied to files matched by `include` + exclude: [ + '**/*.d.ts', + '**/*.test.ts', + ], + + // Thresholds + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + }, + + // v5 (v8 only): also cover node:child_process / node:worker_threads + // spawned during the run (adds overhead via NODE_V8_COVERAGE) + autoAttachSubprocess: false, + }, + }, +}) +``` + +## Providers + +### V8 (Default) + +```bash +npm i -D @vitest/coverage-v8 +``` + +- Faster, no pre-instrumentation +- Uses V8's native coverage +- v4 uses **AST-based remapping** (as accurate as Istanbul); expect coverage numbers to shift when upgrading from v3 +- Recommended for most projects + +### Istanbul + +```bash +npm i -D @vitest/coverage-istanbul +``` + +- Pre-instruments code +- Works in any JS runtime +- More overhead but widely compatible + +## Reporters + +```ts +coverage: { + reporter: [ + 'text', // Terminal output + 'text-summary', // Summary only + 'json', // JSON file + 'html', // HTML report + 'lcov', // For CI tools + 'cobertura', // XML format + ], + reportsDirectory: './coverage', +} +``` + +## Thresholds + +Fail tests if coverage is below threshold: + +```ts +coverage: { + thresholds: { + // Global thresholds + lines: 80, + functions: 75, + branches: 70, + statements: 80, + + // Per-file thresholds + perFile: true, + + // Auto-update thresholds (for gradual improvement) + // v5: a function receives (newThreshold, previousThreshold) + autoUpdate: true, + + // v5: glob thresholds no longer inherit top-level `perFile` — set it per glob + 'src/utils/**': { lines: 80, perFile: true }, + }, +} +``` + +## Ignoring Code + +### V8 + +```ts +/* v8 ignore next -- @preserve */ +function ignored() { + return 'not covered' +} + +/* v8 ignore start -- @preserve */ +// All code here ignored +/* v8 ignore stop -- @preserve */ +``` + +### Istanbul + +```ts +/* istanbul ignore next -- @preserve */ +function ignored() {} + +/* istanbul ignore if -- @preserve */ +if (condition) { + // ignored +} +``` + +Note: `@preserve` keeps comments through esbuild. + +## Package.json Scripts + +```json +{ + "scripts": { + "test": "vitest", + "test:coverage": "vitest run --coverage", + "test:coverage:watch": "vitest --coverage" + } +} +``` + +## Vitest UI Coverage + +Enable HTML coverage in Vitest UI: + +```ts +coverage: { + enabled: true, + reporter: ['text', 'html'], +} +``` + +Run with `vitest --ui` to view coverage visually. + +## CI Integration + +```yaml +# GitHub Actions +- name: Run tests with coverage + run: npm run test:coverage + +- name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage/lcov.info +``` + +## Coverage with Sharding + +Merge coverage from sharded runs (blobs default to `.vitest/blob/`): + +```bash +vitest run --shard=1/3 --coverage --reporter=blob +vitest run --shard=2/3 --coverage --reporter=blob +vitest run --shard=3/3 --coverage --reporter=blob + +vitest --merge-reports --coverage --reporter=json +``` + +## v5 Changes + +- **`include`/`exclude` match relative paths** (not absolute-with-`contains`), so patterns catch fewer files than v4 — a wildcard-free pattern like `'src'` means `src/**`. Re-verify the reported file set after upgrading. +- **Glob thresholds don't inherit top-level `perFile`** — set `perFile` on each glob that needs it. +- **`coverage.autoAttachSubprocess`** (v8) tracks child-process/worker-thread coverage. +- **Istanbul moved to the maintained [`@vitest/istanbuljs`](https://github.com/vitest-dev/istanbuljs) fork**; the v8 provider merges reports with bounded memory. + +## v4 Changes + +- **`coverage.all` and `coverage.extensions` removed** — only covered files are reported unless `coverage.include` is set. +- **`coverage.ignoreEmptyLines` removed**; lines without runtime code are no longer counted. +- **`coverage.experimentalAstAwareRemapping` removed** — AST remapping is the default and only mode for V8. +- Programmatic coverage APIs moved from `vitest/coverage` to `vitest/node`. + +## Key Points + +- V8 is faster, Istanbul is more compatible +- Use `--coverage` flag or `coverage.enabled: true` +- Define `coverage.include` to report uncovered source files +- Set thresholds to enforce minimum coverage +- Use `@preserve` comment to keep ignore hints (e.g. `/* v8 ignore next -- @preserve */`) + + diff --git a/.agents/skills/vitest/references/features-filtering.md b/.agents/skills/vitest/references/features-filtering.md new file mode 100644 index 0000000..fc8396d --- /dev/null +++ b/.agents/skills/vitest/references/features-filtering.md @@ -0,0 +1,212 @@ +--- +name: test-filtering +description: Filter tests by name, file patterns, and tags +--- + +# Test Filtering + +## CLI Filtering + +### By File Path + +```bash +# Run files containing "user" +vitest user + +# Multiple patterns +vitest user auth + +# Specific file +vitest src/user.test.ts + +# By line number +vitest src/user.test.ts:25 +``` + +### By Test Name + +```bash +# Tests matching pattern +vitest -t "login" +vitest --testNamePattern "should.*work" + +# Regex patterns +vitest -t "/user|auth/" +``` + +## Changed Files + +```bash +# Uncommitted changes +vitest --changed + +# Since specific commit +vitest --changed HEAD~1 +vitest --changed abc123 + +# Since branch +vitest --changed origin/main +``` + +## Related Files + +Run tests that import specific files: + +```bash +vitest related src/utils.ts src/api.ts --run +``` + +Useful with lint-staged: + +```js +// .lintstagedrc.js +export default { + '*.{ts,tsx}': 'vitest related --run', +} +``` + +## Focus Tests (.only) + +```ts +test.only('only this runs', () => {}) + +describe.only('only this suite', () => { + test('runs', () => {}) +}) +``` + +In CI, `.only` throws error unless configured: + +```ts +defineConfig({ + test: { + allowOnly: true, // Allow .only in CI + }, +}) +``` + +## Skip Tests + +```ts +test.skip('skipped', () => {}) + +// Conditional +test.skipIf(process.env.CI)('not in CI', () => {}) +test.runIf(!process.env.CI)('local only', () => {}) + +// Dynamic skip +test('dynamic', ({ skip }) => { + skip(someCondition, 'reason') +}) +``` + +## Tags + +Tags must be declared in config, then applied to tests/suites and filtered with a tag expression: + +```ts +// vitest.config.ts +defineConfig({ + test: { + tags: [{ name: 'db' }, { name: 'slow' }, { name: 'flaky' }], + }, +}) + +// test file +test('database test', { tags: ['db'] }, () => {}) +``` + +```bash +vitest --tagsFilter "db && !flaky" +vitest --tagsFilter "unit || e2e" +vitest --list-tags # show defined tags +``` + +Full syntax, priority, and per-tag options: see [features-test-tags](features-test-tags.md). + +## Include/Exclude Patterns + +```ts +defineConfig({ + test: { + // Test file patterns + include: ['**/*.{test,spec}.{ts,tsx}'], + + // Exclude patterns + exclude: [ + '**/node_modules/**', + '**/e2e/**', + '**/*.skip.test.ts', + ], + + // Include source for in-source testing + includeSource: ['src/**/*.ts'], + + // Scope discovery to a directory (faster than broad excludes) + dir: './src', + }, +}) +``` + +> v4 simplified default `exclude` to only `node_modules`/`.git`. Prefer `test.dir` to limit where tests are found; spread `configDefaults.exclude` to restore the old excludes. + +## Watch Mode Filtering + +In watch mode, press: +- `p` - Filter by filename pattern +- `t` - Filter by test name pattern +- `a` - Run all tests +- `f` - Run only failed tests + +## Projects Filtering + +Run specific project: + +```bash +vitest --project unit +vitest --project integration --project e2e +``` + +## Environment-based Filtering + +```ts +const isDev = process.env.NODE_ENV === 'development' +const isCI = process.env.CI + +describe.skipIf(isCI)('local only tests', () => {}) +describe.runIf(isDev)('dev tests', () => {}) +``` + +## Combining Filters + +```bash +# File pattern + test name + changed +vitest user -t "login" --changed + +# Related files + run mode +vitest related src/auth.ts --run +``` + +## List Tests Without Running + +```bash +vitest list # Show all test names +vitest list -t "user" # Filter by name +vitest list --filesOnly # Show only file paths +vitest list --json # JSON output +``` + +## Key Points + +- Use `-t` for test name pattern filtering +- `--changed` runs only tests affected by changes +- `--related` runs tests importing specific files +- Tags provide semantic test grouping +- Use `.only` for debugging, but configure CI to reject it +- Watch mode has interactive filtering + + diff --git a/.agents/skills/vitest/references/features-mocking.md b/.agents/skills/vitest/references/features-mocking.md new file mode 100644 index 0000000..81192fd --- /dev/null +++ b/.agents/skills/vitest/references/features-mocking.md @@ -0,0 +1,334 @@ +--- +name: mocking +description: Mock functions, modules, timers, and dates with vi utilities +--- + +# Mocking + +## Mock Functions + +```ts +import { expect, vi } from 'vitest' + +// Create mock function +const fn = vi.fn() +fn('hello') + +expect(fn).toHaveBeenCalled() +expect(fn).toHaveBeenCalledWith('hello') + +// With implementation +const add = vi.fn((a, b) => a + b) +expect(add(1, 2)).toBe(3) + +// Mock return values +fn.mockReturnValue(42) +fn.mockReturnValueOnce(1).mockReturnValueOnce(2) +fn.mockResolvedValue({ data: true }) +fn.mockRejectedValue(new Error('fail')) + +// Mock implementation +fn.mockImplementation((x) => x * 2) +fn.mockImplementationOnce(() => 'first call') +``` + +## Spying on Objects + +```ts +const cart = { + getTotal: () => 100, +} + +const spy = vi.spyOn(cart, 'getTotal') +cart.getTotal() + +expect(spy).toHaveBeenCalled() + +// Mock implementation +spy.mockReturnValue(200) +expect(cart.getTotal()).toBe(200) + +// Restore original +spy.mockRestore() +``` + +Since v4, `vi.spyOn`/`vi.fn` can mock **constructors** — provide a `function` or `class` implementation (an arrow function throws "not a constructor"): + +```ts +const Spy = vi.spyOn(cart, 'Apples').mockImplementation(class { + getApples() { return 0 } +}) +const instance = new Spy() +``` + +## Conditional Mocking with vi.when (v5) + +Define per-argument behaviors without writing `if`/`switch` in `mockImplementation`: + +```ts +vi.when(db.findById) + .calledWith(1) + .thenResolve({ id: 1, name: 'Ella' }) + .calledWith(2) + .thenResolve({ id: 2, name: 'Gracie' }) + +// Actions: thenReturn / thenThrow / thenResolve / thenReject (+ *Once variants) +// `calledWith` supports asymmetric matchers +vi.when(sendEmail).calledWith(expect.stringContaining('@')).thenReturn({ ok: true }) +``` + +- Behaviors match **first-in-first-out** (register specific before broad); stacked actions on one behavior consume **last-in-first-out**, with `{ times }` to limit. +- Handle unmatched calls with `{ onUnmatched: 'throw' | fn }` (default falls through to the original implementation). +- Assert all behaviors ran with `expect(w).toHaveBeenExhausted()`. + +## Auto-Cleanup with `using` + +In runtimes with Explicit Resource Management (Node 24+, TS 5.2+), declare spies/mocks with `using` to auto-restore when the block exits — works with `vi.spyOn`, `vi.fn`, `vi.doMock`, and `vi.when`: + +```ts +it('mocks console only here', () => { + using spy = vi.spyOn(console, 'log').mockImplementation(() => {}) + debug('message') + expect(spy).toHaveBeenCalled() +} ) // console.log restored automatically — no afterEach +``` + +## Module Mocking + +`vi.mock`, `vi.unmock`, and `vi.hoisted` are hoisted to the top of the file. In v5 calling them inside a function, block, or `describe`/`test` callback **throws** (it only warned before) — keep them at the module top level. Use `vi.doMock`/`vi.doUnmock` for non-hoisted, in-scope mocking. + +```ts +// vi.mock is hoisted to top of file +vi.mock('./api', () => ({ + fetchUser: vi.fn(() => ({ id: 1, name: 'Mock' })), +})) + +import { fetchUser } from './api' + +test('mocked module', () => { + expect(fetchUser()).toEqual({ id: 1, name: 'Mock' }) +}) +``` + +### Partial Mock + +```ts +vi.mock('./utils', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + specificFunction: vi.fn(), + } +}) +``` + +### Auto-mock with Spy + +```ts +// Keep implementation but spy on calls +vi.mock('./calculator', { spy: true }) + +import { add } from './calculator' + +test('spy on module', () => { + const result = add(1, 2) // Real implementation + expect(result).toBe(3) + expect(add).toHaveBeenCalledWith(1, 2) +}) +``` + +### Manual Mocks (__mocks__) + +``` +src/ + __mocks__/ + axios.ts # Mocks 'axios' + api/ + __mocks__/ + client.ts # Mocks './client' + client.ts +``` + +```ts +// Just call vi.mock with no factory +vi.mock('axios') +vi.mock('./api/client') +``` + +## Dynamic Mocking (vi.doMock) + +Not hoisted - use for dynamic imports: + +```ts +test('dynamic mock', async () => { + vi.doMock('./config', () => ({ + apiUrl: 'http://test.local', + })) + + const { apiUrl } = await import('./config') + expect(apiUrl).toBe('http://test.local') + + vi.doUnmock('./config') +}) +``` + +## Mock Timers + +```ts +import { afterEach, beforeEach, vi } from 'vitest' + +beforeEach(() => { + vi.useFakeTimers() +}) + +afterEach(() => { + vi.useRealTimers() +}) + +test('timers', () => { + const fn = vi.fn() + setTimeout(fn, 1000) + + expect(fn).not.toHaveBeenCalled() + + vi.advanceTimersByTime(1000) + expect(fn).toHaveBeenCalled() +}) + +// Other timer methods +vi.runAllTimers() // Run all pending timers +vi.runOnlyPendingTimers() // Run only currently pending +vi.advanceTimersToNextTimer() // Advance to next timer +``` + +### Async Timer Methods + +```ts +test('async timers', async () => { + vi.useFakeTimers() + + let resolved = false + setTimeout(() => Promise.resolve().then(() => { resolved = true }), 100) + + await vi.advanceTimersByTimeAsync(100) + expect(resolved).toBe(true) +}) +``` + +## Mock Dates + +```ts +vi.setSystemTime(new Date('2024-01-01')) +expect(new Date().getFullYear()).toBe(2024) + +vi.useRealTimers() // Restore +``` + +v5 fake timers (and `vi.setSystemTime` used without them) also mock `Temporal` when it's on the global object, not just `Date`: + +```ts +vi.setSystemTime(0) +Temporal.Now.instant().epochMilliseconds // 0 + +// keep Temporal native: +vi.useFakeTimers({ toNotFake: ['Temporal'] }) +``` + +## Mock Globals + +```ts +vi.stubGlobal('fetch', vi.fn(() => + Promise.resolve({ json: () => ({ data: 'mock' }) }) +)) + +// Restore +vi.unstubAllGlobals() +``` + +## Mock Environment Variables + +```ts +vi.stubEnv('API_KEY', 'test-key') +expect(import.meta.env.API_KEY).toBe('test-key') + +// Restore +vi.unstubAllEnvs() +``` + +## Clearing Mocks + +```ts +const fn = vi.fn() +fn() + +fn.mockClear() // Clear call history +fn.mockReset() // Clear history + implementation +fn.mockRestore() // Restore original (for spies) + +// Global +vi.clearAllMocks() +vi.resetAllMocks() +vi.restoreAllMocks() +``` + +## Config Auto-Reset + +```ts +// vitest.config.ts +defineConfig({ + test: { + clearMocks: true, // Clear call history before each test — v5 DEFAULT + mockReset: true, // Reset before each test + restoreMocks: true, // Restore after each test + unstubEnvs: true, // Restore env vars + unstubGlobals: true, // Restore globals + }, +}) +``` + +> **v5:** `clearMocks` defaults to `true`, so mock call history no longer leaks between tests. Mocks set up outside the test body (setup files, module top level, `beforeAll`) are most affected — their recorded calls are cleared before the asserting test runs. Set `clearMocks: false` to restore the old behavior. + +## Hoisted Variables for Mocks + +```ts +const mockFn = vi.hoisted(() => vi.fn()) + +vi.mock('./module', () => ({ + getData: mockFn, +})) + +import { getData } from './module' + +test('hoisted mock', () => { + mockFn.mockReturnValue('test') + expect(getData()).toBe('test') +}) +``` + +## v5 Behavior Changes + +- **Class mocks keep prototype methods.** `vi.fn(Dog)`, `vi.spyOn(obj, 'Dog')`, and `.mockImplementation(class …)` now chain the mock's `prototype` to the implementation's, so instance methods work and `instanceof Dog` passes. `mockReset` reverts the chain. +- **Automocked modules stay automocked in the browser** — their exports return `undefined` unless you pass `{ spy: true }` or a factory. + +## v4 Behavior Changes + +- `vi.fn().getMockName()` returns `'vi.fn()'` (was `'spy'`); snapshots show `[MockFunction]` instead of `[MockFunction spy]`. +- `vi.restoreAllMocks` (and `restoreMocks: true`) now **only restore `vi.spyOn` spies**; automocks are unaffected. `.mockRestore` still resets a mock's implementation/state. +- `vi.fn().mock.invocationCallOrder` starts at `1` (Jest parity). +- Automocked getters return `undefined` by default; automocked methods can't be restored. + +## Key Points + +- Prefer `vi.mock` for module mocking (hoisted - called before imports) +- Use `vi.doMock` for dynamic, non-hoisted mocking +- Use `vi.when` for argument-specific behaviors; `using` for scoped auto-restore +- Use `{ spy: true }` to keep implementation but track calls +- `vi.hoisted` lets you reference variables in mock factories + + diff --git a/.agents/skills/vitest/references/features-reporters.md b/.agents/skills/vitest/references/features-reporters.md new file mode 100644 index 0000000..4b293b0 --- /dev/null +++ b/.agents/skills/vitest/references/features-reporters.md @@ -0,0 +1,110 @@ +--- +name: reporters +description: Built-in reporters, default selection, and CI/output configuration +--- + +# Reporters + +Select reporters via `--reporter` or `reporters` config. Configuring `reporters` **replaces** the default list — spread `configDefaults.reporters` to keep them. + +```ts +import { configDefaults, defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + reporters: ['verbose', ['junit', { suiteName: 'UI tests' }]], + // keep defaults and add one: + // reporters: ['json', ...configDefaults.reporters], + }, +}) +``` + +## Default Selection + +When `reporters` is unset, Vitest auto-selects: + +- `default` for normal terminal runs +- **`minimal` (alias `agent`)** when it detects an **AI coding agent** — only failed tests + errors, no summary or passing logs, optimized to cut token usage +- `github-actions` is added when `process.env.GITHUB_ACTIONS === 'true'` + +## Built-in Reporters + +| Reporter | Use | +|----------|-----| +| `default` | Summary + collapses passing files; prints full tree for single/failing file | +| `verbose` | One line per finished test (flat list in v4); only reporter that shows annotations on pass | +| `tree` | Like `default` but always shows each test (the old v3 verbose) | +| `dot` | One dot per test; details only for failures | +| `minimal` / `agent` | Failures only; best for AI/LLM workflows | +| `junit` | JUnit XML (templated, see below) | +| `json` | Jest-compatible JSON; includes `coverageMap` when coverage enabled | +| `tap` / `tap-flat` | TAP (nested / flat) | +| `html` | Interactive UI report (needs `@vitest/ui`) | +| `blob` | Serialized results for `--merge-reports` | +| `github-actions` | Workflow annotations + job summary | +| `hanging-process` | Lists processes preventing exit (debugging) | + +> v4 removed the `basic` reporter (equivalent to `['default', { summary: false }]`). The old `verbose` flat behavior moved here; use `tree` for the nested view. + +## Output Files + +> **v5:** the `json` and `junit` reporters now **write to a file by default** (`.vitest/json/output.json`, `.vitest/junit/output.xml`) instead of stdout. If you piped output (`vitest --reporter=json | jq`), read the artifact instead or opt back in with `reporters: [['json', { stdout: true }]]`. An explicit `outputFile` is still respected. + +```bash +vitest --reporter=json --outputFile=./test-output.json +``` + +```ts +defineConfig({ + test: { + reporters: ['junit', 'json'], + outputFile: { junit: './junit.xml', json: './report.json' }, + }, +}) +``` + +## JUnit Templating + +```ts +reporters: [['junit', { + suiteNameTemplate: '{title}', // {title} {filename} {basename} {displayName} + classnameTemplate: '{classname}', // {classname} {title} {suitename} {filename} ... + titleTemplate: '{title}', + ancestorSeparator: ' > ', + addFileAttribute: true, +}]] +``` + +`{filename}` is the **relative** path (use `{basename}` for the bare name). Templates can also be functions receiving all variables. + +## HTML Report (v5 paths) + +The HTML reporter writes a directory via `outputDir` (default `.vitest`); the entry is `/index.html`. Use `singleFile: true` for a self-contained shareable file (large; coverage not inlined). + +```ts +reporters: [['html', { singleFile: true }]] +``` + +## Blob & Merge (CI/sharding) + +Blobs default to `.vitest/blob/`. Label environments with `VITEST_BLOB_LABEL` or the reporter `label` option: + +```bash +vitest run --reporter=blob --outputFile=reports/blob-1.json +vitest --merge-reports=reports --reporter=junit --reporter=default +``` + +Blob reports don't include file attachments — merge `attachmentsDir` (`.vitest/attachments/`) separately. `--reporter=blob`/`--merge-reports` don't work in watch mode. + +## Key Points + +- Configuring `reporters` replaces defaults — spread `configDefaults.reporters` to keep them +- The `minimal`/`agent` reporter is auto-selected for AI agents and minimizes token usage +- Use `tree` for the nested per-test view (old v3 `verbose`) +- v5 artifacts (blob, attachments, HTML) live under `.vitest/` + + diff --git a/.agents/skills/vitest/references/features-snapshots.md b/.agents/skills/vitest/references/features-snapshots.md new file mode 100644 index 0000000..60d3d5e --- /dev/null +++ b/.agents/skills/vitest/references/features-snapshots.md @@ -0,0 +1,253 @@ +--- +name: snapshot-testing +description: Snapshot testing with file, inline, and file snapshots +--- + +# Snapshot Testing + +Snapshot tests capture output and compare against stored references. + +## Basic Snapshot + +```ts +import { expect, test } from 'vitest' + +test('snapshot', () => { + const result = generateOutput() + expect(result).toMatchSnapshot() +}) +``` + +First run creates `.snap` file: + +```js +// __snapshots__/test.spec.ts.snap +exports['snapshot 1'] = ` +{ + "id": 1, + "name": "test" +} +` +``` + +## Inline Snapshots + +Stored directly in test file: + +```ts +test('inline snapshot', () => { + const data = { foo: 'bar' } + expect(data).toMatchInlineSnapshot() +}) +``` + +Vitest updates the test file: + +```ts +test('inline snapshot', () => { + const data = { foo: 'bar' } + expect(data).toMatchInlineSnapshot(` + { + "foo": "bar", + } + `) +}) +``` + +## File Snapshots + +Compare against explicit file: + +```ts +test('render html', async () => { + const html = renderComponent() + await expect(html).toMatchFileSnapshot('./expected/component.html') +}) +``` + +## Snapshot Hints + +Add descriptive hints: + +```ts +test('multiple snapshots', () => { + expect(header).toMatchSnapshot('header') + expect(body).toMatchSnapshot('body content') + expect(footer).toMatchSnapshot('footer') +}) +``` + +## Object Shape Matching + +Match partial structure: + +```ts +test('shape snapshot', () => { + const data = { + id: Math.random(), + created: new Date(), + name: 'test' + } + + expect(data).toMatchSnapshot({ + id: expect.any(Number), + created: expect.any(Date), + }) +}) +``` + +## Error Snapshots + +```ts +test('error message', () => { + expect(() => { + throw new Error('Something went wrong') + }).toThrowErrorMatchingSnapshot() +}) + +test('inline error', () => { + expect(() => { + throw new Error('Bad input') + }).toThrowErrorMatchingInlineSnapshot(`[Error: Bad input]`) +}) +``` + +## Updating Snapshots + +```bash +# Update all snapshots +vitest -u +vitest --update + +# In watch mode, press 'u' to update failed snapshots +``` + +In CI (`process.env.CI`), Vitest **never writes** snapshots: mismatches, missing snapshots, and **obsolete snapshots** (entries no longer matching any test) all fail the run. + +## Visual & ARIA Snapshots (Browser Mode) + +```ts +import { expect, test } from 'vitest' +import { page } from 'vitest/browser' // v4: import from 'vitest/browser' + +test('button looks correct', async () => { + await expect(page.getByRole('button')).toMatchScreenshot('primary-button') +}) + +// ARIA snapshot — assert the accessibility tree (4.1+, experimental) +test('nav structure', async () => { + await expect.element(page.getByRole('navigation')).toMatchAriaInlineSnapshot(` + - navigation "Main": + - link "Home" + `) +}) +``` + +## Custom Snapshot Matchers (4.1+) + +Build matchers on the composable `Snapshots` helpers from `vitest` (replaces importing from `jest-snapshot`): + +```ts +import { expect, Snapshots } from 'vitest' + +const { toMatchSnapshot, toMatchInlineSnapshot } = Snapshots + +expect.extend({ + toMatchTrimmedSnapshot(received: string, length: number) { + return toMatchSnapshot.call(this, received.slice(0, length)) + }, + toMatchTrimmedInlineSnapshot(received: string, inlineSnapshot?: string) { + return toMatchInlineSnapshot.call(this, received.slice(0, 10), inlineSnapshot) + }, +}) +``` + +The inline snapshot string must be the **last** argument. File snapshot matchers must be `async`. + +## Custom Serializers + +Add custom snapshot formatting: + +```ts +expect.addSnapshotSerializer({ + test(val) { + return val && typeof val.toJSON === 'function' + }, + serialize(val, config, indentation, depth, refs, printer) { + return printer(val.toJSON(), config, indentation, depth, refs) + }, +}) +``` + +Or via config: + +```ts +// vitest.config.ts +defineConfig({ + test: { + snapshotSerializers: ['./my-serializer.ts'], + }, +}) +``` + +## Snapshot Format Options + +```ts +defineConfig({ + test: { + snapshotFormat: { + printBasicPrototype: false, // Don't print Array/Object prototypes (Vitest default) + escapeString: false, + printShadowRoot: true, // v4 default: custom elements print their shadow root + }, + }, +}) +``` + +## Concurrent Test Snapshots + +Use context's expect: + +```ts +test.concurrent('concurrent 1', async ({ expect }) => { + expect(await getData()).toMatchSnapshot() +}) + +test.concurrent('concurrent 2', async ({ expect }) => { + expect(await getOther()).toMatchSnapshot() +}) +``` + +## Snapshot File Location + +Default: `__snapshots__/.snap` + +Customize: + +```ts +defineConfig({ + test: { + resolveSnapshotPath: (testPath, snapExtension) => { + return testPath.replace('__tests__', '__snapshots__') + snapExtension + }, + }, +}) +``` + +## Key Points + +- Commit snapshot files to version control +- Review snapshot changes in code review +- Use hints for multiple snapshots in one test +- Use `toMatchFileSnapshot` for large outputs (HTML, JSON) +- Inline snapshots auto-update in test file +- Use context's `expect` for concurrent tests +- CI fails on obsolete snapshots; clean them with `--update` +- v4 prints custom-element shadow roots; disable via `snapshotFormat.printShadowRoot: false` + + diff --git a/.agents/skills/vitest/references/features-test-tags.md b/.agents/skills/vitest/references/features-test-tags.md new file mode 100644 index 0000000..d7671c1 --- /dev/null +++ b/.agents/skills/vitest/references/features-test-tags.md @@ -0,0 +1,123 @@ +--- +name: test-tags +description: Label tests with tags to filter runs and apply shared runner options +--- + +# Test Tags (4.1+) + +Tags label tests so you can filter what runs and apply shared options (timeout, retry) to a *category* of tests that span many files. Reach for tags over projects when the category needs different timeouts/retries (not different pools/environments). + +## Defining Tags + +Tags **must be declared in config** — using an undefined tag throws unless `strictTags: false`. Each tag can carry options applied to every test marked with it: + +```ts +// vitest.config.ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + tags: [ + { name: 'frontend', description: 'Frontend tests.' }, + { name: 'db', description: 'Database queries.', timeout: 60_000 }, + { + name: 'flaky', + retry: process.env.CI ? 3 : 0, + timeout: 30_000, + priority: 1, // lower priority number wins on conflicts + }, + ], + strictTags: true, // default: error on unknown tags + }, +}) +``` + +Type-safe tag names (augment `TestTags`, include the file in your tsconfig): + +```ts +import 'vitest' + +declare module 'vitest' { + interface TestTags { + tags: 'frontend' | 'backend' | 'db' | 'flaky' + } +} +``` + +## Applying Tags + +```ts +import { describe, test } from 'vitest' + +test('renders homepage', { tags: ['frontend'] }, () => {}) + +// Tags inherit from the parent suite +describe('API endpoints', { tags: ['backend'] }, () => { + test('validates input', { tags: ['validation'] }, () => { + // has both "backend" (inherited) and "validation" + }) +}) +``` + +Tag every test in a file with a JSDoc `@module-tag` at the top of the file (applies to **all** tests in the file, not just the next one): + +```ts +/** + * @module-tag admin/pages/dashboard + */ +test('dashboard renders', () => {}) +``` + +### Option conflict resolution + +When several tags set the same option on a test, `priority` wins first (lower number), then array order. Options on the test itself always win: + +```ts +test('flaky db test', { tags: ['flaky', 'db'] }) // timeout 30_000 (flaky priority 1), retry 3 +test('override', { tags: ['flaky', 'db'], timeout: 120_000 }) // timeout 120_000, retry 3 +``` + +## Filtering by Tag + +Use `--tagsFilter` with an expression: + +```bash +vitest --tagsFilter "frontend" +vitest --tagsFilter "db && !flaky" +vitest --tagsFilter "(unit || e2e) && !slow" +vitest --tagsFilter "api/*" # wildcard +vitest --list-tags # list defined tags (=json for JSON) +``` + +Operators: `and`/`&&`, `or`/`||`, `not`/`!`, `*` wildcard, `()` grouping. Precedence: `not` > `and` > `or`. Multiple `--tagsFilter` flags combine with AND. Tag names can't be `and`/`or`/`not` or contain special chars/spaces. + +Programmatic: pass `tagsFilter: ['frontend and backend']` to `startVitest`/`createVitest`. + +## Checking the Filter at Runtime + +Skip expensive setup when no matching tests are scheduled: + +```ts +import { beforeAll, TestRunner } from 'vitest' + +beforeAll(async () => { + if (TestRunner.matchesTags(['db'])) { + await seedDatabase() + } +}) +``` + +Returns `true` when the active `--tagsFilter` would include a test with those tags (or when no filter is active). + +## Key Points + +- Tags must be declared in config; the CLI filter is `--tagsFilter` (not `--tags`) +- Tags inherit from parent suites and `@module-tag` JSDoc comments +- Use tags for cross-cutting categories with shared timeout/retry; use [projects](advanced-projects.md) for different pools/environments +- `TestRunner.matchesTags` gates expensive `globalSetup`/`beforeAll` work + + diff --git a/.agents/skills/vue-testing-best-practices/SKILL.md b/.agents/skills/vue-testing-best-practices/SKILL.md new file mode 100644 index 0000000..c600b52 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/SKILL.md @@ -0,0 +1,29 @@ +--- +name: vue-testing-best-practices +version: 1.0.0 +license: MIT +author: github.com/vuejs-ai +description: Use for Vue.js testing. Covers Vitest, Vue Test Utils, component testing, mocking, testing patterns, and Playwright for E2E testing. +--- + +Vue.js testing best practices, patterns, and common gotchas. + +### Testing +- Setting up test infrastructure for Vue 3 projects → See [testing-vitest-recommended-for-vue](reference/testing-vitest-recommended-for-vue.md) +- Tests keep breaking when refactoring component internals → See [testing-component-blackbox-approach](reference/testing-component-blackbox-approach.md) +- Tests fail intermittently with race conditions → See [testing-async-await-flushpromises](reference/testing-async-await-flushpromises.md) +- Composables using lifecycle hooks or inject fail to test → See [testing-composables-helper-wrapper](reference/testing-composables-helper-wrapper.md) +- Getting "injection Symbol(pinia) not found" errors in tests → See [testing-pinia-store-setup](reference/testing-pinia-store-setup.md) +- Components with async setup won't render in tests → See [testing-suspense-async-components](reference/testing-suspense-async-components.md) +- Snapshot tests keep passing despite broken functionality → See [testing-no-snapshot-only](reference/testing-no-snapshot-only.md) +- Choosing end-to-end testing framework for Vue apps → See [testing-e2e-playwright-recommended](reference/testing-e2e-playwright-recommended.md) +- Tests need to verify computed styles or real DOM events → See [testing-browser-vs-node-runners](reference/testing-browser-vs-node-runners.md) +- Testing components created with defineAsyncComponent fails → See [async-component-testing](reference/async-component-testing.md) +- Teleported modal content can't be found in wrapper queries → See [teleport-testing-complexity](reference/teleport-testing-complexity.md) + +## Reference + +- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing) +- [Vue Test Utils](https://test-utils.vuejs.org/) +- [Vitest Documentation](https://vitest.dev/) +- [Playwright Documentation](https://playwright.dev/) diff --git a/.agents/skills/vue-testing-best-practices/reference/async-component-testing.md b/.agents/skills/vue-testing-best-practices/reference/async-component-testing.md new file mode 100644 index 0000000..a4d2c49 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/async-component-testing.md @@ -0,0 +1,163 @@ +--- +title: Use flushPromises for Testing Async Components +impact: HIGH +impactDescription: Without awaiting async operations, tests make assertions before the component has rendered, causing false negatives +type: gotcha +tags: [vue3, testing, async, defineAsyncComponent, flushPromises, vitest] +--- + +# Use flushPromises for Testing Async Components + +**Impact: HIGH** - When testing async components created with `defineAsyncComponent`, you must use `await flushPromises()` to ensure the component has loaded before making assertions. Vue updates asynchronously, so tests that don't account for this will make assertions before the component has rendered. + +## Task Checklist + +- [ ] Use `async/await` in test functions for async components +- [ ] Call `await flushPromises()` after mounting async components +- [ ] Test loading states by making assertions before `flushPromises()` +- [ ] Test error states using rejected promises in `defineAsyncComponent` +- [ ] Use `trigger()` with `await` as it returns a Promise + +**Incorrect:** + +```javascript +import { mount } from '@vue/test-utils' +import { defineAsyncComponent } from 'vue' + +const AsyncWidget = defineAsyncComponent(() => + import('./Widget.vue') +) + +test('renders async component', () => { + const wrapper = mount(AsyncWidget) + + // FAILS: Component hasn't loaded yet + expect(wrapper.text()).toContain('Widget Content') +}) +``` + +**Correct:** + +```javascript +import { mount, flushPromises } from '@vue/test-utils' +import { defineAsyncComponent, nextTick } from 'vue' + +const AsyncWidget = defineAsyncComponent(() => + import('./Widget.vue') +) + +test('renders async component', async () => { + const wrapper = mount(AsyncWidget) + + // Wait for async component to load + await flushPromises() + + expect(wrapper.text()).toContain('Widget Content') +}) + +test('shows loading state initially', async () => { + const AsyncWithLoading = defineAsyncComponent({ + loader: () => import('./Widget.vue'), + loadingComponent: { template: '
Loading...
' }, + delay: 0 + }) + + const wrapper = mount(AsyncWithLoading) + + // Check loading state immediately + expect(wrapper.text()).toContain('Loading...') + + // Wait for component to load + await flushPromises() + + // Check final state + expect(wrapper.text()).toContain('Widget Content') +}) +``` + +## Testing with Suspense + +```javascript +import { mount, flushPromises } from '@vue/test-utils' +import { Suspense, defineAsyncComponent, h } from 'vue' + +const AsyncWidget = defineAsyncComponent(() => + import('./Widget.vue') +) + +test('renders async component with Suspense', async () => { + const wrapper = mount({ + components: { AsyncWidget }, + template: ` + + + + + ` + }) + + // Initially shows fallback + expect(wrapper.text()).toContain('Loading...') + + // Wait for async resolution + await flushPromises() + + // Now shows actual content + expect(wrapper.text()).toContain('Widget Content') +}) +``` + +## Testing Error States + +```javascript +import { mount, flushPromises } from '@vue/test-utils' +import { defineAsyncComponent } from 'vue' + +test('shows error component on load failure', async () => { + const AsyncWithError = defineAsyncComponent({ + loader: () => Promise.reject(new Error('Failed to load')), + errorComponent: { template: '
Error loading component
' } + }) + + const wrapper = mount(AsyncWithError) + + await flushPromises() + + expect(wrapper.text()).toContain('Error loading component') +}) +``` + +## Utilities Reference + +| Utility | Purpose | +|---------|---------| +| `await flushPromises()` | Resolves all pending promises | +| `await nextTick()` | Waits for Vue's next DOM update cycle | +| `await wrapper.trigger('click')` | Triggers event and waits for update | + +## Dynamic Import Handling + +**Note:** Dynamic imports (`import('./File.vue')`) may require additional handling beyond `flushPromises()` in test environments. Test runners like Vitest handle module resolution differently than runtime bundlers, which can cause timing issues with dynamic imports. If `flushPromises()` alone doesn't resolve the component, consider: + +- Mocking the dynamic import to return the component synchronously +- Using multiple `await flushPromises()` calls in sequence +- Wrapping assertions in `waitFor()` or retry utilities +- Configuring your test runner's module resolution settings + +```javascript +// If flushPromises() isn't sufficient, mock the import +vi.mock('./Widget.vue', () => ({ + default: { template: '
Widget Content
' } +})) + +// Or use multiple flush calls for nested async operations +await flushPromises() +await flushPromises() +``` + +## References + +- [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense) +- [Vue.js Async Components Documentation](https://vuejs.org/guide/components/async) diff --git a/.agents/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md b/.agents/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md new file mode 100644 index 0000000..887836f --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md @@ -0,0 +1,158 @@ +--- +title: Teleported Content Requires Special Testing Approach +impact: MEDIUM +impactDescription: Vue Test Utils cannot find teleported content using standard wrapper.find() methods +type: gotcha +tags: [vue3, teleport, testing, vue-test-utils] +--- + +# Teleported Content Requires Special Testing Approach + +**Impact: MEDIUM** - Vue Test Utils scopes queries to the mounted component. Teleported content renders outside the component's DOM tree, so `wrapper.find()` cannot locate it. This leads to failing tests and confusion. + +## Task Checklist + +- [ ] Stub Teleport in unit tests to keep content in component tree +- [ ] Use `document.body` queries for integration tests with real Teleport +- [ ] Consider using `getComponent()` instead of DOM queries for teleported components + +**Problem - Standard Testing Fails:** +```vue + + +``` + +```ts +// Modal.spec.ts - BROKEN +import { mount } from '@vue/test-utils' +import Modal from './Modal.vue' + +test('modal input exists', async () => { + const wrapper = mount(Modal) + await wrapper.find('button').trigger('click') + + // FAILS: Teleported content is not in wrapper's DOM tree + expect(wrapper.find('[data-testid="modal-input"]').exists()).toBe(true) +}) +``` + +**Solution 1 - Stub Teleport:** +```ts +import { mount } from '@vue/test-utils' +import Modal from './Modal.vue' + +test('modal input exists', async () => { + const wrapper = mount(Modal, { + global: { + stubs: { + // Stub teleport to render content inline + Teleport: true + } + } + }) + + await wrapper.find('button').trigger('click') + + // Works: Content renders inside wrapper + expect(wrapper.find('[data-testid="modal-input"]').exists()).toBe(true) +}) +``` + +**Solution 2 - Query Document Body:** +```ts +import { mount } from '@vue/test-utils' +import Modal from './Modal.vue' + +test('modal renders to body', async () => { + const wrapper = mount(Modal, { + attachTo: document.body // Required for Teleport to work + }) + + await wrapper.find('button').trigger('click') + + // Query the actual DOM + const modal = document.querySelector('[data-testid="modal"]') + expect(modal).toBeTruthy() + + const input = document.querySelector('[data-testid="modal-input"]') + expect(input).toBeTruthy() + + // Cleanup + wrapper.unmount() +}) +``` + +**Solution 3 - Custom Teleport Stub with Content Access:** +```ts +import { mount, config } from '@vue/test-utils' +import { h, Teleport } from 'vue' +import Modal from './Modal.vue' + +// Custom stub that renders content in a testable way +const TeleportStub = { + setup(props, { slots }) { + return () => h('div', { class: 'teleport-stub' }, slots.default?.()) + } +} + +test('modal with custom stub', async () => { + const wrapper = mount(Modal, { + global: { + stubs: { + Teleport: TeleportStub + } + } + }) + + await wrapper.find('button').trigger('click') + + // Content is inside .teleport-stub + expect(wrapper.find('.teleport-stub [data-testid="modal-input"]').exists()).toBe(true) +}) +``` + +## Testing Vue Final Modal and UI Libraries + +Libraries like Vue Final Modal use Teleport internally, causing test failures: + +```ts +// Problem: Vue Final Modal teleports to body +import { VueFinalModal } from 'vue-final-modal' + +test('modal content', async () => { + const wrapper = mount(MyComponent, { + global: { + stubs: { + // Stub the modal component to avoid teleport issues + VueFinalModal: true + } + } + }) +}) +``` + +## E2E Testing (Cypress, Playwright) + +E2E tests query the real DOM, so Teleport works naturally: + +```ts +// Cypress +it('opens modal', () => { + cy.visit('/page-with-modal') + cy.get('button').click() + + // Works: Cypress queries the real DOM + cy.get('[data-testid="modal"]').should('be.visible') +}) +``` + +## Reference +- [Vue Test Utils - Teleport](https://test-utils.vuejs.org/guide/advanced/teleport) +- [Vue Test Utils - Stubs](https://test-utils.vuejs.org/guide/advanced/stubs-shallow-mount) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md b/.agents/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md new file mode 100644 index 0000000..597d9c8 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md @@ -0,0 +1,175 @@ +--- +title: Properly Handle Async Updates with nextTick and flushPromises +impact: HIGH +impactDescription: Race conditions and flaky tests occur when async DOM updates or API calls complete after assertions run +type: gotcha +tags: [vue3, testing, async, flushPromises, nextTick, vitest, vue-test-utils, race-condition] +--- + +# Properly Handle Async Updates with nextTick and flushPromises + +**Impact: HIGH** - Vue updates the DOM asynchronously. Without properly awaiting these updates, tests may assert against stale DOM state, causing intermittent failures and false negatives. + +Use `await` with triggers and `setValue`, use `nextTick` for reactive updates, and use `flushPromises` for external async operations like API calls. + +## Task Checklist + +- [ ] Always await `trigger()` and `setValue()` calls +- [ ] Use `await nextTick()` after programmatic reactive state changes +- [ ] Use `await flushPromises()` for external async operations (API calls, timers) +- [ ] Don't chain multiple `nextTick` calls - use `flushPromises` instead +- [ ] Consider using `waitFor` from testing-library for polling assertions + +**Incorrect:** +```javascript +import { mount } from '@vue/test-utils' +import SearchComponent from './SearchComponent.vue' + +// BAD: Not awaiting trigger - assertion runs before DOM updates +test('search filters results', () => { + const wrapper = mount(SearchComponent) + + wrapper.find('input').setValue('vue') // Missing await! + wrapper.find('button').trigger('click') // Missing await! + + // This assertion likely fails - DOM hasn't updated yet + expect(wrapper.findAll('.result').length).toBe(3) +}) + +// BAD: Using nextTick for API calls +test('loads data from API', async () => { + const wrapper = mount(DataLoader) + + await nextTick() // This won't wait for the API call! + + // Assertion runs before fetch completes + expect(wrapper.find('.data').text()).toBe('Loaded data') +}) +``` + +**Correct:** +```javascript +import { mount, flushPromises } from '@vue/test-utils' +import { nextTick } from 'vue' +import SearchComponent from './SearchComponent.vue' +import DataLoader from './DataLoader.vue' + +// CORRECT: Await trigger and setValue +test('search filters results', async () => { + const wrapper = mount(SearchComponent) + + await wrapper.find('input').setValue('vue') + await wrapper.find('button').trigger('click') + + expect(wrapper.findAll('.result').length).toBe(3) +}) + +// CORRECT: Use flushPromises for API calls +test('loads data from API', async () => { + const wrapper = mount(DataLoader) + + // Wait for all pending promises to resolve + await flushPromises() + + expect(wrapper.find('.data').text()).toBe('Loaded data') +}) +``` + +## When to Use Each Method + +### `await trigger()` / `await setValue()` - User Interactions +```javascript +// These methods return nextTick internally +await wrapper.find('button').trigger('click') +await wrapper.find('input').setValue('new value') +await wrapper.find('form').trigger('submit') +``` + +### `await nextTick()` - Programmatic Reactive Updates +```javascript +import { nextTick } from 'vue' + +test('reflects programmatic state changes', async () => { + const wrapper = mount(Counter) + + // Direct state modification (when testing with exposed internals) + wrapper.vm.count = 5 + + await nextTick() // Wait for Vue to update DOM + + expect(wrapper.find('.count').text()).toBe('5') +}) +``` + +### `await flushPromises()` - External Async Operations +```javascript +import { flushPromises } from '@vue/test-utils' + +test('displays fetched data', async () => { + const wrapper = mount(UserProfile, { + props: { userId: 1 } + }) + + // Wait for component's API call to complete + await flushPromises() + + expect(wrapper.find('.username').text()).toBe('John') +}) + +// Sometimes you need multiple flushPromises for chained async operations +test('processes data after fetch', async () => { + const wrapper = mount(DataProcessor) + + await flushPromises() // Wait for fetch + await flushPromises() // Wait for processing triggered by fetch + + expect(wrapper.find('.processed').exists()).toBe(true) +}) +``` + +## Common Pattern: Combining Methods +```javascript +test('submits form and shows success', async () => { + const wrapper = mount(ContactForm) + + // Fill form (awaiting each interaction) + await wrapper.find('#name').setValue('John') + await wrapper.find('#email').setValue('john@example.com') + + // Submit form + await wrapper.find('form').trigger('submit') + + // Wait for API submission to complete + await flushPromises() + + // Assert success state + expect(wrapper.find('.success-message').exists()).toBe(true) +}) +``` + +## Testing with MSW or Mock APIs +```javascript +import { flushPromises } from '@vue/test-utils' +import { rest } from 'msw' +import { setupServer } from 'msw/node' + +const server = setupServer( + rest.get('/api/user', (req, res, ctx) => { + return res(ctx.json({ name: 'John' })) + }) +) + +test('displays user data', async () => { + const wrapper = mount(UserCard) + + // MSW might require multiple flushPromises + await flushPromises() + await flushPromises() + + expect(wrapper.find('.name').text()).toBe('John') +}) +``` + +## Reference +- [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense) +- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md b/.agents/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md new file mode 100644 index 0000000..a112a42 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md @@ -0,0 +1,208 @@ +--- +title: Choose Browser-Based Runner for Style and DOM Event Testing +impact: MEDIUM +impactDescription: Node-based runners cannot test real CSS behavior, native DOM events, cookies, or computed styles +type: capability +tags: [vue3, testing, component-testing, vitest, browser, jsdom] +--- + +# Choose Browser-Based Runner for Style and DOM Event Testing + +**Impact: MEDIUM** - Node-based test runners (Vitest with jsdom/happy-dom) simulate the DOM but cannot test real CSS rendering, native browser events, cookies, computed styles, or cross-browser behavior. Use browser-based runners when these matter. + +Use Vitest for most component tests (fast), but use Vitest Browser Mode when testing visual/DOM-dependent features. + +## Task Checklist + +- [ ] Use Vitest (node) for logic-focused component tests +- [ ] Use Vitest Browser Mode for style-dependent tests +- [ ] Use Vitest Browser Mode for native events (focus, drag, resize) +- [ ] Use Vitest Browser Mode for cookies and computed CSS styles +- [ ] Accept slower speed tradeoff for browser accuracy + +## When to Use Each Approach + +### Node-Based Runner (Vitest + happy-dom/jsdom) +Best for: +- Pure logic testing +- State management +- Event emission +- Props/slots behavior +- Most component interactions +- Fast CI/CD pipelines + +```javascript +// vitest.config.js +export default defineConfig({ + test: { + environment: 'happy-dom', // or 'jsdom' + } +}) +``` + +```javascript +// Fast but limited - fine for most tests +test('button emits click event', async () => { + const wrapper = mount(Button) + await wrapper.trigger('click') + expect(wrapper.emitted('click')).toBeTruthy() +}) +``` + +### Vitest Browser Mode +Required for: +- CSS computed styles verification +- CSS transitions/animations +- Real focus/blur behavior +- Drag and drop +- Cookie operations +- Viewport-dependent behavior +- Cross-browser validation + +## Vitest Browser Mode Setup + +```bash +npm install -D @vitest/browser playwright +``` + +```javascript +// vitest.config.js +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + browser: { + enabled: true, + name: 'chromium', + provider: 'playwright', + }, + }, +}) +``` + +```javascript +// Button.browser.test.js +import { render } from 'vitest-browser-vue' +import Button from './Button.vue' + +test('has correct hover styling', async () => { + const { getByRole } = render(Button, { props: { label: 'Click me' } }) + + const button = getByRole('button') + + // Check initial style + await expect.element(button).toHaveStyle({ + backgroundColor: 'rgb(59, 130, 246)' // blue + }) +}) + +test('maintains focus after click', async () => { + const { getByRole } = render(Button) + + const button = getByRole('button') + await button.click() + + await expect.element(button).toHaveFocus() +}) +``` + +## Examples: What Each Runner Can/Cannot Test + +### Styles - Browser Required +```javascript +// Node runner: CANNOT verify actual CSS +test('danger button has red background', () => { + const wrapper = mount(Button, { props: { variant: 'danger' } }) + // This only checks class exists, not actual color + expect(wrapper.classes()).toContain('bg-red-500') +}) + +// Vitest Browser Mode: CAN verify computed styles +test('danger button renders red', async () => { + const { getByRole } = render(Button, { props: { variant: 'danger' } }) + await expect.element(getByRole('button')).toHaveStyle({ + backgroundColor: 'rgb(239, 68, 68)' + }) +}) +``` + +### Computed CSS Styles - Browser Required +```javascript +// Node runner: CANNOT get real computed styles +test('button has correct padding', () => { + const wrapper = mount(Button) + // getComputedStyle returns empty/default values in jsdom + const style = window.getComputedStyle(wrapper.element) + // style.padding will be empty string, not actual computed value +}) + +// Vitest Browser Mode: Real computed styles +test('button has correct padding', async () => { + const { getByRole } = render(Button) + const button = getByRole('button') + + await expect.element(button).toHaveStyle({ + padding: '12px 24px' + }) +}) +``` + +### Native Events - Browser Required +```javascript +// Node runner: Synthetic events only +test('handles drag and drop', async () => { + const wrapper = mount(DraggableList) + // trigger('dragstart') is synthetic - may not work as expected + await wrapper.find('.item').trigger('dragstart') +}) + +// Vitest Browser Mode: Real native events via userEvent +import { userEvent } from '@vitest/browser/context' + +test('reorders items on drag', async () => { + const { getByTestId } = render(DraggableList) + + const item = getByTestId('item-1') + const target = getByTestId('item-3') + + await userEvent.dragAndDrop(item, target) + + // Assert reordering +}) +``` + +## Recommended Testing Strategy + +```javascript +// vitest.config.js - Separate test configurations + +export default defineConfig({ + test: { + // Default: Node environment for speed + environment: 'happy-dom', + + // Browser tests in separate directory + include: ['src/**/*.test.{js,ts}'], + }, +}) + +// Run browser tests separately +// npx vitest --browser.enabled +``` + +### Directory Structure +``` +tests/ +├── unit/ # Fast node-based tests +│ ├── Button.test.js +│ └── useCounter.test.js +├── component/ # Slower browser-based tests +│ ├── Button.browser.test.js +│ └── DragDrop.browser.test.js +└── e2e/ # Full E2E tests (Playwright) + └── user-flow.spec.ts +``` + +## Reference +- [Vue.js Testing - Component Testing](https://vuejs.org/guide/scaling-up/testing#component-testing) +- [Vitest Browser Mode](https://vitest.dev/guide/browser.html) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md b/.agents/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md new file mode 100644 index 0000000..e3a869c --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md @@ -0,0 +1,144 @@ +--- +title: Test Components Using Blackbox Approach - Focus on Behavior Not Implementation +impact: HIGH +impactDescription: Implementation-aware tests become brittle and break during refactoring, leading to high maintenance burden +type: best-practice +tags: [vue3, testing, component-testing, vitest, vue-test-utils, blackbox] +--- + +# Test Components Using Blackbox Approach - Focus on Behavior Not Implementation + +**Impact: HIGH** - Tests that rely on implementation details (internal state, private methods, component structure) break during refactoring even when functionality remains correct. This leads to false negatives and high test maintenance burden. + +Follow Kent C. Dodds' testing philosophy: "The more your tests resemble how your software is used, the more confidence they can give you." + +## Task Checklist + +- [ ] Test what the component does, not how it does it +- [ ] Query elements by user-visible attributes (text, role, testid) +- [ ] Simulate user interactions (click, type) rather than calling methods directly +- [ ] Assert on rendered output, emitted events, and visible state changes +- [ ] Avoid accessing component internal state or private methods +- [ ] Use data-testid attributes for elements without semantic meaning + +**Incorrect:** +```javascript +import { mount } from '@vue/test-utils' +import Counter from './Counter.vue' + +// BAD: Testing implementation details +test('counter increments', async () => { + const wrapper = mount(Counter) + + // Accessing internal state directly + expect(wrapper.vm.count).toBe(0) + + // Calling internal method instead of simulating user action + wrapper.vm.increment() + + // Checking internal state instead of visible output + expect(wrapper.vm.count).toBe(1) +}) + +// BAD: Testing component structure +test('has increment button', () => { + const wrapper = mount(Counter) + + // Testing implementation detail - what if button becomes an anchor? + expect(wrapper.find('button').exists()).toBe(true) +}) +``` + +**Correct:** +```javascript +import { mount } from '@vue/test-utils' +import Counter from './Counter.vue' + +// CORRECT: Testing behavior like a user would +test('counter displays updated value after clicking increment', async () => { + const wrapper = mount(Counter, { + props: { max: 10 } + }) + + // Assert initial visible state + expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('0') + + // Simulate user action + await wrapper.find('[data-testid="increment-button"]').trigger('click') + + // Assert visible result + expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('1') +}) + +// CORRECT: Testing emitted events (public API) +test('emits change event with new value when incremented', async () => { + const wrapper = mount(Counter) + + await wrapper.find('[data-testid="increment-button"]').trigger('click') + + expect(wrapper.emitted('change')).toHaveLength(1) + expect(wrapper.emitted('change')[0]).toEqual([1]) +}) +``` + +## Using @testing-library/vue for Better Blackbox Tests + +```javascript +import { render, screen, fireEvent } from '@testing-library/vue' +import Counter from './Counter.vue' + +// Testing Library encourages accessible, user-centric queries +test('increments counter on button click', async () => { + render(Counter) + + // Query by role - how screen readers see it + const button = screen.getByRole('button', { name: /increment/i }) + const display = screen.getByText('0') + + await fireEvent.click(button) + + expect(screen.getByText('1')).toBeInTheDocument() +}) +``` + +## What to Test vs What Not to Test + +### DO Test (Public Interface) +```javascript +// Props affect rendered output +test('shows title from props', () => { + const wrapper = mount(Card, { + props: { title: 'Hello World' } + }) + expect(wrapper.text()).toContain('Hello World') +}) + +// Slots render correctly +test('renders slot content', () => { + const wrapper = mount(Card, { + slots: { default: '

Slot content

' } + }) + expect(wrapper.text()).toContain('Slot content') +}) + +// Emitted events +test('emits close event when X clicked', async () => { + const wrapper = mount(Modal) + await wrapper.find('[data-testid="close-button"]').trigger('click') + expect(wrapper.emitted('close')).toBeTruthy() +}) +``` + +### DON'T Test (Implementation Details) +```javascript +// Don't test internal computed properties +// Don't test internal methods +// Don't test component options/setup internals +// Don't test that specific child components are rendered (unless critical) +// Don't rely exclusively on snapshot tests for correctness +``` + +## Reference +- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing) +- [Vue Test Utils - Testing Philosophy](https://test-utils.vuejs.org/guide/) +- [Testing Library Guiding Principles](https://testing-library.com/docs/guiding-principles) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md b/.agents/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md new file mode 100644 index 0000000..f6e4f07 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md @@ -0,0 +1,238 @@ +--- +title: Test Complex Composables with Host Component Wrapper +impact: MEDIUM +impactDescription: Composables using lifecycle hooks or provide/inject fail when tested directly without a component context +type: capability +tags: [vue3, testing, composables, vitest, lifecycle-hooks, provide-inject] +--- + +# Test Complex Composables with Host Component Wrapper + +**Impact: MEDIUM** - Composables that use Vue lifecycle hooks (`onMounted`, `onUnmounted`) or dependency injection (`inject`) require a component context to function. Testing them directly will cause errors or incorrect behavior. + +Simple composables using only reactivity APIs can be tested directly. Complex composables need a helper function that creates a host component context. + +## Task Checklist + +- [ ] Identify if composable uses lifecycle hooks or inject +- [ ] For simple composables (refs, computed only): test directly +- [ ] For complex composables: use `withSetup` helper pattern +- [ ] Clean up by unmounting the test app after each test +- [ ] Use `app.provide()` to mock injected dependencies + +**Simple Composable - Test Directly:** +```javascript +// composables/useCounter.js +import { ref, computed } from 'vue' + +export function useCounter(initialValue = 0) { + const count = ref(initialValue) + const doubled = computed(() => count.value * 2) + const increment = () => count.value++ + + return { count, doubled, increment } +} +``` + +```javascript +// useCounter.test.js +import { describe, it, expect } from 'vitest' +import { useCounter } from './useCounter' + +// CORRECT: Simple composable can be tested directly +describe('useCounter', () => { + it('initializes with default value', () => { + const { count } = useCounter() + expect(count.value).toBe(0) + }) + + it('increments count', () => { + const { count, increment } = useCounter() + increment() + expect(count.value).toBe(1) + }) + + it('computes doubled value', () => { + const { count, doubled, increment } = useCounter(5) + expect(doubled.value).toBe(10) + increment() + expect(doubled.value).toBe(12) + }) +}) +``` + +**Complex Composable - Use Host Wrapper:** +```javascript +// composables/useFetch.js +import { ref, onMounted, onUnmounted, inject } from 'vue' + +export function useFetch(url) { + const data = ref(null) + const error = ref(null) + const loading = ref(true) + let controller = null + + // Uses inject - needs component context + const apiClient = inject('apiClient') + + // Uses lifecycle hooks - needs component context + onMounted(async () => { + controller = new AbortController() + try { + const response = await apiClient.get(url, { signal: controller.signal }) + data.value = response.data + } catch (e) { + if (e.name !== 'AbortError') error.value = e + } finally { + loading.value = false + } + }) + + onUnmounted(() => { + controller?.abort() + }) + + return { data, error, loading } +} +``` + +```javascript +// test-utils.js +import { createApp } from 'vue' + +/** + * Helper to test composables that need component context + */ +export function withSetup(composable) { + let result + + const app = createApp({ + setup() { + result = composable() + // Return a render function to suppress warnings + return () => {} + } + }) + + app.mount(document.createElement('div')) + + return [result, app] +} +``` + +```javascript +// useFetch.test.js +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { flushPromises } from '@vue/test-utils' +import { withSetup } from './test-utils' +import { useFetch } from './useFetch' + +describe('useFetch', () => { + let app + const mockApiClient = { + get: vi.fn() + } + + afterEach(() => { + // IMPORTANT: Clean up to trigger onUnmounted + app?.unmount() + }) + + it('fetches data on mount', async () => { + mockApiClient.get.mockResolvedValue({ data: { id: 1, name: 'Test' } }) + + const [result, testApp] = withSetup(() => useFetch('/api/test')) + app = testApp + + // Provide mocked dependency + app.provide('apiClient', mockApiClient) + + // Wait for async operations + await flushPromises() + + expect(result.data.value).toEqual({ id: 1, name: 'Test' }) + expect(result.loading.value).toBe(false) + expect(result.error.value).toBeNull() + }) + + it('handles errors', async () => { + const testError = new Error('Network error') + mockApiClient.get.mockRejectedValue(testError) + + const [result, testApp] = withSetup(() => useFetch('/api/test')) + app = testApp + app.provide('apiClient', mockApiClient) + + await flushPromises() + + expect(result.error.value).toBe(testError) + expect(result.data.value).toBeNull() + }) +}) +``` + +## Enhanced withSetup Helper with Provide Support +```javascript +// test-utils.js +export function withSetup(composable, options = {}) { + let result + + const app = createApp({ + setup() { + result = composable() + return () => {} + } + }) + + // Apply global provides before mounting + if (options.provide) { + Object.entries(options.provide).forEach(([key, value]) => { + app.provide(key, value) + }) + } + + app.mount(document.createElement('div')) + + return [result, app] +} + +// Usage +const [result, app] = withSetup(() => useMyComposable(), { + provide: { + apiClient: mockApiClient, + currentUser: { id: 1, name: 'Test User' } + } +}) +``` + +## Testing with @vue/test-utils mount +```javascript +import { mount } from '@vue/test-utils' +import { defineComponent } from 'vue' +import { useFetch } from './useFetch' + +test('useFetch in component context', async () => { + const TestComponent = defineComponent({ + setup() { + const { data, loading } = useFetch('/api/users') + return { data, loading } + }, + template: '
{{ loading ? "Loading..." : data }}
' + }) + + const wrapper = mount(TestComponent, { + global: { + provide: { + apiClient: mockApiClient + } + } + }) + + await flushPromises() + expect(wrapper.text()).toContain('Test data') +}) +``` + +## Reference +- [Vue.js Testing Guide - Testing Composables](https://vuejs.org/guide/scaling-up/testing#testing-composables) +- [Vue Test Utils - Mounting Components](https://test-utils.vuejs.org/guide/) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md b/.agents/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md new file mode 100644 index 0000000..3df2ff0 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md @@ -0,0 +1,242 @@ +--- +title: Use Playwright for E2E Testing - Cross-Browser Support and Better DX +impact: MEDIUM +impactDescription: Cypress has browser limitations and some features require paid subscriptions +type: best-practice +tags: [vue3, testing, e2e, playwright, cypress, end-to-end] +--- + +# Use Playwright for E2E Testing - Cross-Browser Support and Better DX + +**Impact: MEDIUM** - Playwright offers superior cross-browser testing (Chromium, WebKit, Firefox), excellent debugging tools, and is fully open source. Cypress has limitations with WebKit support and requires paid subscriptions for some features. + +Use Playwright for new E2E testing setups. Consider Cypress if team already has expertise or for its visual debugging UI. + +## Task Checklist + +- [ ] Install Playwright with browsers for your target platforms +- [ ] Configure for Vue dev server integration +- [ ] Set up projects for different browsers +- [ ] Use locator strategies that match component test patterns +- [ ] Configure CI for parallel test execution +- [ ] Use trace and screenshot features for debugging + +## Quick Setup + +```bash +# Install Playwright +npm init playwright@latest + +# This will create: +# - playwright.config.ts +# - tests/ directory +# - tests-examples/ directory +``` + +**playwright.config.ts:** +```typescript +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + + use: { + // Base URL for navigation + baseURL: 'http://localhost:5173', + // Capture trace on first retry + trace: 'on-first-retry', + // Screenshot on failure + screenshot: 'only-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + // Mobile viewports + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + ], + + // Run local dev server before tests + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + }, +}) +``` + +## E2E Test Example + +```typescript +// e2e/user-flow.spec.ts +import { test, expect } from '@playwright/test' + +test.describe('User Authentication', () => { + test('user can log in and see dashboard', async ({ page }) => { + // Navigate to login + await page.goto('/login') + + // Fill login form + await page.getByLabel('Email').fill('user@example.com') + await page.getByLabel('Password').fill('password123') + await page.getByRole('button', { name: 'Sign In' }).click() + + // Verify redirect to dashboard + await expect(page).toHaveURL('/dashboard') + await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible() + }) + + test('shows error for invalid credentials', async ({ page }) => { + await page.goto('/login') + + await page.getByLabel('Email').fill('wrong@example.com') + await page.getByLabel('Password').fill('wrongpassword') + await page.getByRole('button', { name: 'Sign In' }).click() + + await expect(page.getByRole('alert')).toContainText('Invalid credentials') + await expect(page).toHaveURL('/login') + }) +}) +``` + +## Playwright vs Cypress Comparison + +| Feature | Playwright | Cypress | +|---------|------------|---------| +| Browsers | Chromium, Firefox, WebKit | Chromium, Firefox, Electron (WebKit experimental) | +| Cross-browser | Full support | Limited | +| Parallelization | Built-in | Requires Cypress Cloud | +| Open source | Fully | Core only | +| Mobile testing | Device emulation | Limited | +| Debugging | Inspector, trace viewer | Time-travel UI | +| API testing | Built-in | Plugin required | +| Iframes | Full support | Limited | + +## Testing Vue Components with Data-Testid + +```typescript +// e2e/product-list.spec.ts +import { test, expect } from '@playwright/test' + +test('user can add product to cart', async ({ page }) => { + await page.goto('/products') + + // Use data-testid for reliable selectors + await page.getByTestId('product-card').first().click() + + // Verify product detail page + await expect(page.getByTestId('product-title')).toBeVisible() + + // Add to cart + await page.getByTestId('add-to-cart-button').click() + + // Verify cart updated + await expect(page.getByTestId('cart-count')).toHaveText('1') +}) +``` + +## Page Object Pattern for Vue Apps + +```typescript +// e2e/pages/LoginPage.ts +import { Page, Locator } from '@playwright/test' + +export class LoginPage { + readonly page: Page + readonly emailInput: Locator + readonly passwordInput: Locator + readonly submitButton: Locator + readonly errorMessage: Locator + + constructor(page: Page) { + this.page = page + this.emailInput = page.getByLabel('Email') + this.passwordInput = page.getByLabel('Password') + this.submitButton = page.getByRole('button', { name: 'Sign In' }) + this.errorMessage = page.getByRole('alert') + } + + async goto() { + await this.page.goto('/login') + } + + async login(email: string, password: string) { + await this.emailInput.fill(email) + await this.passwordInput.fill(password) + await this.submitButton.click() + } +} +``` + +```typescript +// e2e/auth.spec.ts +import { test, expect } from '@playwright/test' +import { LoginPage } from './pages/LoginPage' + +test('successful login', async ({ page }) => { + const loginPage = new LoginPage(page) + await loginPage.goto() + await loginPage.login('user@example.com', 'password123') + + await expect(page).toHaveURL('/dashboard') +}) +``` + +## Visual Regression Testing + +```typescript +test('homepage visual regression', async ({ page }) => { + await page.goto('/') + + // Full page screenshot comparison + await expect(page).toHaveScreenshot('homepage.png') + + // Element-specific screenshot + await expect(page.getByTestId('hero-section')).toHaveScreenshot('hero.png') +}) +``` + +## Running Tests + +```bash +# Run all tests +npx playwright test + +# Run in headed mode (see browser) +npx playwright test --headed + +# Run specific file +npx playwright test e2e/auth.spec.ts + +# Run in specific browser +npx playwright test --project=chromium + +# Debug mode +npx playwright test --debug + +# Generate test from actions +npx playwright codegen localhost:5173 +``` + +## Reference +- [Playwright Documentation](https://playwright.dev/) +- [Vue.js E2E Testing Recommendations](https://vuejs.org/guide/scaling-up/testing#e2e-testing) +- [Playwright Best Practices](https://playwright.dev/docs/best-practices) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md b/.agents/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md new file mode 100644 index 0000000..e44f437 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md @@ -0,0 +1,197 @@ +--- +title: Avoid Snapshot-Only Tests - They Don't Prove Correctness +impact: MEDIUM +impactDescription: Snapshot tests verify structure but not functionality, leading to false confidence and brittle tests +type: best-practice +tags: [vue3, testing, snapshot, vitest, vue-test-utils, anti-pattern] +--- + +# Avoid Snapshot-Only Tests - They Don't Prove Correctness + +**Impact: MEDIUM** - Snapshot tests only verify that HTML structure hasn't changed - they don't verify that the component works correctly. Relying exclusively on snapshots leads to false confidence and tests that break on any refactoring, even when functionality is preserved. + +Use snapshots sparingly for regression detection. Prefer behavioral assertions that test what the component does. + +## Task Checklist + +- [ ] Don't use snapshots as the only assertion for component behavior +- [ ] Use snapshots for regression detection on stable UI components +- [ ] Always pair snapshots with behavioral assertions +- [ ] Keep snapshots small and focused (avoid full component snapshots) +- [ ] Review snapshot diffs carefully - don't blindly update +- [ ] Consider inline snapshots for small, critical structures + +**Incorrect:** +```javascript +import { mount } from '@vue/test-utils' +import UserCard from './UserCard.vue' + +// BAD: Snapshot-only test proves nothing about functionality +test('UserCard renders correctly', () => { + const wrapper = mount(UserCard, { + props: { user: { name: 'John', email: 'john@example.com' } } + }) + + expect(wrapper.html()).toMatchSnapshot() +}) + +// This test passes even if: +// - The email isn't clickable +// - The avatar doesn't load +// - User actions are completely broken +// - Accessibility is broken +``` + +**Correct:** +```javascript +import { mount } from '@vue/test-utils' +import UserCard from './UserCard.vue' + +// CORRECT: Test actual behavior +test('UserCard displays user information', () => { + const wrapper = mount(UserCard, { + props: { user: { name: 'John', email: 'john@example.com' } } + }) + + expect(wrapper.find('[data-testid="user-name"]').text()).toBe('John') + expect(wrapper.find('[data-testid="user-email"]').text()).toBe('john@example.com') +}) + +test('UserCard email link is clickable', async () => { + const wrapper = mount(UserCard, { + props: { user: { name: 'John', email: 'john@example.com' } } + }) + + const emailLink = wrapper.find('a[href^="mailto:"]') + expect(emailLink.exists()).toBe(true) + expect(emailLink.attributes('href')).toBe('mailto:john@example.com') +}) + +test('UserCard emits select event when clicked', async () => { + const wrapper = mount(UserCard, { + props: { user: { id: 1, name: 'John' } } + }) + + await wrapper.trigger('click') + + expect(wrapper.emitted('select')).toBeTruthy() + expect(wrapper.emitted('select')[0]).toEqual([{ id: 1, name: 'John' }]) +}) +``` + +## When Snapshots ARE Useful + +### Regression Detection for Stable Components +```javascript +// ACCEPTABLE: Snapshot as additional check, not the only check +test('ErrorBoundary renders error message', () => { + const wrapper = mount(ErrorBoundary, { + props: { error: new Error('Something went wrong') } + }) + + // Primary assertions - verify behavior + expect(wrapper.find('.error-title').text()).toBe('Error') + expect(wrapper.find('.error-message').text()).toContain('Something went wrong') + + // Secondary snapshot - catches unexpected structural changes + expect(wrapper.find('.error-container').html()).toMatchSnapshot() +}) +``` + +### Inline Snapshots for Small Structures +```javascript +// ACCEPTABLE: Inline snapshot for small, critical structure +test('generates correct list markup', () => { + const wrapper = mount(ListItem, { props: { item: 'Test' } }) + + expect(wrapper.html()).toMatchInlineSnapshot(` + "
  • Test
  • " + `) +}) +``` + +### Complex SVG or Icon Output +```javascript +// ACCEPTABLE: Snapshot for complex generated content +test('renders correct chart SVG', () => { + const wrapper = mount(PieChart, { + props: { data: [30, 40, 30] } + }) + + // Verify key behavior + expect(wrapper.findAll('path').length).toBe(3) + + // Snapshot for full SVG structure + expect(wrapper.find('svg').html()).toMatchSnapshot() +}) +``` + +## Better Alternatives to Snapshots + +### Test Specific Elements +```javascript +// Instead of snapshotting entire component +test('renders product with all required fields', () => { + const wrapper = mount(ProductCard, { + props: { product: { name: 'Widget', price: 9.99, inStock: true } } + }) + + expect(wrapper.find('.product-name').text()).toBe('Widget') + expect(wrapper.find('.product-price').text()).toContain('9.99') + expect(wrapper.find('.in-stock-badge').exists()).toBe(true) +}) +``` + +### Test CSS Classes for Styling +```javascript +test('applies danger styling for errors', () => { + const wrapper = mount(Alert, { + props: { type: 'error', message: 'Failed!' } + }) + + expect(wrapper.classes()).toContain('alert-danger') + expect(wrapper.find('.alert-icon').classes()).toContain('icon-error') +}) +``` + +### Use Testing Library Queries +```javascript +import { render, screen } from '@testing-library/vue' + +test('form has accessible labels', () => { + render(LoginForm) + + // Testing Library queries verify accessibility + expect(screen.getByLabelText('Email')).toBeInTheDocument() + expect(screen.getByLabelText('Password')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument() +}) +``` + +## Snapshot Anti-Patterns + +```javascript +// ANTI-PATTERN: Giant component snapshot +test('page renders', () => { + const wrapper = mount(EntirePageComponent) + expect(wrapper.html()).toMatchSnapshot() // 500+ lines of HTML +}) + +// ANTI-PATTERN: Snapshot with dynamic content +test('shows current date', () => { + const wrapper = mount(DateDisplay) + expect(wrapper.html()).toMatchSnapshot() // Fails every day! +}) + +// ANTI-PATTERN: Snapshot after every test +test('button works', async () => { + const wrapper = mount(Counter) + await wrapper.find('button').trigger('click') + expect(wrapper.html()).toMatchSnapshot() // Redundant +}) +``` + +## Reference +- [Vue.js Testing Guide - What Not to Test](https://vuejs.org/guide/scaling-up/testing) +- [Effective Snapshot Testing](https://kentcdodds.com/blog/effective-snapshot-testing) +- [Vitest Snapshot Testing](https://vitest.dev/guide/snapshot.html) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md b/.agents/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md new file mode 100644 index 0000000..3f8d440 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md @@ -0,0 +1,228 @@ +--- +title: Configure Pinia Testing with createTestingPinia and setActivePinia +impact: HIGH +impactDescription: Missing Pinia configuration causes 'injection Symbol(pinia) not found' errors and failing tests +type: gotcha +tags: [vue3, testing, pinia, vitest, store, mocking, createTestingPinia] +--- + +# Configure Pinia Testing with createTestingPinia and setActivePinia + +**Impact: HIGH** - Testing components or composables that use Pinia stores without proper configuration results in "[Vue warn]: injection Symbol(pinia) not found" errors. Tests will fail or behave unexpectedly. + +Use `@pinia/testing` package with `createTestingPinia` for component tests and `setActivePinia(createPinia())` for unit testing stores directly. + +## Task Checklist + +- [ ] Install `@pinia/testing` as a dev dependency +- [ ] Use `createTestingPinia` in component tests with `global.plugins` +- [ ] Use `setActivePinia(createPinia())` in `beforeEach` for store unit tests +- [ ] Configure `createSpy: vi.fn` when NOT using `globals: true` in Vitest +- [ ] Initialize store inside each test to get fresh state +- [ ] Use `stubActions: false` when you need real action execution + +**Incorrect:** +```javascript +import { mount } from '@vue/test-utils' +import UserProfile from './UserProfile.vue' + +// BAD: Missing Pinia - causes injection error +test('displays user name', () => { + const wrapper = mount(UserProfile) // ERROR: injection "Symbol(pinia)" not found + expect(wrapper.text()).toContain('John') +}) +``` + +```javascript +import { useUserStore } from '@/stores/user' + +// BAD: No active Pinia instance +test('user store actions', () => { + const store = useUserStore() // ERROR: no active Pinia + store.login('john', 'password') +}) +``` + +**Correct - Component Testing:** +```javascript +import { mount } from '@vue/test-utils' +import { createTestingPinia } from '@pinia/testing' +import { vi } from 'vitest' +import UserProfile from './UserProfile.vue' +import { useUserStore } from '@/stores/user' + +// CORRECT: Provide testing pinia with stubbed actions +test('displays user name', () => { + const wrapper = mount(UserProfile, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, // Required if not using globals: true + initialState: { + user: { name: 'John', email: 'john@example.com' } + } + }) + ] + } + }) + + expect(wrapper.text()).toContain('John') +}) + +// CORRECT: Test with stubbed actions (default behavior) +test('calls logout action', async () => { + const wrapper = mount(UserProfile, { + global: { + plugins: [createTestingPinia({ createSpy: vi.fn })] + } + }) + + // Get store AFTER mounting with createTestingPinia + const store = useUserStore() + + await wrapper.find('[data-testid="logout"]').trigger('click') + + // Actions are stubbed and wrapped in spies + expect(store.logout).toHaveBeenCalled() +}) +``` + +**Correct - Store Unit Testing:** +```javascript +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useUserStore } from '@/stores/user' + +describe('User Store', () => { + beforeEach(() => { + // Create fresh Pinia instance for each test + setActivePinia(createPinia()) + }) + + it('initializes with empty user', () => { + const store = useUserStore() + expect(store.user).toBeNull() + expect(store.isLoggedIn).toBe(false) + }) + + it('updates user on login', async () => { + const store = useUserStore() + + // Real action executes - not stubbed + await store.login('john', 'password') + + expect(store.user).toEqual({ name: 'John' }) + expect(store.isLoggedIn).toBe(true) + }) + + it('clears user on logout', () => { + const store = useUserStore() + store.user = { name: 'John' } // Set initial state + + store.logout() + + expect(store.user).toBeNull() + }) +}) +``` + +## Testing with Real Actions vs Stubbed Actions + +```javascript +import { createTestingPinia } from '@pinia/testing' + +// Stubbed actions (default) - for isolation +const wrapper = mount(Component, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + // stubActions: true (default) - actions are mocked + }) + ] + } +}) + +// Real actions - for integration testing +const wrapper = mount(Component, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + stubActions: false // Actions execute normally + }) + ] + } +}) +``` + +## Mocking Specific Action Implementations + +```javascript +import { mount } from '@vue/test-utils' +import { createTestingPinia } from '@pinia/testing' +import { vi } from 'vitest' +import { useCartStore } from '@/stores/cart' + +test('handles checkout failure', async () => { + const wrapper = mount(Checkout, { + global: { + plugins: [createTestingPinia({ createSpy: vi.fn })] + } + }) + + const cartStore = useCartStore() + + // Mock specific action behavior + cartStore.checkout.mockRejectedValue(new Error('Payment failed')) + + await wrapper.find('[data-testid="checkout"]').trigger('click') + await flushPromises() + + expect(wrapper.find('.error').text()).toContain('Payment failed') +}) +``` + +## Spying on Actions with vi.spyOn + +```javascript +import { setActivePinia, createPinia } from 'pinia' +import { vi } from 'vitest' +import { useUserStore } from '@/stores/user' + +test('tracks action calls', async () => { + setActivePinia(createPinia()) + const store = useUserStore() + + const loginSpy = vi.spyOn(store, 'login') + loginSpy.mockResolvedValue({ success: true }) + + await store.login('john', 'password') + + expect(loginSpy).toHaveBeenCalledWith('john', 'password') +}) +``` + +## Testing Store $subscribe + +```javascript +import { setActivePinia, createPinia } from 'pinia' +import { useUserStore } from '@/stores/user' + +test('subscription triggers on state change', () => { + setActivePinia(createPinia()) + const store = useUserStore() + + const callback = vi.fn() + store.$subscribe(callback) + + store.user = { name: 'John' } + + expect(callback).toHaveBeenCalled() +}) +``` + +## Reference +- [Pinia Testing Guide](https://pinia.vuejs.org/cookbook/testing.html) +- [@pinia/testing Package](https://www.npmjs.com/package/@pinia/testing) +- [Vue Test Utils - Plugins](https://test-utils.vuejs.org/guide/advanced/plugins.html) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md b/.agents/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md new file mode 100644 index 0000000..f935533 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md @@ -0,0 +1,229 @@ +--- +title: Wrap Async Setup Components in Suspense for Testing +impact: HIGH +impactDescription: Components with async setup() fail to render in tests without Suspense wrapper, causing cryptic errors +type: gotcha +tags: [vue3, testing, suspense, async-setup, vue-test-utils, vitest] +--- + +# Wrap Async Setup Components in Suspense for Testing + +**Impact: HIGH** - Components using `async setup()` require a `` wrapper to function correctly. Testing them without Suspense causes the component to never render, leading to test failures and confusing errors. + +Create a test wrapper component with Suspense or use a `mountSuspense` helper function for testing async components. + +## Task Checklist + +- [ ] Identify components with async setup (uses `await` in ` + + +``` + +### Key Imports + +```ts +// Reactivity +import { ref, shallowRef, computed, reactive, readonly, toRef, toRefs, toValue } from 'vue' + +// Watchers +import { watch, watchEffect, watchPostEffect, onWatcherCleanup } from 'vue' + +// Lifecycle +import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount } from 'vue' + +// Utilities +import { nextTick, defineComponent, defineAsyncComponent } from 'vue' +``` diff --git a/.agents/skills/vue/references/advanced-patterns.md b/.agents/skills/vue/references/advanced-patterns.md new file mode 100644 index 0000000..d30b5e0 --- /dev/null +++ b/.agents/skills/vue/references/advanced-patterns.md @@ -0,0 +1,326 @@ +--- +name: advanced-patterns +description: Vue 3 built-in components (Transition, Teleport, Suspense, KeepAlive) and advanced directives +--- + +# Built-in Components & Directives + +## Transition + +Animate enter/leave of a single element or component. + +```vue + + + +``` + +### CSS Classes + +| Class | When | +|-------|------| +| `{name}-enter-from` | Start state for enter | +| `{name}-enter-active` | Active state for enter (add transitions here) | +| `{name}-enter-to` | End state for enter | +| `{name}-leave-from` | Start state for leave | +| `{name}-leave-active` | Active state for leave | +| `{name}-leave-to` | End state for leave | + +### Transition Modes + +```vue + + + + +``` + +### JavaScript Hooks + +```vue + +
    Content
    +
    + + +``` + +### Appear on Initial Render + +```vue + +
    Shows with animation on mount
    +
    +``` + +## TransitionGroup + +Animate list items. Each child must have a unique `key`. + +```vue + + + +``` + +## Teleport + +Render content to a different DOM location. + +```vue + +``` + +### Props + +```vue + + + + + + + + + + + +``` + +## Suspense + +Handle async dependencies with loading states. **Experimental feature.** + +```vue + +``` + +### Async Dependencies + +Suspense waits for: +- Components with `async setup()` +- Components using top-level `await` in ` +``` + +### Events + +```vue + + ... + +``` + +## KeepAlive + +Cache component instances when toggled. + +```vue + +``` + +### Include/Exclude + +```vue + + + + + + + + + + +``` + +### Lifecycle Hooks + +```ts +import { onActivated, onDeactivated } from 'vue' + +onActivated(() => { + // Called when component is inserted from cache + fetchLatestData() +}) + +onDeactivated(() => { + // Called when component is removed to cache + pauseTimers() +}) +``` + +## v-memo + +Skip re-renders when dependencies unchanged. Use for performance optimization. + +```vue + +``` + +Equivalent to `v-once` when empty: +```vue +
    Never updates
    +``` + +## v-once + +Render once, skip all future updates. + +```vue +Static: {{ neverChanges }} +``` + +## Custom Directives + +Create reusable DOM manipulations. + +```ts +// Directive definition +const vFocus: Directive = { + mounted: (el) => el.focus() +} + +// Full hooks +const vColor: Directive = { + created(el, binding, vnode, prevVnode) {}, + beforeMount(el, binding) {}, + mounted(el, binding) { + el.style.color = binding.value + }, + beforeUpdate(el, binding) {}, + updated(el, binding) { + el.style.color = binding.value + }, + beforeUnmount(el, binding) {}, + unmounted(el, binding) {} +} +``` + +### Directive Arguments & Modifiers + +```vue +
    + + +``` + +### Global Registration + +```ts +// main.ts +app.directive('focus', { + mounted: (el) => el.focus() +}) +``` + +Type global directives by extending the `GlobalDirectives` interface (not `ComponentCustomProperties`): + +```ts +import type { Directive } from 'vue' + +declare module 'vue' { + interface GlobalDirectives { + vFocus: Directive + } +} +``` + + diff --git a/.agents/skills/vue/references/core-new-apis.md b/.agents/skills/vue/references/core-new-apis.md new file mode 100644 index 0000000..4f9dc25 --- /dev/null +++ b/.agents/skills/vue/references/core-new-apis.md @@ -0,0 +1,275 @@ +--- +name: core-new-apis +description: Vue 3 reactivity system, lifecycle hooks, and composable patterns +--- + +# Reactivity, Lifecycle & Composables + +## Reactivity + +### ref vs shallowRef + +```ts +import { ref, shallowRef } from 'vue' + +// ref - deep reactivity (tracks nested changes) +const user = ref({ name: 'John', profile: { age: 30 } }) +user.value.profile.age = 31 // Triggers reactivity + +// shallowRef - only .value assignment triggers reactivity (better performance) +const data = shallowRef({ items: [] }) +data.value.items.push('new') // Does NOT trigger reactivity +data.value = { items: ['new'] } // Triggers reactivity +``` + +**Prefer `shallowRef`** for large data structures or when deep reactivity is unnecessary. + +### computed + +```ts +import { ref, computed } from 'vue' + +const count = ref(0) + +// Read-only computed +const doubled = computed(() => count.value * 2) + +// Writable computed +const plusOne = computed({ + get: () => count.value + 1, + set: (val) => { count.value = val - 1 } +}) +``` + +### reactive & readonly + +```ts +import { reactive, readonly } from 'vue' + +const state = reactive({ count: 0, nested: { value: 1 } }) +state.count++ // Reactive + +const readonlyState = readonly(state) +readonlyState.count++ // Warning, mutation blocked +``` + +Note: `reactive()` loses reactivity on destructuring. Use `ref()` or `toRefs()`. + +### Inspecting reactive values + +```ts +import { isRef, isReactive, isReadonly, isProxy, isShallow, unref, toRaw } from 'vue' + +isShallow(shallowRef(0)) // true, also true for shallowReactive/shallowReadonly +isShallow(ref(0)) // false +``` + +`isShallow()` reports whether a proxy was created with `shallowRef`, `shallowReactive()`, or `shallowReadonly()`. + +## Watchers + +### watch + +```ts +import { ref, watch } from 'vue' + +const count = ref(0) + +// Watch single ref +watch(count, (newVal, oldVal) => { + console.log(`Changed from ${oldVal} to ${newVal}`) +}) + +// Watch getter +watch( + () => props.id, + (id) => fetchData(id), + { immediate: true } +) + +// Watch multiple sources +watch([firstName, lastName], ([first, last]) => { + fullName.value = `${first} ${last}` +}) + +// Deep watch with depth limit (Vue 3.5+) +watch(state, callback, { deep: 2 }) + +// Once (Vue 3.4+) +watch(source, callback, { once: true }) +``` + +### watchEffect + +Runs immediately and auto-tracks dependencies. + +```ts +import { ref, watchEffect, onWatcherCleanup } from 'vue' + +const id = ref(1) + +watchEffect(async () => { + const controller = new AbortController() + + // Cleanup on re-run or unmount (Vue 3.5+) + onWatcherCleanup(() => controller.abort()) + + const res = await fetch(`/api/${id.value}`, { signal: controller.signal }) + data.value = await res.json() +}) + +// Pause/resume (Vue 3.5+) +const { pause, resume, stop } = watchEffect(() => {}) +pause() +resume() +stop() +``` + +### Flush Timing + +```ts +// 'pre' (default) - before component update +// 'post' - after component update (access updated DOM) +// 'sync' - immediate, use with caution + +watch(source, callback, { flush: 'post' }) +watchPostEffect(() => {}) // Alias for flush: 'post' +``` + +## Lifecycle Hooks + +```ts +import { + onBeforeMount, + onMounted, + onBeforeUpdate, + onUpdated, + onBeforeUnmount, + onUnmounted, + onErrorCaptured, + onActivated, // KeepAlive + onDeactivated, // KeepAlive + onServerPrefetch // SSR only +} from 'vue' + +onMounted(() => { + console.log('DOM is ready') +}) + +onUnmounted(() => { + // Cleanup timers, listeners, etc. +}) + +// Error boundary +onErrorCaptured((err, instance, info) => { + console.error(err) + return false // Stop propagation +}) +``` + +## Effect Scope + +Group reactive effects for batch disposal. + +```ts +import { effectScope, onScopeDispose } from 'vue' + +const scope = effectScope() + +scope.run(() => { + const count = ref(0) + const doubled = computed(() => count.value * 2) + + watch(count, () => console.log(count.value)) + + // Cleanup when scope stops + onScopeDispose(() => { + console.log('Scope disposed') + }) +}) + +// Dispose all effects +scope.stop() +``` + +## Composables + +Composables are functions that encapsulate stateful logic using Composition API. + +### Naming Convention + +- Start with `use`: `useMouse`, `useFetch`, `useCounter` + +### Pattern + +```ts +// composables/useMouse.ts +import { ref, onMounted, onUnmounted } from 'vue' + +export function useMouse() { + const x = ref(0) + const y = ref(0) + + const update = (e: MouseEvent) => { + x.value = e.pageX + y.value = e.pageY + } + + onMounted(() => window.addEventListener('mousemove', update)) + onUnmounted(() => window.removeEventListener('mousemove', update)) + + return { x, y } +} +``` + +### Accept Reactive Input + +Use `toValue()` (Vue 3.3+) to normalize refs, getters, or plain values. + +```ts +import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue' + +export function useFetch(url: MaybeRefOrGetter) { + const data = ref(null) + const error = ref(null) + + watchEffect(async () => { + data.value = null + error.value = null + + try { + const res = await fetch(toValue(url)) + data.value = await res.json() + } catch (e) { + error.value = e + } + }) + + return { data, error } +} + +// Usage - all work: +useFetch('/api/users') +useFetch(urlRef) +useFetch(() => `/api/users/${props.id}`) +``` + +### Return Refs (Not Reactive) + +Always return plain object with refs for destructuring compatibility. + +```ts +// Good - preserves reactivity when destructured +return { x, y } + +// Bad - loses reactivity when destructured +return reactive({ x, y }) +``` + + diff --git a/.agents/skills/vue/references/script-setup-macros.md b/.agents/skills/vue/references/script-setup-macros.md new file mode 100644 index 0000000..2f5c690 --- /dev/null +++ b/.agents/skills/vue/references/script-setup-macros.md @@ -0,0 +1,209 @@ +--- +name: script-setup-macros +description: Vue 3 script setup syntax and compiler macros for defining props, emits, models, and more +--- + +# Script Setup & Macros + +` + + +``` + +## defineProps + +Declare component props with full TypeScript support. + +```ts +// Type-based declaration (recommended) +const props = defineProps<{ + title: string + count?: number + items: string[] +}>() + +// Imported/aliased interfaces are supported (relative import, path alias like +// @/types, or a node_modules dependency). Requires TypeScript as a peer dep. +import type { Props } from './types' +const props = defineProps() + +// With defaults (Vue 3.5+) +const { title, count = 0 } = defineProps<{ + title: string + count?: number +}>() + +// With defaults (Vue 3.4 and below) +const props = withDefaults(defineProps<{ + title: string + items?: string[] +}>(), { + items: () => [] // Use factory for arrays/objects +}) +``` + +## defineEmits + +Declare emitted events with typed payloads. + +```ts +// Named tuple syntax (recommended) +const emit = defineEmits<{ + update: [value: string] + change: [id: number, name: string] + close: [] +}>() + +emit('update', 'new value') +emit('change', 1, 'name') +emit('close') +``` + +## defineModel + +Two-way binding prop consumed via `v-model`. Available in Vue 3.4+. + +```ts +// Basic usage - creates "modelValue" prop +const model = defineModel() +model.value = 'hello' // Emits "update:modelValue" + +// Named model - consumed via v-model:name +const count = defineModel('count', { default: 0 }) + +// With modifiers +const [value, modifiers] = defineModel() +if (modifiers.trim) { + // Handle trim modifier +} + +// With transformers +const [value, modifiers] = defineModel({ + get(val) { return val?.toLowerCase() }, + set(val) { return modifiers.trim ? val?.trim() : val } +}) +``` + +Parent usage: +```vue + + + +``` + +## defineExpose + +Explicitly expose properties to parent via template refs. Components are closed by default. + +```ts +import { ref } from 'vue' + +const count = ref(0) +const reset = () => { count.value = 0 } + +defineExpose({ + count, + reset +}) +``` + +Parent access: +```ts +const childRef = ref<{ count: number; reset: () => void }>() +childRef.value?.reset() +``` + +## defineOptions + +Declare component options without a separate ` +``` + +Multiple generics with constraints: +```vue + +``` + +## Local Custom Directives + +Use `vNameOfDirective` naming convention. + +```ts +const vFocus = { + mounted: (el: HTMLElement) => el.focus() +} + +// Or import and rename +import { myDirective as vMyDirective } from './directives' +``` + +```vue + +``` + +## Top-level await + +Use `await` directly in ` +``` + + diff --git a/.claude/skills/better-auth-best-practices b/.claude/skills/better-auth-best-practices new file mode 120000 index 0000000..d28d6bd --- /dev/null +++ b/.claude/skills/better-auth-best-practices @@ -0,0 +1 @@ +../../.agents/skills/better-auth-best-practices \ No newline at end of file diff --git a/.claude/skills/better-auth-security b/.claude/skills/better-auth-security new file mode 120000 index 0000000..79e85e9 --- /dev/null +++ b/.claude/skills/better-auth-security @@ -0,0 +1 @@ +../../.agents/skills/better-auth-security \ No newline at end of file diff --git a/.claude/skills/cloudflare b/.claude/skills/cloudflare new file mode 120000 index 0000000..cacabe8 --- /dev/null +++ b/.claude/skills/cloudflare @@ -0,0 +1 @@ +../../.agents/skills/cloudflare \ No newline at end of file diff --git a/.claude/skills/dockerfile-validator b/.claude/skills/dockerfile-validator new file mode 120000 index 0000000..5cb1052 --- /dev/null +++ b/.claude/skills/dockerfile-validator @@ -0,0 +1 @@ +../../.agents/skills/dockerfile-validator \ No newline at end of file diff --git a/.claude/skills/github-actions-generator b/.claude/skills/github-actions-generator new file mode 120000 index 0000000..0209c0d --- /dev/null +++ b/.claude/skills/github-actions-generator @@ -0,0 +1 @@ +../../.agents/skills/github-actions-generator \ No newline at end of file diff --git a/.claude/skills/install-anti-slop b/.claude/skills/install-anti-slop new file mode 120000 index 0000000..77aef81 --- /dev/null +++ b/.claude/skills/install-anti-slop @@ -0,0 +1 @@ +../../.agents/skills/install-anti-slop \ No newline at end of file diff --git a/.claude/skills/modern-css b/.claude/skills/modern-css new file mode 120000 index 0000000..1647a87 --- /dev/null +++ b/.claude/skills/modern-css @@ -0,0 +1 @@ +../../.agents/skills/modern-css \ No newline at end of file diff --git a/.claude/skills/nuxt-better-auth b/.claude/skills/nuxt-better-auth new file mode 120000 index 0000000..1e86918 --- /dev/null +++ b/.claude/skills/nuxt-better-auth @@ -0,0 +1 @@ +../../.agents/skills/nuxt-better-auth \ No newline at end of file diff --git a/.claude/skills/postgres b/.claude/skills/postgres new file mode 120000 index 0000000..710abef --- /dev/null +++ b/.claude/skills/postgres @@ -0,0 +1 @@ +../../.agents/skills/postgres \ No newline at end of file diff --git a/.claude/skills/postgres-drizzle b/.claude/skills/postgres-drizzle new file mode 120000 index 0000000..cd1e744 --- /dev/null +++ b/.claude/skills/postgres-drizzle @@ -0,0 +1 @@ +../../.agents/skills/postgres-drizzle \ No newline at end of file diff --git a/.claude/skills/requesting-code-review b/.claude/skills/requesting-code-review new file mode 120000 index 0000000..c0358a8 --- /dev/null +++ b/.claude/skills/requesting-code-review @@ -0,0 +1 @@ +../../.agents/skills/requesting-code-review \ No newline at end of file diff --git a/.claude/skills/stripe-best-practices b/.claude/skills/stripe-best-practices new file mode 120000 index 0000000..6e25ed9 --- /dev/null +++ b/.claude/skills/stripe-best-practices @@ -0,0 +1 @@ +../../.agents/skills/stripe-best-practices \ No newline at end of file diff --git a/.claude/skills/taste-skill b/.claude/skills/taste-skill new file mode 120000 index 0000000..4c0d2f2 --- /dev/null +++ b/.claude/skills/taste-skill @@ -0,0 +1 @@ +../../.agents/skills/taste-skill \ No newline at end of file diff --git a/.claude/skills/unocss b/.claude/skills/unocss new file mode 120000 index 0000000..5087c2f --- /dev/null +++ b/.claude/skills/unocss @@ -0,0 +1 @@ +../../.agents/skills/unocss \ No newline at end of file diff --git a/.claude/skills/vitest b/.claude/skills/vitest new file mode 120000 index 0000000..7661536 --- /dev/null +++ b/.claude/skills/vitest @@ -0,0 +1 @@ +../../.agents/skills/vitest \ No newline at end of file diff --git a/.claude/skills/vue b/.claude/skills/vue new file mode 120000 index 0000000..7fe99c4 --- /dev/null +++ b/.claude/skills/vue @@ -0,0 +1 @@ +../../.agents/skills/vue \ No newline at end of file diff --git a/.claude/skills/vue-testing-best-practices b/.claude/skills/vue-testing-best-practices new file mode 120000 index 0000000..d3f0d13 --- /dev/null +++ b/.claude/skills/vue-testing-best-practices @@ -0,0 +1 @@ +../../.agents/skills/vue-testing-best-practices \ No newline at end of file From 52d869e33a33f8b5c95e630b580f3ffeef1e65ee Mon Sep 17 00:00:00 2001 From: RedStar Date: Sat, 17 Oct 2026 19:32:49 +0200 Subject: [PATCH 03/19] refactor: move the runtime packages under packages/core agent, runner, models, config and shared now live in packages/core/*. Their npm names are unchanged, so imports stay identical; only relative tooling paths, TypeScript project references, the workspace globs and the documentation that names their directories change. --- .github/copilot-instructions.md | 2 +- .github/workflows/ci.yaml | 2 +- .github/workflows/release.yaml | 2 +- .skills/code-zero-architecture/SKILL.md | 2 +- .skills/code-zero-safety/SKILL.md | 2 +- AGENTS.md | 22 +- AI_POLICY.md | 2 +- CONTRIBUTING.md | 10 +- README.md | 16 +- apps/docs/content/1.guide/2.tech-stack.md | 2 +- .../content/1.guide/5.codebase/1.structure.md | 20 +- apps/docs/content/1.guide/7.configuration.md | 2 +- apps/docs/content/1.guide/8.safety.md | 2 +- apps/marketing/README.md | 14 +- .../en/blog/one-boundary-for-writes.md | 2 +- docs/architecture.md | 16 +- docs/model-providers.md | 4 +- docs/sandbox-providers.md | 2 +- packages/agent/tsdown.config.ts | 3 - packages/api/src/access.test.ts | 2 +- packages/api/src/operations.test.ts | 2 +- packages/api/tsconfig.json | 10 +- packages/cli/tsconfig.json | 10 +- packages/config/tsdown.config.ts | 3 - packages/{ => core}/agent/package.json | 4 +- packages/{ => core}/agent/src/agent.test.ts | 0 packages/{ => core}/agent/src/agent.ts | 0 packages/{ => core}/agent/src/index.ts | 0 packages/{ => core}/agent/src/state.test.ts | 0 packages/{ => core}/agent/src/state.ts | 0 .../{ => core}/agent/src/validation.test.ts | 0 packages/{ => core}/agent/src/validation.ts | 0 packages/{ => core}/agent/tsconfig.json | 2 +- packages/core/agent/tsdown.config.ts | 3 + packages/{ => core}/config/package.json | 4 +- packages/{ => core}/config/src/checks.test.ts | 2 +- packages/{ => core}/config/src/checks.ts | 2 +- .../{ => core}/config/src/deployment.test.ts | 0 packages/{ => core}/config/src/deployment.ts | 0 packages/{ => core}/config/src/index.test.ts | 0 packages/{ => core}/config/src/index.ts | 0 packages/{ => core}/config/tsconfig.json | 2 +- packages/core/config/tsdown.config.ts | 3 + packages/{ => core}/models/package.json | 4 +- packages/{ => core}/models/src/index.test.ts | 0 packages/{ => core}/models/src/index.ts | 0 .../models/src/providers/subscription.test.ts | 0 .../models/src/providers/subscription.ts | 6 +- packages/{ => core}/models/tsconfig.json | 2 +- packages/core/models/tsdown.config.ts | 3 + packages/{ => core}/runner/package.json | 4 +- packages/{ => core}/runner/src/boundary.ts | 0 packages/{ => core}/runner/src/container.ts | 0 packages/{ => core}/runner/src/index.test.ts | 0 packages/{ => core}/runner/src/index.ts | 0 packages/{ => core}/runner/src/local.ts | 0 .../{ => core}/runner/src/process.test.ts | 0 packages/{ => core}/runner/src/process.ts | 0 .../{ => core}/runner/src/sandbox.test.ts | 0 packages/{ => core}/runner/src/sandbox.ts | 0 packages/{ => core}/runner/tsconfig.json | 2 +- packages/core/runner/tsdown.config.ts | 3 + packages/{ => core}/shared/package.json | 4 +- .../{ => core}/shared/src/evidence.test.ts | 0 packages/{ => core}/shared/src/evidence.ts | 0 packages/{ => core}/shared/src/index.ts | 0 packages/{ => core}/shared/src/redact.test.ts | 0 packages/{ => core}/shared/src/redact.ts | 0 packages/{ => core}/shared/src/types.ts | 0 packages/{ => core}/shared/tsconfig.json | 2 +- packages/core/shared/tsdown.config.ts | 3 + packages/models/tsdown.config.ts | 3 - packages/runner/tsdown.config.ts | 3 - packages/shared/tsdown.config.ts | 3 - packages/source-control/tsconfig.json | 2 +- pnpm-lock.yaml | 252 +++++++++--------- pnpm-workspace.yaml | 1 + tsconfig.json | 10 +- 78 files changed, 242 insertions(+), 241 deletions(-) delete mode 100644 packages/agent/tsdown.config.ts delete mode 100644 packages/config/tsdown.config.ts rename packages/{ => core}/agent/package.json (81%) rename packages/{ => core}/agent/src/agent.test.ts (100%) rename packages/{ => core}/agent/src/agent.ts (100%) rename packages/{ => core}/agent/src/index.ts (100%) rename packages/{ => core}/agent/src/state.test.ts (100%) rename packages/{ => core}/agent/src/state.ts (100%) rename packages/{ => core}/agent/src/validation.test.ts (100%) rename packages/{ => core}/agent/src/validation.ts (100%) rename packages/{ => core}/agent/tsconfig.json (84%) create mode 100644 packages/core/agent/tsdown.config.ts rename packages/{ => core}/config/package.json (81%) rename packages/{ => core}/config/src/checks.test.ts (97%) rename packages/{ => core}/config/src/checks.ts (99%) rename packages/{ => core}/config/src/deployment.test.ts (100%) rename packages/{ => core}/config/src/deployment.ts (100%) rename packages/{ => core}/config/src/index.test.ts (100%) rename packages/{ => core}/config/src/index.ts (100%) rename packages/{ => core}/config/tsconfig.json (75%) create mode 100644 packages/core/config/tsdown.config.ts rename packages/{ => core}/models/package.json (84%) rename packages/{ => core}/models/src/index.test.ts (100%) rename packages/{ => core}/models/src/index.ts (100%) rename packages/{ => core}/models/src/providers/subscription.test.ts (100%) rename packages/{ => core}/models/src/providers/subscription.ts (98%) rename packages/{ => core}/models/tsconfig.json (75%) create mode 100644 packages/core/models/tsdown.config.ts rename packages/{ => core}/runner/package.json (81%) rename packages/{ => core}/runner/src/boundary.ts (100%) rename packages/{ => core}/runner/src/container.ts (100%) rename packages/{ => core}/runner/src/index.test.ts (100%) rename packages/{ => core}/runner/src/index.ts (100%) rename packages/{ => core}/runner/src/local.ts (100%) rename packages/{ => core}/runner/src/process.test.ts (100%) rename packages/{ => core}/runner/src/process.ts (100%) rename packages/{ => core}/runner/src/sandbox.test.ts (100%) rename packages/{ => core}/runner/src/sandbox.ts (100%) rename packages/{ => core}/runner/tsconfig.json (75%) create mode 100644 packages/core/runner/tsdown.config.ts rename packages/{ => core}/shared/package.json (78%) rename packages/{ => core}/shared/src/evidence.test.ts (100%) rename packages/{ => core}/shared/src/evidence.ts (100%) rename packages/{ => core}/shared/src/index.ts (100%) rename packages/{ => core}/shared/src/redact.test.ts (100%) rename packages/{ => core}/shared/src/redact.ts (100%) rename packages/{ => core}/shared/src/types.ts (100%) rename packages/{ => core}/shared/tsconfig.json (68%) create mode 100644 packages/core/shared/tsdown.config.ts delete mode 100644 packages/models/tsdown.config.ts delete mode 100644 packages/runner/tsdown.config.ts delete mode 100644 packages/shared/tsdown.config.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cedc2ad..722a41b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,7 +4,7 @@ Read `/AGENTS.md` before editing and load the relevant skill from `/.agents/skil - Use aube and the root scripts. It keeps `pnpm-lock.yaml` and `pnpm-workspace.yaml` in place. - Preserve the package dependency direction documented in `/docs/architecture.md`. -- Keep runtime command execution and target-repository mutation inside `packages/runner`. +- Keep runtime command execution and target-repository mutation inside `packages/core/runner`. - Keep `observe` mode read-only. - Add deterministic tests for behavior, state transitions, and safety-sensitive changes. - Keep `apps/dashboard` frontend-only, use `@bomb.sh/args` plus `@clack/prompts` in the CLI, tsdown for package builds, and Oxlint/Oxfmt for code quality. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 57c24aa..0a22378 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -88,7 +88,7 @@ jobs: - name: 📦 Verify package version run: | - node --input-type=module --eval "Promise.all([import('./packages/shared/dist/index.mjs'), import('./packages/shared/package.json', { with: { type: 'json' } })]).then(([built, pkg]) => { if (built.version !== pkg.default.version) { console.error('Injected version', built.version, 'does not match package.json version', pkg.default.version); process.exit(1) } })" + node --input-type=module --eval "Promise.all([import('./packages/core/shared/dist/index.mjs'), import('./packages/core/shared/package.json', { with: { type: 'json' } })]).then(([built, pkg]) => { if (built.version !== pkg.default.version) { console.error('Injected version', built.version, 'does not match package.json version', pkg.default.version); process.exit(1) } })" knip: name: 🧹 Unused code check diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0beeafc..8ac645f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -33,7 +33,7 @@ jobs: - name: Verify injected version run: | - node --input-type=module --eval "Promise.all([import('./packages/shared/dist/index.mjs'), import('./packages/shared/package.json', { with: { type: 'json' } })]).then(([built, pkg]) => { if (built.version !== pkg.default.version) { console.error('Injected version', built.version, 'does not match package.json version', pkg.default.version); process.exit(1) } })" + node --input-type=module --eval "Promise.all([import('./packages/core/shared/dist/index.mjs'), import('./packages/core/shared/package.json', { with: { type: 'json' } })]).then(([built, pkg]) => { if (built.version !== pkg.default.version) { console.error('Injected version', built.version, 'does not match package.json version', pkg.default.version); process.exit(1) } })" # Publishing remains intentionally absent until the @code-zero package # scope and npm trusted publishing policy are configured. diff --git a/.skills/code-zero-architecture/SKILL.md b/.skills/code-zero-architecture/SKILL.md index 864af24..4ed68be 100644 --- a/.skills/code-zero-architecture/SKILL.md +++ b/.skills/code-zero-architecture/SKILL.md @@ -77,5 +77,5 @@ Keep dependency direction explicit while changing the monorepo. - Cross-package imports from another package's `src/` directory. - A capability package importing another capability package. When `runner` needs policy, it declares the fields it needs structurally instead of importing `config`. `auth` depending on `database` is the one sanctioned exception: persistence is a layer beneath policy, and the dependency runs only in that direction. - A table declared, a connection opened, or a migration written outside `packages/database`. -- Direct filesystem or `child_process` access outside `packages/runner`, including in the agent's discovery step. +- Direct filesystem or `child_process` access outside `packages/core/runner`, including in the agent's discovery step. - A second place that decides whether a run may write, or whether a run is verified. Both have exactly one home. diff --git a/.skills/code-zero-safety/SKILL.md b/.skills/code-zero-safety/SKILL.md index f509eef..967b1a4 100644 --- a/.skills/code-zero-safety/SKILL.md +++ b/.skills/code-zero-safety/SKILL.md @@ -11,7 +11,7 @@ Safety properties are behavior, not documentation. Back every change with determ - `observe` is the default and cannot mutate a target checkout. `suggest` cannot either. - `fix` requires an explicit mode and repository policy permission. Ask `mayModifyRepository`; do not re-derive the rule. -- Only `packages/runner` executes commands or changes target files at runtime. +- Only `packages/core/runner` executes commands or changes target files at runtime. - A runner created read-only refuses every write. Enforce the boundary mechanically, not by convention. - Working directories must remain inside the validated checkout, including after symlinks are resolved. - Nothing reads or writes inside `.git`. diff --git a/AGENTS.md b/AGENTS.md index 9aee323..f8e399c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,12 +35,12 @@ Code Zero is an open-source autonomous engineer that finds, fixes, and verifies | Path | Name | Description | | ------------------------- | --------------------------- | ---------------------------------------------------------------- | -| `packages/agent` | `@code-zero/agent` | Orchestration and state transitions only | -| `packages/runner` | `@code-zero/runner` | The only boundary allowed to run commands or mutate a checkout | -| `packages/models` | `@code-zero/models` | Model-provider abstractions | +| `packages/core/agent` | `@code-zero/agent` | Orchestration and state transitions only | +| `packages/core/runner` | `@code-zero/runner` | The only boundary allowed to run commands or mutate a checkout | +| `packages/core/models` | `@code-zero/models` | Model-provider abstractions | | `packages/source-control` | `@code-zero/source-control` | Provider-neutral contracts plus GitHub, GitLab, Bitbucket, Gitea | -| `packages/config` | `@code-zero/config` | Configuration parsing and policy | -| `packages/shared` | `@code-zero/shared` | Stable cross-package contracts | +| `packages/core/config` | `@code-zero/config` | Configuration parsing and policy | +| `packages/core/shared` | `@code-zero/shared` | Stable cross-package contracts | | `packages/build-env` | `@code-zero/build-env` | Build metadata resolution and the Nuxt module that publishes it | | `packages/cli` | `@code-zero/cli` | Argument parsing and terminal presentation | | `packages/database` | `@code-zero/database` | Schema, Drizzle client, and checked-in migrations | @@ -127,13 +127,13 @@ Use the smallest relevant check while iterating, then run the complete set befor ## Architecture boundaries -- `packages/agent`: orchestration and state transitions only. -- `packages/runner`: the only boundary allowed to execute repository commands or mutate a checkout. -- `packages/models`: model-provider abstractions. +- `packages/core/agent`: orchestration and state transitions only. +- `packages/core/runner`: the only boundary allowed to execute repository commands or mutate a checkout. +- `packages/core/models`: model-provider abstractions. - `packages/source-control`: provider-neutral source-control contracts, with GitHub, GitLab, Bitbucket, and Gitea adapters underneath. -- `packages/config`: configuration parsing and policy. -- `packages/shared`: stable cross-package contracts. -- `packages/build-env`: build metadata (version, commit, branch, deploy channel) and the Nuxt module that publishes it under `runtimeConfig.public.buildInfo`. Build-time resolution reads the hosting provider's variables first and the checkout second; the deployed server completes only what the build could not resolve. Not a runtime package: nothing in `packages/agent`, `packages/runner`, or their adapters may import it. +- `packages/core/config`: configuration parsing and policy. +- `packages/core/shared`: stable cross-package contracts. +- `packages/build-env`: build metadata (version, commit, branch, deploy channel) and the Nuxt module that publishes it under `runtimeConfig.public.buildInfo`. Build-time resolution reads the hosting provider's variables first and the checkout second; the deployed server completes only what the build could not resolve. Not a runtime package: nothing in `packages/core/agent`, `packages/core/runner`, or their adapters may import it. - `packages/cli`: argument parsing and terminal presentation. - `packages/database`: the schema, the Drizzle client, and the checked-in migrations. The only package that talks to Postgres. No policy, no HTTP, no runtime imports. - `packages/auth`: authentication policy and the Better Auth options factory. Reads the store through `packages/database`. No HTTP server, no runtime imports. diff --git a/AI_POLICY.md b/AI_POLICY.md index 0d8af62..728e869 100644 --- a/AI_POLICY.md +++ b/AI_POLICY.md @@ -10,7 +10,7 @@ This policy exists because of a growing volume of low-quality, AI-generated cont Understand your code, test it, and be ready to explain why it is correct and how it interacts with the rest of the system — without re-prompting a model. This is no different from what we expect of any contribution; AI just makes it easier to skip the work. Please do not skip the work. **Respect the boundaries.** -This repository is a set of deliberate package boundaries, and models are good at quietly crossing them. Before you submit, check your diff against the [architecture boundaries](AGENTS.md#architecture-boundaries) and the matching skill in `.agents/skills/`. A change that reaches the database from outside `packages/database`, executes a command outside `packages/runner`, or makes the runtime depend on an adapter will be closed regardless of how well it is written. +This repository is a set of deliberate package boundaries, and models are good at quietly crossing them. Before you submit, check your diff against the [architecture boundaries](AGENTS.md#architecture-boundaries) and the matching skill in `.agents/skills/`. A change that reaches the database from outside `packages/database`, executes a command outside `packages/core/runner`, or makes the runtime depend on an adapter will be closed regardless of how well it is written. **Prove it works.** Before submitting, verify the change actually works end to end. Do not rely on "it compiles" or "tests pass" alone. Run the full check set: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ebf1930..3e056f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,12 +35,12 @@ aube test | Change | Location | | ---------------------------------------------------- | ------------------------- | -| Agent lifecycle and decisions | `packages/agent` | -| Commands and repository mutation | `packages/runner` | -| LLM providers | `packages/models` | +| Agent lifecycle and decisions | `packages/core/agent` | +| Commands and repository mutation | `packages/core/runner` | +| LLM providers | `packages/core/models` | | Source-control provider adapters | `packages/source-control` | -| Configuration and policy | `packages/config` | -| Shared contracts | `packages/shared` | +| Configuration and policy | `packages/core/config` | +| Shared contracts | `packages/core/shared` | | CLI parsing and presentation | `packages/cli` | | Database schema and migrations | `packages/database` | | Authentication policy | `packages/auth` | diff --git a/README.md b/README.md index f80f91c..8a63643 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Feedback is never treated as truth merely because it came from a human or an AI - **Confidence and impact gates** – automatic fixes require confidence, an allowed change-risk class, repository permission, and verification. - **Issues become reviewable pull requests** – a labeled, repository-scoped issue can be investigated, implemented on an isolated branch, verified, and published as a pull request that carries its acceptance criteria and evidence; never as a direct commit. - **`observe` by default** – the safe mode inspects and reports, and never writes to a target repository. -- **One execution boundary** – `packages/runner` is the only code allowed to run commands or mutate a checkout. +- **One execution boundary** – `packages/core/runner` is the only code allowed to run commands or mutate a checkout. - **Adapters at the edges** – the runtime stays independent of HTTP, source-control platforms, terminal UI, and model providers. --- @@ -56,12 +56,12 @@ Nuxt dashboard ─── UI, oRPC + OpenAPI routes, and Better Auth, one app ─ | Package | Responsibility | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------ | -| [`packages/agent`](./packages/agent) | Orchestration and state transitions | -| [`packages/runner`](./packages/runner) | The only boundary that executes commands or mutates a checkout | -| [`packages/models`](./packages/models) | Model-provider abstractions | +| [`packages/core/agent`](./packages/core/agent) | Orchestration and state transitions | +| [`packages/core/runner`](./packages/core/runner) | The only boundary that executes commands or mutates a checkout | +| [`packages/core/models`](./packages/core/models) | Model-provider abstractions | | [`packages/source-control`](./packages/source-control) | Provider-neutral source-control contracts and adapters (GitHub, GitLab, Bitbucket, Gitea) | -| [`packages/config`](./packages/config) | Configuration parsing and policy | -| [`packages/shared`](./packages/shared) | Stable cross-package contracts | +| [`packages/core/config`](./packages/core/config) | Configuration parsing and policy | +| [`packages/core/shared`](./packages/core/shared) | Stable cross-package contracts | | [`packages/cli`](./packages/cli) | Argument parsing and terminal presentation | | [`packages/database`](./packages/database) | Schema, Drizzle client, and checked-in migrations; the only package that talks to Postgres | | [`packages/auth`](./packages/auth) | Authentication policy and the Better Auth options factory | @@ -280,7 +280,7 @@ Known limits, all of which follow from the session being local: - **Host-bound.** The run must execute where `claude login` / `codex login` was completed, and the Code Zero process must be allowed to spawn subprocesses. - **`codex-cli`'s process is not runner-routed.** `claude-code`'s CLI process is spawned through - `packages/runner`'s `spawnManagedProcess`, the same boundary as every repository check; a + `packages/core/runner`'s `spawnManagedProcess`, the same boundary as every repository check; a composition root wires this in (`modelFromEnvironment`'s `ClaudeCodeProcessSpawner` parameter). `ai-sdk-provider-codex-cli` exposes no equivalent hook, so Codex's process is always spawned by the vendor SDK directly. Its read-only sandbox, disabled approvals, and disabled MCP servers are @@ -463,7 +463,7 @@ GitHub Actions run typecheck, build/export validation, Oxlint, Oxfmt, tests, and ## Security model -The included `LocalRunner` is intended for trusted local development. Production deployments must place it inside Docker, a microVM, or another ephemeral sandbox with CPU, memory, filesystem, and network policies. Only `packages/runner` may invoke shell commands. +The included `LocalRunner` is intended for trusted local development. Production deployments must place it inside Docker, a microVM, or another ephemeral sandbox with CPU, memory, filesystem, and network policies. Only `packages/core/runner` may invoke shell commands. Report vulnerabilities privately as described in [SECURITY.md](./SECURITY.md). Do not open a public issue. diff --git a/apps/docs/content/1.guide/2.tech-stack.md b/apps/docs/content/1.guide/2.tech-stack.md index 359f02a..4baec87 100644 --- a/apps/docs/content/1.guide/2.tech-stack.md +++ b/apps/docs/content/1.guide/2.tech-stack.md @@ -32,7 +32,7 @@ Code Zero is a TypeScript monorepo. Every tool below was chosen deliberately; th - **[Drizzle](https://orm.drizzle.team) + Postgres** — the session store, the only database in the repository, owned by `packages/database` with reviewable checked-in SQL migrations. - **[Maizzle](https://maizzle.com)** — transactional email templates in `packages/mail`. - **[@nuxtjs/i18n](https://i18n.nuxtjs.org)** + **[Lunaria](https://lunaria.dev)** — English and Italian dictionaries with translation-staleness reporting. -- **[Vercel AI SDK](https://sdk.vercel.ai)** — the model-provider integrations behind one `ModelProvider` contract in `packages/models`. +- **[Vercel AI SDK](https://sdk.vercel.ai)** — the model-provider integrations behind one `ModelProvider` contract in `packages/core/models`. ## Agent tooling diff --git a/apps/docs/content/1.guide/5.codebase/1.structure.md b/apps/docs/content/1.guide/5.codebase/1.structure.md index abac63e..e177f9d 100644 --- a/apps/docs/content/1.guide/5.codebase/1.structure.md +++ b/apps/docs/content/1.guide/5.codebase/1.structure.md @@ -32,12 +32,12 @@ code-zero | Package | Responsibility | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `packages/agent` | Orchestration and state transitions only. Knows nothing of HTTP or terminals. | -| `packages/runner` | The only boundary allowed to execute repository commands or mutate a checkout. | -| `packages/models` | Model-provider abstractions: one `ModelProvider` contract over OpenAI, Anthropic, Google, AI Gateway, and OpenAI-compatible endpoints. | +| `packages/core/agent` | Orchestration and state transitions only. Knows nothing of HTTP or terminals. | +| `packages/core/runner` | The only boundary allowed to execute repository commands or mutate a checkout. | +| `packages/core/models` | Model-provider abstractions: one `ModelProvider` contract over OpenAI, Anthropic, Google, AI Gateway, and OpenAI-compatible endpoints. | | `packages/source-control` | Provider-neutral source-control contracts, with GitHub, GitLab, Bitbucket, and Gitea adapters underneath. | -| `packages/config` | Configuration parsing and policy for `.code-zero.yml`. | -| `packages/shared` | Stable cross-package contracts. Zero dependencies; must not import feature packages. | +| `packages/core/config` | Configuration parsing and policy for `.code-zero.yml`. | +| `packages/core/shared` | Stable cross-package contracts. Zero dependencies; must not import feature packages. | | `packages/cli` | Argument parsing and terminal presentation for the `zero` binary. | | `packages/database` | The schema, the Drizzle client, and the checked-in migrations. The only package that talks to Postgres. No policy, no HTTP, no runtime imports. | | `packages/auth` | Authentication policy and the Better Auth options factory. Reads the store through `packages/database`. No HTTP server, no runtime imports. | @@ -63,12 +63,12 @@ If a change would create a reverse dependency, move the shared contract inward i | Change | Location | | ---------------------------------------------------- | ------------------------- | -| Agent lifecycle and decisions | `packages/agent` | -| Commands and repository mutation | `packages/runner` | -| LLM providers | `packages/models` | +| Agent lifecycle and decisions | `packages/core/agent` | +| Commands and repository mutation | `packages/core/runner` | +| LLM providers | `packages/core/models` | | Source-control provider adapters | `packages/source-control` | -| Configuration and policy | `packages/config` | -| Shared contracts | `packages/shared` | +| Configuration and policy | `packages/core/config` | +| Shared contracts | `packages/core/shared` | | CLI parsing and presentation | `packages/cli` | | Database schema and migrations | `packages/database` | | Authentication policy | `packages/auth` | diff --git a/apps/docs/content/1.guide/7.configuration.md b/apps/docs/content/1.guide/7.configuration.md index 9a79a4f..d9d3764 100644 --- a/apps/docs/content/1.guide/7.configuration.md +++ b/apps/docs/content/1.guide/7.configuration.md @@ -10,7 +10,7 @@ zero init Repository policy is deliberately limited: it can restrict what Code Zero does, but it can never name endpoints or credentials — those come only from the [environment](/guide/environment-variables), so untrusted repository configuration cannot redirect a secret. -The annotated template lives at [`.code-zero.example.yml`](https://github.com/wolfstar-project/code-zero/blob/main/.code-zero.example.yml). `packages/config` parses and validates it. +The annotated template lives at [`.code-zero.example.yml`](https://github.com/wolfstar-project/code-zero/blob/main/.code-zero.example.yml). `packages/core/config` parses and validates it. ## Mode diff --git a/apps/docs/content/1.guide/8.safety.md b/apps/docs/content/1.guide/8.safety.md index 3dd2c75..f01833d 100644 --- a/apps/docs/content/1.guide/8.safety.md +++ b/apps/docs/content/1.guide/8.safety.md @@ -29,7 +29,7 @@ Review feedback, model output, issue text, and remote content are all untrusted ## One execution boundary -Only `packages/runner` may invoke shell commands or mutate a checkout. Commands run without a shell, so operators such as `&&`, `|`, `;`, and `$()` are rejected. Working directories, arguments, timeouts, and output limits are validated at the boundary. See [Execution boundary](/guide/architecture/execution-boundary). +Only `packages/core/runner` may invoke shell commands or mutate a checkout. Commands run without a shell, so operators such as `&&`, `|`, `;`, and `$()` are rejected. Working directories, arguments, timeouts, and output limits are validated at the boundary. See [Execution boundary](/guide/architecture/execution-boundary). ## Secrets diff --git a/apps/marketing/README.md b/apps/marketing/README.md index ef62269..83feb84 100644 --- a/apps/marketing/README.md +++ b/apps/marketing/README.md @@ -9,13 +9,13 @@ because it exists to be crawled, prerendering every route rather than serving a ## Boundaries -| Rule | Why | -| -------------------------------- | ------------------------------------------------------------------------------------------------- | -| No runtime-package imports | Nothing here may reach `packages/agent`, `packages/runner`, or any other runtime package. | -| No persistence, no auth handlers | Sign-in links out to the dashboard origin; this app never sees a session. | -| No secrets | Everything in `config/` is public by construction and ships in the client bundle. | -| Nitro is for rendering only | The only server routes are the ones `@nuxtjs/seo` generates (`/robots.txt`, sitemaps). | -| Contact form has no backend | `ContactForm` builds a `mailto:` link client-side; nothing is submitted to or stored by this app. | +| Rule | Why | +| -------------------------------- | --------------------------------------------------------------------------------------------------- | +| No runtime-package imports | Nothing here may reach `packages/core/agent`, `packages/core/runner`, or any other runtime package. | +| No persistence, no auth handlers | Sign-in links out to the dashboard origin; this app never sees a session. | +| No secrets | Everything in `config/` is public by construction and ships in the client bundle. | +| Nitro is for rendering only | The only server routes are the ones `@nuxtjs/seo` generates (`/robots.txt`, sitemaps). | +| Contact form has no backend | `ContactForm` builds a `mailto:` link client-side; nothing is submitted to or stored by this app. | `@nuxt/content` keeps a local SQLite index of `content/` at build/dev time to serve queries — that is a build-time content cache, not application persistence: it holds no user data and no diff --git a/apps/marketing/content/en/blog/one-boundary-for-writes.md b/apps/marketing/content/en/blog/one-boundary-for-writes.md index 3ae77ce..350a277 100644 --- a/apps/marketing/content/en/blog/one-boundary-for-writes.md +++ b/apps/marketing/content/en/blog/one-boundary-for-writes.md @@ -10,7 +10,7 @@ tag: 'architecture' Ask a security reviewer what they think of an autonomous coding agent and they will ask you one question back: _what, exactly, can it execute?_ -In Code Zero the answer fits in a sentence: only `packages/runner` may run a command or mutate a +In Code Zero the answer fits in a sentence: only `packages/core/runner` may run a command or mutate a checkout, and everything else must ask it through typed contracts. Working directories are validated, escape attempts are rejected, timeouts and output limits are enforced — in one place. diff --git a/docs/architecture.md b/docs/architecture.md index 384f8d2..5c78822 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -45,7 +45,7 @@ If a change creates a reverse dependency, move the shared contract inward instea ## Execution boundary -Only `packages/runner` may execute commands or mutate a target repository at runtime. The boundary is responsible for validating working directories, arguments, timeouts, output limits, and execution mode. A transport handler, source-control adapter, model provider, or state transition must request runner work through typed contracts rather than invoking a shell directly. +Only `packages/core/runner` may execute commands or mutate a target repository at runtime. The boundary is responsible for validating working directories, arguments, timeouts, output limits, and execution mode. A transport handler, source-control adapter, model provider, or state transition must request runner work through typed contracts rather than invoking a shell directly. `observe` is the default mode. It can inspect and report but cannot write. Enabling `fix` requires both an explicit mode and repository policy permission. @@ -55,15 +55,15 @@ Git inspection runs in the trusting process because its argv is fixed by the run `createRunner` and `runnerOptionsFromPolicy` are the only mapping from policy to a concrete boundary. `runnerOptionsFromPolicy` declares the policy fields it needs structurally, so the runner does not depend on the configuration package. Composition roots call both; nothing else constructs a runner. -Hosted sandboxes follow the same rule. `RunnerPool` lives in `packages/runner`, accepts credential-free `SandboxRequest` values, enforces global/repository quotas and lease ceilings before provisioning, and returns only a `Runner`. Provider credentials are constructor state of a vendor adapter and never enter a request, lease snapshot, agent state, or log. A composition root may schedule and release a lease, but it cannot execute a command itself. +Hosted sandboxes follow the same rule. `RunnerPool` lives in `packages/core/runner`, accepts credential-free `SandboxRequest` values, enforces global/repository quotas and lease ceilings before provisioning, and returns only a `Runner`. Provider credentials are constructor state of a vendor adapter and never enter a request, lease snapshot, agent state, or log. A composition root may schedule and release a lease, but it cannot execute a command itself. -Model transports follow the same adapter rule. `packages/models` owns the AI SDK integrations for OpenAI, Anthropic, Google, AI Gateway, and OpenAI-compatible endpoints behind one `ModelProvider` contract. Composition roots pass the validated provider policy; credentials come only from fixed provider-specific environment variables, and a custom endpoint can only come from the operator-owned `CODE_ZERO_MODEL_BASE_URL` environment variable. The agent runtime sees neither SDK objects nor credentials, and all adapters share one structured-output, usage-accounting, timeout, and error-redaction path. +Model transports follow the same adapter rule. `packages/core/models` owns the AI SDK integrations for OpenAI, Anthropic, Google, AI Gateway, and OpenAI-compatible endpoints behind one `ModelProvider` contract. Composition roots pass the validated provider policy; credentials come only from fixed provider-specific environment variables, and a custom endpoint can only come from the operator-owned `CODE_ZERO_MODEL_BASE_URL` environment variable. The agent runtime sees neither SDK objects nor credentials, and all adapters share one structured-output, usage-accounting, timeout, and error-redaction path. Two of those transports are subscription-based: `claude-code` and `codex-cli` drive a vendor CLI that is already logged in on the host, so `modelProviderCredentialKind` reports `subscription` and there is no credential for a composition root to supply, redact, or persist. They are the only transports whose SDK spawns a subprocess, which is why three things hold: each stays inert unless its operator flag is exactly `true`, the vendor SDK is imported lazily so an unused transport costs nothing, and the CLI is configured with its own tools disabled (Claude Code) or read-only with approvals off (Codex) so it cannot read outside the supplied context or edit a checkout behind the runner boundary. -`packages/models` still contains no `child_process` import of its own — it never spawns anything itself, matching every other package outside `packages/runner`. `subscriptionProbeCommand` returns the liveness command as a string for `zero doctor` to run through the runner like every other command, and the CLI process behind a live `decide()` call is spawned the same way: `modelFromEnvironment` takes an optional `ClaudeCodeProcessSpawner`, and `packages/cli` and `packages/api` supply one backed by `packages/runner`'s `spawnManagedProcess` — the streaming counterpart to `execFileProcessRunner`, for a caller that needs a live duplex process instead of one buffered result. Wired that way, the `claude-code` transport's CLI process is spawned through the same boundary as every repository check, not through the vendor SDK's own default `child_process.spawn`. `codex-cli` cannot be closed the same way: `ai-sdk-provider-codex-cli` exposes no equivalent spawn hook, so that transport's process is spawned by the vendor SDK directly regardless of what a composition root supplies — a vendor limitation, not a choice this codebase makes. The same read-only, no-MCP, approvals-off configuration is still the containment for that one transport. +`packages/core/models` still contains no `child_process` import of its own — it never spawns anything itself, matching every other package outside `packages/core/runner`. `subscriptionProbeCommand` returns the liveness command as a string for `zero doctor` to run through the runner like every other command, and the CLI process behind a live `decide()` call is spawned the same way: `modelFromEnvironment` takes an optional `ClaudeCodeProcessSpawner`, and `packages/cli` and `packages/api` supply one backed by `packages/core/runner`'s `spawnManagedProcess` — the streaming counterpart to `execFileProcessRunner`, for a caller that needs a live duplex process instead of one buffered result. Wired that way, the `claude-code` transport's CLI process is spawned through the same boundary as every repository check, not through the vendor SDK's own default `child_process.spawn`. `codex-cli` cannot be closed the same way: `ai-sdk-provider-codex-cli` exposes no equivalent spawn hook, so that transport's process is spawned by the vendor SDK directly regardless of what a composition root supplies — a vendor limitation, not a choice this codebase makes. The same read-only, no-MCP, approvals-off configuration is still the containment for that one transport. -Both composition roots additionally read `config.runner.isolation` to decide how they build that spawner, in a local module (`subscription-isolation.ts`, duplicated in `packages/cli` and `packages/api` rather than pulled into a shared package — this decision is composition-root-specific, needing both `CodeZeroConfig` and an operator environment variable, and neither `packages/models` nor `packages/runner` should own it). On `local` isolation it wires `spawnManagedProcess` directly, spawning the CLI on the host, same as before. On `container` isolation it wires `spawnManagedProcess`'s `container` option instead: `packages/runner` exports `ManagedProcessContainerOptions` and `containerizedProcessArgv`, a `docker`/`podman run` invocation deliberately distinct from `ContainerRunner.engineArguments()` — no repository-checkout volume (the CLI never touches one) and no `--network` tied to `permissions.network` (that policy contains an _untrusted checkout's_ commands, not Code Zero's own necessary calls to the vendor API). When container isolation is declared but no CLI container image is configured (`CODE_ZERO_CLAUDE_CODE_CONTAINER_IMAGE`), the composition root refuses the transport rather than silently falling back to an unisolated host spawn. The refusal is reported to `modelFromEnvironment` as a `subscriptionRefusalReason` — a synchronous throw from Code Zero's own code the moment the transport is asked to build a model, never touching the vendor SDK — rather than by turning the enable flag off: the flag also gates fallback selection, so disabling it would have reported the transport as never configured at all and skipped a configured `CODE_ZERO_MODEL_FALLBACK_PROVIDER` entirely, turning a run that could have degraded into one that fails outright. `environmentForModel` (used only by `zero doctor`'s diagnostics, which want a plain "not ready" signal rather than this nuance) is the one place that still disables the flag. +Both composition roots additionally read `config.runner.isolation` to decide how they build that spawner, in a local module (`subscription-isolation.ts`, duplicated in `packages/cli` and `packages/api` rather than pulled into a shared package — this decision is composition-root-specific, needing both `CodeZeroConfig` and an operator environment variable, and neither `packages/core/models` nor `packages/core/runner` should own it). On `local` isolation it wires `spawnManagedProcess` directly, spawning the CLI on the host, same as before. On `container` isolation it wires `spawnManagedProcess`'s `container` option instead: `packages/core/runner` exports `ManagedProcessContainerOptions` and `containerizedProcessArgv`, a `docker`/`podman run` invocation deliberately distinct from `ContainerRunner.engineArguments()` — no repository-checkout volume (the CLI never touches one) and no `--network` tied to `permissions.network` (that policy contains an _untrusted checkout's_ commands, not Code Zero's own necessary calls to the vendor API). When container isolation is declared but no CLI container image is configured (`CODE_ZERO_CLAUDE_CODE_CONTAINER_IMAGE`), the composition root refuses the transport rather than silently falling back to an unisolated host spawn. The refusal is reported to `modelFromEnvironment` as a `subscriptionRefusalReason` — a synchronous throw from Code Zero's own code the moment the transport is asked to build a model, never touching the vendor SDK — rather than by turning the enable flag off: the flag also gates fallback selection, so disabling it would have reported the transport as never configured at all and skipped a configured `CODE_ZERO_MODEL_FALLBACK_PROVIDER` entirely, turning a run that could have degraded into one that fails outright. `environmentForModel` (used only by `zero doctor`'s diagnostics, which want a plain "not ready" signal rather than this nuance) is the one place that still disables the flag. A `RunnerPool` lease is a separate isolation mechanism from `config.runner.isolation`, and `SandboxProvider` (`vitehub`/`cloudflare`/`vercel`/`custom`) returns only the ordinary `Runner` contract — bounded command execution, never a live process handle — so there is no `claude-code` spawner that could route the CLI's duplex stream through the same boundary a lease already gives repository commands. `runTask` (`packages/api/src/operations.ts`) refuses `claude-code` outright whenever `options.runnerPool` is configured, regardless of `runner.isolation`, the same way it refuses container isolation without an image and for the same reason: a `subscriptionRefusalReason`, not a disabled flag, so a configured fallback still gets its turn rather than the run failing outright. `packages/cli` has no `RunnerPool` concept at all, so this check lives only in `packages/api`. @@ -175,7 +175,7 @@ discover -> understand -> validate -> plan -> execute -> verify -> review └────── repair ──────┘ ``` -`LifecycleMachine` in `packages/agent` holds the transition table and refuses any move it does not define, so an implementation mistake becomes a thrown error rather than an unverified result that looks finished. Notably, `executing` cannot reach `completed` without passing through `verifying`, and `planning` cannot skip to `reviewing`. Every non-terminal state can reach `failed`. +`LifecycleMachine` in `packages/core/agent` holds the transition table and refuses any move it does not define, so an implementation mistake becomes a thrown error rather than an unverified result that looks finished. Notably, `executing` cannot reach `completed` without passing through `verifying`, and `planning` cannot skip to `reviewing`. Every non-terminal state can reach `failed`. Each stage owns one decision: @@ -197,11 +197,11 @@ Proactive review is repository opt-in. Its model decision carries severity, conf ## Verdicts and evidence -Validation lives in `packages/agent/src/validation.ts` and is independent of any provider. It rejects a claim that cites no evidence, names no existing file, or quotes repository content that is not there; it reports a supported but low-confidence claim as inconclusive. Rejection reasons are collected in full rather than short-circuiting on the first, because the report is the product. +Validation lives in `packages/core/agent/src/validation.ts` and is independent of any provider. It rejects a claim that cites no evidence, names no existing file, or quotes repository content that is not there; it reports a supported but low-confidence claim as inconclusive. Rejection reasons are collected in full rather than short-circuiting on the first, because the report is the product. `TaskResult.verified` is derived in exactly one place, at the point a run produces its terminal result: it requires a completed state, an applied change, and every executed check passing. No branch can assert verification it did not earn, which is what makes "a failed verification is never presented as success" a property of the code rather than a convention. -`EvidenceBundle` and its Markdown renderer live in `packages/shared` because both the source-control adapters and the CLI consume them, and because rendering is a pure function over contracts with no I/O. Terminal states map deterministically onto a provider-neutral run outcome in `packages/source-control`, which each provider adapter translates into its own status vocabulary — explicitly noting any conclusion the platform cannot express (see `docs/source-control-providers.md`). +`EvidenceBundle` and its Markdown renderer live in `packages/core/shared` because both the source-control adapters and the CLI consume them, and because rendering is a pure function over contracts with no I/O. Terminal states map deterministically onto a provider-neutral run outcome in `packages/source-control`, which each provider adapter translates into its own status vocabulary — explicitly noting any conclusion the platform cannot express (see `docs/source-control-providers.md`). diff --git a/docs/model-providers.md b/docs/model-providers.md index c4d3f6d..822a069 100644 --- a/docs/model-providers.md +++ b/docs/model-providers.md @@ -1,6 +1,6 @@ # Model providers -Code Zero supports native OpenAI, Anthropic, and Google Generative AI adapters, Vercel AI Gateway, and arbitrary OpenAI-compatible endpoints — all behind one `ModelProvider` contract in `packages/models`, sharing one structured-output, usage-accounting, timeout, and error-redaction path. +Code Zero supports native OpenAI, Anthropic, and Google Generative AI adapters, Vercel AI Gateway, and arbitrary OpenAI-compatible endpoints — all behind one `ModelProvider` contract in `packages/core/models`, sharing one structured-output, usage-accounting, timeout, and error-redaction path. ## Select a provider @@ -43,5 +43,5 @@ Usage (tokens and, when configured, cost) is accounted per run and carried in th ## Guarantees - Credentials are read only from the provider's documented environment variable and are never persisted with task evidence. -- The agent runtime sees neither SDK objects nor credentials — `packages/models` keeps AI SDK types behind the adapter boundary. +- The agent runtime sees neither SDK objects nor credentials — `packages/core/models` keeps AI SDK types behind the adapter boundary. - Errors are redacted before they surface, so a provider exception cannot leak a credential into logs or evidence. diff --git a/docs/sandbox-providers.md b/docs/sandbox-providers.md index 6c4d0a9..b0858ff 100644 --- a/docs/sandbox-providers.md +++ b/docs/sandbox-providers.md @@ -1,6 +1,6 @@ # Hosted sandbox provider evaluation -Code Zero v0.3 defines the vendor-neutral lifecycle in `packages/runner`: provision a credential-free request, expose the resulting checkout only as a `Runner`, stop it explicitly, and expire bounded leases. A provider adapter owns its client and credentials privately. No SDK response or secret is copied into agent state, task history, lease snapshots, or logs. +Code Zero v0.3 defines the vendor-neutral lifecycle in `packages/core/runner`: provision a credential-free request, expose the resulting checkout only as a `Runner`, stop it explicitly, and expire bounded leases. A provider adapter owns its client and credentials privately. No SDK response or secret is copied into agent state, task history, lease snapshots, or logs. ## Evaluation diff --git a/packages/agent/tsdown.config.ts b/packages/agent/tsdown.config.ts deleted file mode 100644 index bcb566f..0000000 --- a/packages/agent/tsdown.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { definePackageConfig } from '../../scripts/tsdown.config.ts'; - -export default definePackageConfig(); diff --git a/packages/api/src/access.test.ts b/packages/api/src/access.test.ts index fec9cad..e414e5f 100644 --- a/packages/api/src/access.test.ts +++ b/packages/api/src/access.test.ts @@ -76,7 +76,7 @@ describe('accessFromEnvironment', () => { }); it('grants nothing to a principal whose configured grant resolved to no valid mode', () => { - // `packages/config`'s `parseDeploymentConfig` refuses a grant that named an unknown mode, but + // `packages/core/config`'s `parseDeploymentConfig` refuses a grant that named an unknown mode, but // still records the principal with an empty mode list rather than omitting it — omitting it // would land here as "no grant configured" and widen the principal to the non-writable // defaults, which is worse than the mistake it was meant to catch. diff --git a/packages/api/src/operations.test.ts b/packages/api/src/operations.test.ts index 5e1a0f2..c2f7192 100644 --- a/packages/api/src/operations.test.ts +++ b/packages/api/src/operations.test.ts @@ -219,7 +219,7 @@ describe('runTask', () => { // nothing to degrade to and the run fails outright rather than spawning the CLI unisolated // on the host. `modelFromEnvironment` proves the companion guarantee — that a configured // fallback IS honored here instead of being skipped — at the unit level in - // packages/models/src/index.test.ts ("preserves a configured fallback when a composition + // packages/core/models/src/index.test.ts ("preserves a configured fallback when a composition // root refuses the transport"), since exercising a real fallback provider end to end here // would require a live model call. expect(result.state).toBe('failed'); diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index b6e73a0..3335d6c 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -3,11 +3,11 @@ "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": ["src/**/*.ts"], "references": [ - { "path": "../agent" }, - { "path": "../config" }, - { "path": "../models" }, - { "path": "../runner" }, - { "path": "../shared" }, + { "path": "../core/agent" }, + { "path": "../core/config" }, + { "path": "../core/models" }, + { "path": "../core/runner" }, + { "path": "../core/shared" }, { "path": "../source-control" } ] } diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 29c6b98..530fae5 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -3,10 +3,10 @@ "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": ["src/**/*.ts"], "references": [ - { "path": "../agent" }, - { "path": "../config" }, - { "path": "../models" }, - { "path": "../runner" }, - { "path": "../shared" } + { "path": "../core/agent" }, + { "path": "../core/config" }, + { "path": "../core/models" }, + { "path": "../core/runner" }, + { "path": "../core/shared" } ] } diff --git a/packages/config/tsdown.config.ts b/packages/config/tsdown.config.ts deleted file mode 100644 index bcb566f..0000000 --- a/packages/config/tsdown.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { definePackageConfig } from '../../scripts/tsdown.config.ts'; - -export default definePackageConfig(); diff --git a/packages/agent/package.json b/packages/core/agent/package.json similarity index 81% rename from packages/agent/package.json rename to packages/core/agent/package.json index 05abe3c..67972d9 100644 --- a/packages/agent/package.json +++ b/packages/core/agent/package.json @@ -21,8 +21,8 @@ "scripts": { "build": "tsdown", "clean": "tsc -b --clean", - "lint": "oxlint --config ../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", - "lint:fix": "oxlint --fix --config ../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", + "lint": "oxlint --config ../../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", + "lint:fix": "oxlint --fix --config ../../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", "test": "vitest run src --passWithNoTests", "typecheck": "tsc --project tsconfig.json --pretty false --noEmit" }, diff --git a/packages/agent/src/agent.test.ts b/packages/core/agent/src/agent.test.ts similarity index 100% rename from packages/agent/src/agent.test.ts rename to packages/core/agent/src/agent.test.ts diff --git a/packages/agent/src/agent.ts b/packages/core/agent/src/agent.ts similarity index 100% rename from packages/agent/src/agent.ts rename to packages/core/agent/src/agent.ts diff --git a/packages/agent/src/index.ts b/packages/core/agent/src/index.ts similarity index 100% rename from packages/agent/src/index.ts rename to packages/core/agent/src/index.ts diff --git a/packages/agent/src/state.test.ts b/packages/core/agent/src/state.test.ts similarity index 100% rename from packages/agent/src/state.test.ts rename to packages/core/agent/src/state.test.ts diff --git a/packages/agent/src/state.ts b/packages/core/agent/src/state.ts similarity index 100% rename from packages/agent/src/state.ts rename to packages/core/agent/src/state.ts diff --git a/packages/agent/src/validation.test.ts b/packages/core/agent/src/validation.test.ts similarity index 100% rename from packages/agent/src/validation.test.ts rename to packages/core/agent/src/validation.test.ts diff --git a/packages/agent/src/validation.ts b/packages/core/agent/src/validation.ts similarity index 100% rename from packages/agent/src/validation.ts rename to packages/core/agent/src/validation.ts diff --git a/packages/agent/tsconfig.json b/packages/core/agent/tsconfig.json similarity index 84% rename from packages/agent/tsconfig.json rename to packages/core/agent/tsconfig.json index 78ecafd..33c0ff3 100644 --- a/packages/agent/tsconfig.json +++ b/packages/core/agent/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": ["src/**/*.ts"], "references": [ diff --git a/packages/core/agent/tsdown.config.ts b/packages/core/agent/tsdown.config.ts new file mode 100644 index 0000000..f97ee9e --- /dev/null +++ b/packages/core/agent/tsdown.config.ts @@ -0,0 +1,3 @@ +import { definePackageConfig } from '../../../scripts/tsdown.config.ts'; + +export default definePackageConfig(); diff --git a/packages/config/package.json b/packages/core/config/package.json similarity index 81% rename from packages/config/package.json rename to packages/core/config/package.json index bc694ff..c84ae03 100644 --- a/packages/config/package.json +++ b/packages/core/config/package.json @@ -21,8 +21,8 @@ "scripts": { "build": "tsdown", "clean": "tsc -b --clean", - "lint": "oxlint --config ../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", - "lint:fix": "oxlint --fix --config ../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", + "lint": "oxlint --config ../../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", + "lint:fix": "oxlint --fix --config ../../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", "test": "vitest run src --passWithNoTests", "typecheck": "tsc --project tsconfig.json --pretty false --noEmit" }, diff --git a/packages/config/src/checks.test.ts b/packages/core/config/src/checks.test.ts similarity index 97% rename from packages/config/src/checks.test.ts rename to packages/core/config/src/checks.test.ts index 8089326..4a7749d 100644 --- a/packages/config/src/checks.test.ts +++ b/packages/core/config/src/checks.test.ts @@ -104,7 +104,7 @@ describe('assertExecutableCommand', () => { }); it('rejects only empty commands and defers shell syntax to the runner', () => { - // Shell semantics are owned by packages/runner, where ViteHub Shell analyzes every command + // Shell semantics are owned by packages/core/runner, where ViteHub Shell analyzes every command // immediately before execution; configuration only enforces that a command exists at all. for (const command of [ 'pnpm test && pnpm build', diff --git a/packages/config/src/checks.ts b/packages/core/config/src/checks.ts similarity index 99% rename from packages/config/src/checks.ts rename to packages/core/config/src/checks.ts index 7c7b3a2..6c4dd4a 100644 --- a/packages/config/src/checks.ts +++ b/packages/core/config/src/checks.ts @@ -102,7 +102,7 @@ export function resolveChecks(configured: readonly string[], probe: RepositoryPr /** * Validate only the configuration-level invariant here. * - * Command syntax and shell semantics are deliberately owned by `packages/runner`, where ViteHub + * Command syntax and shell semantics are deliberately owned by `packages/core/runner`, where ViteHub * Shell performs the authoritative analysis immediately before execution. Keeping that decision in * one layer avoids the config and runner drifting into two subtly different shell parsers. */ diff --git a/packages/config/src/deployment.test.ts b/packages/core/config/src/deployment.test.ts similarity index 100% rename from packages/config/src/deployment.test.ts rename to packages/core/config/src/deployment.test.ts diff --git a/packages/config/src/deployment.ts b/packages/core/config/src/deployment.ts similarity index 100% rename from packages/config/src/deployment.ts rename to packages/core/config/src/deployment.ts diff --git a/packages/config/src/index.test.ts b/packages/core/config/src/index.test.ts similarity index 100% rename from packages/config/src/index.test.ts rename to packages/core/config/src/index.test.ts diff --git a/packages/config/src/index.ts b/packages/core/config/src/index.ts similarity index 100% rename from packages/config/src/index.ts rename to packages/core/config/src/index.ts diff --git a/packages/config/tsconfig.json b/packages/core/config/tsconfig.json similarity index 75% rename from packages/config/tsconfig.json rename to packages/core/config/tsconfig.json index 5594984..6effd7f 100644 --- a/packages/config/tsconfig.json +++ b/packages/core/config/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": ["src/**/*.ts"], "references": [{ "path": "../shared" }] diff --git a/packages/core/config/tsdown.config.ts b/packages/core/config/tsdown.config.ts new file mode 100644 index 0000000..f97ee9e --- /dev/null +++ b/packages/core/config/tsdown.config.ts @@ -0,0 +1,3 @@ +import { definePackageConfig } from '../../../scripts/tsdown.config.ts'; + +export default definePackageConfig(); diff --git a/packages/models/package.json b/packages/core/models/package.json similarity index 84% rename from packages/models/package.json rename to packages/core/models/package.json index 113f26b..cbb43db 100644 --- a/packages/models/package.json +++ b/packages/core/models/package.json @@ -21,8 +21,8 @@ "scripts": { "build": "tsdown", "clean": "tsc -b --clean", - "lint": "oxlint --config ../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", - "lint:fix": "oxlint --fix --config ../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", + "lint": "oxlint --config ../../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", + "lint:fix": "oxlint --fix --config ../../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", "test": "vitest run src --passWithNoTests", "typecheck": "tsc --project tsconfig.json --pretty false --noEmit" }, diff --git a/packages/models/src/index.test.ts b/packages/core/models/src/index.test.ts similarity index 100% rename from packages/models/src/index.test.ts rename to packages/core/models/src/index.test.ts diff --git a/packages/models/src/index.ts b/packages/core/models/src/index.ts similarity index 100% rename from packages/models/src/index.ts rename to packages/core/models/src/index.ts diff --git a/packages/models/src/providers/subscription.test.ts b/packages/core/models/src/providers/subscription.test.ts similarity index 100% rename from packages/models/src/providers/subscription.test.ts rename to packages/core/models/src/providers/subscription.test.ts diff --git a/packages/models/src/providers/subscription.ts b/packages/core/models/src/providers/subscription.ts similarity index 98% rename from packages/models/src/providers/subscription.ts rename to packages/core/models/src/providers/subscription.ts index 659c88b..7ad91da 100644 --- a/packages/models/src/providers/subscription.ts +++ b/packages/core/models/src/providers/subscription.ts @@ -114,7 +114,7 @@ export function createSubscriptionSession(): SubscriptionSession { /** * Spawns the Claude Code CLI process on the vendor SDK's behalf. * - * `packages/models` never calls `child_process` itself: this is the vendor SDK's own + * `packages/core/models` never calls `child_process` itself: this is the vendor SDK's own * `spawnClaudeCodeProcess` hook, structurally typed against Node's `ChildProcess` rather than * against the vendor's re-exported type names (`ai-sdk-provider-claude-code` does not publicly * export `SpawnOptions`/`SpawnedProcess`, and `ChildProcess` already satisfies the vendor's shape @@ -165,7 +165,7 @@ export function isSubscriptionProviderEnabled( /** * The command that proves the CLI is installed and runnable. * - * Returned rather than executed: `packages/models` never spawns anything itself, so a composition + * Returned rather than executed: `packages/core/models` never spawns anything itself, so a composition * root runs this through the runner boundary like every other command. */ export function subscriptionProbeCommand( @@ -403,7 +403,7 @@ function adaptSpawnedProcess(spawnProcess: ClaudeCodeProcessSpawner) { * * Unlike `claudeCodeLanguageModel`, this never receives a spawner: `ai-sdk-provider-codex-cli` * exposes no `spawnClaudeCodeProcess`-equivalent hook, so its `child_process.spawn` call cannot be - * routed through `packages/runner` regardless of the deployment's runner isolation. A containerized + * routed through `packages/core/runner` regardless of the deployment's runner isolation. A containerized * runner still runs this CLI's process directly on the control-plane host — the read-only sandbox, * disabled approvals, and disabled MCP servers below are the containment for that gap, not a * substitute for the process isolation this cannot provide. Documented for operators in README diff --git a/packages/models/tsconfig.json b/packages/core/models/tsconfig.json similarity index 75% rename from packages/models/tsconfig.json rename to packages/core/models/tsconfig.json index 5594984..6effd7f 100644 --- a/packages/models/tsconfig.json +++ b/packages/core/models/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": ["src/**/*.ts"], "references": [{ "path": "../shared" }] diff --git a/packages/core/models/tsdown.config.ts b/packages/core/models/tsdown.config.ts new file mode 100644 index 0000000..f97ee9e --- /dev/null +++ b/packages/core/models/tsdown.config.ts @@ -0,0 +1,3 @@ +import { definePackageConfig } from '../../../scripts/tsdown.config.ts'; + +export default definePackageConfig(); diff --git a/packages/runner/package.json b/packages/core/runner/package.json similarity index 81% rename from packages/runner/package.json rename to packages/core/runner/package.json index 69df057..598be14 100644 --- a/packages/runner/package.json +++ b/packages/core/runner/package.json @@ -21,8 +21,8 @@ "scripts": { "build": "tsdown", "clean": "tsc -b --clean", - "lint": "oxlint --config ../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", - "lint:fix": "oxlint --fix --config ../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", + "lint": "oxlint --config ../../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", + "lint:fix": "oxlint --fix --config ../../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", "test": "vitest run src --passWithNoTests", "typecheck": "tsc --project tsconfig.json --pretty false --noEmit" }, diff --git a/packages/runner/src/boundary.ts b/packages/core/runner/src/boundary.ts similarity index 100% rename from packages/runner/src/boundary.ts rename to packages/core/runner/src/boundary.ts diff --git a/packages/runner/src/container.ts b/packages/core/runner/src/container.ts similarity index 100% rename from packages/runner/src/container.ts rename to packages/core/runner/src/container.ts diff --git a/packages/runner/src/index.test.ts b/packages/core/runner/src/index.test.ts similarity index 100% rename from packages/runner/src/index.test.ts rename to packages/core/runner/src/index.test.ts diff --git a/packages/runner/src/index.ts b/packages/core/runner/src/index.ts similarity index 100% rename from packages/runner/src/index.ts rename to packages/core/runner/src/index.ts diff --git a/packages/runner/src/local.ts b/packages/core/runner/src/local.ts similarity index 100% rename from packages/runner/src/local.ts rename to packages/core/runner/src/local.ts diff --git a/packages/runner/src/process.test.ts b/packages/core/runner/src/process.test.ts similarity index 100% rename from packages/runner/src/process.test.ts rename to packages/core/runner/src/process.test.ts diff --git a/packages/runner/src/process.ts b/packages/core/runner/src/process.ts similarity index 100% rename from packages/runner/src/process.ts rename to packages/core/runner/src/process.ts diff --git a/packages/runner/src/sandbox.test.ts b/packages/core/runner/src/sandbox.test.ts similarity index 100% rename from packages/runner/src/sandbox.test.ts rename to packages/core/runner/src/sandbox.test.ts diff --git a/packages/runner/src/sandbox.ts b/packages/core/runner/src/sandbox.ts similarity index 100% rename from packages/runner/src/sandbox.ts rename to packages/core/runner/src/sandbox.ts diff --git a/packages/runner/tsconfig.json b/packages/core/runner/tsconfig.json similarity index 75% rename from packages/runner/tsconfig.json rename to packages/core/runner/tsconfig.json index 5594984..6effd7f 100644 --- a/packages/runner/tsconfig.json +++ b/packages/core/runner/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": ["src/**/*.ts"], "references": [{ "path": "../shared" }] diff --git a/packages/core/runner/tsdown.config.ts b/packages/core/runner/tsdown.config.ts new file mode 100644 index 0000000..f97ee9e --- /dev/null +++ b/packages/core/runner/tsdown.config.ts @@ -0,0 +1,3 @@ +import { definePackageConfig } from '../../../scripts/tsdown.config.ts'; + +export default definePackageConfig(); diff --git a/packages/shared/package.json b/packages/core/shared/package.json similarity index 78% rename from packages/shared/package.json rename to packages/core/shared/package.json index 6dee8f9..afdf8e7 100644 --- a/packages/shared/package.json +++ b/packages/core/shared/package.json @@ -21,8 +21,8 @@ "scripts": { "build": "tsdown", "clean": "tsc -b --clean", - "lint": "oxlint --config ../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", - "lint:fix": "oxlint --fix --config ../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", + "lint": "oxlint --config ../../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", + "lint:fix": "oxlint --fix --config ../../../tooling/oxc/packages.oxlintrc.json --type-aware --type-check src", "test": "vitest run src --passWithNoTests", "typecheck": "tsc --project tsconfig.json --pretty false --noEmit" }, diff --git a/packages/shared/src/evidence.test.ts b/packages/core/shared/src/evidence.test.ts similarity index 100% rename from packages/shared/src/evidence.test.ts rename to packages/core/shared/src/evidence.test.ts diff --git a/packages/shared/src/evidence.ts b/packages/core/shared/src/evidence.ts similarity index 100% rename from packages/shared/src/evidence.ts rename to packages/core/shared/src/evidence.ts diff --git a/packages/shared/src/index.ts b/packages/core/shared/src/index.ts similarity index 100% rename from packages/shared/src/index.ts rename to packages/core/shared/src/index.ts diff --git a/packages/shared/src/redact.test.ts b/packages/core/shared/src/redact.test.ts similarity index 100% rename from packages/shared/src/redact.test.ts rename to packages/core/shared/src/redact.test.ts diff --git a/packages/shared/src/redact.ts b/packages/core/shared/src/redact.ts similarity index 100% rename from packages/shared/src/redact.ts rename to packages/core/shared/src/redact.ts diff --git a/packages/shared/src/types.ts b/packages/core/shared/src/types.ts similarity index 100% rename from packages/shared/src/types.ts rename to packages/core/shared/src/types.ts diff --git a/packages/shared/tsconfig.json b/packages/core/shared/tsconfig.json similarity index 68% rename from packages/shared/tsconfig.json rename to packages/core/shared/tsconfig.json index 5ee9c86..7b6976c 100644 --- a/packages/shared/tsconfig.json +++ b/packages/core/shared/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": ["src/**/*.ts"] } diff --git a/packages/core/shared/tsdown.config.ts b/packages/core/shared/tsdown.config.ts new file mode 100644 index 0000000..f97ee9e --- /dev/null +++ b/packages/core/shared/tsdown.config.ts @@ -0,0 +1,3 @@ +import { definePackageConfig } from '../../../scripts/tsdown.config.ts'; + +export default definePackageConfig(); diff --git a/packages/models/tsdown.config.ts b/packages/models/tsdown.config.ts deleted file mode 100644 index bcb566f..0000000 --- a/packages/models/tsdown.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { definePackageConfig } from '../../scripts/tsdown.config.ts'; - -export default definePackageConfig(); diff --git a/packages/runner/tsdown.config.ts b/packages/runner/tsdown.config.ts deleted file mode 100644 index bcb566f..0000000 --- a/packages/runner/tsdown.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { definePackageConfig } from '../../scripts/tsdown.config.ts'; - -export default definePackageConfig(); diff --git a/packages/shared/tsdown.config.ts b/packages/shared/tsdown.config.ts deleted file mode 100644 index bcb566f..0000000 --- a/packages/shared/tsdown.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { definePackageConfig } from '../../scripts/tsdown.config.ts'; - -export default definePackageConfig(); diff --git a/packages/source-control/tsconfig.json b/packages/source-control/tsconfig.json index 5594984..52fc0d8 100644 --- a/packages/source-control/tsconfig.json +++ b/packages/source-control/tsconfig.json @@ -2,5 +2,5 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": ["src/**/*.ts"], - "references": [{ "path": "../shared" }] + "references": [{ "path": "../core/shared" }] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0aed57e..b311e84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -367,40 +367,6 @@ importers: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) - packages/agent: - dependencies: - '@code-zero/config': - specifier: workspace:* - version: 0.4.0 - '@code-zero/models': - specifier: workspace:* - version: 0.4.0 - '@code-zero/runner': - specifier: workspace:* - version: 0.4.0 - '@code-zero/shared': - specifier: workspace:* - version: 0.4.0 - devDependencies: - oxlint: - specifier: ^1.44.0 - version: 1.78.0(oxlint-tsgolint@7.0.2001) - oxlint-tsgolint: - specifier: ^7.0.2001 - version: 7.0.2001 - tsdown: - specifier: ^0.22.14 - version: 0.22.14(@arethetypeswrong/core@0.18.5)(@vitejs/devtools@0.4.12(vite@8.2.1))(publint@0.3.23)(tsx@4.23.12) - typescript: - specifier: ^5.9.2 - version: typescript-native-bridge@6.0.3-bridge.12.tsgo.7.0.2 - vite: - specifier: ^6.0.0 || ^7.0.0 || ^8.0.0 - version: 8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) - vitest: - specifier: ^4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) - packages/api: dependencies: '@code-zero/agent': @@ -573,7 +539,41 @@ importers: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) - packages/config: + packages/core/agent: + dependencies: + '@code-zero/config': + specifier: workspace:* + version: 0.4.0 + '@code-zero/models': + specifier: workspace:* + version: 0.4.0 + '@code-zero/runner': + specifier: workspace:* + version: 0.4.0 + '@code-zero/shared': + specifier: workspace:* + version: 0.4.0 + devDependencies: + oxlint: + specifier: ^1.44.0 + version: 1.78.0(oxlint-tsgolint@7.0.2001) + oxlint-tsgolint: + specifier: ^7.0.2001 + version: 7.0.2001 + tsdown: + specifier: ^0.22.14 + version: 0.22.14(@arethetypeswrong/core@0.18.5)(@vitejs/devtools@0.4.12(vite@8.2.1))(publint@0.3.23)(tsx@4.23.12) + typescript: + specifier: ^5.9.2 + version: typescript-native-bridge@6.0.3-bridge.12.tsgo.7.0.2 + vite: + specifier: ^6.0.0 || ^7.0.0 || ^8.0.0 + version: 8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) + vitest: + specifier: ^4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) + + packages/core/config: dependencies: '@code-zero/shared': specifier: workspace:* @@ -610,18 +610,36 @@ importers: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) - packages/database: + packages/core/models: dependencies: - drizzle-orm: - specifier: ^0.45.2 - version: 0.45.2(@electric-sql/pglite@0.4.3)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)))(@types/pg@8.23.0)(better-sqlite3@12.11.1)(kysely@0.29.5)(mysql2@3.15.3)(pg@8.23.0)(postgres@3.4.9)(prisma@7.9.1(better-sqlite3@12.11.1))(sql.js@1.14.2) - postgres: - specifier: ^3.4.9 - version: 3.4.9 + '@ai-sdk/anthropic': + specifier: ^4.0.36 + version: 4.0.39(zod@4.4.3) + '@ai-sdk/google': + specifier: ^4.0.39 + version: 4.0.44(zod@4.4.3) + '@ai-sdk/openai': + specifier: ^4.0.36 + version: 4.0.42(zod@4.4.3) + '@ai-sdk/openai-compatible': + specifier: ^3.0.16 + version: 3.0.31(zod@4.4.3) + '@code-zero/shared': + specifier: workspace:* + version: 0.4.0 + ai: + specifier: ^7.0.40 + version: 7.0.66(zod@4.4.3) + ai-sdk-provider-claude-code: + specifier: ^4.1.0 + version: 4.1.1(zod@4.4.3) + ai-sdk-provider-codex-cli: + specifier: ^2.1.2 + version: 2.1.2(zod@4.4.3) + zod: + specifier: ^4.4.3 + version: 4.4.3 devDependencies: - drizzle-kit: - specifier: ^0.31.10 - version: 0.31.10 oxlint: specifier: ^1.44.0 version: 1.78.0(oxlint-tsgolint@7.0.2001) @@ -641,21 +659,18 @@ importers: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) - packages/i18n: + packages/core/runner: dependencies: - '@lunariajs/core': - specifier: ^0.2.0 - version: 0.2.0(supports-color@8.1.1) - vue-i18n-extract: - specifier: 2.0.7 - version: 2.0.7 + '@code-zero/shared': + specifier: workspace:* + version: 0.4.0 + '@vite-hub/shell': + specifier: ^0.0.3 + version: 0.0.3(supports-color@8.1.1) + magic-regexp: + specifier: ^0.11.0 + version: 0.11.0(esbuild@0.28.2)(rolldown@1.2.4)(rollup@4.62.4)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))(webpack@5.109.2) devDependencies: - '@intlify/core-base': - specifier: ^11.4.8 - version: 11.4.8 - '@nuxtjs/i18n': - specifier: ^10.6.0 - version: 10.6.0(@azure/identity@4.13.1)(@azure/storage-blob@12.33.0)(@netlify/blobs@10.7.13)(@vercel/blob@2.8.0)(@vercel/functions@3.9.3(@aws-sdk/credential-provider-web-identity@3.972.49)(ws@8.21.3))(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.4.3)(@libsql/client@0.17.4)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)))(@types/pg@8.23.0)(better-sqlite3@12.11.1)(kysely@0.29.5)(mysql2@3.15.3)(pg@8.23.0)(postgres@3.4.9)(prisma@7.9.1(better-sqlite3@12.11.1))(sql.js@1.14.2))(mysql2@3.15.3))(esbuild@0.28.2)(ioredis@5.11.1)(magicast@0.5.4)(rolldown@1.2.4)(rollup@4.62.4)(uploadthing@7.7.4(express@5.2.1)(h3@1.15.11)(tailwindcss@4.3.3))(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41)(webpack@5.109.2) oxlint: specifier: ^1.44.0 version: 1.78.0(oxlint-tsgolint@7.0.2001) @@ -675,35 +690,14 @@ importers: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) - packages/mail: + packages/core/shared: devDependencies: - '@maizzle/framework': - specifier: ^6.0.13 - version: 6.1.0(shiki@4.4.3)(tailwind-merge@3.6.0)(vue@3.5.41) - '@maizzle/tailwindcss': - specifier: ^1.5.6 - version: 1.5.6 - '@types/nodemailer': - specifier: ^8.0.1 - version: 8.0.1 - nodemailer: - specifier: ^9.0.5 - version: 9.0.5 oxlint: specifier: ^1.44.0 version: 1.78.0(oxlint-tsgolint@7.0.2001) oxlint-tsgolint: specifier: ^7.0.2001 version: 7.0.2001 - resend: - specifier: 6.18.0 - version: 6.18.0 - shiki: - specifier: ^1 || ^2 || ^3 || ^4 - version: 4.4.3 - tailwind-merge: - specifier: ^2 || ^3 - version: 3.6.0 tsdown: specifier: ^0.22.14 version: 0.22.14(@arethetypeswrong/core@0.18.5)(@vitejs/devtools@0.4.12(vite@8.2.1))(publint@0.3.23)(tsx@4.23.12) @@ -716,40 +710,19 @@ importers: vitest: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) - vue: - specifier: ^3 - version: 3.5.41 - packages/models: + packages/database: dependencies: - '@ai-sdk/anthropic': - specifier: ^4.0.36 - version: 4.0.39(zod@4.4.3) - '@ai-sdk/google': - specifier: ^4.0.39 - version: 4.0.44(zod@4.4.3) - '@ai-sdk/openai': - specifier: ^4.0.36 - version: 4.0.42(zod@4.4.3) - '@ai-sdk/openai-compatible': - specifier: ^3.0.16 - version: 3.0.31(zod@4.4.3) - '@code-zero/shared': - specifier: workspace:* - version: 0.4.0 - ai: - specifier: ^7.0.40 - version: 7.0.66(zod@4.4.3) - ai-sdk-provider-claude-code: - specifier: ^4.1.0 - version: 4.1.1(zod@4.4.3) - ai-sdk-provider-codex-cli: - specifier: ^2.1.2 - version: 2.1.2(zod@4.4.3) - zod: - specifier: ^4.4.3 - version: 4.4.3 + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@electric-sql/pglite@0.4.3)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)))(@types/pg@8.23.0)(better-sqlite3@12.11.1)(kysely@0.29.5)(mysql2@3.15.3)(pg@8.23.0)(postgres@3.4.9)(prisma@7.9.1(better-sqlite3@12.11.1))(sql.js@1.14.2) + postgres: + specifier: ^3.4.9 + version: 3.4.9 devDependencies: + drizzle-kit: + specifier: ^0.31.10 + version: 0.31.10 oxlint: specifier: ^1.44.0 version: 1.78.0(oxlint-tsgolint@7.0.2001) @@ -769,18 +742,21 @@ importers: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) - packages/runner: + packages/i18n: dependencies: - '@code-zero/shared': - specifier: workspace:* - version: 0.4.0 - '@vite-hub/shell': - specifier: ^0.0.3 - version: 0.0.3(supports-color@8.1.1) - magic-regexp: - specifier: ^0.11.0 - version: 0.11.0(esbuild@0.28.2)(rolldown@1.2.4)(rollup@4.62.4)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))(webpack@5.109.2) + '@lunariajs/core': + specifier: ^0.2.0 + version: 0.2.0(supports-color@8.1.1) + vue-i18n-extract: + specifier: 2.0.7 + version: 2.0.7 devDependencies: + '@intlify/core-base': + specifier: ^11.4.8 + version: 11.4.8 + '@nuxtjs/i18n': + specifier: ^10.6.0 + version: 10.6.0(@azure/identity@4.13.1)(@azure/storage-blob@12.33.0)(@netlify/blobs@10.7.13)(@vercel/blob@2.8.0)(@vercel/functions@3.9.3(@aws-sdk/credential-provider-web-identity@3.972.49)(ws@8.21.3))(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.4.3)(@libsql/client@0.17.4)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)))(@types/pg@8.23.0)(better-sqlite3@12.11.1)(kysely@0.29.5)(mysql2@3.15.3)(pg@8.23.0)(postgres@3.4.9)(prisma@7.9.1(better-sqlite3@12.11.1))(sql.js@1.14.2))(mysql2@3.15.3))(esbuild@0.28.2)(ioredis@5.11.1)(magicast@0.5.4)(rolldown@1.2.4)(rollup@4.62.4)(uploadthing@7.7.4(express@5.2.1)(h3@1.15.11)(tailwindcss@4.3.3))(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41)(webpack@5.109.2) oxlint: specifier: ^1.44.0 version: 1.78.0(oxlint-tsgolint@7.0.2001) @@ -800,14 +776,35 @@ importers: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) - packages/shared: + packages/mail: devDependencies: + '@maizzle/framework': + specifier: ^6.0.13 + version: 6.1.0(shiki@4.4.3)(tailwind-merge@3.6.0)(vue@3.5.41) + '@maizzle/tailwindcss': + specifier: ^1.5.6 + version: 1.5.6 + '@types/nodemailer': + specifier: ^8.0.1 + version: 8.0.1 + nodemailer: + specifier: ^9.0.5 + version: 9.0.5 oxlint: specifier: ^1.44.0 version: 1.78.0(oxlint-tsgolint@7.0.2001) oxlint-tsgolint: specifier: ^7.0.2001 version: 7.0.2001 + resend: + specifier: 6.18.0 + version: 6.18.0 + shiki: + specifier: ^1 || ^2 || ^3 || ^4 + version: 4.4.3 + tailwind-merge: + specifier: ^2 || ^3 + version: 3.6.0 tsdown: specifier: ^0.22.14 version: 0.22.14(@arethetypeswrong/core@0.18.5)(@vitejs/devtools@0.4.12(vite@8.2.1))(publint@0.3.23)(tsx@4.23.12) @@ -820,6 +817,9 @@ importers: vitest: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(happy-dom@20.11.2)(vite@8.2.1(@types/node@24.13.3)(@vitejs/devtools@0.4.12)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) + vue: + specifier: ^3 + version: 3.5.41 packages/source-control: dependencies: @@ -18167,7 +18167,7 @@ snapshots: '@better-auth/core': 1.6.26(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1) '@better-auth/utils': 0.4.2 optionalDependencies: - drizzle-orm: 0.45.2(@electric-sql/pglite@0.4.3)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)))(@types/pg@8.23.0)(better-sqlite3@12.11.1)(kysely@0.29.5)(mysql2@3.15.3)(pg@8.23.0)(postgres@3.4.9)(prisma@7.9.1(better-sqlite3@12.11.1))(sql.js@1.14.2) + drizzle-orm: 0.45.2(@electric-sql/pglite@0.4.3)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)))(@types/pg@8.23.0)(better-sqlite3@12.11.1)(kysely@0.29.5)(mysql2@3.15.3)(pg@8.23.0)(postgres@3.4.9)(prisma@7.9.1)(sql.js@1.14.2) '@better-auth/drizzle-adapter@1.7.0-rc.5(@better-auth/core@1.7.0-rc.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)))(@types/pg@8.23.0)(better-sqlite3@12.11.1)(kysely@0.29.5)(mysql2@3.15.3)(pg@8.23.0)(postgres@3.4.9)(prisma@7.9.1(better-sqlite3@12.11.1))(sql.js@1.14.2))': dependencies: @@ -18227,7 +18227,7 @@ snapshots: '@better-auth/core': 1.6.26(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1) '@better-auth/utils': 0.4.2 optionalDependencies: - '@prisma/client': 5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)) + '@prisma/client': 5.22.0(prisma@7.9.1) prisma: 7.9.1 '@better-auth/prisma-adapter@1.7.0-rc.5(@better-auth/core@1.7.0-rc.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1))(@better-auth/utils@0.4.2)(@prisma/client@5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)))(prisma@7.9.1)': @@ -25049,9 +25049,9 @@ snapshots: nanostores: 1.5.1 zod: 4.4.3 optionalDependencies: - '@prisma/client': 5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)) + '@prisma/client': 5.22.0(prisma@7.9.1) drizzle-kit: 0.31.10 - drizzle-orm: 0.45.2(@electric-sql/pglite@0.4.3)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)))(@types/pg@8.23.0)(better-sqlite3@12.11.1)(kysely@0.29.5)(mysql2@3.15.3)(pg@8.23.0)(postgres@3.4.9)(prisma@7.9.1(better-sqlite3@12.11.1))(sql.js@1.14.2) + drizzle-orm: 0.45.2(@electric-sql/pglite@0.4.3)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0(prisma@7.9.1(better-sqlite3@12.11.1)))(@types/pg@8.23.0)(better-sqlite3@12.11.1)(kysely@0.29.5)(mysql2@3.15.3)(pg@8.23.0)(postgres@3.4.9)(prisma@7.9.1)(sql.js@1.14.2) mongodb: 7.5.0(@mongodb-js/zstd@7.0.0)(socks@2.8.9) mysql2: 3.15.3 pg: 8.23.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 11e08ef..60b2882 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - apps/* - packages/* + - packages/core/* # Drop-in `typescript` replacement whose checker runs on tsgo in-process. # Must be an exact version: the fork only publishes prerelease versions, which diff --git a/tsconfig.json b/tsconfig.json index 6d583d3..2786e53 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,13 @@ { "files": [], "references": [ - { "path": "packages/shared" }, + { "path": "packages/core/shared" }, { "path": "packages/build-env" }, - { "path": "packages/config" }, - { "path": "packages/models" }, - { "path": "packages/runner" }, + { "path": "packages/core/config" }, + { "path": "packages/core/models" }, + { "path": "packages/core/runner" }, { "path": "packages/source-control" }, - { "path": "packages/agent" }, + { "path": "packages/core/agent" }, { "path": "packages/cli" }, { "path": "packages/auth" }, { "path": "packages/api" }, From f91ef285f0296b0921044ba92fdad8ab4abadd69 Mon Sep 17 00:00:00 2001 From: RedStar Date: Sat, 17 Oct 2026 19:45:06 +0200 Subject: [PATCH 04/19] refactor: rename the dashboard app to apps/web The deployable app becomes @code-zero/web in apps/web, matching the v2 layout. Paths, the Turborepo task key, the Dockerfile, the CD workflow, the Vercel build command, i18n tooling globs and the docs follow. The September dashboard plan moves to docs/plans unchanged. --- .env.example | 2 +- .github/copilot-instructions.md | 2 +- .github/workflows/cd.yml | 4 +- .skills/code-zero-architecture/SKILL.md | 2 +- .skills/orpc-server/SKILL.md | 16 +- AGENTS.md | 12 +- CONTRIBUTING.md | 4 +- Dockerfile | 6 +- README.md | 20 +- .../docs/content/1.guide/10.api/1.overview.md | 8 +- .../1.guide/10.api/2.define-endpoints.md | 2 +- .../1.guide/11.authentication/1.overview.md | 4 +- apps/docs/content/1.guide/12.organizations.md | 2 +- apps/docs/content/1.guide/13.frontend.md | 6 +- .../1.guide/15.internationalization.md | 4 +- apps/docs/content/1.guide/16.deployment.md | 6 +- apps/docs/content/1.guide/3.installation.md | 4 +- .../1.guide/4.environment-variables.md | 4 +- .../content/1.guide/5.codebase/1.structure.md | 6 +- apps/docs/content/1.guide/9.database.md | 2 +- apps/marketing/README.md | 4 +- apps/marketing/nuxt.config.ts | 2 +- apps/{dashboard => web}/.env.example | 6 +- apps/{dashboard => web}/.nuxtrc | 0 apps/{dashboard => web}/app/app.vue | 0 .../app/assets/css/main.css | 0 apps/{dashboard => web}/app/auth.config.ts | 0 apps/{dashboard => web}/app/error.vue | 0 apps/{dashboard => web}/app/layouts/auth.vue | 0 .../app/layouts/default.vue | 0 .../app/pages/(auth)/login.vue | 0 .../app/pages/(auth)/signup.vue | 0 .../app/pages/(dashboard)/audit.vue | 0 .../app/pages/(dashboard)/index.vue | 0 .../organizations/accept-invitation/[id].vue | 0 .../(organizations)/organizations/index.vue | 0 apps/{dashboard => web}/app/pages/device.vue | 0 apps/{dashboard => web}/app/pages/invite.vue | 0 .../app/plugins/orpc.client.ts | 0 .../app/plugins/orpc.server.ts | 0 .../app/plugins/vue-query.ts | 0 apps/{dashboard => web}/app/types/.gitkeep | 0 apps/{dashboard => web}/config/env.ts | 0 apps/{dashboard => web}/golar.config.ts | 0 apps/{dashboard => web}/i18n/i18n.config.ts | 0 apps/{dashboard => web}/i18n/locales | 0 .../modules/audit/components/OutcomeBadge.vue | 0 .../modules/audit/components/SourceBadge.vue | 0 .../modules/audit/components/Table.vue | 0 .../audit/composables/useAuditLogs.test.ts | 0 .../modules/audit/composables/useAuditLogs.ts | 0 .../audit/composables/useAuditTrail.test.ts | 0 .../audit/composables/useAuditTrail.ts | 0 .../composables/useAuthAuditLogs.test.ts | 0 .../audit/composables/useAuthAuditLogs.ts | 0 .../{dashboard => web}/modules/audit/index.ts | 0 .../modules/audit/types/audit.ts | 0 .../modules/auth/components/UserMenu.vue | 0 .../auth/composables/useAuthErrorMessage.ts | 0 apps/{dashboard => web}/modules/auth/index.ts | 0 .../dashboard/components/NewTaskForm.vue | 0 .../dashboard/components/RunnerMetrics.vue | 0 .../dashboard/components/task/Inspector.vue | 0 .../dashboard/components/task/Status.vue | 0 .../dashboard/components/task/Table.vue | 0 .../dashboard/components/task/Timeline.vue | 0 .../dashboard/composables/useLiveOverview.ts | 0 .../modules/dashboard/index.ts | 0 .../modules/dashboard/types/dashboard.ts | 0 .../modules/i18n-strip-empty/index.ts | 0 .../organizations/components/InviteForm.vue | 0 .../organizations/components/MemberList.vue | 0 .../organizations/components/Switcher.vue | 0 .../composables/useOrganizations.ts | 0 .../modules/organizations/index.ts | 0 .../organizations/types/organization.ts | 0 .../shared/components/BuildEnvironment.vue | 0 .../shared/components/ColorModeToggle.vue | 0 .../modules/shared/components/ErrorPage.vue | 0 .../shared/components/LocaleSwitcher.vue | 0 .../shared/components/app/ShortcutsDialog.vue | 0 .../modules/shared/components/app/Sidebar.vue | 0 .../shared/composables/useAppShortcuts.ts | 0 .../shared/composables/useSidebarCollapsed.ts | 0 .../modules/shared/index.ts | 0 .../modules/shared/utils/error-status.ts | 0 apps/{dashboard => web}/nuxt.config.ts | 0 apps/{dashboard => web}/package.json | 2 +- apps/{dashboard => web}/playwright.config.ts | 0 apps/{dashboard => web}/public/robots.txt | 0 .../server/api/events.get.ts | 0 .../{dashboard => web}/server/api/v1/[...].ts | 0 apps/{dashboard => web}/server/auth.config.ts | 0 .../server/plugins/poller.ts | 0 .../server/routes/rpc/[...].ts | 0 .../server/routes/webhooks/github.post.ts | 0 .../{dashboard => web}/server/utils/access.ts | 0 .../server/utils/context.ts | 0 .../server/utils/database.ts | 0 .../server/utils/deployment.ts | 0 .../server/utils/environment.ts | 0 .../{dashboard => web}/server/utils/errors.ts | 0 .../server/utils/openapi.ts | 0 .../server/utils/overview.ts | 0 .../{dashboard => web}/server/utils/poller.ts | 0 .../server/utils/repositories.ts | 0 apps/{dashboard => web}/server/utils/store.ts | 0 apps/{dashboard => web}/test/e2e/auth.spec.ts | 0 .../test/e2e/dashboard.spec.ts | 0 .../{dashboard => web}/test/e2e/test-utils.ts | 0 .../test/nuxt/auth-config.spec.ts | 0 .../test/nuxt/build-env.spec.ts | 0 .../test/nuxt/components/AuditTable.spec.ts | 0 .../nuxt/components/BuildEnvironment.spec.ts | 0 .../nuxt/components/LocaleSwitcher.spec.ts | 0 .../test/nuxt/components/NewTaskForm.spec.ts | 0 .../nuxt/components/ShortcutsDialog.spec.ts | 0 .../nuxt/components/TaskInspector.spec.ts | 0 .../test/nuxt/components/TaskTable.spec.ts | 0 .../test/nuxt/components/UserMenu.spec.ts | 0 .../composables/useAuthErrorMessage.spec.ts | 0 .../test/nuxt/pages/invite.spec.ts | 0 .../test/nuxt/pages/login.spec.ts | 0 .../test/nuxt/pages/signup.spec.ts | 0 apps/{dashboard => web}/test/nuxt/setup.ts | 0 .../test/unit/access.test.ts | 0 apps/{dashboard => web}/test/unit/env.test.ts | 0 .../test/unit/environment.test.ts | 0 .../test/unit/error-status.test.ts | 0 .../test/unit/errors.test.ts | 0 .../test/unit/i18n-empty-placeholders.test.ts | 0 .../test/unit/openapi.test.ts | 0 .../test/unit/overview.test.ts | 0 .../test/unit/poller.test.ts | 0 .../test/unit/repositories.test.ts | 0 .../test/unit/store.test.ts | 0 apps/{dashboard => web}/tsconfig.json | 0 apps/{dashboard => web}/uno-preset-a11y.ts | 0 apps/{dashboard => web}/uno-preset-rtl.ts | 0 apps/{dashboard => web}/uno.config.ts | 0 apps/{dashboard => web}/uno.theme.ts | 0 apps/{dashboard => web}/vercel.json | 2 +- apps/{dashboard => web}/vitest.config.ts | 0 docs/architecture.md | 16 +- .../2026-09-05-dashboard-live-control.md} | 0 knip.jsonc | 4 +- package.json | 2 +- packages/api/src/operations.test.ts | 2 +- packages/api/src/operations.ts | 2 +- packages/api/src/orpc/auth.ts | 2 +- packages/api/src/orpc/logging.ts | 2 +- packages/api/src/repositories.ts | 2 +- packages/auth/src/auth.ts | 2 +- packages/cli/src/login.ts | 2 +- packages/database/.env.example | 2 +- packages/i18n/scripts/compare-translations.ts | 2 +- .../i18n/scripts/find-invalid-translations.ts | 4 +- packages/i18n/scripts/generate-i18n-schema.ts | 2 +- .../scripts/remove-unused-translations.ts | 4 +- pnpm-lock.yaml | 252 +++++++++--------- pnpm-workspace.yaml | 2 +- tsconfig.json | 2 +- turbo.jsonc | 2 +- 163 files changed, 220 insertions(+), 220 deletions(-) rename apps/{dashboard => web}/.env.example (98%) rename apps/{dashboard => web}/.nuxtrc (100%) rename apps/{dashboard => web}/app/app.vue (100%) rename apps/{dashboard => web}/app/assets/css/main.css (100%) rename apps/{dashboard => web}/app/auth.config.ts (100%) rename apps/{dashboard => web}/app/error.vue (100%) rename apps/{dashboard => web}/app/layouts/auth.vue (100%) rename apps/{dashboard => web}/app/layouts/default.vue (100%) rename apps/{dashboard => web}/app/pages/(auth)/login.vue (100%) rename apps/{dashboard => web}/app/pages/(auth)/signup.vue (100%) rename apps/{dashboard => web}/app/pages/(dashboard)/audit.vue (100%) rename apps/{dashboard => web}/app/pages/(dashboard)/index.vue (100%) rename apps/{dashboard => web}/app/pages/(organizations)/organizations/accept-invitation/[id].vue (100%) rename apps/{dashboard => web}/app/pages/(organizations)/organizations/index.vue (100%) rename apps/{dashboard => web}/app/pages/device.vue (100%) rename apps/{dashboard => web}/app/pages/invite.vue (100%) rename apps/{dashboard => web}/app/plugins/orpc.client.ts (100%) rename apps/{dashboard => web}/app/plugins/orpc.server.ts (100%) rename apps/{dashboard => web}/app/plugins/vue-query.ts (100%) rename apps/{dashboard => web}/app/types/.gitkeep (100%) rename apps/{dashboard => web}/config/env.ts (100%) rename apps/{dashboard => web}/golar.config.ts (100%) rename apps/{dashboard => web}/i18n/i18n.config.ts (100%) rename apps/{dashboard => web}/i18n/locales (100%) rename apps/{dashboard => web}/modules/audit/components/OutcomeBadge.vue (100%) rename apps/{dashboard => web}/modules/audit/components/SourceBadge.vue (100%) rename apps/{dashboard => web}/modules/audit/components/Table.vue (100%) rename apps/{dashboard => web}/modules/audit/composables/useAuditLogs.test.ts (100%) rename apps/{dashboard => web}/modules/audit/composables/useAuditLogs.ts (100%) rename apps/{dashboard => web}/modules/audit/composables/useAuditTrail.test.ts (100%) rename apps/{dashboard => web}/modules/audit/composables/useAuditTrail.ts (100%) rename apps/{dashboard => web}/modules/audit/composables/useAuthAuditLogs.test.ts (100%) rename apps/{dashboard => web}/modules/audit/composables/useAuthAuditLogs.ts (100%) rename apps/{dashboard => web}/modules/audit/index.ts (100%) rename apps/{dashboard => web}/modules/audit/types/audit.ts (100%) rename apps/{dashboard => web}/modules/auth/components/UserMenu.vue (100%) rename apps/{dashboard => web}/modules/auth/composables/useAuthErrorMessage.ts (100%) rename apps/{dashboard => web}/modules/auth/index.ts (100%) rename apps/{dashboard => web}/modules/dashboard/components/NewTaskForm.vue (100%) rename apps/{dashboard => web}/modules/dashboard/components/RunnerMetrics.vue (100%) rename apps/{dashboard => web}/modules/dashboard/components/task/Inspector.vue (100%) rename apps/{dashboard => web}/modules/dashboard/components/task/Status.vue (100%) rename apps/{dashboard => web}/modules/dashboard/components/task/Table.vue (100%) rename apps/{dashboard => web}/modules/dashboard/components/task/Timeline.vue (100%) rename apps/{dashboard => web}/modules/dashboard/composables/useLiveOverview.ts (100%) rename apps/{dashboard => web}/modules/dashboard/index.ts (100%) rename apps/{dashboard => web}/modules/dashboard/types/dashboard.ts (100%) rename apps/{dashboard => web}/modules/i18n-strip-empty/index.ts (100%) rename apps/{dashboard => web}/modules/organizations/components/InviteForm.vue (100%) rename apps/{dashboard => web}/modules/organizations/components/MemberList.vue (100%) rename apps/{dashboard => web}/modules/organizations/components/Switcher.vue (100%) rename apps/{dashboard => web}/modules/organizations/composables/useOrganizations.ts (100%) rename apps/{dashboard => web}/modules/organizations/index.ts (100%) rename apps/{dashboard => web}/modules/organizations/types/organization.ts (100%) rename apps/{dashboard => web}/modules/shared/components/BuildEnvironment.vue (100%) rename apps/{dashboard => web}/modules/shared/components/ColorModeToggle.vue (100%) rename apps/{dashboard => web}/modules/shared/components/ErrorPage.vue (100%) rename apps/{dashboard => web}/modules/shared/components/LocaleSwitcher.vue (100%) rename apps/{dashboard => web}/modules/shared/components/app/ShortcutsDialog.vue (100%) rename apps/{dashboard => web}/modules/shared/components/app/Sidebar.vue (100%) rename apps/{dashboard => web}/modules/shared/composables/useAppShortcuts.ts (100%) rename apps/{dashboard => web}/modules/shared/composables/useSidebarCollapsed.ts (100%) rename apps/{dashboard => web}/modules/shared/index.ts (100%) rename apps/{dashboard => web}/modules/shared/utils/error-status.ts (100%) rename apps/{dashboard => web}/nuxt.config.ts (100%) rename apps/{dashboard => web}/package.json (98%) rename apps/{dashboard => web}/playwright.config.ts (100%) rename apps/{dashboard => web}/public/robots.txt (100%) rename apps/{dashboard => web}/server/api/events.get.ts (100%) rename apps/{dashboard => web}/server/api/v1/[...].ts (100%) rename apps/{dashboard => web}/server/auth.config.ts (100%) rename apps/{dashboard => web}/server/plugins/poller.ts (100%) rename apps/{dashboard => web}/server/routes/rpc/[...].ts (100%) rename apps/{dashboard => web}/server/routes/webhooks/github.post.ts (100%) rename apps/{dashboard => web}/server/utils/access.ts (100%) rename apps/{dashboard => web}/server/utils/context.ts (100%) rename apps/{dashboard => web}/server/utils/database.ts (100%) rename apps/{dashboard => web}/server/utils/deployment.ts (100%) rename apps/{dashboard => web}/server/utils/environment.ts (100%) rename apps/{dashboard => web}/server/utils/errors.ts (100%) rename apps/{dashboard => web}/server/utils/openapi.ts (100%) rename apps/{dashboard => web}/server/utils/overview.ts (100%) rename apps/{dashboard => web}/server/utils/poller.ts (100%) rename apps/{dashboard => web}/server/utils/repositories.ts (100%) rename apps/{dashboard => web}/server/utils/store.ts (100%) rename apps/{dashboard => web}/test/e2e/auth.spec.ts (100%) rename apps/{dashboard => web}/test/e2e/dashboard.spec.ts (100%) rename apps/{dashboard => web}/test/e2e/test-utils.ts (100%) rename apps/{dashboard => web}/test/nuxt/auth-config.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/build-env.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/components/AuditTable.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/components/BuildEnvironment.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/components/LocaleSwitcher.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/components/NewTaskForm.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/components/ShortcutsDialog.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/components/TaskInspector.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/components/TaskTable.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/components/UserMenu.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/composables/useAuthErrorMessage.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/pages/invite.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/pages/login.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/pages/signup.spec.ts (100%) rename apps/{dashboard => web}/test/nuxt/setup.ts (100%) rename apps/{dashboard => web}/test/unit/access.test.ts (100%) rename apps/{dashboard => web}/test/unit/env.test.ts (100%) rename apps/{dashboard => web}/test/unit/environment.test.ts (100%) rename apps/{dashboard => web}/test/unit/error-status.test.ts (100%) rename apps/{dashboard => web}/test/unit/errors.test.ts (100%) rename apps/{dashboard => web}/test/unit/i18n-empty-placeholders.test.ts (100%) rename apps/{dashboard => web}/test/unit/openapi.test.ts (100%) rename apps/{dashboard => web}/test/unit/overview.test.ts (100%) rename apps/{dashboard => web}/test/unit/poller.test.ts (100%) rename apps/{dashboard => web}/test/unit/repositories.test.ts (100%) rename apps/{dashboard => web}/test/unit/store.test.ts (100%) rename apps/{dashboard => web}/tsconfig.json (100%) rename apps/{dashboard => web}/uno-preset-a11y.ts (100%) rename apps/{dashboard => web}/uno-preset-rtl.ts (100%) rename apps/{dashboard => web}/uno.config.ts (100%) rename apps/{dashboard => web}/uno.theme.ts (100%) rename apps/{dashboard => web}/vercel.json (91%) rename apps/{dashboard => web}/vitest.config.ts (100%) rename docs/{PLAN.md => plans/2026-09-05-dashboard-live-control.md} (100%) diff --git a/.env.example b/.env.example index e62714d..2b07241 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ # # .env this file — the `zero` CLI: model providers, # subscription transports, GITHUB_TOKEN -# apps/dashboard/.env the dashboard process, loaded by Nuxt itself: +# apps/web/.env the dashboard process, loaded by Nuxt itself: # control plane (CODE_ZERO_CONTROL_PLANE_*), # webhook ingress, DATABASE_URL, authentication, # mail, NUXT_PUBLIC_SITE_URL diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 722a41b..11fda1c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,6 +7,6 @@ Read `/AGENTS.md` before editing and load the relevant skill from `/.agents/skil - Keep runtime command execution and target-repository mutation inside `packages/core/runner`. - Keep `observe` mode read-only. - Add deterministic tests for behavior, state transitions, and safety-sensitive changes. -- Keep `apps/dashboard` frontend-only, use `@bomb.sh/args` plus `@clack/prompts` in the CLI, tsdown for package builds, and Oxlint/Oxfmt for code quality. +- Keep `apps/web` frontend-only, use `@bomb.sh/args` plus `@clack/prompts` in the CLI, tsdown for package builds, and Oxlint/Oxfmt for code quality. - Do not introduce Hono, ESLint, Prettier, npm, Yarn, or Bun without an accepted architectural proposal. - Run `aube run check:repo`, `aube run lint:ci`, `aube run typecheck`, `aube test`, and `aube run build` before handing off a complete change. diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index df42c9d..b30aeff 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -59,9 +59,9 @@ jobs: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | if [[ "$REF_TYPE" == "tag" ]]; then - version="$(node --print "require('./apps/dashboard/package.json').version")" + version="$(node --print "require('./apps/web/package.json').version")" if [[ "$REF_NAME" != "v$version" ]]; then - echo "::error::Tag $REF_NAME does not match apps/dashboard version v$version" >&2 + echo "::error::Tag $REF_NAME does not match apps/web version v$version" >&2 exit 1 fi build_env=release diff --git a/.skills/code-zero-architecture/SKILL.md b/.skills/code-zero-architecture/SKILL.md index 4ed68be..94072a8 100644 --- a/.skills/code-zero-architecture/SKILL.md +++ b/.skills/code-zero-architecture/SKILL.md @@ -51,7 +51,7 @@ Keep dependency direction explicit while changing the monorepo. - `api`: the oRPC router and control-plane operations (task persistence, scheduling). Composes the runtime, GitHub, models, and config packages; nothing composes into it, and it does not depend on `auth`. Holds no HTTP host of its own. -- `apps/dashboard`: the single deployable app and composition root. A Nuxt app that constructs a +- `apps/web`: the single deployable app and composition root. A Nuxt app that constructs a runner and, from its `server/` directory, serves `packages/api`'s router over `/rpc/**` (RPC) and `/api/v1/**` (OpenAPI), and mounts Better Auth in-process at `/api/auth/**` via `server/auth.config.ts` (`@onmax/nuxt-better-auth`, full mode, SSR-aware). The only process that diff --git a/.skills/orpc-server/SKILL.md b/.skills/orpc-server/SKILL.md index f3aeb39..168f5c8 100644 --- a/.skills/orpc-server/SKILL.md +++ b/.skills/orpc-server/SKILL.md @@ -1,11 +1,11 @@ --- name: orpc-server -description: Use when changing apps/dashboard server routes, oRPC contracts, handlers, middleware, transport setup, or typed clients. +description: Use when changing apps/web server routes, oRPC contracts, handlers, middleware, transport setup, or typed clients. --- # oRPC server -`packages/api` owns the router and its runtime delegation; `apps/dashboard`'s `server/` directory is the transport adapter and composition root that serves it, alongside the app's own Nuxt frontend. +`packages/api` owns the router and its runtime delegation; `apps/web`'s `server/` directory is the transport adapter and composition root that serves it, alongside the app's own Nuxt frontend. ## Rules @@ -14,7 +14,7 @@ description: Use when changing apps/dashboard server routes, oRPC contracts, han `control-plane.ts`, `access.ts`, and `dashboard.ts`. `packages/api` holds no HTTP host of its own — it only exports `rpcRouter`, `requestLoggerStorage`, and the plain functions those depend on. It does not depend on `packages/auth`. -- The HTTP host is Nuxt's own Nitro server. Routes live in `apps/dashboard/server/`: +- The HTTP host is Nuxt's own Nitro server. Routes live in `apps/web/server/`: - `routes/rpc/[...].ts` mounts `rpcRouter` over `RPCHandler` (the typed RPC transport). - `api/v1/[...].ts` mounts the same `rpcRouter` over `OpenAPIHandler` (REST + docs at `/api/v1/docs`). - `auth.config.ts` (at the `server/` root) configures `@onmax/nuxt-better-auth`, which mounts @@ -28,9 +28,9 @@ description: Use when changing apps/dashboard server routes, oRPC contracts, han plain-Node unit test loads directly (`server/utils/errors.ts` imports `createError` from `h3` for exactly that reason). `app/**/*.ts` is unchanged and still uses explicit imports. `typecheck` is one `nuxt typecheck` pass - (Golar, configured in `apps/dashboard/golar.config.ts`) covering `app/`, `modules/`, `server/`, + (Golar, configured in `apps/web/golar.config.ts`) covering `app/`, `modules/`, `server/`, and `test/` — there is no separate `tsc --project` or `vue-tsc` invocation. -- `apps/dashboard` pins `nuxt` to the stable v4 channel (`^4.5.2` in `package.json`), whose +- `apps/web` pins `nuxt` to the stable v4 channel (`^4.5.2` in `package.json`), whose `@nuxt/nitro-server` depends on `nitropack` v2 + classic h3 (`h3@^1.15.11`, also a direct dependency), not the standalone Nitro v3 package. Route handlers use classic h3's API: `import { defineEventHandler, toWebRequest } from 'h3';`. Call `toWebRequest(event)` to get a @@ -52,9 +52,9 @@ description: Use when changing apps/dashboard server routes, oRPC contracts, han - Procedures call the agent runtime through typed APIs. They do not execute shell commands or mutate checkouts directly. - Persist through the `KeyValueStorage` contract, adapted over the ViteHub KV Runtime Helper in - `apps/dashboard/server/utils/store.ts`, so KV drivers stay interchangeable; never store review + `apps/web/server/utils/store.ts`, so KV drivers stay interchangeable; never store review input or checkout paths. ViteHub is composed into Nuxt's own Nitro build from - `apps/dashboard/nuxt.config.ts`, which registers `vite-hub/nuxt` as an ordinary module entry + `apps/web/nuxt.config.ts`, which registers `vite-hub/nuxt` as an ordinary module entry (`['vite-hub/nuxt', { preset, kv: true }]`) with the deployment preset from `config/env.ts`. Prove KV changes with a real `nuxt build` and a request against a route that reads the store, not just `nuxt prepare` or a type check. - Transport failures are `H3Error`s from the catalogue in `server/utils/errors.ts` @@ -86,5 +86,5 @@ description: Use when changing apps/dashboard server routes, oRPC contracts, han 3. Keep the handler thin: validate, authorize, delegate, translate. 4. Add router tests with `createRouterClient`, without opening a real network port. 5. Update the README client example when the public router shape changes. -6. Run `aube run test --filter @code-zero/api`, then `--filter @code-zero/dashboard`, typecheck, +6. Run `aube run test --filter @code-zero/api`, then `--filter @code-zero/web`, typecheck, and build. For KV or transport-mounting changes, also run a real `nuxt build` and hit the route. diff --git a/AGENTS.md b/AGENTS.md index f8e399c..a80290f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,7 +48,7 @@ Code Zero is an open-source autonomous engineer that finds, fixes, and verifies | `packages/api` | `@code-zero/api` | The oRPC router and control-plane operations | | `packages/i18n` | `@code-zero/i18n` | Locale messages and i18n tooling | | `packages/mail` | `@code-zero/mail` | Transactional mail templates | -| `apps/dashboard` | `@code-zero/dashboard` | The single deployable app and composition root | +| `apps/web` | `@code-zero/web` | The single deployable app and composition root | | `apps/docs` | `@code-zero/docs` | Docus documentation site (not deployed with the dashboard) | | `apps/marketing` | `@code-zero/marketing` | Frontend-only public marketing site | | `apps/mail-preview` | `@code-zero/mail-preview` | Dev-only Maizzle preview server for `packages/mail` | @@ -59,7 +59,7 @@ Code Zero is an open-source autonomous engineer that finds, fixes, and verifies - Use aube for dependencies and scripts. It reads and writes `pnpm-lock.yaml` and `pnpm-workspace.yaml` in place; keep both files and do not create npm, Yarn, or Bun lockfiles. - `typescript` is overridden to `typescript-native-bridge` in `pnpm-workspace.yaml`, so `tsc` and every Compiler API consumer type-check on tsgo. Keep the pin exact and reinstall after changing it. - Use Turborepo through the root scripts; do not duplicate orchestration in package scripts. -- Use tsdown through each tsdown-built package's `tsdown.config.ts` and the shared `scripts/tsdown.config.ts`. `apps/dashboard` uses the Nuxt build pipeline, registering `vite-hub/nuxt` from `apps/dashboard/nuxt.config.ts` to compose ViteHub into Nuxt's own Nitro build. +- Use tsdown through each tsdown-built package's `tsdown.config.ts` and the shared `scripts/tsdown.config.ts`. `apps/web` uses the Nuxt build pipeline, registering `vite-hub/nuxt` from `apps/web/nuxt.config.ts` to compose ViteHub into Nuxt's own Nitro build. - Use Oxlint with type-aware checks and Oxfmt. Do not add ESLint or Prettier. - Do not edit `dist/`, `.turbo/`, or generated declaration files. @@ -74,7 +74,7 @@ aube ci # Seed local environment files cp .env.example .env -cp apps/dashboard/.env.example apps/dashboard/.env +cp apps/web/.env.example apps/web/.env ``` `aube run diff --git a/apps/marketing/modules/home/components/LogoCloud.vue b/apps/marketing/modules/home/components/LogoCloud.vue index 0d54380..23b8b14 100644 --- a/apps/marketing/modules/home/components/LogoCloud.vue +++ b/apps/marketing/modules/home/components/LogoCloud.vue @@ -1,14 +1,14 @@