diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..171358e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +_primitives/_rust/target/ +**/target/ +.DS_Store diff --git a/_blocks/stack-astro.md b/_blocks/stack-astro.md new file mode 100644 index 0000000..dd2005e --- /dev/null +++ b/_blocks/stack-astro.md @@ -0,0 +1,36 @@ +# STACK — Astro 6 (Content + Marketing + Islands) + +Use for marketing sites, content-heavy sites, docs, and landing pages. Zero-JS by default; interactivity is opt-in per component via islands. + +**When to pick:** the page is >70% static content (marketing, blog, docs, portfolio). For app-like surfaces (dashboards, editors, long session state) prefer `stack-nextjs` or `stack-react-vite`. + +**Routing:** file-based (`src/pages/`). `.astro` components render to HTML at build time. Dynamic routes via `[slug].astro` + `getStaticPaths`. + +**Islands:** any framework component (React / Svelte / Vue / Solid) renders via an integration and takes a `client:*` directive: + +- `client:load` — hydrate immediately (interactive from first paint) +- `client:idle` — hydrate when main thread idle +- `client:visible` — hydrate when visible (default for below-fold widgets) +- `client:media="(max-width: 768px)"` — hydrate only on matching viewport +- `client:only="react"` — skip SSR entirely (client-only components) + +No directive = zero JS shipped. Never add one "just in case". + +**React integration:** `npx astro add react` → installs `@astrojs/react`. Then import and use `.tsx` components inside `.astro` with a `client:*` directive where interactivity is needed. + +**Deploy adapter (Cloudflare default):** `npx astro add cloudflare` → installs `@astrojs/cloudflare`. In `astro.config.mjs` set `output: "server"` (for per-request SSR) or `"hybrid"` (pre-render by default, SSR where opted in). Static-only builds need no adapter — `astro build` emits `dist/`. + +**Content collections:** `src/content//*.md(x)` + `src/content/config.ts` (Zod schema). Type-safe queries via `getCollection(name)`. Use for blog posts, case studies, docs. + +**View Transitions:** `import { ViewTransitions } from "astro:transitions"` — 2 lines in the base layout, zero JS overhead. Pairs well with the `motion-design` skill. + +**Env vars:** +- Build-time: `import.meta.env.FOO` (inlined). `PUBLIC_*` prefix is client-visible; everything else is build-host only. +- Runtime (server/SSR): via adapter runtime (`context.locals.runtime.env` on Cloudflare Workers). +- Secrets go in platform env (Cloudflare dashboard / `.dev.vars` locally). NEVER in `astro.config.mjs`. + +**Images:** `` from `astro:assets` — automatic `srcset`, `sizes`, `width`/`height`, AVIF/WebP fallback. Pair with `web-assets` skill for pipeline details. + +**Typical stack:** Astro 6 + TypeScript + Tailwind 4 (via `stack-tailwind`) + `@astrojs/react` for islands + `adapter-cloudflare` + Content Collections. Files > 200 LOC get split (Constructor Pattern). + +**Forbidden:** `client:load` on static content, importing React at the top of `.astro` pages that don't render interactive components, secrets in `PUBLIC_*` vars, mixing two UI frameworks without a concrete reason (ships multiple hydration runtimes). diff --git a/_blocks/stack-react-vite.md b/_blocks/stack-react-vite.md new file mode 100644 index 0000000..50aa350 --- /dev/null +++ b/_blocks/stack-react-vite.md @@ -0,0 +1,26 @@ +# STACK — Vite + React 19 + TypeScript (SPA) + +Use for single-page applications, internal dashboards, editors, design tools — surfaces where the page IS the app and SEO / zero-JS don't matter. For marketing use `stack-astro`. For full-stack React with Server Components use `stack-nextjs`. + +**Scaffold:** `npm create vite@latest -- --template react-ts`. Dev server via `vite`; production via `vite build` → `dist/` (static files). + +**Routing:** `react-router-dom` v7 (data routers, `createBrowserRouter`). One file per route under `src/routes/`. For file-based routing prefer TanStack Router (first-class TS inference). + +**Data:** +- Server state → TanStack Query v5 (`useQuery` / `useMutation`). Never `useEffect + fetch`. +- Client state → Zustand or `useState` — pick one per feature, don't layer Redux unless the team already uses it. +- Form state → React Hook Form v7 + Zod resolver (single schema client + server). + +**Rendering:** React 19's `useActionState`, `useOptimistic`, and the `use()` hook for promise unwrapping. `Suspense` + `ErrorBoundary` on every route boundary. No conditional rendering that hides suspense errors. + +**Types first:** Props/interfaces declared BEFORE the component. Discriminated unions for variant props. `as const` for finite-set string unions. No `any` in new code — use `unknown` + type-guards. + +**Env vars:** `import.meta.env.VITE_*` — anything NOT prefixed with `VITE_` is stripped at build time. Secrets → backend, never in `VITE_*` (ships to browser). + +**Styling:** Tailwind 4 (via `stack-tailwind`) OR CSS Modules — never both in the same project. `className` + token classes; no inline `style={{}}` except for dynamic CSS custom properties. + +**Testing:** Vitest + React Testing Library + Playwright for E2E. Tests co-located next to source (`Component.test.tsx`). + +**Deploy target:** the SPA is a static bundle — Cloudflare Pages, Vercel, S3+CloudFront, or any static host. No adapter needed. + +**Forbidden:** `create-react-app` (deprecated), `fetch` inside `useEffect` (use TanStack Query), `any` in new code, secret env vars without the `VITE_` prefix (shipped to client), mixing Redux + Zustand in the same feature, CSS-in-JS runtimes (ships extra KB — use Tailwind or CSS Modules). diff --git a/_blocks/stack-sveltekit.md b/_blocks/stack-sveltekit.md new file mode 100644 index 0000000..2da76cf --- /dev/null +++ b/_blocks/stack-sveltekit.md @@ -0,0 +1,34 @@ +# STACK — SvelteKit (Svelte 5 Runes + TS) + +Use for animation-heavy sites, mobile-first interactive surfaces, and apps where smallest-possible runtime matters. Svelte 5 compiles to minimal JS; Runes replace the legacy reactive-label syntax. + +**Scaffold:** `npm create svelte@latest ` → choose "SvelteKit", TypeScript strict, ESLint + Prettier. + +**Routing:** file-based (`src/routes/`). Each route is a folder containing `+page.svelte` (UI) + optionally `+page.server.ts` (server load / actions) / `+page.ts` (universal load) / `+layout.svelte`. Dynamic routes via `[slug]/+page.svelte`. + +**Runes (Svelte 5):** +- `$state(x)` — reactive value (replaces `let x = ...` + `$:` label) +- `$derived(expr)` — computed (replaces `$:` derivations) +- `$effect(() => {...})` — side effect (replaces `$:` statements with side effects) +- `$props()` — component props (replaces `export let`) +- `$bindable()` — two-way binding opt-in + +No more legacy `export let` / `$: foo = bar` in new code. Runes are the canonical API from Svelte 5 onwards. + +**Data flow:** +- `+page.server.ts` `load({ fetch, params })` — runs on server only, DB/secrets OK. +- `+page.ts` `load(...)` — runs both server (SSR) + client (navigation) — no secrets. +- Form actions: `export const actions = { default: async ({ request }) => {...} }` in `+page.server.ts`. Use `
` + progressive enhancement — works without JS. + +**Env vars:** +- `$env/static/private` + `$env/dynamic/private` — server-only, secrets OK. +- `$env/static/public` + `$env/dynamic/public` — must be prefixed `PUBLIC_`, ships to client. +- SvelteKit refuses to build if a private env is imported into a client module — enforcement built in. + +**Deploy adapter (Cloudflare default):** `npm i -D @sveltejs/adapter-cloudflare` and set it in `svelte.config.js`. Alternatives: `adapter-node`, `adapter-vercel`, `adapter-static`. Cloudflare adapter supports KV / R2 / D1 via `platform.env.*` inside load functions. + +**Stores (legacy, still supported):** `writable`, `readable`, `derived` from `svelte/store`. Prefer `$state` in components; use stores only for cross-component shared state that truly needs it. + +**Testing:** Vitest for unit + `@testing-library/svelte` for components + Playwright for E2E. + +**Forbidden:** legacy `export let` + `$:` label syntax for NEW code (use Runes), `$env/static/private` imported into a client-reachable module, mixing runes + legacy reactivity in the same component, hardcoded secrets in `svelte.config.js` (ships to client bundle), adding React/Vue into a SvelteKit app without a very specific reason. diff --git a/_blocks/stack-tailwind.md b/_blocks/stack-tailwind.md new file mode 100644 index 0000000..7122117 --- /dev/null +++ b/_blocks/stack-tailwind.md @@ -0,0 +1,32 @@ +# STACK — Tailwind CSS 4 (compositional add-on) + +This is a **compositional** block — it does NOT stand alone. Layer on top of `stack-nextjs`, `stack-react-vite`, `stack-astro`, or `stack-sveltekit`. Any of those + this = the canonical 2026 "Tailwind project" shape. + +**Version:** Tailwind 4.x — ships as a Vite / PostCSS / CLI plugin, NOT as a dependency you import into your framework code. Config lives in CSS via `@theme` (not `tailwind.config.ts` — that is v3). + +**Minimal setup (v4):** +```css +/* src/styles/app.css */ +@import "tailwindcss"; + +@theme { + --color-brand: oklch(0.6 0.2 250); + --font-display: "Fraunces Variable", serif; + --font-body: "Inter Variable", sans-serif; + --radius-card: 0.75rem; +} +``` + +Any `--color-*`, `--font-*`, `--radius-*`, `--spacing-*`, `--breakpoint-*` declared in `@theme` auto-generates utilities (`bg-brand`, `font-display`, `rounded-card`, etc.). + +**Design tokens are CSS custom properties**, not JS config. Same tokens reachable from runtime (`var(--color-brand)`) + Tailwind classes (`text-brand`). Single source of truth; no duplication. + +**Dark mode:** `@custom-variant dark (&:where(.dark, .dark *))` (or `@media (prefers-color-scheme: dark)` for system-driven). Then `dark:bg-neutral-900` works as expected. + +**Utilities forbidden in new code:** `@apply` in component CSS (makes purge harder, obscures which utilities render). Use the class attribute directly, or extract to a real component. + +**Class composition:** use `clsx` or `tailwind-merge` (`cn()` helper pattern) for conditional classes. Never `className={"bg-red " + (active ? "opacity-100" : "opacity-0")}` — use `cn()`. + +**Component libraries:** shadcn/ui (copy-paste, source-owned), Radix primitives, Headless UI — all compatible. Avoid UI kits that ship their own runtime CSS (MUI, Chakra) on top of Tailwind — the two design systems will fight. + +**Forbidden:** `tailwind.config.js` for NEW v4 projects (use `@theme` in CSS), `@apply` beyond tiny one-offs, mixing Tailwind with MUI / Chakra / Bootstrap, hardcoded hex colors in `className` (`bg-[#ff0000]`) outside prototyping — those bypass the token system and drift. diff --git a/_primitives/_rust/Cargo.lock b/_primitives/_rust/Cargo.lock index 4e8eae3..c3e0e28 100644 --- a/_primitives/_rust/Cargo.lock +++ b/_primitives/_rust/Cargo.lock @@ -2,6 +2,48 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -38,7 +80,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -49,7 +91,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -58,11 +100,98 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] [[package]] name = "cfg-if" @@ -70,6 +199,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "clap" version = "4.6.1" @@ -116,6 +257,134 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -129,15 +398,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "firewall-diff" version = "0.1.0" @@ -149,12 +467,146 @@ dependencies = [ "tempfile", ] +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -163,11 +615,34 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] +[[package]] +name = "git2" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -183,18 +658,200 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -219,6 +876,71 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kei-changelog" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "git2", + "regex", +] + +[[package]] +name = "kei-ledger" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "rusqlite", + "serde", + "serde_json", + "tempfile", +] + +[[package]] +name = "kei-migrate" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "sha2", + "sqlx", + "tempfile", + "tokio", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -231,24 +953,205 @@ version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +[[package]] +name = "libgit2-sys" +version = "0.17.0+1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mock-render" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "sha2", + "tempfile", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -261,6 +1164,126 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -280,6 +1303,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + [[package]] name = "quote" version = "1.0.45" @@ -289,12 +1318,143 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustix" version = "1.1.4" @@ -305,15 +1465,67 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "semver" version = "1.0.28" @@ -363,6 +1575,18 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -376,6 +1600,301 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27144619c6e5802f1380337a209d2ac1c431002dd74c6e60aebff3c506dc4f0c" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a999083c1af5b5d6c071d34a708a19ba3e02106ad82ef7bbd69f5e48266b613b" +dependencies = [ + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.14.5", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23217eb7d86c584b8cbe0337b9eacf12ab76fe7673c513141ec42565698bb88" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a099220ae541c5db479c6424bdf1b200987934033c2584f79a0e1693601e776" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5afe4c38a9b417b6a9a5eeffe7235d0a106716495536e7727d1c7f4b1ff3eba6" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1dbb157e65f10dbe01f729339c06d239120221c9ad9fa0ba8408c4cc18ecf21" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2cdd83c008a622d94499c0006d8ee5f821f36c89b7d625c900e5dc30b5c5ee" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "tracing", + "url", +] + [[package]] name = "ssh-check" version = "0.1.0" @@ -386,12 +1905,35 @@ dependencies = [ "tempfile", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -403,6 +1945,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -410,36 +1963,242 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokens-sync" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tempfile", +] + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "visual-diff" +version = "0.1.0" +dependencies = [ + "image", + "tempfile", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.3+wasi-0.2.9" @@ -458,6 +2217,57 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -492,12 +2302,99 @@ dependencies = [ "semver", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -507,6 +2404,127 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -601,6 +2619,115 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/_primitives/_rust/mock-render/Cargo.toml b/_primitives/_rust/mock-render/Cargo.toml new file mode 100644 index 0000000..344d49c --- /dev/null +++ b/_primitives/_rust/mock-render/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mock-render" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[[bin]] +name = "mock-render" +path = "src/main.rs" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } + +[dev-dependencies] +tempfile = "3" diff --git a/_primitives/_rust/mock-render/src/hash.rs b/_primitives/_rust/mock-render/src/hash.rs new file mode 100644 index 0000000..a80001d --- /dev/null +++ b/_primitives/_rust/mock-render/src/hash.rs @@ -0,0 +1,48 @@ +//! SHA-256 helpers. Used for WYSIWYD invariant checks +//! (source file must not mutate between lock and verify). + +use sha2::{Digest, Sha256}; +use std::fs; +use std::path::Path; + +pub fn hash_file(path: &Path) -> Result { + let bytes = fs::read(path).map_err(|e| format!("read {}: {e}", path.display()))?; + let mut hasher = Sha256::new(); + hasher.update(&bytes); + Ok(format!("{:x}", hasher.finalize())) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn hashes_empty_file() { + let mut f = tempfile::NamedTempFile::new().unwrap(); + f.write_all(b"").unwrap(); + let h = hash_file(f.path()).unwrap(); + // sha256("") — well-known constant + assert_eq!( + h, + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ); + } + + #[test] + fn hashes_hello_world() { + let mut f = tempfile::NamedTempFile::new().unwrap(); + f.write_all(b"hello world").unwrap(); + let h = hash_file(f.path()).unwrap(); + assert_eq!( + h, + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" + ); + } + + #[test] + fn returns_err_on_missing_file() { + let result = hash_file(Path::new("/nonexistent/path/xyz")); + assert!(result.is_err()); + } +} diff --git a/_primitives/_rust/mock-render/src/main.rs b/_primitives/_rust/mock-render/src/main.rs new file mode 100644 index 0000000..ed9a091 --- /dev/null +++ b/_primitives/_rust/mock-render/src/main.rs @@ -0,0 +1,226 @@ +//! mock-render — enforces the WYSIWYD invariant (What You See Is What's Deployed) +//! for block-based site-builder. Every section = one source file; screenshot is +//! a render of that file; lock freezes the hash; verify fails if source mutated. +//! +//! USAGE +//! mock-render screenshot --out [--viewport WxH] +//! mock-render lock --project --section [--screenshot ] +//! mock-render verify --project --section +//! mock-render status --project + +mod hash; +mod render; +mod state; + +use state::{Section, SiteState}; +use std::env; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +fn main() -> ExitCode { + let args: Vec = env::args().skip(1).collect(); + match args.first().map(String::as_str) { + Some("screenshot") => cmd_screenshot(&args[1..]), + Some("lock") => cmd_lock(&args[1..]), + Some("verify") => cmd_verify(&args[1..]), + Some("status") => cmd_status(&args[1..]), + Some("--help") | Some("-h") | None => { + print_help(); + ExitCode::SUCCESS + } + Some(cmd) => { + eprintln!("mock-render: unknown command '{cmd}'. Run with --help."); + ExitCode::from(1) + } + } +} + +fn print_help() { + println!( + "mock-render — WYSIWYD invariant enforcer for site-builder + +USAGE + mock-render screenshot --out [--viewport WxH] + mock-render lock --project --section [--screenshot ] + mock-render verify --project --section + mock-render status --project + +EXIT + 0 ok + 1 usage / missing args + 2 WYSIWYD invariant violated (file drift / hash mismatch)" + ); +} + +fn cmd_screenshot(args: &[String]) -> ExitCode { + let Some(url) = args.first().cloned() else { + eprintln!("screenshot: required"); + return ExitCode::from(1); + }; + let out = match flag(args, "--out") { + Some(p) => PathBuf::from(p), + None => { + eprintln!("screenshot: --out required"); + return ExitCode::from(1); + } + }; + let viewport = flag(args, "--viewport").and_then(parse_viewport); + + match render::screenshot(&url, &out, viewport) { + Ok(()) => { + println!("{}", out.display()); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("mock-render: {e}"); + ExitCode::from(1) + } + } +} + +fn cmd_lock(args: &[String]) -> ExitCode { + let (project, section) = match require_project_section(args) { + Ok(v) => v, + Err(e) => { + eprintln!("lock: {e}"); + return ExitCode::from(1); + } + }; + let screenshot = flag(args, "--screenshot"); + + let Ok(hash_now) = hash::hash_file(§ion) else { + eprintln!("lock: cannot hash {}", section.display()); + return ExitCode::from(2); + }; + + let mut st = match SiteState::load(&project) { + Ok(s) => s, + Err(e) => { + eprintln!("lock: {e}"); + return ExitCode::from(2); + } + }; + + let key = SiteState::key_for(§ion); + st.sections.insert( + key.clone(), + Section { + path: section.display().to_string(), + sha256: hash_now.clone(), + locked: true, + screenshot: screenshot.map(String::from), + }, + ); + + if let Err(e) = st.save(&project) { + eprintln!("lock: {e}"); + return ExitCode::from(2); + } + + println!("locked {key} ({})", &hash_now[..12]); + ExitCode::SUCCESS +} + +fn cmd_verify(args: &[String]) -> ExitCode { + let (project, section) = match require_project_section(args) { + Ok(v) => v, + Err(e) => { + eprintln!("verify: {e}"); + return ExitCode::from(1); + } + }; + + let st = match SiteState::load(&project) { + Ok(s) => s, + Err(e) => { + eprintln!("verify: {e}"); + return ExitCode::from(2); + } + }; + + let key = SiteState::key_for(§ion); + let Some(entry) = st.sections.get(&key) else { + eprintln!("verify: section '{key}' not in site-state.json (not locked yet)"); + return ExitCode::SUCCESS; + }; + if !entry.locked { + return ExitCode::SUCCESS; + } + + let Ok(hash_now) = hash::hash_file(§ion) else { + eprintln!("verify: cannot hash {}", section.display()); + return ExitCode::from(2); + }; + + if hash_now != entry.sha256 { + eprintln!( + "WYSIWYD VIOLATION: {key} drifted\n locked : {}\n current: {}\nThe screenshot user approved no longer matches the source.\nRerun render + user-approval before deploy.", + &entry.sha256[..12], + &hash_now[..12] + ); + return ExitCode::from(2); + } + println!("ok {key} ({})", &hash_now[..12]); + ExitCode::SUCCESS +} + +fn cmd_status(args: &[String]) -> ExitCode { + let project = flag(args, "--project") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")); + + let st = match SiteState::load(&project) { + Ok(s) => s, + Err(e) => { + eprintln!("status: {e}"); + return ExitCode::from(2); + } + }; + + if st.sections.is_empty() { + println!("(no sections tracked)"); + return ExitCode::SUCCESS; + } + + for (name, sec) in &st.sections { + let lock = if sec.locked { "LOCKED" } else { "open" }; + let drift = match hash::hash_file(Path::new(&sec.path)) { + Ok(h) if h == sec.sha256 => "clean", + Ok(_) => "DRIFT", + Err(_) => "missing", + }; + println!( + "{:<20} {:>6} {:<7} {} ({})", + name, + lock, + drift, + sec.path, + &sec.sha256[..12] + ); + } + ExitCode::SUCCESS +} + +fn flag<'a>(args: &'a [String], name: &str) -> Option<&'a str> { + args.windows(2) + .find(|w| w[0] == name) + .map(|w| w[1].as_str()) +} + +fn parse_viewport(s: &str) -> Option<(u32, u32)> { + let (w, h) = s.split_once('x')?; + Some((w.parse().ok()?, h.parse().ok()?)) +} + +fn require_project_section(args: &[String]) -> Result<(PathBuf, PathBuf), String> { + let project = flag(args, "--project") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")); + let section = flag(args, "--section") + .map(PathBuf::from) + .ok_or_else(|| "--section required".to_string())?; + if !section.exists() { + return Err(format!("section file not found: {}", section.display())); + } + Ok((project, section)) +} diff --git a/_primitives/_rust/mock-render/src/render.rs b/_primitives/_rust/mock-render/src/render.rs new file mode 100644 index 0000000..68e1bfb --- /dev/null +++ b/_primitives/_rust/mock-render/src/render.rs @@ -0,0 +1,49 @@ +//! Playwright subprocess wrapper (RULE 0.2 exception 6 — JS-only binding). +//! Calls `npx playwright screenshot` with clear error messages. +//! +//! Requires Node + `npx`. Playwright browsers installable via: +//! npx playwright install chromium + +use std::path::Path; +use std::process::Command; + +/// Render a URL (typically http://localhost:/) or a file:// URL +/// to a PNG via Playwright's CLI. +pub fn screenshot(url: &str, out: &Path, viewport: Option<(u32, u32)>) -> Result<(), String> { + let mut args = vec![ + "--yes".to_string(), + "playwright".to_string(), + "screenshot".to_string(), + "--full-page".to_string(), + ]; + + if let Some((w, h)) = viewport { + args.push("--viewport-size".to_string()); + args.push(format!("{w},{h}")); + } + + args.push(url.to_string()); + args.push(out.display().to_string()); + + let output = Command::new("npx") + .args(&args) + .output() + .map_err(|e| format!("npx spawn: {e} — is Node installed?"))?; + + if !output.status.success() { + return Err(format!( + "playwright screenshot failed (exit {}):\n{}", + output.status.code().unwrap_or(-1), + String::from_utf8_lossy(&output.stderr) + )); + } + + if !out.exists() { + return Err(format!( + "playwright claimed success but {} was not written", + out.display() + )); + } + + Ok(()) +} diff --git a/_primitives/_rust/mock-render/src/state.rs b/_primitives/_rust/mock-render/src/state.rs new file mode 100644 index 0000000..d09bf11 --- /dev/null +++ b/_primitives/_rust/mock-render/src/state.rs @@ -0,0 +1,88 @@ +//! site-state.json — single file that tracks which sections are locked +//! and what their approved SHA-256 is. One row per section. + +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +const DEFAULT_STATE: &str = "site-state.json"; + +#[derive(Serialize, Deserialize, Default)] +pub struct SiteState { + pub sections: BTreeMap, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Section { + pub path: String, + pub sha256: String, + pub locked: bool, + pub screenshot: Option, +} + +impl SiteState { + pub fn load(project: &Path) -> Result { + let p = Self::path(project); + if !p.exists() { + return Ok(Self::default()); + } + let text = fs::read_to_string(&p).map_err(|e| format!("read state: {e}"))?; + serde_json::from_str(&text).map_err(|e| format!("parse state: {e}")) + } + + pub fn save(&self, project: &Path) -> Result<(), String> { + let p = Self::path(project); + let text = serde_json::to_string_pretty(self).map_err(|e| format!("serialize: {e}"))?; + fs::write(&p, text).map_err(|e| format!("write state: {e}")) + } + + fn path(project: &Path) -> PathBuf { + project.join(DEFAULT_STATE) + } + + pub fn key_for(section_path: &Path) -> String { + section_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unknown") + .to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn key_uses_file_stem() { + let k = SiteState::key_for(Path::new("src/sections/Hero.astro")); + assert_eq!(k, "Hero"); + } + + #[test] + fn load_missing_returns_default() { + let tmp = tempfile::tempdir().unwrap(); + let st = SiteState::load(tmp.path()).unwrap(); + assert!(st.sections.is_empty()); + } + + #[test] + fn save_and_reload_roundtrips() { + let tmp = tempfile::tempdir().unwrap(); + let mut st = SiteState::default(); + st.sections.insert( + "Hero".into(), + Section { + path: "src/sections/Hero.astro".into(), + sha256: "abcdef".repeat(10), + locked: true, + screenshot: Some("mocks/Hero.png".into()), + }, + ); + st.save(tmp.path()).unwrap(); + let reloaded = SiteState::load(tmp.path()).unwrap(); + assert_eq!(reloaded.sections.len(), 1); + assert!(reloaded.sections.get("Hero").unwrap().locked); + } +} diff --git a/_primitives/_rust/tokens-sync/Cargo.toml b/_primitives/_rust/tokens-sync/Cargo.toml new file mode 100644 index 0000000..e726c30 --- /dev/null +++ b/_primitives/_rust/tokens-sync/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tokens-sync" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[[bin]] +name = "tokens-sync" +path = "src/main.rs" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } + +[dev-dependencies] +tempfile = "3" diff --git a/_primitives/_rust/tokens-sync/src/emit.rs b/_primitives/_rust/tokens-sync/src/emit.rs new file mode 100644 index 0000000..0d4af42 --- /dev/null +++ b/_primitives/_rust/tokens-sync/src/emit.rs @@ -0,0 +1,127 @@ +//! Tokens → Tailwind config (TypeScript) + CSS custom-property emission. +//! Emits stable, deterministically-ordered output (BTreeMap input guarantees). + +use crate::parse::Tokens; +use std::fmt::Write as _; +use std::fs; +use std::path::Path; + +pub fn tailwind_config(t: &Tokens, out: &Path) -> Result<(), String> { + let mut s = String::new(); + s.push_str("// GENERATED by tokens-sync — do not edit by hand.\n"); + s.push_str("import type { Config } from 'tailwindcss';\n\n"); + s.push_str("const config: Config = {\n"); + s.push_str(" content: [],\n"); + s.push_str(" theme: {\n extend: {\n"); + + emit_category(&mut s, "colors", &t.colors); + emit_category(&mut s, "fontFamily", &t.fonts); + emit_category(&mut s, "spacing", &t.spacing); + emit_category(&mut s, "borderRadius", &t.radius); + + s.push_str(" },\n },\n plugins: [],\n};\n\nexport default config;\n"); + + fs::write(out, s).map_err(|e| format!("write {}: {e}", out.display())) +} + +pub fn css_vars(t: &Tokens, out: &Path) -> Result<(), String> { + let mut s = String::new(); + s.push_str("/* GENERATED by tokens-sync — do not edit by hand. */\n:root {\n"); + + for (k, v) in &t.colors { + let _ = writeln!(s, " --color-{}: {};", css_ident(k), v); + } + for (k, v) in &t.fonts { + let _ = writeln!(s, " --font-{}: {};", css_ident(k), v); + } + for (k, v) in &t.spacing { + let _ = writeln!(s, " --space-{}: {};", css_ident(k), v); + } + for (k, v) in &t.radius { + let _ = writeln!(s, " --radius-{}: {};", css_ident(k), v); + } + + s.push_str("}\n"); + + fs::write(out, s).map_err(|e| format!("write {}: {e}", out.display())) +} + +fn emit_category( + s: &mut String, + key: &str, + map: &std::collections::BTreeMap, +) { + if map.is_empty() { + return; + } + let _ = writeln!(s, " {key}: {{"); + for (k, v) in map { + // Escape JS single quotes inside the value string. + let escaped = v.replace('\'', "\\'"); + let _ = writeln!(s, " '{k}': '{escaped}',"); + } + s.push_str(" },\n"); +} + +fn css_ident(raw: &str) -> String { + raw.chars() + .map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '-' }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeMap; + + fn sample_tokens() -> Tokens { + let mut colors = BTreeMap::new(); + colors.insert("primary".into(), "oklch(0.6 0.2 250)".into()); + colors.insert("surface".into(), "oklch(0.99 0 0)".into()); + let mut fonts = BTreeMap::new(); + fonts.insert("body".into(), "Inter, sans-serif".into()); + let mut spacing = BTreeMap::new(); + spacing.insert("md".into(), "1rem".into()); + let mut radius = BTreeMap::new(); + radius.insert("card".into(), "0.75rem".into()); + Tokens { colors, fonts, spacing, radius } + } + + #[test] + fn tailwind_emits_extend_categories() { + let dir = tempfile::tempdir().unwrap(); + let out = dir.path().join("tw.ts"); + tailwind_config(&sample_tokens(), &out).unwrap(); + let text = fs::read_to_string(&out).unwrap(); + assert!(text.contains("colors: {")); + assert!(text.contains("'primary': 'oklch(0.6 0.2 250)'")); + assert!(text.contains("fontFamily: {")); + assert!(text.contains("borderRadius: {")); + } + + #[test] + fn css_emits_vars_under_root() { + let dir = tempfile::tempdir().unwrap(); + let out = dir.path().join("tokens.css"); + css_vars(&sample_tokens(), &out).unwrap(); + let text = fs::read_to_string(&out).unwrap(); + assert!(text.contains(":root {")); + assert!(text.contains("--color-primary: oklch(0.6 0.2 250);")); + assert!(text.contains("--font-body: Inter, sans-serif;")); + assert!(text.contains("--space-md: 1rem;")); + assert!(text.contains("--radius-card: 0.75rem;")); + } + + #[test] + fn empty_tokens_still_emit_valid_shells() { + let dir = tempfile::tempdir().unwrap(); + let t = Tokens::default(); + let tw = dir.path().join("tw.ts"); + let css = dir.path().join("c.css"); + tailwind_config(&t, &tw).unwrap(); + css_vars(&t, &css).unwrap(); + let css_text = fs::read_to_string(&css).unwrap(); + assert!(css_text.contains(":root {")); + assert!(css_text.trim_end().ends_with('}')); + } +} diff --git a/_primitives/_rust/tokens-sync/src/main.rs b/_primitives/_rust/tokens-sync/src/main.rs new file mode 100644 index 0000000..285cea9 --- /dev/null +++ b/_primitives/_rust/tokens-sync/src/main.rs @@ -0,0 +1,85 @@ +//! tokens-sync — emit Tailwind config + CSS custom properties from a single +//! design-tokens JSON file. One SSoT; no drift between CSS/JS sides. +//! +//! USAGE +//! tokens-sync --out-tailwind --out-css +//! +//! Input JSON shape (minimum): +//! { +//! "colors": { "primary": "oklch(0.6 0.2 250)", ... }, +//! "fonts": { "display": "Fraunces Variable, serif", ... }, +//! "spacing": { "sm": "0.5rem", ... }, +//! "radius": { "card": "0.75rem", ... } +//! } +//! +//! At least one of --out-tailwind or --out-css must be supplied. + +mod emit; +mod parse; + +use std::env; +use std::path::PathBuf; +use std::process::ExitCode; + +fn main() -> ExitCode { + let args: Vec = env::args().skip(1).collect(); + + if args.iter().any(|a| a == "-h" || a == "--help") { + print_help(); + return ExitCode::SUCCESS; + } + + let Some(input) = args.iter().find(|a| !a.starts_with("--")).cloned() else { + eprintln!("tokens-sync: required"); + print_help(); + return ExitCode::from(1); + }; + let tailwind = flag(&args, "--out-tailwind").map(PathBuf::from); + let css = flag(&args, "--out-css").map(PathBuf::from); + + if tailwind.is_none() && css.is_none() { + eprintln!("tokens-sync: need at least one of --out-tailwind or --out-css"); + return ExitCode::from(1); + } + + let tokens = match parse::load(&PathBuf::from(&input)) { + Ok(t) => t, + Err(e) => { + eprintln!("tokens-sync: {e}"); + return ExitCode::from(1); + } + }; + + if let Some(path) = tailwind { + if let Err(e) = emit::tailwind_config(&tokens, &path) { + eprintln!("tokens-sync: emit tailwind: {e}"); + return ExitCode::from(1); + } + println!("tailwind -> {}", path.display()); + } + if let Some(path) = css { + if let Err(e) = emit::css_vars(&tokens, &path) { + eprintln!("tokens-sync: emit css: {e}"); + return ExitCode::from(1); + } + println!("css -> {}", path.display()); + } + + ExitCode::SUCCESS +} + +fn print_help() { + println!( + "tokens-sync — design tokens JSON → Tailwind config + CSS vars + +USAGE + tokens-sync --out-tailwind --out-css + +Write at least one of --out-tailwind or --out-css (both allowed). +JSON schema: see source for minimum shape." + ); +} + +fn flag<'a>(args: &'a [String], name: &str) -> Option<&'a str> { + args.windows(2).find(|w| w[0] == name).map(|w| w[1].as_str()) +} diff --git a/_primitives/_rust/tokens-sync/src/parse.rs b/_primitives/_rust/tokens-sync/src/parse.rs new file mode 100644 index 0000000..0e05e14 --- /dev/null +++ b/_primitives/_rust/tokens-sync/src/parse.rs @@ -0,0 +1,71 @@ +//! JSON → Tokens model. Flat string-keyed maps per category (colors, fonts, +//! spacing, radius). Unknown categories are ignored; missing categories +//! default to empty. + +use serde::Deserialize; +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +#[derive(Deserialize, Default)] +pub struct Tokens { + #[serde(default)] + pub colors: BTreeMap, + #[serde(default)] + pub fonts: BTreeMap, + #[serde(default)] + pub spacing: BTreeMap, + #[serde(default)] + pub radius: BTreeMap, +} + +pub fn load(path: &Path) -> Result { + let text = fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?; + serde_json::from_str(&text).map_err(|e| format!("parse {}: {e}", path.display())) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn parses_full_shape() { + let mut f = tempfile::NamedTempFile::new().unwrap(); + writeln!( + f, + r#"{{ + "colors": {{ "primary": "oklch(0.6 0.2 250)", "surface": "oklch(0.995 0 0)" }}, + "fonts": {{ "display": "Fraunces, serif", "body": "Inter, sans-serif" }}, + "spacing": {{ "sm": "0.5rem", "md": "1rem" }}, + "radius": {{ "card": "0.75rem" }} + }}"# + ) + .unwrap(); + let tokens = load(f.path()).unwrap(); + assert_eq!(tokens.colors.len(), 2); + assert_eq!(tokens.colors.get("primary").unwrap(), "oklch(0.6 0.2 250)"); + assert_eq!(tokens.fonts.get("body").unwrap(), "Inter, sans-serif"); + assert_eq!(tokens.spacing.len(), 2); + assert_eq!(tokens.radius.len(), 1); + } + + #[test] + fn missing_categories_default_empty() { + let mut f = tempfile::NamedTempFile::new().unwrap(); + let payload = r##"{ "colors": { "primary": "#000" } }"##; + f.write_all(payload.as_bytes()).unwrap(); + let tokens = load(f.path()).unwrap(); + assert_eq!(tokens.colors.len(), 1); + assert!(tokens.fonts.is_empty()); + assert!(tokens.spacing.is_empty()); + assert!(tokens.radius.is_empty()); + } + + #[test] + fn invalid_json_errors() { + let mut f = tempfile::NamedTempFile::new().unwrap(); + writeln!(f, "not json").unwrap(); + assert!(load(f.path()).is_err()); + } +} diff --git a/_primitives/_rust/visual-diff/Cargo.toml b/_primitives/_rust/visual-diff/Cargo.toml new file mode 100644 index 0000000..f6c92fa --- /dev/null +++ b/_primitives/_rust/visual-diff/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "visual-diff" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[[bin]] +name = "visual-diff" +path = "src/main.rs" + +[dependencies] +image = { workspace = true } + +[dev-dependencies] +tempfile = "3" diff --git a/_primitives/_rust/visual-diff/src/diff.rs b/_primitives/_rust/visual-diff/src/diff.rs new file mode 100644 index 0000000..c3ef167 --- /dev/null +++ b/_primitives/_rust/visual-diff/src/diff.rs @@ -0,0 +1,136 @@ +//! PNG diff routine. Decodes both images, compares pixels with a per-channel +//! tolerance, writes a red-tinted overlay where pixels differ, and reports +//! mismatch percentage. + +use image::{ImageBuffer, Rgba, RgbaImage}; +use std::path::Path; + +pub struct Report { + pub pct: f64, + pub diff_px: u64, + pub total_px: u64, + pub diff_png_written: bool, +} + +const CHANNEL_TOLERANCE: u8 = 4; + +pub fn compare(a: &Path, b: &Path, out: &Path) -> Result { + let img_a = image::open(a).map_err(|e| format!("open {}: {e}", a.display()))?.to_rgba8(); + let img_b = image::open(b).map_err(|e| format!("open {}: {e}", b.display()))?.to_rgba8(); + + let (wa, ha) = img_a.dimensions(); + let (wb, hb) = img_b.dimensions(); + + if (wa, ha) != (wb, hb) { + return Err(format!( + "dimension mismatch: a={wa}x{ha} b={wb}x{hb} (resize before comparing)" + )); + } + + let total_px = u64::from(wa) * u64::from(ha); + let mut diff_px: u64 = 0; + let mut overlay: RgbaImage = ImageBuffer::new(wa, ha); + + for y in 0..ha { + for x in 0..wa { + let pa = img_a.get_pixel(x, y); + let pb = img_b.get_pixel(x, y); + if pixel_differs(pa, pb, CHANNEL_TOLERANCE) { + diff_px += 1; + overlay.put_pixel(x, y, Rgba([255, 0, 64, 200])); // red flag + } else { + // faded original to give context + let faded = Rgba([pa[0] / 3, pa[1] / 3, pa[2] / 3, 255]); + overlay.put_pixel(x, y, faded); + } + } + } + + let pct = if total_px == 0 { + 0.0 + } else { + (diff_px as f64 / total_px as f64) * 100.0 + }; + + let mut diff_png_written = false; + if diff_px > 0 { + overlay + .save(out) + .map_err(|e| format!("write {}: {e}", out.display()))?; + diff_png_written = true; + } + + Ok(Report { + pct, + diff_px, + total_px, + diff_png_written, + }) +} + +fn pixel_differs(a: &Rgba, b: &Rgba, tol: u8) -> bool { + (0..4).any(|i| a[i].abs_diff(b[i]) > tol) +} + +#[cfg(test)] +mod tests { + use super::*; + use image::ImageBuffer; + + fn write_solid(path: &Path, w: u32, h: u32, color: [u8; 4]) { + let img: RgbaImage = ImageBuffer::from_pixel(w, h, Rgba(color)); + img.save(path).unwrap(); + } + + #[test] + fn identical_images_match() { + let dir = tempfile::tempdir().unwrap(); + let a = dir.path().join("a.png"); + let b = dir.path().join("b.png"); + let out = dir.path().join("diff.png"); + write_solid(&a, 16, 16, [255, 255, 255, 255]); + write_solid(&b, 16, 16, [255, 255, 255, 255]); + let r = compare(&a, &b, &out).unwrap(); + assert_eq!(r.diff_px, 0); + assert_eq!(r.total_px, 256); + assert!(!r.diff_png_written); + } + + #[test] + fn fully_different_images() { + let dir = tempfile::tempdir().unwrap(); + let a = dir.path().join("a.png"); + let b = dir.path().join("b.png"); + let out = dir.path().join("diff.png"); + write_solid(&a, 8, 8, [0, 0, 0, 255]); + write_solid(&b, 8, 8, [255, 255, 255, 255]); + let r = compare(&a, &b, &out).unwrap(); + assert_eq!(r.diff_px, 64); + assert_eq!(r.total_px, 64); + assert!((r.pct - 100.0).abs() < 1e-6); + assert!(r.diff_png_written); + } + + #[test] + fn dimension_mismatch_errors() { + let dir = tempfile::tempdir().unwrap(); + let a = dir.path().join("a.png"); + let b = dir.path().join("b.png"); + let out = dir.path().join("diff.png"); + write_solid(&a, 8, 8, [0, 0, 0, 255]); + write_solid(&b, 16, 16, [0, 0, 0, 255]); + assert!(compare(&a, &b, &out).is_err()); + } + + #[test] + fn tolerance_absorbs_tiny_delta() { + let dir = tempfile::tempdir().unwrap(); + let a = dir.path().join("a.png"); + let b = dir.path().join("b.png"); + let out = dir.path().join("diff.png"); + write_solid(&a, 8, 8, [100, 100, 100, 255]); + write_solid(&b, 8, 8, [103, 103, 103, 255]); // within CHANNEL_TOLERANCE=4 + let r = compare(&a, &b, &out).unwrap(); + assert_eq!(r.diff_px, 0); + } +} diff --git a/_primitives/_rust/visual-diff/src/main.rs b/_primitives/_rust/visual-diff/src/main.rs new file mode 100644 index 0000000..054a1bb --- /dev/null +++ b/_primitives/_rust/visual-diff/src/main.rs @@ -0,0 +1,83 @@ +//! visual-diff — pixel-level PNG comparator for WYSIWYD drift detection. +//! +//! USAGE +//! visual-diff [--out diff.png] [--threshold 5] +//! +//! Exit codes: +//! 0 images equal (within threshold) +//! 1 usage error +//! 2 images differ beyond threshold +//! +//! Prints percentage of mismatched pixels to stdout. Writes a red-overlay +//! diff PNG to (default: ./diff.png) when images differ. + +mod diff; + +use std::env; +use std::path::PathBuf; +use std::process::ExitCode; + +fn main() -> ExitCode { + let args: Vec = env::args().skip(1).collect(); + + if args.iter().any(|a| a == "-h" || a == "--help") { + print_help(); + return ExitCode::SUCCESS; + } + + let positional: Vec<&String> = args.iter().filter(|a| !a.starts_with("--")).collect(); + if positional.len() < 2 { + eprintln!("visual-diff: need "); + print_help(); + return ExitCode::from(1); + } + + let a = PathBuf::from(positional[0]); + let b = PathBuf::from(positional[1]); + let out = flag(&args, "--out") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("diff.png")); + let threshold: f64 = flag(&args, "--threshold") + .and_then(|s| s.parse().ok()) + .unwrap_or(1.0); + + match diff::compare(&a, &b, &out) { + Ok(report) => { + println!("{:.4}% differ ({} px of {})", report.pct, report.diff_px, report.total_px); + if report.diff_png_written { + eprintln!("wrote diff: {}", out.display()); + } + if report.pct > threshold { + ExitCode::from(2) + } else { + ExitCode::SUCCESS + } + } + Err(e) => { + eprintln!("visual-diff: {e}"); + ExitCode::from(1) + } + } +} + +fn print_help() { + println!( + "visual-diff — pixel-level PNG comparator + +USAGE + visual-diff [--out diff.png] [--threshold 5] + +OPTIONS + --out FILE write red-overlay diff PNG (default: diff.png) + --threshold PCT fail (exit 2) if mismatch exceeds PCT%% (default: 1.0) + +EXIT + 0 equal (within threshold) + 1 usage / IO error + 2 differ beyond threshold" + ); +} + +fn flag<'a>(args: &'a [String], name: &str) -> Option<&'a str> { + args.windows(2).find(|w| w[0] == name).map(|w| w[1].as_str()) +} diff --git a/_primitives/design-scrape.sh b/_primitives/design-scrape.sh new file mode 100755 index 0000000..74e9f22 --- /dev/null +++ b/_primitives/design-scrape.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env sh +# design-scrape — Playwright-based scrape of a live website into tokens, +# section map, and full-page screenshots. Output: one directory per URL. +# +# USAGE +# design-scrape [--out ] +# +# OUTPUT +# /desktop.png +# /mobile.png +# /tokens.json +# /structure.json +# +# Requires: npx (Node), Playwright (`npx playwright install chromium` once). + +set -eu + +URL="${1:-}" +OUT="${OUT:-./design-scrape}" + +usage() { + cat <<'EOF' +Usage: design-scrape [--out ] + +Captures two full-page screenshots (desktop 1280x900, mobile 375x812), +extracts computed tokens + DOM structure via Playwright. Writes all +artefacts under (default: ./design-scrape). +EOF +} + +[ -z "$URL" ] || [ "$URL" = "-h" ] || [ "$URL" = "--help" ] && { usage; [ -z "$URL" ] && exit 1; exit 0; } + +# --out arg parsing (positional URL first) +shift +while [ $# -gt 0 ]; do + case "$1" in + --out) OUT="$2"; shift 2 ;; + *) echo "design-scrape: unknown arg: $1" >&2; exit 1 ;; + esac +done + +if ! command -v npx >/dev/null 2>&1; then + echo "design-scrape: npx not found. Install Node 20+." >&2 + exit 1 +fi +if ! command -v jq >/dev/null 2>&1; then + echo "design-scrape: jq not found. brew install jq / apt install jq" >&2 + exit 1 +fi + +mkdir -p "$OUT" + +# Inline Playwright script piped on stdin. We generate a self-contained .mjs +# so npx resolves `playwright` from the nearest node_modules OR installs ephemeral. +SCRIPT="$OUT/.scrape.mjs" +cat > "$SCRIPT" <<'MJS' +import { chromium } from "playwright"; +import { writeFileSync } from "node:fs"; +import { argv } from "node:process"; + +const url = argv[2]; +const outDir = argv[3]; + +const browser = await chromium.launch(); + +async function shot(viewport, name) { + const ctx = await browser.newContext({ viewport }); + const page = await ctx.newPage(); + await page.goto(url, { waitUntil: "networkidle", timeout: 45000 }); + await page.screenshot({ path: `${outDir}/${name}.png`, fullPage: true }); + await ctx.close(); +} + +await shot({ width: 1280, height: 900 }, "desktop"); +await shot({ width: 375, height: 812 }, "mobile"); + +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); +const page = await ctx.newPage(); +await page.goto(url, { waitUntil: "networkidle", timeout: 45000 }); + +const tokens = await page.evaluate(() => { + const g = (sel) => { const el = document.querySelector(sel); return el ? getComputedStyle(el) : null; }; + const body = g("body"); const h1 = g("h1"); + return { + colors: { background: body?.backgroundColor, text: body?.color, heading: h1?.color }, + typography: { bodyFont: body?.fontFamily, bodySize: body?.fontSize, h1Font: h1?.fontFamily, h1Size: h1?.fontSize }, + }; +}); + +const structure = await page.evaluate(() => ({ + title: document.title, + sections: document.querySelectorAll("section, [class*='section']").length, + headings: Array.from(document.querySelectorAll("h1,h2,h3")).map(h => ({ t: h.tagName, x: h.textContent.trim().slice(0,80) })), +})); + +writeFileSync(`${outDir}/tokens.json`, JSON.stringify(tokens, null, 2)); +writeFileSync(`${outDir}/structure.json`, JSON.stringify(structure, null, 2)); + +await browser.close(); +MJS + +echo "[design-scrape] capturing $URL -> $OUT" >&2 +if ! npx --yes playwright --version >/dev/null 2>&1; then + echo "[design-scrape] note: Playwright not yet installed — first run will download Chromium" >&2 +fi + +node "$SCRIPT" "$URL" "$OUT" +rm -f "$SCRIPT" + +echo "[design-scrape] done:" +ls -1 "$OUT" diff --git a/_primitives/figma-tokens.sh b/_primitives/figma-tokens.sh new file mode 100755 index 0000000..9b345e4 --- /dev/null +++ b/_primitives/figma-tokens.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env sh +# figma-tokens — fetch a Figma file's design tokens (Variables + Styles) via +# the REST API and emit a tokens.json usable by tokens-sync. +# +# USAGE +# FIGMA_TOKEN=figd_xxx figma-tokens [--out tokens.json] +# +# The Figma personal-access-token (legacy) OR OAuth bearer token lives in +# $FIGMA_TOKEN. Never hardcode into this file — per RULE 0.8. + +set -eu + +FILE_KEY="${1:-}" +OUT="tokens.json" + +usage() { + cat <<'EOF' +Usage: FIGMA_TOKEN= figma-tokens [--out ] + +file-key: the part after /design/ or /file/ in the Figma URL + e.g. https://www.figma.com/design/ABC123xyz/Design-System + ^^^^^^^^^^ +Output JSON shape: { "colors": {...}, "fonts": {...}, "spacing": {...}, "radius": {...} } +Pipe into tokens-sync to generate Tailwind config + CSS vars. +EOF +} + +[ -z "$FILE_KEY" ] || [ "$FILE_KEY" = "-h" ] || [ "$FILE_KEY" = "--help" ] && { + usage + [ -z "$FILE_KEY" ] && exit 1 || exit 0 +} + +shift +while [ $# -gt 0 ]; do + case "$1" in + --out) OUT="$2"; shift 2 ;; + *) echo "figma-tokens: unknown arg: $1" >&2; exit 1 ;; + esac +done + +if [ -z "${FIGMA_TOKEN:-}" ]; then + echo "figma-tokens: \$FIGMA_TOKEN not set. Export via shell or \`source ~/.claude/secrets/.env\`." >&2 + exit 1 +fi +if ! command -v curl >/dev/null 2>&1; then + echo "figma-tokens: curl not found" >&2; exit 1 +fi +if ! command -v jq >/dev/null 2>&1; then + echo "figma-tokens: jq not found (brew install jq)" >&2; exit 1 +fi + +API="https://api.figma.com/v1" +# Variables + local styles (styles gives colors/fonts for files that predate Variables) +VARS=$(curl -fsSL -H "X-Figma-Token: ${FIGMA_TOKEN}" "${API}/files/${FILE_KEY}/variables/local" 2>/dev/null || echo '{}') +STYLES=$(curl -fsSL -H "X-Figma-Token: ${FIGMA_TOKEN}" "${API}/files/${FILE_KEY}/styles" 2>/dev/null || echo '{}') + +# Minimal extractor — colors from Variables local collection (modern files). +# Falls back to an empty colors map if the file uses Styles only. +jq -n --argjson vars "$VARS" --argjson styles "$STYLES" ' + { + colors: ($vars.meta.variables // {} + | to_entries + | map(select(.value.resolvedType == "COLOR")) + | map({key: .value.name, value: (.value.valuesByMode | (to_entries|first.value) | tostring)}) + | from_entries), + fonts: {}, + spacing: {}, + radius: {} + } +' > "$OUT" + +echo "[figma-tokens] wrote $OUT" +jq '{colors: (.colors | length), fonts: (.fonts | length), spacing: (.spacing | length), radius: (.radius | length)}' "$OUT" diff --git a/_primitives/frontend-inspect.sh b/_primitives/frontend-inspect.sh new file mode 100755 index 0000000..d9d91a3 --- /dev/null +++ b/_primitives/frontend-inspect.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env sh +# frontend-inspect — scan a project directory and report what it is: +# framework (Astro/Next/SvelteKit/Vite-React), styling (Tailwind/CSS-Modules/ +# styled-components), UI-component count, and package-manager lockfile. +# +# USAGE +# frontend-inspect [] # default: current directory +# frontend-inspect --json # machine-readable JSON output + +set -eu + +DIR="${1:-.}" +JSON=0 +[ "${2:-}" = "--json" ] && JSON=1 + +usage() { + cat <<'EOF' +Usage: frontend-inspect [] [--json] + +Reports: + - Framework (astro / next / sveltekit / vite-react / static / unknown) + - Styling (tailwind4 / tailwind3 / css-modules / plain) + - Package manager (npm / pnpm / yarn / bun) + - Component file count (.tsx / .vue / .svelte / .astro) + - Contains tests? (yes/no) +EOF +} + +[ "$DIR" = "-h" ] || [ "$DIR" = "--help" ] && { usage; exit 0; } +[ -d "$DIR" ] || { echo "frontend-inspect: $DIR not a directory" >&2; exit 1; } + +PKG="$DIR/package.json" + +has_dep() { + # $1 = dep name + [ -f "$PKG" ] || return 1 + if command -v jq >/dev/null 2>&1; then + jq -e --arg d "$1" '(.dependencies[$d] // .devDependencies[$d] // null) != null' "$PKG" >/dev/null 2>&1 + else + grep -q "\"$1\"" "$PKG" 2>/dev/null + fi +} + +detect_framework() { + if has_dep astro; then echo astro; return; fi + if has_dep next; then echo next; return; fi + if has_dep "@sveltejs/kit"; then echo sveltekit; return; fi + if has_dep vite && has_dep react; then echo vite-react; return; fi + if has_dep vite && has_dep vue; then echo vite-vue; return; fi + if has_dep vite; then echo vite; return; fi + [ -f "$DIR/index.html" ] && echo static && return + echo unknown +} + +detect_styling() { + if has_dep tailwindcss; then + # Tailwind 4 has `@theme` in CSS and no tailwind.config.js, usually; rough heuristic: + if [ -f "$DIR/tailwind.config.ts" ] || [ -f "$DIR/tailwind.config.js" ] || [ -f "$DIR/tailwind.config.mjs" ]; then + echo tailwind3 + else + echo tailwind4 + fi + return + fi + if has_dep "styled-components"; then echo styled-components; return; fi + if find "$DIR/src" -maxdepth 3 -name '*.module.css' -print -quit 2>/dev/null | grep -q .; then + echo css-modules + return + fi + echo plain +} + +detect_pm() { + [ -f "$DIR/pnpm-lock.yaml" ] && echo pnpm && return + [ -f "$DIR/yarn.lock" ] && echo yarn && return + [ -f "$DIR/bun.lockb" ] && echo bun && return + [ -f "$DIR/package-lock.json" ] && echo npm && return + echo none +} + +count_components() { + find "$DIR/src" -type f \( -name '*.tsx' -o -name '*.vue' -o -name '*.svelte' -o -name '*.astro' \) 2>/dev/null | wc -l | tr -d ' ' +} + +has_tests() { + if [ -f "$PKG" ] && (has_dep vitest || has_dep jest || has_dep "@playwright/test"); then + echo yes + else + echo no + fi +} + +FW="$(detect_framework)" +ST="$(detect_styling)" +PM="$(detect_pm)" +CC="$(count_components)" +TS="$(has_tests)" + +if [ "$JSON" = "1" ]; then + printf '{"dir":"%s","framework":"%s","styling":"%s","pm":"%s","components":%s,"tests":"%s"}\n' \ + "$DIR" "$FW" "$ST" "$PM" "$CC" "$TS" +else + printf "dir: %s\n" "$DIR" + printf "framework: %s\n" "$FW" + printf "styling: %s\n" "$ST" + printf "pm: %s\n" "$PM" + printf "components: %s\n" "$CC" + printf "tests: %s\n" "$TS" +fi diff --git a/_primitives/live-preview.sh b/_primitives/live-preview.sh new file mode 100755 index 0000000..4b1c74f --- /dev/null +++ b/_primitives/live-preview.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env sh +# live-preview — start / stop / status for a project's dev server. +# Detects framework from package.json; stores PID in .keisei/dev-server.pid. +# +# USAGE +# live-preview start +# live-preview stop [pid] # default: reads .keisei/dev-server.pid +# live-preview status + +set -eu + +CMD="${1:-}" + +usage() { + cat <<'EOF' +Usage: live-preview start — start `npm run dev` in , record PID + live-preview stop [pid] — stop running server (default: recorded PID) + live-preview status — show whether a server is running +EOF +} + +PID_FILE() { + dir="${1:-.}" + mkdir -p "$dir/.keisei" + printf '%s/.keisei/dev-server.pid\n' "$dir" +} + +detect_script() { + pkg="$1/package.json" + [ -f "$pkg" ] || { echo "dev"; return; } + if command -v jq >/dev/null 2>&1; then + jq -r '.scripts.dev // .scripts.start // "dev"' "$pkg" + else + echo "dev" + fi +} + +case "$CMD" in + start) + DIR="${2:-}" + [ -z "$DIR" ] && { usage; exit 1; } + [ -d "$DIR" ] || { echo "live-preview: $DIR not a directory" >&2; exit 1; } + + PID_F="$(PID_FILE "$DIR")" + if [ -f "$PID_F" ]; then + OLD="$(cat "$PID_F" 2>/dev/null || true)" + if [ -n "$OLD" ] && kill -0 "$OLD" 2>/dev/null; then + echo "live-preview: server already running pid=$OLD (pidfile $PID_F)" >&2 + exit 1 + fi + fi + + SCRIPT="$(detect_script "$DIR")" + echo "[live-preview] starting 'npm run $SCRIPT' in $DIR" >&2 + ( + cd "$DIR" + nohup npm run "$SCRIPT" >.keisei/dev-server.log 2>&1 & + echo $! > ".keisei/dev-server.pid" + ) + NEW="$(cat "$PID_F")" + echo "live-preview: started pid=$NEW log=$DIR/.keisei/dev-server.log" + ;; + stop) + TARGET="${2:-}" + if [ -z "$TARGET" ]; then + PID_F="$(PID_FILE ".")" + [ -f "$PID_F" ] || { echo "live-preview: no pidfile at $PID_F" >&2; exit 1; } + TARGET="$(cat "$PID_F")" + fi + if kill -0 "$TARGET" 2>/dev/null; then + kill "$TARGET" + echo "live-preview: stopped pid=$TARGET" + [ -f ".keisei/dev-server.pid" ] && rm -f ".keisei/dev-server.pid" + else + echo "live-preview: pid=$TARGET not running (cleaning pidfile)" >&2 + [ -f ".keisei/dev-server.pid" ] && rm -f ".keisei/dev-server.pid" + exit 1 + fi + ;; + status) + PID_F="$(PID_FILE ".")" + if [ ! -f "$PID_F" ]; then + echo "live-preview: no pidfile (not running from $(pwd))" + exit 0 + fi + PID="$(cat "$PID_F")" + if kill -0 "$PID" 2>/dev/null; then + echo "live-preview: running pid=$PID" + else + echo "live-preview: stale pidfile (pid=$PID exited)" + rm -f "$PID_F" + fi + ;; + -h|--help|help|"") + usage + ;; + *) + echo "live-preview: unknown command '$CMD'" >&2 + usage + exit 1 + ;; +esac diff --git a/_primitives/screenshot-decode.sh b/_primitives/screenshot-decode.sh new file mode 100755 index 0000000..48a1e35 --- /dev/null +++ b/_primitives/screenshot-decode.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env sh +# screenshot-decode — send a screenshot to Claude's vision API and return +# a structured description (tokens / layout / sections). For use in teardown +# and audit pipelines. +# +# USAGE +# ANTHROPIC_API_KEY=sk-ant-xxx screenshot-decode [--prompt ] +# +# Reads $ANTHROPIC_API_KEY from env (RULE 0.8: never hardcoded). +# Requires: curl, jq, base64. + +set -eu + +IMG="${1:-}" +PROMPT="Describe this UI. Extract design tokens (colors, fonts), section layout, and key components. Output as JSON." + +usage() { + cat <<'EOF' +Usage: ANTHROPIC_API_KEY= screenshot-decode [--prompt ] + +Posts + prompt to Anthropic Messages API (claude-sonnet-4) and prints +the text response. Default prompt asks for token + layout extraction. +EOF +} + +[ -z "$IMG" ] || [ "$IMG" = "-h" ] || [ "$IMG" = "--help" ] && { + usage + [ -z "$IMG" ] && exit 1 || exit 0 +} +[ -f "$IMG" ] || { echo "screenshot-decode: file not found: $IMG" >&2; exit 1; } + +shift +while [ $# -gt 0 ]; do + case "$1" in + --prompt) PROMPT="$2"; shift 2 ;; + *) echo "screenshot-decode: unknown arg: $1" >&2; exit 1 ;; + esac +done + +[ -n "${ANTHROPIC_API_KEY:-}" ] || { echo "screenshot-decode: \$ANTHROPIC_API_KEY not set" >&2; exit 1; } +command -v curl >/dev/null 2>&1 || { echo "screenshot-decode: curl not found" >&2; exit 1; } +command -v jq >/dev/null 2>&1 || { echo "screenshot-decode: jq not found" >&2; exit 1; } + +B64=$(base64 < "$IMG" | tr -d '\n') + +PAYLOAD=$(jq -n --arg img "$B64" --arg prompt "$PROMPT" '{ + model: "claude-sonnet-4-5", + max_tokens: 2048, + messages: [{ + role: "user", + content: [ + { type: "image", source: { type: "base64", media_type: "image/png", data: $img } }, + { type: "text", text: $prompt } + ] + }] +}') + +curl -fsSL https://api.anthropic.com/v1/messages \ + -H "x-api-key: ${ANTHROPIC_API_KEY}" \ + -H "anthropic-version: 2023-06-01" \ + -H "content-type: application/json" \ + -d "$PAYLOAD" \ + | jq -r '.content[0].text // .error.message // "(no response)"' diff --git a/skills/a11y-audit/SKILL.md b/skills/a11y-audit/SKILL.md new file mode 100644 index 0000000..27331f5 --- /dev/null +++ b/skills/a11y-audit/SKILL.md @@ -0,0 +1,102 @@ +--- +name: a11y-audit +description: Use when auditing accessibility — WCAG 2.2 AA compliance, contrast checks, keyboard navigation, screen reader support, prefers-reduced-motion. Triggers on "accessibility", "a11y", "wcag", "screen reader", "contrast check". +arguments: + - name: command + description: "Command: scan, fix, contrast, checklist, report" + required: false + - name: target + description: URL or file path to audit + required: false +--- + +# Accessibility Audit — WCAG 2.2 AA + +## Legal Context + +- **EAA (EU):** In force since June 2025. Penalties: up to 100K EUR / 4% revenue +- **ADA (US):** References WCAG 2.2 AA +- **Standard:** WCAG 2.2 AA is minimum for any commercial site targeting US/EU + +## Top 10 Violations + +1. Missing alt text on images +2. Low contrast text (4.5:1 normal, 3:1 large text) +3. Keyboard traps in menus +4. Missing form labels +5. Skipped heading levels +6. No skip links +7. Non-semantic HTML (`
` instead of `