Merge pull request #12 from carlosframework/docs-site
🤖 /docs: ten customer pages, the pipeline, and a ship runbook that keeps them
38 files changed,
+3815
−38
.gitignore+2 −0AGENTS.md+219 −0CLAUDE.md+5 −35PRODUCT.md+1 −1README.md+19 −2eleventy.config.js+178 −0hack/check-docs.mjs+175 −0package-lock.json+1725 −0package.json+15 −0src/_data/docsanchors.json+15 −0src/_data/docsnav.json+24 −0src/_includes/docs.njk+85 −0src/docs-index.njk+20 −0src/docs.css+450 −0src/docs.njk+13 −0src/docs/channels.md+101 −0src/docs/config.md+78 −0src/docs/console.md+89 −0src/docs/deploying.md+89 −0src/docs/docs.json+5 −0src/docs/domains.md+70 −0src/docs/index.md+13 −0src/docs/limits.md+76 −0src/docs/logs.md+67 −0src/docs/members.md+109 −0src/docs/sleeping.md+86 −0src/docs/storage.md+77 −0src/docsmd.njk+9 −0favicon.svg => src/favicon.svg+0 −0{fonts => src/fonts}/Geist-Variable.woff2binary{fonts => src/fonts}/GeistMono-Variable.woff2binary{fonts => src/fonts}/LICENSE-OFL.txt+0 −0{get-started => src/get-started}/index.html+0 −0{img => src/img}/dashboard-dark.pngbinary{img => src/img}/dashboard-light.pngbinaryindex.html => src/index.html+0 −0site.css => src/site.css+0 −0site.js => src/site.js+0 −0
diff --git a/.gitignore b/.gitignore| new file mode 100644 |
| index 0000000..3a799fb |
| --- /dev/null |
| +++ b/.gitignore |
| @@ -0,0 +1,2 @@ |
| +node_modules/ |
| +_site/ |
diff --git a/AGENTS.md b/AGENTS.md| new file mode 100644 |
| index 0000000..caabe96 |
| --- /dev/null |
| +++ b/AGENTS.md |
| @@ -0,0 +1,219 @@ |
| +# 🤖 AGENTS.md |
| + |
| +The canonical agent-instructions file for this repo — keep it current here, |
| +not in a tool-specific file. (Claude Code reads it via the `CLAUDE.md` |
| +pointer.) |
| + |
| +The marketing site for **Carloku** — the hosted CARLOS platform |
| +(console.carloku.com). Brand boundary: Carloku is the *hosted platform* |
| +brand; CARLOS is the framework/CLI/console name. Never rename CARLOS things |
| +to Carloku here. |
| + |
| +**Voice lives in PRODUCT.md** — the one feeling is lightness ("finally, |
| +simple"); the landing page appeals to the heart, /get-started carries the |
| +tech. Read it before touching copy. |
| + |
| +Rules inherited from the `website` repo, which governs the sibling site: |
| + |
| +- **Small site, nothing fetched.** `/` and `/get-started/` are hand-written |
| + and share one `site.css`. No frameworks, no analytics, and nothing pulled |
| + from another origin — the two Geist faces in `fonts/` are self-hosted. The |
| + only script is `site.js`: 2KB, hand-written, and pure enhancement (the hero |
| + terminal types itself, the header grows a hairline on scroll). The pages |
| + read fine with it blocked; keep it that way, and don't add a second one. |
| +- **`/docs` is built; the rest of the site is not.** Docs pages come from the |
| + markdown in `src/docs/` through Eleventy, the same pipeline rastrillo.org |
| + uses, run here before you ship. `_site/` is gitignored and never committed; |
| + Eleventy and markdown-it are devDependencies that no visitor ever meets. |
| + See "Building the docs" below. |
| +- **Light and dark** via `prefers-color-scheme` — keep both working. |
| +- **AI authorship is always marked** with a visible 🤖 (cascades from a |
| + heading); person-emoji (👨/👤/🧑) blocks are certified human and |
| + off-limits to LLM edits. Baseline: everything here is AI-written unless |
| + marked otherwise. |
| +- **Don't overclaim.** The console requires Keymail sign-in; pricing does |
| + not exist yet, so the site says nothing about it. |
| + |
| +## Building the docs |
| + |
| +`/docs` is ten pages of markdown under `src/docs/`, rendered by Eleventy into |
| +`_site/` before you ship. **The corpus is authored here, by hand.** That is the |
| +opposite of carlosframework.com, whose `/docs` corpus is vendored out of the |
| +platform repo by a sync script: there is no sync step in this repo, nothing |
| +under `src/docs/` or `src/_data/docsnav.json` is generated, and nothing will |
| +overwrite an edit you make. The markdown IS the source — edit it here. |
| + |
| +``` |
| +npm install |
| +npm run check # builds, then gates the rendered output |
| +npm run serve # localhost:8080, live reload |
| +``` |
| + |
| +`npm run check` is `eleventy` followed by `hack/check-docs.mjs`, which reads |
| +`_site/` after the build: the hand-written pages and shared assets were |
| +emitted at all; every nav entry has a built page and a built `.md` twin; every |
| +internal `/docs` href resolves to a built file, and to a real `id="…"` when it |
| +carries a fragment; and `slugify` in `eleventy.config.js` agrees with Go's |
| +`internal/docsite.Anchor` on every case in `src/_data/docsanchors.json`. That |
| +last one is why the anchor fixture is vendored rather than retyped: a heading |
| +whose fragment the platform's Go gate accepts cannot 404 in a browser here |
| +because the two slug rules drifted. |
| + |
| +**Eleventy does not clean stale output.** Delete or rename a page and its old |
| +built HTML stays in `_site/` until you remove it — and the gate will *not* |
| +notice, because every check it runs is satisfied by a file that should no |
| +longer exist. `_site/` is gitignored, so a stale page never shows up in a diff |
| +either. **Build clean whenever you are verifying anything:** |
| + |
| +``` |
| +rm -rf _site && npm run check |
| +``` |
| + |
| +The deploy sequence below gets this for free: it builds inside a fresh |
| +`git archive` export, which has no `_site/` in it at all. |
| + |
| +`index.html` and `get-started/index.html` are hand-written and pass through the |
| +build byte-identical — `templateFormats` excludes `html` on purpose, so |
| +Eleventy copies them instead of rendering them as templates. If either changes |
| +in `_site/` without you editing its source, something is wrong. |
| + |
| +## Deploying |
| + |
| +Merging does **not** publish. `carloku.com` is a bucket-mode CARLOS app on the |
| +flagship, and the live site is stale until someone ships. |
| + |
| +**There is a build step now, so what ships is `_site/` — never the repo tree.** |
| +A tree ship has no `_site/` in it: it would publish `index.html` buried under |
| +`src/`, no built pages, and no `/docs` at all. |
| + |
| +The deployment coordinates, confirmed against the bucket on 2026-08-27: |
| + |
| +| | | |
| +| --- | --- | |
| +| app | `carloku` | |
| +| account (sqid) | `bab` | |
| +| channel | `canary/rehearsal` — the app's only channel | |
| +| kind | `static` | |
| +| bucket | `carlos-flagship-271376211898` | |
| +| AWS profile / region | `keymail` / `eu-west-1` | |
| + |
| +``` |
| +SHA=$(git rev-parse --short HEAD) # the sha you are shipping |
| + |
| +# 1. Clean export. Never ship a working checkout — PackDir packs every |
| +# regular file it sees, including .git, node_modules/ and .claude/. |
| +rm -rf /tmp/carloku-ship && mkdir -p /tmp/carloku-ship |
| +git archive "$SHA" --prefix=export/ | tar -x -C /tmp/carloku-ship |
| +cd /tmp/carloku-ship/export |
| + |
| +# 2. Build, and gate the rendered output. `check` runs eleventy, then |
| +# hack/check-docs.mjs over what it produced. |
| +npm ci |
| +npm run check |
| + |
| +# 3. Cache-bust BOTH stylesheets across every built page — see Caching |
| +# below; this is not optional. |
| +find _site -name '*.html' -exec sed -i \ |
| + -e "s/\.css\"/.css?v=$SHA\"/g" \ |
| + -e "s/\.css?v=[^\"]*\"/.css?v=$SHA\"/g" {} + |
| + |
| +grep -rho '\.css?v=[^"]*"' _site --include='*.html' | sort -u # ONE line: your sha |
| +grep -rn '\.css"' _site --include='*.html' \ |
| + && echo "UNBUSTED LINK — do not ship" || echo "cache-bust ok" |
| + |
| +# 4. Ship the BUILT OUTPUT, then promote. The pinfra scripts are the |
| +# canonical form for this app: the signing key is read from SSM inside |
| +# the script and never appears in a terminal or a transcript. |
| +cd ~/github.com/carlosframework/platform-infrastructure |
| +CARLOS_BIN=$(command -v carlos) scripts/ship-app.sh \ |
| + keymail carlos-flagship-271376211898 \ |
| + carloku "$SHA" canary/rehearsal static /tmp/carloku-ship/export/_site |
| +``` |
| + |
| +`ship-app.sh` takes `<aws-profile> <bucket> <app> <version> <channel> <kind> |
| +<file-or-dir>` and does both halves. The equivalent by hand, if you are not in |
| +the pinfra checkout, is: |
| + |
| +``` |
| +export AWS_PROFILE=keymail AWS_REGION=eu-west-1 \ |
| + CARLOS_DEPLOYMENT_BUCKET=carlos-flagship-271376211898 |
| +carlos ship --app carloku --kind static --version "$SHA" _site |
| +CARLOS_RELEASE_KEY=$(aws ssm get-parameter --name /carlos/release-key \ |
| + --with-decryption --query Parameter.Value --output text) \ |
| + carlos promote --app carloku "$SHA" canary/rehearsal |
| +``` |
| + |
| +The env matters: without `CARLOS_DEPLOYMENT_BUCKET` the CLI goes through the |
| +console API, where this app was never registered, and fails with "not found". |
| +This is bucket mode. Use a current `carlos` binary — the ship/promote pair is |
| +the platform's own release path, and an old binary on your `PATH` is a real |
| +hazard here. |
| + |
| +**Step 3 explained, because both halves are load-bearing.** The links in `src/` |
| +are plain (`href="/site.css"`), so the first expression stamps a token onto a |
| +bare link and the second re-stamps a link that already carries one. Both end at |
| +the closing quote on purpose, and so does the verification grep. A sha |
| +beginning with `0` — this app has already shipped `01b5365` — makes |
| +`?v=01b5365` contain the literal substring `?v=0`, so an unanchored grep for |
| +`?v=0` calls every freshly bumped file stale (13 of 13, measured), and an |
| +unanchored `sed` run twice yields `?v=01b536501b5365`. Anchoring on `"` fixes |
| +both and makes step 3 idempotent: run it again and again, the answer is the |
| +same. Today that is 24 links across 13 built pages — 13 × `site.css`, 11 × |
| +`docs.css`. Don't hard-code those counts anywhere; the corpus grows, which is |
| +also why the `find` walks `_site` instead of naming files. |
| + |
| +**Caching: the edge sends no `Cache-Control` and no `ETag` on this static |
| +route — only `Last-Modified`** (confirmed live on carloku.com 2026-08-27). |
| +Browsers therefore apply HEURISTIC caching, roughly 10% of the age since |
| +`Last-Modified`, so a returning visitor can hold a stale page for days. This |
| +bit the sibling site the day its redesign shipped: one browser served the whole |
| +old page, another served the NEW html against the OLD stylesheet. The real fix |
| +is server-side `Cache-Control` on static routes — platform issue |
| +**carlosframework/platform#234**. Until that lands, **step 3 is a required part |
| +of deploying this site**, and it is required for `docs.css` exactly as much as |
| +for `site.css`: the docs pages link both, and a docs page rendered against a |
| +stale `docs.css` loses its whole layout. |
| + |
| +**Convergence is seconds, not minutes** (platform PR #108, live 2026-08-08): a |
| +promote is picked up by the edge within ~2s. Verify by header and by content, |
| +and verify `/docs` **specifically** — the landing page looks right whether or |
| +not the built docs made it into the artifact: |
| + |
| +``` |
| +# by header — --resolve so a stale DNS answer cannot satisfy it. 99.81.104.219 |
| +# is the flagship, where the carloku.com A record points. |
| +curl -sI --resolve carloku.com:443:99.81.104.219 \ |
| + https://carloku.com/docs/ | grep -i x-carlos-version |
| + |
| +# by content |
| +curl -s https://carloku.com/ | grep -i "<something from the change>" |
| +curl -s https://carloku.com/docs/ | grep -o 'docs\.css?v=[^"]*' # your sha |
| +``` |
| + |
| +Static routes do carry `X-Carlos-Version` (platform#112). `-I` is fine for that |
| +header, but it sends a HEAD, and HEAD is excluded from edge compression — |
| +anything about `Content-Encoding` needs a GET. |
| + |
| +**`carlos channels` cannot verify this app.** It is direct-bucket and invisible |
| +to the console API: `carlos channels -app carloku` answers "not found" |
| +(re-confirmed 2026-08-27). Verify by header or content as above, or read the |
| +pointer out of the bucket |
| +(`aws s3 cp s3://carlos-flagship-271376211898/apps/bab/carloku/channels/canary/rehearsal.json -`). |
| + |
| +Rollback is a pointer move, like any CARLOS app — promote the previous good sha |
| +back onto the same channel; the edge picks it up in seconds: |
| + |
| +``` |
| +cd ~/github.com/carlosframework/platform-infrastructure |
| +CARLOS_BIN=$(command -v carlos) scripts/promote-app.sh \ |
| + keymail carlos-flagship-271376211898 bab carloku <previous-sha> canary/rehearsal |
| +``` |
| + |
| +That previous release is still in the bucket under |
| +`apps/bab/carloku/releases/<sha>/`, cache-busted as it was shipped, so a |
| +rollback restores a self-consistent page-plus-stylesheet pair. |
| + |
| +(The channel is `canary/rehearsal`, not `stable`, for the same reason the |
| +sibling site's is: `stable` bakes 72h on a box's *first* sighting of a channel |
| +head, which would mean 72h of downtime for a never-before-served route. A |
| +future `stable` flip is optional cleanup, not required.) |
diff --git a/CLAUDE.md b/CLAUDE.md| index 2e7dd85..270f5c9 100644 |
| --- a/CLAUDE.md |
| +++ b/CLAUDE.md |
| @@ -1,36 +1,6 @@ |
| -# 🤖 CLAUDE.md |
| +# CLAUDE.md |
| -The marketing site for **Carloku** — the hosted CARLOS platform |
| -(console.carloku.com). Brand boundary: Carloku is the *hosted platform* |
| -brand; CARLOS is the framework/CLI/console name. Never rename CARLOS things |
| -to Carloku here. |
| - |
| -**Voice lives in PRODUCT.md** — the one feeling is lightness ("finally, |
| -simple"); the landing page appeals to the heart, /get-started carries the |
| -tech. Read it before touching copy. |
| - |
| -Rules inherited from the `website` repo, which governs the sibling site: |
| - |
| -- **Small site, zero dependencies.** Two pages (`/`, `/get-started/`) and |
| - one shared `site.css`. No frameworks, no fetched fonts, no analytics, no |
| - JavaScript, no build step. |
| -- **Light and dark** via `prefers-color-scheme` — keep both working. |
| -- **AI authorship is always marked** with a visible 🤖 (cascades from a |
| - heading); person-emoji (👨/👤/🧑) blocks are certified human and |
| - off-limits to LLM edits. Baseline: everything here is AI-written unless |
| - marked otherwise. |
| -- **Don't overclaim.** The console requires Keymail sign-in; pricing does |
| - not exist yet, so the site says nothing about it. |
| - |
| -Deploys: bucket-mode CARLOS app `carloku` on flagship — ship a clean |
| -`git archive` export via the pinfra `scripts/ship-app.sh` / `promote-app.sh` |
| -(see the platform repo's CLAUDE.md "Deploy reality"). Merging does NOT |
| -publish; the site is stale until someone ships. Since platform PR #108 |
| -(2026-08-08), the fleet converges within seconds of ship+promote — no |
| -need to wait for the next adoption pass. Verify by content, e.g. |
| -`curl -s https://carloku.com/ | grep -i "<something from the change>"`, |
| -or with `carlos channels -app carloku` showing the new sha promoted — |
| -static routes carry `X-Carlos-Version` too (since the platform#112 |
| -roll — verified live 2026-08-23: the header reports the promoted sha), |
| -so `curl -sI` is also proof. The pinfra ship+promote scripts remain the |
| -operator default, and for this app that pair IS the deploy. |
| +Guidance for AI coding agents working in this repository lives in |
| +**AGENTS.md**. Claude Code: read AGENTS.md. Update AGENTS.md — not this |
| +file — going forward; this file is only a pointer so Claude Code finds |
| +it. |
diff --git a/PRODUCT.md b/PRODUCT.md| index 0183a33..bb4603c 100644 |
| --- a/PRODUCT.md |
| +++ b/PRODUCT.md |
| @@ -46,7 +46,7 @@ division of labor between the pages. |
| - Never on the landing page: immutable, content-addressed, canary, |
| signature/signed, provision, TLS, converge, infra, artifact, registry. |
| All of it lives happily on /get-started ("Under the hood") or in docs. |
| -- No pricing language until billing exists (CLAUDE.md rule). Describe |
| +- No pricing language until billing exists (AGENTS.md rule). Describe |
| hibernation by behavior, never by cost. |
| - No claims we can't cash today. Where the product is young, say young — |
| honesty reads as confidence. |
diff --git a/README.md b/README.md| index 74d15ab..dbec513 100644 |
| --- a/README.md |
| +++ b/README.md |
| @@ -1,5 +1,22 @@ |
| # carloku.com |
| -🤖 Marketing site for Carloku, the hosted CARLOS platform. One static page, |
| -no build step. See CLAUDE.md for the rules; see |
| +🤖 Marketing site for Carloku, the hosted CARLOS platform, plus the customer |
| +docs at /docs. The marketing pages are hand-written static HTML; the docs are |
| +markdown under `src/docs/`, built by Eleventy before shipping. |
| + |
| +**There is a build step, and it decides what the site serves.** Eleventy reads |
| +`src/` and writes `_site/`, and `_site/` is what ships — never the repo tree. |
| +So: **if a URL should return it, the file goes under `src/`.** A file at the |
| +repo root is a repo file, not a page, and will not be served. Anything the site |
| +needs — a page, a stylesheet, a font, an image — lives under `src/`, and a |
| +non-template asset also needs an `addPassthroughCopy` line in |
| +`eleventy.config.js` to be emitted at all. |
| + |
| +``` |
| +npm install |
| +npm run check # build, then gate the rendered output |
| +npm run serve # localhost:8080 |
| +``` |
| + |
| +See AGENTS.md for the deploy runbook and the rules of the repo; see |
| https://carlosframework.com for the framework itself. |
diff --git a/eleventy.config.js b/eleventy.config.js| new file mode 100644 |
| index 0000000..05d185a |
| --- /dev/null |
| +++ b/eleventy.config.js |
| @@ -0,0 +1,178 @@ |
| +// 🤖 Build-time only — the served site ships no JavaScript beyond site.js. |
| +import markdownIt from "markdown-it"; |
| +import markdownItAnchor from "markdown-it-anchor"; |
| + |
| +// slugify must match internal/docsite's Anchor in the platform repo |
| +// exactly, or an internal fragment link passes that repo's Go gate and |
| +// 404s in a browser. Both sides are deliberately plain — lowercase |
| +// alphanumerics, everything else a separator — because |
| +// markdown-it-anchor's own default URI-encodes punctuation, which would |
| +// anchor a heading naming `db.Open` differently on each side. |
| +// src/_data/docsanchors.json (vendored from docs/site/anchors.json) is |
| +// the contract both sides are checked against. |
| +export function slugify(text) { |
| + let out = ""; |
| + let pendingSep = false; |
| + for (const ch of String(text).toLowerCase()) { |
| + if ((ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9")) { |
| + if (pendingSep && out.length > 0) out += "-"; |
| + pendingSep = false; |
| + out += ch; |
| + } else { |
| + pendingSep = true; |
| + } |
| + } |
| + return out; |
| +} |
| + |
| +// fenceDelim/sameFence port internal/docsite/docsite.go's fence-matching |
| +// exactly, for docsSections below: a trimmed line opens or closes a |
| +// fence, and a closer only closes if it is the same character as the |
| +// opener and at least as long a run. |
| +function fenceDelim(line) { |
| + let r = null; |
| + if (line.startsWith("```")) r = "`"; |
| + else if (line.startsWith("~~~")) r = "~"; |
| + else return null; |
| + let n = 0; |
| + while (n < line.length && line[n] === r) n++; |
| + return { mark: line.slice(0, n), info: line.slice(n).trim() }; |
| +} |
| + |
| +function sameFence(closing, opening) { |
| + if (!closing || !opening || closing[0] !== opening[0]) return false; |
| + return closing.length >= opening.length; |
| +} |
| + |
| +export default function (eleventyConfig) { |
| + eleventyConfig.addPassthroughCopy({ "src/index.html": "index.html" }); |
| + eleventyConfig.addPassthroughCopy({ "src/get-started": "get-started" }); |
| + eleventyConfig.addPassthroughCopy({ "src/site.css": "site.css" }); |
| + eleventyConfig.addPassthroughCopy({ "src/docs.css": "docs.css" }); |
| + eleventyConfig.addPassthroughCopy({ "src/site.js": "site.js" }); |
| + eleventyConfig.addPassthroughCopy({ "src/favicon.svg": "favicon.svg" }); |
| + eleventyConfig.addPassthroughCopy({ "src/fonts": "fonts" }); |
| + eleventyConfig.addPassthroughCopy({ "src/img": "img" }); |
| + |
| + const md = markdownIt({ html: true, linkify: false, typographer: false }).use( |
| + markdownItAnchor, |
| + { slugify, level: [2, 3, 4], tabIndex: false }, |
| + ); |
| + eleventyConfig.setLibrary("md", md); |
| + |
| + // The docs corpus is authored here, by hand, under src/docs/ — there |
| + // is no vendoring and no sync step. Pages are addressed by slug — |
| + // "cli" — which is also how src/_data/docsnav.json (the corpus's |
| + // hand-maintained table of contents) and every internal /docs link |
| + // name them. |
| + eleventyConfig.addCollection("docs", (api) => |
| + api |
| + .getFilteredByGlob("src/docs/**/*.md") |
| + .map((page) => { |
| + const slug = page.filePathStem.replace(/^\/docs\//, ""); |
| + return { slug, page }; |
| + }) |
| + .filter(({ slug }) => slug !== "index"), |
| + ); |
| + |
| + // The /docs index page's own prose, as its own collection. |
| + // |
| + // docs-index.njk renders this page's content above the cards. It must |
| + // NOT reach for collections.all to find it: that collection contains |
| + // docs-index.njk itself, and Eleventy rejects a template that reads |
| + // templateContent from a collection it belongs to as a circular |
| + // reference. A one-page collection it is not a member of is the way |
| + // to read another template's rendered output. |
| + eleventyConfig.addCollection("docsIndex", (api) => |
| + api.getFilteredByGlob("src/docs/index.md"), |
| + ); |
| + |
| + // Section headings of one page, for the sidebar's sub-nav. Read from |
| + // the source rather than the rendered HTML so it needs no DOM. |
| + // |
| + // Fence tracking is ported from internal/docsite/docsite.go's |
| + // fenceDelim/sameFence in the platform repo, not invented here: a |
| + // closing marker must match the opener's character and be at least as |
| + // long, so a ``` block nested inside an outer ~~~ fence stays inside |
| + // that outer fence instead of prematurely toggling out of it (which |
| + // would both hide real "##" headings after the true close and expose |
| + // fence-internal "##" lines as fake ones). |
| + eleventyConfig.addFilter("docsSections", (raw) => { |
| + const out = []; |
| + let inFence = false; |
| + let fenceMark = ""; |
| + for (const line of String(raw).split("\n")) { |
| + const trimmed = line.trim(); |
| + const fd = fenceDelim(trimmed); |
| + if (fd) { |
| + if (!inFence) { |
| + inFence = true; |
| + fenceMark = fd.mark; |
| + continue; |
| + } else if (sameFence(fd.mark, fenceMark)) { |
| + inFence = false; |
| + continue; |
| + } |
| + // A fence-looking line that doesn't close the current fence |
| + // (different character, or a shorter run) is body content of |
| + // the still-open fence, same as the Go side. |
| + } |
| + if (inFence) continue; |
| + const m = /^##\s+(.+?)\s*#*$/.exec(trimmed); |
| + if (m) out.push({ title: m[1], anchor: slugify(m[1]) }); |
| + } |
| + return out; |
| + }); |
| + |
| + eleventyConfig.addFilter("docsSlugify", slugify); |
| + |
| + // A page's own "# " title. The docs markdown carries no Eleventy front |
| + // matter — it's plain prose with a leading "# " heading, authored that |
| + // way on purpose so the corpus reads the same whether you're in an |
| + // editor, on GitHub, or fetched raw via docsmd.njk — so the title is |
| + // read from the source rather than declared twice. |
| + eleventyConfig.addFilter("docsTitle", (raw) => { |
| + for (const line of String(raw).split("\n")) { |
| + const m = /^#\s+(.+?)\s*#*$/.exec(line); |
| + if (m) return m[1].replace(/^🤖\s*/, ""); |
| + } |
| + return ""; |
| + }); |
| + |
| + // A page's raw markdown, looked up from the collection by slug. |
| + // |
| + // NOT passed through eleventyComputed: a computed value in Nunjucks |
| + // front matter is a string interpolation, which HTML-escapes it, and |
| + // an escaped apostrophe turns "carlos's" into "carlos-39-s" in every |
| + // anchor derived from it. The headings themselves are fine — |
| + // markdown-it-anchor reads the raw token — so the result was a |
| + // sub-nav whose links pointed at ids no page had. |
| + eleventyConfig.addFilter("docsRawFor", (docs, slug) => { |
| + for (const doc of docs) { |
| + if (doc.slug === slug) return doc.page.rawInput; |
| + } |
| + return ""; |
| + }); |
| + |
| + // A page's one-line blurb, from nav.json — the same string the /docs |
| + // index renders on its card, reused as the meta description so the |
| + // two cannot drift. |
| + eleventyConfig.addFilter("docsBlurb", (nav, slug) => { |
| + for (const section of nav.sections) { |
| + for (const entry of section.pages) { |
| + if (entry.slug === slug) return entry.blurb; |
| + } |
| + } |
| + return ""; |
| + }); |
| + |
| + return { |
| + // "html" is deliberately excluded: index.html and get-started/index.html |
| + // are hand-written and must ship byte-identical. If html stayed a |
| + // template format, Eleventy's default html engine (liquid) would |
| + // render them as templates in addition to the passthrough copies |
| + // above, and any literal "{{" in their inline SVG/script would break. |
| + templateFormats: ["njk", "md"], |
| + dir: { input: "src", includes: "_includes", data: "_data", output: "_site" }, |
| + }; |
| +} |
diff --git a/hack/check-docs.mjs b/hack/check-docs.mjs| new file mode 100644 |
| index 0000000..ef3ae9d |
| --- /dev/null |
| +++ b/hack/check-docs.mjs |
| @@ -0,0 +1,175 @@ |
| +// 🤖 Fails the build when the built docs site is internally broken. |
| +// |
| +// The platform repo gates the corpus as markdown: nav and files agree, |
| +// links resolve, anchors exist, symbols are covered. This checks what |
| +// that cannot — that the rendering actually produced the pages and ids |
| +// those links point at. A link can be correct in the source and still |
| +// 404 in a browser if the renderer disagrees with the gate about how a |
| +// heading becomes an anchor, which is exactly the failure worth |
| +// catching here. |
| +import { readdirSync, readFileSync, statSync } from "node:fs"; |
| +import { join, relative } from "node:path"; |
| + |
| +const site = "_site"; |
| +const docs = join(site, "docs"); |
| +const nav = JSON.parse(readFileSync("src/_data/docsnav.json", "utf8")); |
| + |
| +const problems = []; |
| +const fail = (m) => problems.push(m); |
| + |
| +function walk(dir, match) { |
| + const out = []; |
| + for (const entry of readdirSync(dir, { withFileTypes: true })) { |
| + const p = join(dir, entry.name); |
| + if (entry.isDirectory()) out.push(...walk(p, match)); |
| + else if (match(entry.name)) out.push(p); |
| + } |
| + return out; |
| +} |
| + |
| +const exists = (p) => { |
| + try { |
| + statSync(p); |
| + return true; |
| + } catch { |
| + return false; |
| + } |
| +}; |
| + |
| +// 1. The site as a whole was actually built. Everything below this check |
| +// is scoped to _site/docs — deliberately, that's the docs-specific |
| +// gate this file owns — but nothing else in the pipeline verifies |
| +// that the hand-written pages and shared assets were emitted at all. |
| +// Ported from the website's check-docs.mjs, which paid for this the |
| +// hard way: a passthrough file list going stale there dropped a live |
| +// page and shipped a 404ing stylesheet, both silently, because |
| +// npm run check only ever looked under _site/docs and exited 0 |
| +// either time. A build missing its homepage should fail before |
| +// anyone reads about anchors, so this runs first. |
| +// When one of these goes missing, it is almost always because an |
| +// addPassthroughCopy line in eleventy.config.js was removed or |
| +// renamed — check there first. |
| +const requiredSitePaths = [ |
| + "index.html", |
| + "get-started/index.html", |
| + "site.css", |
| + "site.js", |
| + "favicon.svg", |
| + "docs.css", |
| + "fonts/Geist-Variable.woff2", |
| + "fonts/GeistMono-Variable.woff2", |
| + "img/dashboard-light.png", |
| + "img/dashboard-dark.png", |
| +]; |
| +for (const rel of requiredSitePaths) { |
| + if (!exists(join(site, rel))) { |
| + fail(`_site/${rel} was not built — check the addPassthroughCopy lines in eleventy.config.js`); |
| + } |
| +} |
| + |
| +// 2. Every nav entry has a built page and a built .md twin. |
| +// |
| +// docsnav.json is hand-maintained here — there is no generator to blame |
| +// or re-run, so every failure below is an edit someone made. The floor |
| +// after the loop is the point of the whole gate: a loop over an empty nav |
| +// asserts nothing, so a nav trimmed down to {"sections": []} would print |
| +// "docs ok — 0 pages" and exit 0 on a corpus that had vanished. Same shape |
| +// as the pages.length guard below. |
| +const slugs = []; |
| +const sections = Array.isArray(nav.sections) ? nav.sections : []; |
| +if (!Array.isArray(nav.sections)) fail("docsnav.json has no sections array at all"); |
| +for (const section of sections) { |
| + if (!Array.isArray(section.pages)) { |
| + fail(`docsnav.json section ${JSON.stringify(section.title ?? "?")} has no "pages" key — every section in src/_data/docsnav.json needs one, listing that section's pages`); |
| + continue; |
| + } |
| + for (const entry of section.pages) { |
| + slugs.push(entry.slug); |
| + if (!exists(join(docs, entry.slug, "index.html"))) fail(`no built page for /docs/${entry.slug}/`); |
| + if (!exists(join(docs, `${entry.slug}.md`))) fail(`no markdown twin for /docs/${entry.slug}.md`); |
| + if (!entry.blurb) fail(`nav entry ${entry.slug} has no blurb`); |
| + if (!entry.label) fail(`nav entry ${entry.slug} has no label`); |
| + } |
| +} |
| +if (slugs.length === 0) { |
| + fail("src/_data/docsnav.json lists no pages — every page in src/docs/ needs an entry there; nothing in check 2 can fail against an empty nav"); |
| +} |
| +if (!exists(join(docs, "index.html"))) fail("no built /docs/ index"); |
| + |
| +// 3. Every internal href resolves — to a built page, and to a real id |
| +// when it carries a fragment. |
| +const pages = walk(docs, (n) => n.endsWith(".html")); |
| +if (pages.length === 0) fail("no built docs pages at all"); |
| + |
| +const idsFor = new Map(); |
| +for (const p of pages) { |
| + const html = readFileSync(p, "utf8"); |
| + idsFor.set(p, new Set([...html.matchAll(/\bid="([^"]+)"/g)].map((m) => m[1]))); |
| +} |
| + |
| +// Docs links are "/docs", "/docs/", "/docs/<slug>/" or "/docs/<slug>.md". |
| +// Anchored to a slash (or end) on purpose: this site links a stylesheet |
| +// as /docs.css, and a bare /docs prefix match claimed that as a page and |
| +// failed the build on it. |
| +const docsHref = /href="(\/docs(?:\/[^"]*)?)"/g; |
| + |
| +const pageFor = (href) => { |
| + const clean = href.replace(/[?#].*$/, ""); |
| + if (clean !== "/docs" && !clean.startsWith("/docs/")) return null; |
| + const rel = clean.replace(/^\/docs\/?/, "").replace(/\/$/, ""); |
| + if (rel === "") return join(docs, "index.html"); |
| + if (rel.endsWith(".md")) return join(docs, rel); |
| + return join(docs, rel, "index.html"); |
| +}; |
| + |
| +for (const p of pages) { |
| + const html = readFileSync(p, "utf8"); |
| + const where = relative(site, p); |
| + for (const m of html.matchAll(docsHref)) { |
| + const href = m[1]; |
| + const target = pageFor(href); |
| + if (!target || !exists(target)) { |
| + fail(`${where}: href="${href}" does not resolve to a built file`); |
| + continue; |
| + } |
| + const hash = href.includes("#") ? href.split("#")[1] : ""; |
| + if (hash && !(idsFor.get(target) ?? new Set()).has(hash)) { |
| + fail(`${where}: href="${href}" — ${relative(site, target)} has no id="${hash}"`); |
| + } |
| + } |
| + // Same-page fragments. |
| + const ids = idsFor.get(p); |
| + for (const m of html.matchAll(/href="#([^"]+)"/g)) { |
| + if (!ids.has(m[1])) fail(`${where}: href="#${m[1]}" has no matching id`); |
| + } |
| +} |
| + |
| +// 4. The anchor rule agrees on both sides. The Go gate accepted these |
| +// fragments; if the renderer slugifies differently they are dead |
| +// links no earlier check can see. The fixture is the platform repo's |
| +// docs/site/anchors.json, vendored — both languages read this file |
| +// rather than each other's implementation. |
| +// |
| +// The "//" key is not a case: it is generated-by metadata some fixture |
| +// writers prepend to every JSON file they emit. Skip it by name rather |
| +// than by loosening the comparison — the strictness IS this file's |
| +// purpose. |
| +const { slugify } = await import("../eleventy.config.js"); |
| +const fixture = JSON.parse(readFileSync("src/_data/docsanchors.json", "utf8")); |
| +const cases = Object.entries(fixture).filter(([input]) => input !== "//"); |
| +if (cases.length < 5) { |
| + fail("src/_data/docsanchors.json has too few cases to pin anything — it has been emptied or truncated; restore it from the platform repo's docs/site/anchors.json"); |
| +} |
| +for (const [input, want] of cases) { |
| + const got = slugify(input); |
| + if (got !== want) { |
| + fail(`slugify(${JSON.stringify(input)}) = ${JSON.stringify(got)}, want ${JSON.stringify(want)} — the renderer and internal/docsite.Anchor disagree`); |
| + } |
| +} |
| + |
| +if (problems.length) { |
| + console.error(`docs check failed (${problems.length}):`); |
| + for (const p of problems) console.error(` ${p}`); |
| + process.exit(1); |
| +} |
| +console.log(`docs ok — ${slugs.length} pages, ${pages.length} built, ${cases.length} anchor cases agree`); |
diff --git a/package-lock.json b/package-lock.json| new file mode 100644 |
| index 0000000..490bb04 |
| --- /dev/null |
| +++ b/package-lock.json |
| @@ -0,0 +1,1725 @@ |
| +{ |
| + "name": "carloku", |
| + "lockfileVersion": 3, |
| + "requires": true, |
| + "packages": { |
| + "": { |
| + "name": "carloku", |
| + "devDependencies": { |
| + "@11ty/eleventy": "^3.0.0", |
| + "markdown-it": "^14.1.0", |
| + "markdown-it-anchor": "^9.2.0" |
| + } |
| + }, |
| + "node_modules/@11ty/dependency-tree": { |
| + "version": "4.0.2", |
| + "resolved": "https://registry.npmjs.org/@11ty/dependency-tree/-/dependency-tree-4.0.2.tgz", |
| + "integrity": "sha512-RTF6VTZHatYf7fSZBUN3RKwiUeJh5dhWV61gDPrHhQF2/gzruAkYz8yXuvGLx3w3ZBKreGrR+MfYpSVkdbdbLA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "@11ty/eleventy-utils": "^2.0.1" |
| + } |
| + }, |
| + "node_modules/@11ty/dependency-tree-esm": { |
| + "version": "2.0.4", |
| + "resolved": "https://registry.npmjs.org/@11ty/dependency-tree-esm/-/dependency-tree-esm-2.0.4.tgz", |
| + "integrity": "sha512-MYKC0Ac77ILr1HnRJalzKDlb9Z8To3kXQCltx299pUXXUFtJ1RIONtULlknknqW8cLe19DLVgmxVCtjEFm7h0A==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "@11ty/eleventy-utils": "^2.0.7", |
| + "acorn": "^8.15.0", |
| + "dependency-graph": "^1.0.0", |
| + "normalize-path": "^3.0.0" |
| + } |
| + }, |
| + "node_modules/@11ty/eleventy": { |
| + "version": "3.1.6", |
| + "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-3.1.6.tgz", |
| + "integrity": "sha512-ZlSiR1PLdS2lv7TelBgWAhcvMiLNZkPBlLEb+lh7kGYZ+Mk0bo9qcYgVsewvw9W7Em0RH3wd01h5fAstNDh0zA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "@11ty/dependency-tree": "^4.0.2", |
| + "@11ty/dependency-tree-esm": "^2.0.4", |
| + "@11ty/eleventy-dev-server": "^2.0.8", |
| + "@11ty/eleventy-plugin-bundle": "^3.0.7", |
| + "@11ty/eleventy-utils": "^2.0.7", |
| + "@11ty/lodash-custom": "^4.17.21", |
| + "@11ty/posthtml-urls": "^1.0.3", |
| + "@11ty/recursive-copy": "^4.0.4", |
| + "@sindresorhus/slugify": "^2.2.1", |
| + "bcp-47-normalize": "^2.3.0", |
| + "chokidar": "^3.6.0", |
| + "debug": "^4.4.3", |
| + "dependency-graph": "^1.0.0", |
| + "entities": "^6.0.1", |
| + "filesize": "^10.1.6", |
| + "gray-matter": "^4.0.3", |
| + "iso-639-1": "^3.1.5", |
| + "js-yaml": "^4.1.1", |
| + "kleur": "^4.1.5", |
| + "liquidjs": "^10.27.0", |
| + "luxon": "^3.7.2", |
| + "markdown-it": "^14.2.0", |
| + "minimist": "^1.2.8", |
| + "moo": "0.5.2", |
| + "node-retrieve-globals": "^6.0.1", |
| + "nunjucks": "^3.2.4", |
| + "picomatch": "^4.0.4", |
| + "please-upgrade-node": "^3.2.0", |
| + "posthtml": "^0.16.7", |
| + "posthtml-match-helper": "^2.0.3", |
| + "semver": "^7.8.1", |
| + "slugify": "^1.6.9", |
| + "tinyglobby": "^0.2.16" |
| + }, |
| + "bin": { |
| + "eleventy": "cmd.cjs" |
| + }, |
| + "engines": { |
| + "node": ">=18" |
| + }, |
| + "funding": { |
| + "type": "opencollective", |
| + "url": "https://opencollective.com/11ty" |
| + } |
| + }, |
| + "node_modules/@11ty/eleventy-dev-server": { |
| + "version": "2.0.8", |
| + "resolved": "https://registry.npmjs.org/@11ty/eleventy-dev-server/-/eleventy-dev-server-2.0.8.tgz", |
| + "integrity": "sha512-15oC5M1DQlCaOMUq4limKRYmWiGecDaGwryr7fTE/oM9Ix8siqMvWi+I8VjsfrGr+iViDvWcH/TVI6D12d93mA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "@11ty/eleventy-utils": "^2.0.1", |
| + "chokidar": "^3.6.0", |
| + "debug": "^4.4.0", |
| + "finalhandler": "^1.3.1", |
| + "mime": "^3.0.0", |
| + "minimist": "^1.2.8", |
| + "morphdom": "^2.7.4", |
| + "please-upgrade-node": "^3.2.0", |
| + "send": "^1.1.0", |
| + "ssri": "^11.0.0", |
| + "urlpattern-polyfill": "^10.0.0", |
| + "ws": "^8.18.1" |
| + }, |
| + "bin": { |
| + "eleventy-dev-server": "cmd.js" |
| + }, |
| + "engines": { |
| + "node": ">=18" |
| + }, |
| + "funding": { |
| + "type": "opencollective", |
| + "url": "https://opencollective.com/11ty" |
| + } |
| + }, |
| + "node_modules/@11ty/eleventy-plugin-bundle": { |
| + "version": "3.0.7", |
| + "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.7.tgz", |
| + "integrity": "sha512-QK1tRFBhQdZASnYU8GMzpTdsMMFLVAkuU0gVVILqNyp09xJJZb81kAS3AFrNrwBCsgLxTdWHJ8N64+OTTsoKkA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "@11ty/eleventy-utils": "^2.0.2", |
| + "debug": "^4.4.0", |
| + "posthtml-match-helper": "^2.0.3" |
| + }, |
| + "engines": { |
| + "node": ">=18" |
| + }, |
| + "funding": { |
| + "type": "opencollective", |
| + "url": "https://opencollective.com/11ty" |
| + } |
| + }, |
| + "node_modules/@11ty/eleventy-utils": { |
| + "version": "2.0.7", |
| + "resolved": "https://registry.npmjs.org/@11ty/eleventy-utils/-/eleventy-utils-2.0.7.tgz", |
| + "integrity": "sha512-6QE+duqSQ0GY9rENXYb4iPR4AYGdrFpqnmi59tFp9VrleOl0QSh8VlBr2yd6dlhkdtj7904poZW5PvGr9cMiJQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=18" |
| + }, |
| + "funding": { |
| + "type": "opencollective", |
| + "url": "https://opencollective.com/11ty" |
| + } |
| + }, |
| + "node_modules/@11ty/lodash-custom": { |
| + "version": "4.17.21", |
| + "resolved": "https://registry.npmjs.org/@11ty/lodash-custom/-/lodash-custom-4.17.21.tgz", |
| + "integrity": "sha512-Mqt6im1xpb1Ykn3nbcCovWXK3ggywRJa+IXIdoz4wIIK+cvozADH63lexcuPpGS/gJ6/m2JxyyXDyupkMr5DHw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=14" |
| + }, |
| + "funding": { |
| + "type": "opencollective", |
| + "url": "https://opencollective.com/11ty" |
| + } |
| + }, |
| + "node_modules/@11ty/posthtml-urls": { |
| + "version": "1.0.3", |
| + "resolved": "https://registry.npmjs.org/@11ty/posthtml-urls/-/posthtml-urls-1.0.3.tgz", |
| + "integrity": "sha512-1YvhnkaNlFnnJic1rBMWmTC2adbuy+JQiBfl1Hecr1Wjjik1pQZmGyk/eC9zKX/FQv52s2Nht1Gi/UwhYqrBeg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "evaluate-value": "^2.0.0", |
| + "http-equiv-refresh": "^2.0.1", |
| + "list-to-array": "^1.1.0", |
| + "parse-srcset": "^1.0.2" |
| + }, |
| + "engines": { |
| + "node": ">= 6" |
| + } |
| + }, |
| + "node_modules/@11ty/recursive-copy": { |
| + "version": "4.0.4", |
| + "resolved": "https://registry.npmjs.org/@11ty/recursive-copy/-/recursive-copy-4.0.4.tgz", |
| + "integrity": "sha512-oI7m8pa7/IAU/3lqRU9vjBbs20iKFo7x+1K9kT3aVira6scc1X9MjBdgLCHzLJeJ7iB6wydioA+kr9/qPnvmlQ==", |
| + "dev": true, |
| + "license": "ISC", |
| + "dependencies": { |
| + "errno": "^1.0.0", |
| + "junk": "^3.1.0", |
| + "minimatch": "^3.1.5", |
| + "slash": "^3.0.0" |
| + }, |
| + "engines": { |
| + "node": ">=18" |
| + } |
| + }, |
| + "node_modules/@sindresorhus/slugify": { |
| + "version": "2.2.1", |
| + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", |
| + "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "@sindresorhus/transliterate": "^1.0.0", |
| + "escape-string-regexp": "^5.0.0" |
| + }, |
| + "engines": { |
| + "node": ">=12" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/sponsors/sindresorhus" |
| + } |
| + }, |
| + "node_modules/@sindresorhus/transliterate": { |
| + "version": "1.6.0", |
| + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", |
| + "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "escape-string-regexp": "^5.0.0" |
| + }, |
| + "engines": { |
| + "node": ">=12" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/sponsors/sindresorhus" |
| + } |
| + }, |
| + "node_modules/@types/linkify-it": { |
| + "version": "5.0.0", |
| + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", |
| + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", |
| + "dev": true, |
| + "license": "MIT", |
| + "peer": true |
| + }, |
| + "node_modules/@types/markdown-it": { |
| + "version": "14.2.0", |
| + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz", |
| + "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "peer": true, |
| + "dependencies": { |
| + "@types/linkify-it": "^5", |
| + "@types/mdurl": "^2" |
| + } |
| + }, |
| + "node_modules/@types/mdurl": { |
| + "version": "2.0.0", |
| + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", |
| + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "peer": true |
| + }, |
| + "node_modules/a-sync-waterfall": { |
| + "version": "1.0.1", |
| + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", |
| + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/acorn": { |
| + "version": "8.18.0", |
| + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", |
| + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "bin": { |
| + "acorn": "bin/acorn" |
| + }, |
| + "engines": { |
| + "node": ">=0.4.0" |
| + } |
| + }, |
| + "node_modules/acorn-walk": { |
| + "version": "8.3.5", |
| + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", |
| + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "acorn": "^8.11.0" |
| + }, |
| + "engines": { |
| + "node": ">=0.4.0" |
| + } |
| + }, |
| + "node_modules/anymatch": { |
| + "version": "3.1.3", |
| + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", |
| + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", |
| + "dev": true, |
| + "license": "ISC", |
| + "dependencies": { |
| + "normalize-path": "^3.0.0", |
| + "picomatch": "^2.0.4" |
| + }, |
| + "engines": { |
| + "node": ">= 8" |
| + } |
| + }, |
| + "node_modules/anymatch/node_modules/picomatch": { |
| + "version": "2.3.2", |
| + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", |
| + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=8.6" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/sponsors/jonschlinkert" |
| + } |
| + }, |
| + "node_modules/argparse": { |
| + "version": "2.0.1", |
| + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", |
| + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", |
| + "dev": true, |
| + "license": "Python-2.0" |
| + }, |
| + "node_modules/asap": { |
| + "version": "2.0.6", |
| + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", |
| + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/balanced-match": { |
| + "version": "1.0.2", |
| + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", |
| + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/bcp-47": { |
| + "version": "2.1.1", |
| + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz", |
| + "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "is-alphabetical": "^2.0.0", |
| + "is-alphanumerical": "^2.0.0", |
| + "is-decimal": "^2.0.0" |
| + }, |
| + "funding": { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/wooorm" |
| + } |
| + }, |
| + "node_modules/bcp-47-match": { |
| + "version": "2.0.3", |
| + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", |
| + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "funding": { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/wooorm" |
| + } |
| + }, |
| + "node_modules/bcp-47-normalize": { |
| + "version": "2.3.0", |
| + "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz", |
| + "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "bcp-47": "^2.0.0", |
| + "bcp-47-match": "^2.0.0" |
| + }, |
| + "funding": { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/wooorm" |
| + } |
| + }, |
| + "node_modules/binary-extensions": { |
| + "version": "2.3.0", |
| + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", |
| + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=8" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/sponsors/sindresorhus" |
| + } |
| + }, |
| + "node_modules/brace-expansion": { |
| + "version": "1.1.18", |
| + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", |
| + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "balanced-match": "^1.0.0", |
| + "concat-map": "0.0.1" |
| + } |
| + }, |
| + "node_modules/braces": { |
| + "version": "3.0.3", |
| + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", |
| + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "fill-range": "^7.1.1" |
| + }, |
| + "engines": { |
| + "node": ">=8" |
| + } |
| + }, |
| + "node_modules/chokidar": { |
| + "version": "3.6.0", |
| + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", |
| + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "anymatch": "~3.1.2", |
| + "braces": "~3.0.2", |
| + "glob-parent": "~5.1.2", |
| + "is-binary-path": "~2.1.0", |
| + "is-glob": "~4.0.1", |
| + "normalize-path": "~3.0.0", |
| + "readdirp": "~3.6.0" |
| + }, |
| + "engines": { |
| + "node": ">= 8.10.0" |
| + }, |
| + "funding": { |
| + "url": "https://paulmillr.com/funding/" |
| + }, |
| + "optionalDependencies": { |
| + "fsevents": "~2.3.2" |
| + } |
| + }, |
| + "node_modules/commander": { |
| + "version": "10.0.1", |
| + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", |
| + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=14" |
| + } |
| + }, |
| + "node_modules/concat-map": { |
| + "version": "0.0.1", |
| + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", |
| + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/debug": { |
| + "version": "4.4.3", |
| + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", |
| + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "ms": "^2.1.3" |
| + }, |
| + "engines": { |
| + "node": ">=6.0" |
| + }, |
| + "peerDependenciesMeta": { |
| + "supports-color": { |
| + "optional": true |
| + } |
| + } |
| + }, |
| + "node_modules/depd": { |
| + "version": "2.0.0", |
| + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", |
| + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 0.8" |
| + } |
| + }, |
| + "node_modules/dependency-graph": { |
| + "version": "1.0.0", |
| + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", |
| + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=4" |
| + } |
| + }, |
| + "node_modules/dom-serializer": { |
| + "version": "1.4.1", |
| + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", |
| + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "domelementtype": "^2.0.1", |
| + "domhandler": "^4.2.0", |
| + "entities": "^2.0.0" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" |
| + } |
| + }, |
| + "node_modules/dom-serializer/node_modules/entities": { |
| + "version": "2.2.0", |
| + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", |
| + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", |
| + "dev": true, |
| + "license": "BSD-2-Clause", |
| + "funding": { |
| + "url": "https://github.com/fb55/entities?sponsor=1" |
| + } |
| + }, |
| + "node_modules/domelementtype": { |
| + "version": "2.3.0", |
| + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", |
| + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", |
| + "dev": true, |
| + "funding": [ |
| + { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/fb55" |
| + } |
| + ], |
| + "license": "BSD-2-Clause" |
| + }, |
| + "node_modules/domhandler": { |
| + "version": "4.3.1", |
| + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", |
| + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", |
| + "dev": true, |
| + "license": "BSD-2-Clause", |
| + "dependencies": { |
| + "domelementtype": "^2.2.0" |
| + }, |
| + "engines": { |
| + "node": ">= 4" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/fb55/domhandler?sponsor=1" |
| + } |
| + }, |
| + "node_modules/domutils": { |
| + "version": "2.8.0", |
| + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", |
| + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", |
| + "dev": true, |
| + "license": "BSD-2-Clause", |
| + "dependencies": { |
| + "dom-serializer": "^1.0.1", |
| + "domelementtype": "^2.2.0", |
| + "domhandler": "^4.2.0" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/fb55/domutils?sponsor=1" |
| + } |
| + }, |
| + "node_modules/ee-first": { |
| + "version": "1.1.1", |
| + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", |
| + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/encodeurl": { |
| + "version": "2.0.0", |
| + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", |
| + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 0.8" |
| + } |
| + }, |
| + "node_modules/entities": { |
| + "version": "6.0.1", |
| + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", |
| + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", |
| + "dev": true, |
| + "license": "BSD-2-Clause", |
| + "engines": { |
| + "node": ">=0.12" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/fb55/entities?sponsor=1" |
| + } |
| + }, |
| + "node_modules/errno": { |
| + "version": "1.0.0", |
| + "resolved": "https://registry.npmjs.org/errno/-/errno-1.0.0.tgz", |
| + "integrity": "sha512-3zV5mFS1E8/1bPxt/B0xxzI1snsg3uSCIh6Zo1qKg6iMw93hzPANk9oBFzSFBFrwuVoQuE3rLoouAUfwOAj1wQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "prr": "~1.0.1" |
| + }, |
| + "bin": { |
| + "errno": "cli.js" |
| + } |
| + }, |
| + "node_modules/escape-html": { |
| + "version": "1.0.3", |
| + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", |
| + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/escape-string-regexp": { |
| + "version": "5.0.0", |
| + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", |
| + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=12" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/sponsors/sindresorhus" |
| + } |
| + }, |
| + "node_modules/esm-import-transformer": { |
| + "version": "3.0.5", |
| + "resolved": "https://registry.npmjs.org/esm-import-transformer/-/esm-import-transformer-3.0.5.tgz", |
| + "integrity": "sha512-1GKLvfuMnnpI75l8c6sHoz0L3Z872xL5akGuBudgqTDPv4Vy6f2Ec7jEMKTxlqWl/3kSvNbHELeimJtnqgYniw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "acorn": "^8.15.0" |
| + } |
| + }, |
| + "node_modules/esprima": { |
| + "version": "4.0.1", |
| + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", |
| + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", |
| + "dev": true, |
| + "license": "BSD-2-Clause", |
| + "bin": { |
| + "esparse": "bin/esparse.js", |
| + "esvalidate": "bin/esvalidate.js" |
| + }, |
| + "engines": { |
| + "node": ">=4" |
| + } |
| + }, |
| + "node_modules/etag": { |
| + "version": "1.8.1", |
| + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", |
| + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 0.6" |
| + } |
| + }, |
| + "node_modules/evaluate-value": { |
| + "version": "2.0.0", |
| + "resolved": "https://registry.npmjs.org/evaluate-value/-/evaluate-value-2.0.0.tgz", |
| + "integrity": "sha512-VonfiuDJc0z4sOO7W0Pd130VLsXN6vmBWZlrog1mCb/o7o/Nl5Lr25+Kj/nkCCAhG+zqeeGjxhkK9oHpkgTHhQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 8" |
| + } |
| + }, |
| + "node_modules/extend-shallow": { |
| + "version": "2.0.1", |
| + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", |
| + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "is-extendable": "^0.1.0" |
| + }, |
| + "engines": { |
| + "node": ">=0.10.0" |
| + } |
| + }, |
| + "node_modules/fdir": { |
| + "version": "6.5.0", |
| + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", |
| + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=12.0.0" |
| + }, |
| + "peerDependencies": { |
| + "picomatch": "^3 || ^4" |
| + }, |
| + "peerDependenciesMeta": { |
| + "picomatch": { |
| + "optional": true |
| + } |
| + } |
| + }, |
| + "node_modules/filesize": { |
| + "version": "10.1.6", |
| + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", |
| + "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", |
| + "dev": true, |
| + "license": "BSD-3-Clause", |
| + "engines": { |
| + "node": ">= 10.4.0" |
| + } |
| + }, |
| + "node_modules/fill-range": { |
| + "version": "7.1.1", |
| + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", |
| + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "to-regex-range": "^5.0.1" |
| + }, |
| + "engines": { |
| + "node": ">=8" |
| + } |
| + }, |
| + "node_modules/finalhandler": { |
| + "version": "1.3.2", |
| + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", |
| + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "debug": "2.6.9", |
| + "encodeurl": "~2.0.0", |
| + "escape-html": "~1.0.3", |
| + "on-finished": "~2.4.1", |
| + "parseurl": "~1.3.3", |
| + "statuses": "~2.0.2", |
| + "unpipe": "~1.0.0" |
| + }, |
| + "engines": { |
| + "node": ">= 0.8" |
| + } |
| + }, |
| + "node_modules/finalhandler/node_modules/debug": { |
| + "version": "2.6.9", |
| + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", |
| + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "ms": "2.0.0" |
| + } |
| + }, |
| + "node_modules/finalhandler/node_modules/ms": { |
| + "version": "2.0.0", |
| + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", |
| + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/fresh": { |
| + "version": "2.0.0", |
| + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", |
| + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 0.8" |
| + } |
| + }, |
| + "node_modules/fsevents": { |
| + "version": "2.3.3", |
| + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", |
| + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", |
| + "dev": true, |
| + "hasInstallScript": true, |
| + "license": "MIT", |
| + "optional": true, |
| + "os": [ |
| + "darwin" |
| + ], |
| + "engines": { |
| + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" |
| + } |
| + }, |
| + "node_modules/glob-parent": { |
| + "version": "5.1.2", |
| + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", |
| + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", |
| + "dev": true, |
| + "license": "ISC", |
| + "dependencies": { |
| + "is-glob": "^4.0.1" |
| + }, |
| + "engines": { |
| + "node": ">= 6" |
| + } |
| + }, |
| + "node_modules/gray-matter": { |
| + "version": "4.0.3", |
| + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", |
| + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "js-yaml": "^3.13.1", |
| + "kind-of": "^6.0.2", |
| + "section-matter": "^1.0.0", |
| + "strip-bom-string": "^1.0.0" |
| + }, |
| + "engines": { |
| + "node": ">=6.0" |
| + } |
| + }, |
| + "node_modules/gray-matter/node_modules/argparse": { |
| + "version": "1.0.10", |
| + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", |
| + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "sprintf-js": "~1.0.2" |
| + } |
| + }, |
| + "node_modules/gray-matter/node_modules/js-yaml": { |
| + "version": "3.15.1", |
| + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", |
| + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "argparse": "^1.0.7", |
| + "esprima": "^4.0.0" |
| + }, |
| + "bin": { |
| + "js-yaml": "bin/js-yaml.js" |
| + } |
| + }, |
| + "node_modules/htmlparser2": { |
| + "version": "7.2.0", |
| + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", |
| + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", |
| + "dev": true, |
| + "funding": [ |
| + "https://github.com/fb55/htmlparser2?sponsor=1", |
| + { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/fb55" |
| + } |
| + ], |
| + "license": "MIT", |
| + "dependencies": { |
| + "domelementtype": "^2.0.1", |
| + "domhandler": "^4.2.2", |
| + "domutils": "^2.8.0", |
| + "entities": "^3.0.1" |
| + } |
| + }, |
| + "node_modules/htmlparser2/node_modules/entities": { |
| + "version": "3.0.1", |
| + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", |
| + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", |
| + "dev": true, |
| + "license": "BSD-2-Clause", |
| + "engines": { |
| + "node": ">=0.12" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/fb55/entities?sponsor=1" |
| + } |
| + }, |
| + "node_modules/http-equiv-refresh": { |
| + "version": "2.0.1", |
| + "resolved": "https://registry.npmjs.org/http-equiv-refresh/-/http-equiv-refresh-2.0.1.tgz", |
| + "integrity": "sha512-XJpDL/MLkV3dKwLzHwr2dY05dYNfBNlyPu4STQ8WvKCFdc6vC5tPXuq28of663+gHVg03C+16pHHs/+FmmDjcw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 6" |
| + } |
| + }, |
| + "node_modules/http-errors": { |
| + "version": "2.0.1", |
| + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", |
| + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "depd": "~2.0.0", |
| + "inherits": "~2.0.4", |
| + "setprototypeof": "~1.2.0", |
| + "statuses": "~2.0.2", |
| + "toidentifier": "~1.0.1" |
| + }, |
| + "engines": { |
| + "node": ">= 0.8" |
| + }, |
| + "funding": { |
| + "type": "opencollective", |
| + "url": "https://opencollective.com/express" |
| + } |
| + }, |
| + "node_modules/inherits": { |
| + "version": "2.0.4", |
| + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", |
| + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", |
| + "dev": true, |
| + "license": "ISC" |
| + }, |
| + "node_modules/is-alphabetical": { |
| + "version": "2.0.1", |
| + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", |
| + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "funding": { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/wooorm" |
| + } |
| + }, |
| + "node_modules/is-alphanumerical": { |
| + "version": "2.0.1", |
| + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", |
| + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "is-alphabetical": "^2.0.0", |
| + "is-decimal": "^2.0.0" |
| + }, |
| + "funding": { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/wooorm" |
| + } |
| + }, |
| + "node_modules/is-binary-path": { |
| + "version": "2.1.0", |
| + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", |
| + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "binary-extensions": "^2.0.0" |
| + }, |
| + "engines": { |
| + "node": ">=8" |
| + } |
| + }, |
| + "node_modules/is-decimal": { |
| + "version": "2.0.1", |
| + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", |
| + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", |
| + "dev": true, |
| + "license": "MIT", |
| + "funding": { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/wooorm" |
| + } |
| + }, |
| + "node_modules/is-extendable": { |
| + "version": "0.1.1", |
| + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", |
| + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=0.10.0" |
| + } |
| + }, |
| + "node_modules/is-extglob": { |
| + "version": "2.1.1", |
| + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", |
| + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=0.10.0" |
| + } |
| + }, |
| + "node_modules/is-glob": { |
| + "version": "4.0.3", |
| + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", |
| + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "is-extglob": "^2.1.1" |
| + }, |
| + "engines": { |
| + "node": ">=0.10.0" |
| + } |
| + }, |
| + "node_modules/is-json": { |
| + "version": "2.0.1", |
| + "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", |
| + "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", |
| + "dev": true, |
| + "license": "ISC" |
| + }, |
| + "node_modules/is-number": { |
| + "version": "7.0.0", |
| + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", |
| + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=0.12.0" |
| + } |
| + }, |
| + "node_modules/iso-639-1": { |
| + "version": "3.1.6", |
| + "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.6.tgz", |
| + "integrity": "sha512-ZFar/L4ngX7wZh2QX+Fiftmuf0igWJsrJtfizrovWifF1gAWkfmRa5Z1m0LQZbm0hKCHRDYhLRSLFrSqNe4EJA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=6.0" |
| + } |
| + }, |
| + "node_modules/js-yaml": { |
| + "version": "4.3.2", |
| + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", |
| + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", |
| + "dev": true, |
| + "funding": [ |
| + { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/puzrin" |
| + }, |
| + { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/nodeca" |
| + } |
| + ], |
| + "license": "MIT", |
| + "dependencies": { |
| + "argparse": "^2.0.1" |
| + }, |
| + "bin": { |
| + "js-yaml": "bin/js-yaml.js" |
| + } |
| + }, |
| + "node_modules/junk": { |
| + "version": "3.1.0", |
| + "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", |
| + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=8" |
| + } |
| + }, |
| + "node_modules/kind-of": { |
| + "version": "6.0.3", |
| + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", |
| + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=0.10.0" |
| + } |
| + }, |
| + "node_modules/kleur": { |
| + "version": "4.1.5", |
| + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", |
| + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=6" |
| + } |
| + }, |
| + "node_modules/linkify-it": { |
| + "version": "5.0.2", |
| + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", |
| + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", |
| + "dev": true, |
| + "funding": [ |
| + { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/puzrin" |
| + }, |
| + { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/markdown-it" |
| + } |
| + ], |
| + "license": "MIT", |
| + "dependencies": { |
| + "uc.micro": "^2.0.0" |
| + } |
| + }, |
| + "node_modules/liquidjs": { |
| + "version": "10.29.0", |
| + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.29.0.tgz", |
| + "integrity": "sha512-pCVOhs6FLAR8su3ItJ07diN26t6W5dHQRnmTMy8HPyTFuv1+oSCVJIGp5pGjfQyOZfh50KswvKtMTp6p4JEIdw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "commander": "^10.0.0" |
| + }, |
| + "bin": { |
| + "liquid": "bin/liquid.js", |
| + "liquidjs": "bin/liquid.js" |
| + }, |
| + "engines": { |
| + "node": ">=16" |
| + }, |
| + "funding": { |
| + "type": "opencollective", |
| + "url": "https://opencollective.com/liquidjs" |
| + } |
| + }, |
| + "node_modules/list-to-array": { |
| + "version": "1.1.0", |
| + "resolved": "https://registry.npmjs.org/list-to-array/-/list-to-array-1.1.0.tgz", |
| + "integrity": "sha512-+dAZZ2mM+/m+vY9ezfoueVvrgnHIGi5FvgSymbIgJOFwiznWyA59mav95L+Mc6xPtL3s9gm5eNTlNtxJLbNM1g==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/luxon": { |
| + "version": "3.7.2", |
| + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", |
| + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=12" |
| + } |
| + }, |
| + "node_modules/markdown-it": { |
| + "version": "14.3.0", |
| + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", |
| + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", |
| + "dev": true, |
| + "funding": [ |
| + { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/puzrin" |
| + }, |
| + { |
| + "type": "github", |
| + "url": "https://github.com/sponsors/markdown-it" |
| + } |
| + ], |
| + "license": "MIT", |
| + "dependencies": { |
| + "argparse": "^2.0.1", |
| + "entities": "^4.5.0", |
| + "linkify-it": "^5.0.2", |
| + "mdurl": "^2.0.0", |
| + "punycode.js": "^2.3.1", |
| + "uc.micro": "^2.1.0" |
| + }, |
| + "bin": { |
| + "markdown-it": "bin/markdown-it.mjs" |
| + } |
| + }, |
| + "node_modules/markdown-it-anchor": { |
| + "version": "9.2.1", |
| + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-9.2.1.tgz", |
| + "integrity": "sha512-p6APiLJDFAW2GEvaavDvhIBn7jrX2jLv77NkBGgNacFTurbORYc4pyYySg/mI6mpR6cHQuAtzKtmqgQr4K8dsQ==", |
| + "dev": true, |
| + "license": "Unlicense", |
| + "peerDependencies": { |
| + "@types/markdown-it": "*", |
| + "markdown-it": "*" |
| + } |
| + }, |
| + "node_modules/markdown-it/node_modules/entities": { |
| + "version": "4.5.0", |
| + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", |
| + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", |
| + "dev": true, |
| + "license": "BSD-2-Clause", |
| + "engines": { |
| + "node": ">=0.12" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/fb55/entities?sponsor=1" |
| + } |
| + }, |
| + "node_modules/mdurl": { |
| + "version": "2.1.0", |
| + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", |
| + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/mime": { |
| + "version": "3.0.0", |
| + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", |
| + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", |
| + "dev": true, |
| + "license": "MIT", |
| + "bin": { |
| + "mime": "cli.js" |
| + }, |
| + "engines": { |
| + "node": ">=10.0.0" |
| + } |
| + }, |
| + "node_modules/mime-db": { |
| + "version": "1.54.0", |
| + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", |
| + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 0.6" |
| + } |
| + }, |
| + "node_modules/mime-types": { |
| + "version": "3.0.2", |
| + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", |
| + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "mime-db": "^1.54.0" |
| + }, |
| + "engines": { |
| + "node": ">=18" |
| + }, |
| + "funding": { |
| + "type": "opencollective", |
| + "url": "https://opencollective.com/express" |
| + } |
| + }, |
| + "node_modules/minimatch": { |
| + "version": "3.1.5", |
| + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", |
| + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", |
| + "dev": true, |
| + "license": "ISC", |
| + "dependencies": { |
| + "brace-expansion": "^1.1.7" |
| + }, |
| + "engines": { |
| + "node": "*" |
| + } |
| + }, |
| + "node_modules/minimist": { |
| + "version": "1.2.8", |
| + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", |
| + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "funding": { |
| + "url": "https://github.com/sponsors/ljharb" |
| + } |
| + }, |
| + "node_modules/minipass": { |
| + "version": "7.1.3", |
| + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", |
| + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", |
| + "dev": true, |
| + "license": "BlueOak-1.0.0", |
| + "engines": { |
| + "node": ">=16 || 14 >=14.17" |
| + } |
| + }, |
| + "node_modules/moo": { |
| + "version": "0.5.2", |
| + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", |
| + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", |
| + "dev": true, |
| + "license": "BSD-3-Clause" |
| + }, |
| + "node_modules/morphdom": { |
| + "version": "2.7.8", |
| + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.8.tgz", |
| + "integrity": "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/ms": { |
| + "version": "2.1.3", |
| + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", |
| + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/node-retrieve-globals": { |
| + "version": "6.0.1", |
| + "resolved": "https://registry.npmjs.org/node-retrieve-globals/-/node-retrieve-globals-6.0.1.tgz", |
| + "integrity": "sha512-j0DeFuZ/Wg3VlklfbxUgZF/mdHMTEiEipBb3q0SpMMbHaV3AVfoUQF8UGxh1s/yjqO0TgRZd4Pi/x2yRqoQ4Eg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "acorn": "^8.14.1", |
| + "acorn-walk": "^8.3.4", |
| + "esm-import-transformer": "^3.0.3" |
| + } |
| + }, |
| + "node_modules/normalize-path": { |
| + "version": "3.0.0", |
| + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", |
| + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=0.10.0" |
| + } |
| + }, |
| + "node_modules/nunjucks": { |
| + "version": "3.2.4", |
| + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", |
| + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", |
| + "dev": true, |
| + "license": "BSD-2-Clause", |
| + "dependencies": { |
| + "a-sync-waterfall": "^1.0.0", |
| + "asap": "^2.0.3", |
| + "commander": "^5.1.0" |
| + }, |
| + "bin": { |
| + "nunjucks-precompile": "bin/precompile" |
| + }, |
| + "engines": { |
| + "node": ">= 6.9.0" |
| + }, |
| + "peerDependencies": { |
| + "chokidar": "^3.3.0" |
| + }, |
| + "peerDependenciesMeta": { |
| + "chokidar": { |
| + "optional": true |
| + } |
| + } |
| + }, |
| + "node_modules/nunjucks/node_modules/commander": { |
| + "version": "5.1.0", |
| + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", |
| + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 6" |
| + } |
| + }, |
| + "node_modules/on-finished": { |
| + "version": "2.4.1", |
| + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", |
| + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "ee-first": "1.1.1" |
| + }, |
| + "engines": { |
| + "node": ">= 0.8" |
| + } |
| + }, |
| + "node_modules/parse-srcset": { |
| + "version": "1.0.2", |
| + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", |
| + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/parseurl": { |
| + "version": "1.3.3", |
| + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", |
| + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 0.8" |
| + } |
| + }, |
| + "node_modules/picomatch": { |
| + "version": "4.0.7", |
| + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", |
| + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=12" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/sponsors/jonschlinkert" |
| + } |
| + }, |
| + "node_modules/please-upgrade-node": { |
| + "version": "3.2.0", |
| + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", |
| + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "semver-compare": "^1.0.0" |
| + } |
| + }, |
| + "node_modules/posthtml": { |
| + "version": "0.16.7", |
| + "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz", |
| + "integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "posthtml-parser": "^0.11.0", |
| + "posthtml-render": "^3.0.0" |
| + }, |
| + "engines": { |
| + "node": ">=12.0.0" |
| + } |
| + }, |
| + "node_modules/posthtml-match-helper": { |
| + "version": "2.0.3", |
| + "resolved": "https://registry.npmjs.org/posthtml-match-helper/-/posthtml-match-helper-2.0.3.tgz", |
| + "integrity": "sha512-p9oJgTdMF2dyd7WE54QI1LvpBIkNkbSiiECKezNnDVYhGhD1AaOnAkw0Uh0y5TW+OHO8iBdSqnd8Wkpb6iUqmw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=18" |
| + }, |
| + "peerDependencies": { |
| + "posthtml": "^0.16.6" |
| + } |
| + }, |
| + "node_modules/posthtml-parser": { |
| + "version": "0.11.0", |
| + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz", |
| + "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "htmlparser2": "^7.1.1" |
| + }, |
| + "engines": { |
| + "node": ">=12" |
| + } |
| + }, |
| + "node_modules/posthtml-render": { |
| + "version": "3.0.0", |
| + "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz", |
| + "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "is-json": "^2.0.1" |
| + }, |
| + "engines": { |
| + "node": ">=12" |
| + } |
| + }, |
| + "node_modules/prr": { |
| + "version": "1.0.1", |
| + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", |
| + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/punycode.js": { |
| + "version": "2.3.1", |
| + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", |
| + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=6" |
| + } |
| + }, |
| + "node_modules/range-parser": { |
| + "version": "1.3.0", |
| + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", |
| + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 0.6" |
| + }, |
| + "funding": { |
| + "type": "opencollective", |
| + "url": "https://opencollective.com/express" |
| + } |
| + }, |
| + "node_modules/readdirp": { |
| + "version": "3.6.0", |
| + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", |
| + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "picomatch": "^2.2.1" |
| + }, |
| + "engines": { |
| + "node": ">=8.10.0" |
| + } |
| + }, |
| + "node_modules/readdirp/node_modules/picomatch": { |
| + "version": "2.3.2", |
| + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", |
| + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=8.6" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/sponsors/jonschlinkert" |
| + } |
| + }, |
| + "node_modules/section-matter": { |
| + "version": "1.0.0", |
| + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", |
| + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "extend-shallow": "^2.0.1", |
| + "kind-of": "^6.0.0" |
| + }, |
| + "engines": { |
| + "node": ">=4" |
| + } |
| + }, |
| + "node_modules/semver": { |
| + "version": "7.8.5", |
| + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", |
| + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", |
| + "dev": true, |
| + "license": "ISC", |
| + "bin": { |
| + "semver": "bin/semver.js" |
| + }, |
| + "engines": { |
| + "node": ">=10" |
| + } |
| + }, |
| + "node_modules/semver-compare": { |
| + "version": "1.0.0", |
| + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", |
| + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/send": { |
| + "version": "1.2.1", |
| + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", |
| + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "debug": "^4.4.3", |
| + "encodeurl": "^2.0.0", |
| + "escape-html": "^1.0.3", |
| + "etag": "^1.8.1", |
| + "fresh": "^2.0.0", |
| + "http-errors": "^2.0.1", |
| + "mime-types": "^3.0.2", |
| + "ms": "^2.1.3", |
| + "on-finished": "^2.4.1", |
| + "range-parser": "^1.2.1", |
| + "statuses": "^2.0.2" |
| + }, |
| + "engines": { |
| + "node": ">= 18" |
| + }, |
| + "funding": { |
| + "type": "opencollective", |
| + "url": "https://opencollective.com/express" |
| + } |
| + }, |
| + "node_modules/setprototypeof": { |
| + "version": "1.2.0", |
| + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", |
| + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", |
| + "dev": true, |
| + "license": "ISC" |
| + }, |
| + "node_modules/slash": { |
| + "version": "3.0.0", |
| + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", |
| + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=8" |
| + } |
| + }, |
| + "node_modules/slugify": { |
| + "version": "1.6.9", |
| + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz", |
| + "integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=8.0.0" |
| + } |
| + }, |
| + "node_modules/sprintf-js": { |
| + "version": "1.0.3", |
| + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", |
| + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", |
| + "dev": true, |
| + "license": "BSD-3-Clause" |
| + }, |
| + "node_modules/ssri": { |
| + "version": "11.0.0", |
| + "resolved": "https://registry.npmjs.org/ssri/-/ssri-11.0.0.tgz", |
| + "integrity": "sha512-aZpUoMN/Jj2MqA4vMCeiKGnc/8SuSyHbGSBdgFbZxP8OJGF/lFkIuElzPxsN0q8TQQ+prw3P4EDfB3TBHHgfXw==", |
| + "dev": true, |
| + "license": "ISC", |
| + "dependencies": { |
| + "minipass": "^7.0.3" |
| + }, |
| + "engines": { |
| + "node": "^16.14.0 || >=18.0.0" |
| + } |
| + }, |
| + "node_modules/statuses": { |
| + "version": "2.0.2", |
| + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", |
| + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 0.8" |
| + } |
| + }, |
| + "node_modules/strip-bom-string": { |
| + "version": "1.0.0", |
| + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", |
| + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=0.10.0" |
| + } |
| + }, |
| + "node_modules/tinyglobby": { |
| + "version": "0.2.17", |
| + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", |
| + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "fdir": "^6.5.0", |
| + "picomatch": "^4.0.4" |
| + }, |
| + "engines": { |
| + "node": ">=12.0.0" |
| + }, |
| + "funding": { |
| + "url": "https://github.com/sponsors/SuperchupuDev" |
| + } |
| + }, |
| + "node_modules/to-regex-range": { |
| + "version": "5.0.1", |
| + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", |
| + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "dependencies": { |
| + "is-number": "^7.0.0" |
| + }, |
| + "engines": { |
| + "node": ">=8.0" |
| + } |
| + }, |
| + "node_modules/toidentifier": { |
| + "version": "1.0.1", |
| + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", |
| + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=0.6" |
| + } |
| + }, |
| + "node_modules/uc.micro": { |
| + "version": "2.1.0", |
| + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", |
| + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/unpipe": { |
| + "version": "1.0.0", |
| + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", |
| + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">= 0.8" |
| + } |
| + }, |
| + "node_modules/urlpattern-polyfill": { |
| + "version": "10.1.0", |
| + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", |
| + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", |
| + "dev": true, |
| + "license": "MIT" |
| + }, |
| + "node_modules/ws": { |
| + "version": "8.21.3", |
| + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", |
| + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", |
| + "dev": true, |
| + "license": "MIT", |
| + "engines": { |
| + "node": ">=10.0.0" |
| + }, |
| + "peerDependencies": { |
| + "bufferutil": "^4.0.1", |
| + "utf-8-validate": ">=5.0.2" |
| + }, |
| + "peerDependenciesMeta": { |
| + "bufferutil": { |
| + "optional": true |
| + }, |
| + "utf-8-validate": { |
| + "optional": true |
| + } |
| + } |
| + } |
| + } |
| +} |
diff --git a/package.json b/package.json| new file mode 100644 |
| index 0000000..059597f |
| --- /dev/null |
| +++ b/package.json |
| @@ -0,0 +1,15 @@ |
| +{ |
| + "name": "carloku", |
| + "private": true, |
| + "type": "module", |
| + "scripts": { |
| + "build": "eleventy", |
| + "serve": "eleventy --serve", |
| + "check": "eleventy && node hack/check-docs.mjs" |
| + }, |
| + "devDependencies": { |
| + "@11ty/eleventy": "^3.0.0", |
| + "markdown-it": "^14.1.0", |
| + "markdown-it-anchor": "^9.2.0" |
| + } |
| +} |
diff --git a/src/_data/docsanchors.json b/src/_data/docsanchors.json| new file mode 100644 |
| index 0000000..d2f9211 |
| --- /dev/null |
| +++ b/src/_data/docsanchors.json |
| @@ -0,0 +1,15 @@ |
| +{ |
| + "Getting started": "getting-started", |
| + "The `carlos ship` contract": "the-carlos-ship-contract", |
| + "404, never 403": "404-never-403", |
| + "Routes & instances": "routes-instances", |
| + " Leading and trailing ": "leading-and-trailing", |
| + "Hyphen-joined words": "hyphen-joined-words", |
| + "`-app` and `--app`": "app-and-app", |
| + "canary/rehearsal": "canary-rehearsal", |
| + "X-Carlos-Version": "x-carlos-version", |
| + "-app": "app", |
| + "--app": "app", |
| + "Ship — then promote": "ship-then-promote", |
| + "Montréal edge": "montr-al-edge" |
| +} |
diff --git a/src/_data/docsnav.json b/src/_data/docsnav.json| new file mode 100644 |
| index 0000000..bb07b74 |
| --- /dev/null |
| +++ b/src/_data/docsnav.json |
| @@ -0,0 +1,24 @@ |
| +{ |
| + "sections": [ |
| + { |
| + "title": "Running your app", |
| + "pages": [ |
| + { "slug": "deploying", "label": "Deploying", "blurb": "Ship a version, put it in front of people, and know it landed." }, |
| + { "slug": "channels", "label": "Channels", "blurb": "How a release reaches production, and how to put it back." }, |
| + { "slug": "domains", "label": "Custom domains", "blurb": "Your own hostname on your app, and the DNS record it needs." }, |
| + { "slug": "config", "label": "Env and secrets", "blurb": "Two stores for configuration, and which one a value belongs in." }, |
| + { "slug": "storage", "label": "Object storage", "blurb": "A bucket per app: creating one, granting access, rotating keys." }, |
| + { "slug": "logs", "label": "Logs", "blurb": "One timeline of your app's output and the platform's events about it." }, |
| + { "slug": "sleeping", "label": "Restarting and sleeping", "blurb": "Cycling an app, and what hibernation means for your visitors." } |
| + ] |
| + }, |
| + { |
| + "title": "Your account", |
| + "pages": [ |
| + { "slug": "console", "label": "The console", "blurb": "What each page shows, and what you can do from it." }, |
| + { "slug": "members", "label": "Members and service credentials", "blurb": "Who can act on your apps, and credentials a server holds." }, |
| + { "slug": "limits", "label": "Limits and getting help", "blurb": "The rate limits you can hit, and where to ask when stuck." } |
| + ] |
| + } |
| + ] |
| +} |
diff --git a/src/_includes/docs.njk b/src/_includes/docs.njk| new file mode 100644 |
| index 0000000..4e44386 |
| --- /dev/null |
| +++ b/src/_includes/docs.njk |
| @@ -0,0 +1,85 @@ |
| +{# |
| + 🤖 The documentation shell: sidebar, section sub-nav, article column. |
| + |
| + One layout serves the index and every page, because they share the |
| + whole chrome. Zero client-side JavaScript beyond site.js's existing |
| + enhancements (both no-ops here, guarded by querySelector) — there is |
| + no search box, and findability is the nav, the card index, and the |
| + per-page section list. |
| + |
| + The sidebar precedes the article in document order, which on a narrow |
| + viewport is what puts the nav above the content without a toggle. |
| + |
| + The sidebar is a <nav>, not an <aside>: with no search, it IS this |
| + site's whole navigation model, not supplementary content. The article |
| + is wrapped in <main> so the skip link (first focusable element in |
| + <body>) has a landmark to target — with dozens of sidebar links ahead |
| + of it on every page, a keyboard user needs a way past them. |
| +#} |
| +<!doctype html> |
| +<html lang="en"> |
| +<head> |
| +<meta charset="utf-8"> |
| +<meta name="viewport" content="width=device-width, initial-scale=1"> |
| +<title>{{ title }} — Carloku docs</title> |
| +<meta name="description" content="{{ description }}"> |
| +<link rel="icon" href="/favicon.svg" type="image/svg+xml"> |
| +<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"> |
| +<meta name="theme-color" content="#0e0c13" media="(prefers-color-scheme: dark)"> |
| +<meta property="og:title" content="{{ title }} — Carloku docs"> |
| +<meta property="og:description" content="{{ description }}"> |
| +<meta property="og:type" content="article"> |
| +<meta property="og:url" content="https://carloku.com/docs/{% if not isIndex %}{{ docsSlug }}/{% endif %}"> |
| +<link rel="preload" href="/fonts/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin> |
| +<link rel="preload" href="/fonts/GeistMono-Variable.woff2" as="font" type="font/woff2" crossorigin> |
| +<link rel="stylesheet" href="/site.css"> |
| +<link rel="stylesheet" href="/docs.css"> |
| +</head> |
| +<body class="docs-body"> |
| +<a class="skip" href="#main">Skip to content</a> |
| +<p class="brandline"><a href="/"><b>Carloku</b></a> <span aria-hidden="true">/</span> <span class="here">docs</span><span class="folio" aria-hidden="true">carloku.com/docs</span></p> |
| + |
| +<div class="docs-shell"> |
| + <nav class="docs-nav" aria-label="Documentation"> |
| + <p class="docs-nav-title"><a href="/docs/"{% if isIndex %} class="active" aria-current="page"{% endif %}>Documentation</a></p> |
| + {% for section in docsnav.sections %} |
| + <p class="docs-nav-section">{{ section.title }}</p> |
| + <ul> |
| + {% for entry in section.pages %} |
| + <li> |
| + <a href="/docs/{{ entry.slug }}/"{% if entry.slug == docsSlug %} class="active" aria-current="page"{% endif %}>{{ entry.label }}</a> |
| + {% if entry.slug == docsSlug %} |
| + {% set sections = collections.docs | docsRawFor(docsSlug) | docsSections %} |
| + {% if sections.length %} |
| + <ul class="docs-subnav"> |
| + {% for s in sections %}<li><a href="#{{ s.anchor }}">{{ s.title }}</a></li>{% endfor %} |
| + </ul> |
| + {% endif %} |
| + {% endif %} |
| + </li> |
| + {% endfor %} |
| + </ul> |
| + {% endfor %} |
| + </nav> |
| + |
| + <main id="main"> |
| + <article class="docs-article"> |
| + {{ content | safe }} |
| + |
| + {% if not isIndex %} |
| + <p class="docs-foot"> |
| + <a href="/docs/{{ docsSlug }}.md">Read this page as markdown</a> — |
| + exact, unstyled, and cheap for an agent to fetch. |
| + </p> |
| + {% endif %} |
| + </article> |
| + </main> |
| +</div> |
| + |
| +<footer class="docs-sitefoot"> |
| + <p>🤖 These pages were written by an LLM, on the ideas, instruction, and editing of humans. AI-written text here is always marked, visibly — that's <a href="https://11factor.org/#x">factor X</a>.</p> |
| + <p class="docs-provenance">These pages are written and maintained in the <a href="https://github.com/carlosframework/carloku">carloku</a> repo, alongside the rest of this site.</p> |
| +</footer> |
| +<script src="/site.js" defer></script> |
| +</body> |
| +</html> |
diff --git a/src/docs-index.njk b/src/docs-index.njk| new file mode 100644 |
| index 0000000..fc17a38 |
| --- /dev/null |
| +++ b/src/docs-index.njk |
| @@ -0,0 +1,20 @@ |
| +--- |
| +layout: docs.njk |
| +title: Documentation |
| +description: "Carloku documentation: how to ship, promote, and run apps on the hosted CARLOS platform." |
| +isIndex: true |
| +permalink: /docs/index.html |
| +--- |
| +{{ collections.docsIndex[0].templateContent | safe }} |
| + |
| +{% for section in docsnav.sections %} |
| +<h2 id="{{ section.title | docsSlugify }}">{{ section.title }}</h2> |
| +<ul class="docs-cards"> |
| + {% for entry in section.pages %} |
| + <li> |
| + <a href="/docs/{{ entry.slug }}/"><strong>{{ entry.label }}</strong></a> |
| + <span>{{ entry.blurb }}</span> |
| + </li> |
| + {% endfor %} |
| +</ul> |
| +{% endfor %} |
diff --git a/src/docs.css b/src/docs.css| new file mode 100644 |
| index 0000000..2e5feed |
| --- /dev/null |
| +++ b/src/docs.css |
| @@ -0,0 +1,450 @@ |
| +/* 🤖 carloku.com/docs — the documentation skin. |
| + |
| + Loaded after site.css, never instead of it. Every colour, measure, |
| + type stack, focus ring, scrollbar and selection colour here comes |
| + from that file's tokens, so both schemes come along for free: the |
| + whole palette swaps under `prefers-color-scheme: dark`, which is the |
| + one theme mechanism this site has. There is no [data-theme] switch to |
| + match, and inventing a second, half-supported one for these pages |
| + only would put the docs out of step with / and /get-started. |
| + |
| + What this file adds is the shape site.css has no page for — a |
| + sidebar, a reference column, and fenced code. It stays inside the |
| + house rules in DESIGN.md while it does it: |
| + |
| + - hairlines divide, boxes do not enclose (no bordered cards, and |
| + no border heavier than 1px anywhere below); |
| + - one purple carries links, focus and the you-are-here mark, over |
| + an accent-soft wash — the same wash the code chips and the quiet |
| + button's hover already use; |
| + - a fence wears the terminal's clothes and stays dark in both |
| + schemes, because anything terminal-shaped does (--term-*); |
| + - mono carries commands and addresses, never prose; |
| + - weights come off the variable axis at the tuned steps |
| + (480/560/620/640/650/680), and tracking tightens as size grows. |
| + |
| + Same build rules as the rest of the site: nothing fetched, no |
| + @import, no JavaScript. Motion is colour-only and short, so the |
| + global prefers-reduced-motion kill switch in site.css covers it. */ |
| + |
| +/* ---------------------------------------------------------------- tokens -- */ |
| + |
| +:root { |
| + /* The two measures site.css lacks, both computed from the real Geist |
| + metrics rather than borrowed. |
| + |
| + Body copy sets at 1rem (site.css `body`). Geist's prose advance — |
| + its letters weighted by English frequency, plus a space every 5.7 |
| + characters — is 0.467em, so a 65-75 character line wants 30.4rem |
| + to 35.0rem. --docs-measure sits near the top of that band at |
| + roughly 73 characters, and is also site.css's existing lede width, |
| + so the docs read at the same measure the rest of the site does. |
| + |
| + The column is wider than the measure because a reference page also |
| + carries command lines. The longest in the corpus is 66 characters; |
| + in Geist Mono (a true 0.6em advance) at the 0.88rem fences use, |
| + that is 34.9rem, and 42rem of column leaves it 4.6rem of room |
| + inside the fence's padding. So the ordinary command sits in its box |
| + and only a genuinely long one has to scroll. 42rem is also what |
| + site.css already caps /get-started's prose at. */ |
| + --docs-measure: 34rem; |
| + --docs-column: 42rem; |
| +} |
| + |
| +/* ------------------------------------------------------------ page frame -- */ |
| + |
| +/* .docs-body and .language-sh carry nothing of their own, deliberately. |
| + site.css's `body` rule already sets the ground, the type and the |
| + voice; and the site ships no highlighter, so a fence's language class |
| + has nothing to say. Inventing a declaration so the hooks appear in |
| + this file would be worse than leaving them alone. */ |
| + |
| +/* Where you are, in words, above a hairline. The drawn wordmark and the |
| + sticky translucent header belong to the pages that are a destination; |
| + a reference page gets a line of text and gets out of the way. */ |
| +.brandline { |
| + display: flex; |
| + align-items: baseline; |
| + flex-wrap: wrap; |
| + gap: 0.5rem; |
| + max-width: var(--wrap); |
| + margin-inline: auto; |
| + padding: 1.3rem 1.5rem 0.85rem; |
| + border-bottom: 1px solid var(--line); |
| + font-size: 0.9rem; |
| + color: var(--muted); |
| +} |
| +.brandline a { color: var(--muted); text-decoration: none; transition: color 120ms ease-out; } |
| +.brandline a:hover { color: var(--accent); } |
| +.brandline b { font-weight: 640; letter-spacing: -0.01em; color: var(--ink); } |
| +.brandline a:hover b { color: inherit; } |
| +.brandline .here { color: var(--ink); font-weight: 560; } |
| + |
| +/* The folio is an address, so it sets as data — mono, the way every |
| + other identifier on this site does — and it is the first thing a |
| + narrow viewport gives up. */ |
| +.folio { |
| + margin-left: auto; |
| + font-family: var(--mono); |
| + font-size: 0.78rem; |
| + letter-spacing: 0.01em; |
| + color: var(--faint); |
| +} |
| +@media (max-width: 40rem) { .folio { display: none; } } |
| + |
| +/* The skip link is site.css's .skip, whole: parked at top: -3rem and |
| + slid into view on :focus-visible. It is the site's own idiom and it |
| + already does what these pages need — with 40+ sidebar links ahead of |
| + the article and no search to route around them, a keyboard user needs |
| + the way past. Restating it here would only let the two drift. */ |
| + |
| +/* ----------------------------------------------------------------- shell -- */ |
| + |
| +.docs-shell { |
| + max-width: var(--wrap); |
| + margin-inline: auto; |
| + padding: clamp(1.75rem, 4vw, 2.75rem) 1.5rem; |
| + display: grid; |
| + grid-template-columns: minmax(0, 1fr); |
| + gap: 2.25rem; |
| + align-items: start; |
| +} |
| + |
| +/* Both children are grid items, so both get min-width: 0. Without it a |
| + grid child's min-width: auto refuses to shrink below its widest |
| + unbreakable content, and a long command line would scroll the page |
| + sideways instead of scrolling inside its own fence. */ |
| +.docs-shell > * { min-width: 0; } |
| + |
| +/* Two columns once there is room for both. Below this the nav keeps its |
| + document order — it is written before the article — so it simply |
| + stacks above the content, with no toggle and no script. */ |
| +@media (min-width: 60rem) { |
| + .docs-shell { |
| + grid-template-columns: 15rem minmax(0, 1fr); |
| + gap: clamp(2.5rem, 5vw, 4rem); |
| + } |
| + .docs-nav { |
| + position: sticky; |
| + top: 1.75rem; |
| + max-height: calc(100vh - 3.5rem); |
| + overflow-y: auto; |
| + overscroll-behavior: contain; |
| + } |
| +} |
| + |
| +/* ------------------------------------------------------------------- nav -- */ |
| + |
| +/* Every link is padded rather than pulled out with a negative margin: |
| + the sidebar becomes a scroll container at the two-column breakpoint, |
| + and an overhang inside `overflow-y: auto` would give the nav its own |
| + horizontal scrollbar. The labels above are padded to match, so the |
| + column still aligns with itself. */ |
| +.docs-nav { font-size: 0.94rem; } |
| +.docs-nav ul { list-style: none; padding: 0; } |
| +.docs-nav li { margin-top: 0.1rem; } |
| + |
| +.docs-nav a { |
| + display: block; |
| + padding: 0.26rem 0.55rem; |
| + border-radius: 0.4rem; |
| + color: var(--muted); |
| + font-weight: 480; |
| + text-decoration: none; |
| + transition: color 120ms ease-out, background 120ms ease-out; |
| +} |
| +.docs-nav a:hover { color: var(--ink); } |
| + |
| +/* The page you are on, marked with the accent over the accent-soft wash |
| + — the same pair the code chips, the step numerals and the quiet |
| + button's hover already use, so the mark is house vocabulary rather |
| + than a new device. Keyed to both the class the template stamps and |
| + the aria-current it stamps beside it, so the mark cannot outlive |
| + either one. */ |
| +.docs-nav a.active, |
| +.docs-nav a[aria-current="page"] { |
| + background: var(--accent-soft); |
| + color: var(--accent); |
| + font-weight: 560; |
| +} |
| + |
| +.docs-nav-title { |
| + margin-bottom: 1.15rem; |
| + padding-inline: 0.55rem; |
| + font-size: 1.05rem; |
| + font-weight: 640; |
| + letter-spacing: -0.01em; |
| +} |
| +.docs-nav-title a { color: var(--ink); font-weight: inherit; } |
| +.docs-nav-title a:hover { color: var(--accent); } |
| + |
| +/* A section label, not a heading and not data: it stays in the voice |
| + face at a quiet weight above a hairline. This site never sets tracked |
| + small caps, and importing them here to say "documentation" would be |
| + borrowing a convention rather than using ours. */ |
| +.docs-nav-section { |
| + margin-top: 1.6rem; |
| + padding-top: 0.9rem; |
| + margin-bottom: 0.5rem; |
| + padding-inline: 0.55rem; |
| + border-top: 1px solid var(--line); |
| + font-size: 0.82rem; |
| + font-weight: 560; |
| + color: var(--faint); |
| +} |
| +.docs-nav-title + .docs-nav-section { margin-top: 0; padding-top: 0; border-top: 0; } |
| + |
| +/* The open page's own sections, indented under it against a hairline. */ |
| +.docs-subnav { |
| + margin: 0.35rem 0 0.7rem 0.85rem; |
| + padding-left: 0.5rem; |
| + border-left: 1px solid var(--line); |
| + font-size: 0.875rem; |
| +} |
| +.docs-subnav li { margin-top: 0; } |
| +.docs-subnav a { |
| + padding: 0.16rem 0.45rem; |
| + font-weight: 400; |
| + color: var(--faint); |
| +} |
| +.docs-subnav a:hover { color: var(--accent); } |
| + |
| +/* --------------------------------------------------------------- article -- */ |
| + |
| +.docs-article { max-width: var(--docs-column); min-width: 0; } |
| + |
| +.docs-article h1 { |
| + font-size: clamp(2rem, 4.4vw, 2.7rem); |
| + line-height: 1.06; |
| + letter-spacing: -0.032em; |
| + font-weight: 680; |
| + text-wrap: balance; |
| + /* site.css's element-level h1/h2 rules carry a margin-bottom for the |
| + landing page's rhythm. Here the following element owns the gap, so |
| + the heading gives it up rather than adding to it. */ |
| + margin-bottom: 0; |
| +} |
| +.docs-article h1 + p { |
| + margin-top: 1.15rem; |
| + font-size: 1.12rem; |
| + line-height: 1.65; |
| + max-width: var(--docs-measure); |
| +} |
| + |
| +/* A group opens on a full hairline — the same divider /get-started uses |
| + between its sections. The reference reads as a run of divided |
| + entries, never as a stack of boxes. */ |
| +.docs-article h2 { |
| + margin-top: clamp(2.75rem, 5.5vw, 4rem); |
| + padding-top: clamp(1.6rem, 3.2vw, 2.25rem); |
| + border-top: 1px solid var(--line); |
| + font-size: clamp(1.35rem, 2.4vw, 1.7rem); |
| + line-height: 1.2; |
| + letter-spacing: -0.025em; |
| + font-weight: 650; |
| + text-wrap: balance; |
| + margin-bottom: 0; |
| + /* No measure cap: the hairline is the divider, so it runs the width of |
| + the column rather than stopping short of the fences beneath it. The |
| + longest heading in the corpus is 37 characters — 29rem at this size |
| + — so nothing wraps for want of the cap. */ |
| +} |
| + |
| +/* h3/h4 and the block elements further down are the floor for markdown |
| + this corpus does not use yet — it is authored in this repo and grows |
| + by hand. Kept deliberately short: enough that a new page cannot land |
| + unstyled, not so much that it becomes a theme nobody asked for. */ |
| +.docs-article h3 { |
| + margin-top: 2.2rem; |
| + font-size: 1.08rem; |
| + font-weight: 620; |
| + letter-spacing: -0.01em; |
| + color: var(--ink); |
| +} |
| +.docs-article h4 { |
| + margin-top: 1.8rem; |
| + font-size: 0.98rem; |
| + font-weight: 620; |
| + letter-spacing: -0.01em; |
| + color: var(--ink); |
| +} |
| + |
| +.docs-article :is(h1, h2, h3, h4) { scroll-margin-top: 1.5rem; } |
| + |
| +.docs-article p { |
| + margin-top: 1.05rem; |
| + max-width: var(--docs-measure); |
| + color: var(--muted); |
| + text-wrap: pretty; |
| +} |
| +.docs-article :is(h2, h3, h4) + p { margin-top: 0.85rem; } |
| +.docs-article strong { color: var(--ink); font-weight: 620; } |
| +.docs-article em { color: var(--ink); } |
| + |
| +.docs-article ul:not(.docs-cards), |
| +.docs-article ol { |
| + margin-top: 1rem; |
| + padding-left: 1.2rem; |
| + max-width: var(--docs-measure); |
| + color: var(--muted); |
| +} |
| +/* Scoped with a child combinator rather than `.docs-article li + li`, |
| + which would outrank `.docs-cards li` below and break the index |
| + grid's row alignment. */ |
| +.docs-article ul:not(.docs-cards) > li + li, |
| +.docs-article ol > li + li { margin-top: 0.45rem; } |
| +.docs-article blockquote { |
| + margin-top: 1.15rem; |
| + padding-left: 1.1rem; |
| + border-left: 1px solid var(--line-strong); |
| + max-width: var(--docs-measure); |
| + color: var(--faint); |
| + font-size: 0.95rem; |
| +} |
| +.docs-article hr { |
| + margin-block: 2.25rem; |
| + border: 0; |
| + border-top: 1px solid var(--line); |
| +} |
| +.docs-article table { |
| + margin-top: 1.25rem; |
| + width: 100%; |
| + border-collapse: collapse; |
| + font-size: 0.95rem; |
| +} |
| +.docs-article :is(th, td) { |
| + text-align: left; |
| + padding: 0.6rem 0.9rem 0.6rem 0; |
| + border-top: 1px solid var(--line); |
| + color: var(--muted); |
| + vertical-align: baseline; |
| +} |
| +.docs-article th { color: var(--ink); font-weight: 620; } |
| + |
| +.docs-article a { |
| + color: var(--accent); |
| + text-decoration: none; |
| +} |
| +.docs-article a:hover { text-decoration: underline; text-underline-offset: 0.2em; } |
| + |
| +/* ------------------------------------------------------------------ code -- */ |
| + |
| +/* A fence is a box that scrolls itself: the page must never scroll |
| + sideways because a command was long. It wears the terminal's clothes |
| + and keeps them in both schemes, under the rule that anything |
| + terminal-shaped sits on Terminal Black — the same clothes |
| + /get-started's blocks already wear, and the reason a Carloku page |
| + reads as a Carloku page. */ |
| +.docs-article pre { |
| + margin: 1.15rem 0 0; |
| + max-width: 100%; |
| + min-width: 0; |
| + overflow-x: auto; |
| + overscroll-behavior-x: contain; |
| + -webkit-overflow-scrolling: touch; |
| + padding: 1rem 1.2rem; |
| + border: 1px solid var(--term-line); |
| + border-radius: 0.7rem; |
| + background: var(--term-bg); |
| + color: var(--term-ink); |
| + font-family: var(--mono); |
| + font-size: 0.88rem; |
| + line-height: 1.7; |
| +} |
| +.docs-article p + pre { margin-top: 0.9rem; } |
| + |
| +/* Inline code is a chip: mono on an accent-soft wash, the way the |
| + commands appear in prose everywhere else on this site. Inside a fence |
| + that chip would tile the whole block, so it is undone there. */ |
| +.docs-article code { |
| + font-family: var(--mono); |
| + font-size: 0.88em; |
| + background: var(--accent-soft); |
| + color: var(--accent-ink); |
| + padding: 0.12em 0.4em; |
| + border-radius: 0.3em; |
| +} |
| +.docs-article pre code { |
| + display: block; |
| + padding: 0; |
| + border-radius: 0; |
| + background: none; |
| + color: inherit; |
| + font-size: inherit; |
| + white-space: pre; |
| +} |
| +.docs-article a code { color: inherit; } |
| + |
| +/* ------------------------------------------------------------- the index -- */ |
| + |
| +/* The index's entries: a label and a line saying what is inside. They |
| + are not cards and they are not boxed — this site separates siblings |
| + with a single hairline and whitespace, the way the feature list and |
| + the guide's sections do. Two ruled columns of contents, nothing to |
| + lift off the page. */ |
| +.docs-cards { |
| + margin-top: 1.25rem; |
| + display: grid; |
| + grid-template-columns: minmax(0, 1fr); |
| + column-gap: 2.5rem; |
| + list-style: none; |
| + padding: 0; |
| +} |
| +@media (min-width: 46rem) { |
| + .docs-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } |
| +} |
| +.docs-cards li { |
| + min-width: 0; |
| + margin-top: 0; |
| + padding: 1.05rem 0 1.15rem; |
| + border-top: 1px solid var(--line); |
| +} |
| +.docs-cards a { display: inline-block; text-decoration: none; } |
| +.docs-cards a strong { |
| + font-size: 1.05rem; |
| + font-weight: 620; |
| + letter-spacing: -0.01em; |
| + color: var(--ink); |
| + transition: color 120ms ease-out; |
| +} |
| +.docs-cards a:hover strong { color: var(--accent); } |
| +.docs-cards a:hover { text-decoration: none; } |
| +.docs-cards span { |
| + display: block; |
| + margin-top: 0.25rem; |
| + font-size: 0.95rem; |
| + color: var(--muted); |
| + text-wrap: pretty; |
| +} |
| + |
| +/* The index's h2s head a grid rather than prose, so they need less air |
| + under them and no measure cap. */ |
| +.docs-article h2 + .docs-cards { margin-top: 1.4rem; } |
| + |
| +/* ------------------------------------------------------------------ feet -- */ |
| + |
| +/* Qualified so it outranks `.docs-article p` — this is a <p> inside the |
| + article, and the unqualified class would lose the colour and the top |
| + margin to it. */ |
| +.docs-article .docs-foot { |
| + margin-top: clamp(2.75rem, 5.5vw, 4rem); |
| + padding-top: 1.15rem; |
| + border-top: 1px solid var(--line); |
| + max-width: var(--docs-measure); |
| + font-size: 0.9rem; |
| + color: var(--faint); |
| +} |
| +.docs-article .docs-foot a { color: var(--faint); } |
| +.docs-article .docs-foot a:hover { color: var(--accent); } |
| + |
| +/* site.css's <footer> already gives the rule, the muted voice and the |
| + block padding; these pages have no .wrap element inside it, so the |
| + measure and the gutters are what is left to supply. */ |
| +.docs-sitefoot { |
| + max-width: var(--wrap); |
| + margin-inline: auto; |
| + padding-inline: 1.5rem; |
| +} |
| +.docs-provenance { font-size: 0.85rem; color: var(--faint); } |
| +.docs-provenance a { color: var(--faint); } |
| +.docs-provenance a:hover { color: var(--accent); } |
diff --git a/src/docs.njk b/src/docs.njk| new file mode 100644 |
| index 0000000..d909dbe |
| --- /dev/null |
| +++ b/src/docs.njk |
| @@ -0,0 +1,13 @@ |
| +--- |
| +pagination: |
| + data: collections.docs |
| + size: 1 |
| + alias: doc |
| +layout: docs.njk |
| +eleventyComputed: |
| + title: "{{ doc.page.rawInput | docsTitle }}" |
| + description: "{{ docsnav | docsBlurb(doc.slug) }}" |
| + docsSlug: "{{ doc.slug }}" |
| +permalink: "/docs/{{ doc.slug }}/index.html" |
| +--- |
| +{{ doc.page.templateContent | safe }} |
diff --git a/src/docs/channels.md b/src/docs/channels.md| new file mode 100644 |
| index 0000000..39d535d |
| --- /dev/null |
| +++ b/src/docs/channels.md |
| @@ -0,0 +1,101 @@ |
| +# Channels |
| + |
| +A channel is a pointer with a name. It says which shipped version an app |
| +is serving, and promoting is the act of moving it. Nothing is rebuilt |
| +between one channel and the next — the bytes that were tested are the |
| +bytes that go live, which is the whole reason the platform separates |
| +shipping from promoting. |
| + |
| +## You start with one |
| + |
| +Every app is created with a single channel called `edge`, and |
| +`carlos deploy` lands there. That is enough for most apps: you deploy, |
| +and it is live. To see where things stand: |
| + |
| +```sh |
| +carlos channels -app hello |
| +``` |
| + |
| +Nothing is ever promoted *onto* the entry channel. Deploys land there, and |
| +promotion moves a version from it to whatever comes next — so with one |
| +channel there is no promotion step at all. |
| + |
| +## Adding a stage in front of production |
| + |
| +When you want a version to sit somewhere before a wider audience sees it, |
| +declare a pipeline. The starter template adds a `production` channel |
| +behind `edge`: |
| + |
| +```sh |
| +carlos pipeline init -app hello -template edge-production |
| +``` |
| + |
| +That gives `production` a 24-hour bake window, a passkey requirement on |
| +promotion, and an approval on any change to those rules — so the |
| +protection cannot be quietly removed later either. The other template, |
| +`full-ladder`, sets up `edge → beta → stable` with bakes of 24 and 72 |
| +hours and the passkey on `stable`. |
| + |
| +Once a pipeline exists, deploys still land on `edge` and you move a |
| +version forward by naming it: |
| + |
| +```sh |
| +carlos promote -app hello a1b2c3d production |
| +``` |
| + |
| +A channel is entered from the one immediately before it and no other, so |
| +"it ran on edge for a day first" is a fact about the bytes rather than a |
| +claim about process. Promoting the same version twice succeeds and |
| +changes nothing. |
| + |
| +The rules a channel can carry — bake windows, passkeys, approval counts — |
| +are all editable after the fact with |
| +[carlos pipeline](https://carlosframework.com/docs/cli/#carlos-pipeline), |
| +and [carlos promote](https://carlosframework.com/docs/cli/#carlos-promote) |
| +documents the rest of its own flags. |
| + |
| +## Putting a version back |
| + |
| +Rollback points a channel at whatever it was serving before: |
| + |
| +```sh |
| +carlos rollback -app hello production |
| +``` |
| + |
| +The outgoing version goes onto the channel's history as it leaves, so |
| +running the same command again returns you to where you started — useful |
| +when the rollback turns out to be the wrong call. A bake window does not |
| +delay it: a box already has the version it is going back to, so there is |
| +nothing to hold. |
| + |
| +Two things are worth doing before you need this. Give your ships a |
| +`-label` people can read, because that is what you will be squinting at |
| +during a rollback. And get familiar with |
| +[carlos releases](https://carlosframework.com/docs/cli/#carlos-releases) |
| +while nothing is on fire: it answers "what is there to run", where |
| +`carlos channels` answers "what is running". |
| + |
| +## What reaching stable means |
| + |
| +The `full-ladder` shape has a `stable` channel at the top, and a |
| +promotion onto it can cut a semver tag: one name pointing at one release, |
| +with the changelog of every iteration that led there compiled into it. |
| + |
| +That happens when the version being promoted is an iteration of a target |
| +rather than a commit sha, so set the target once: |
| + |
| +```sh |
| +carlos version target -app hello 0.5.0 |
| +``` |
| + |
| +From then on `carlos ship` with no `-version` mints the next iteration |
| +under it. Promote a sha-named build to stable instead and the promotion |
| +still lands. There is no tag to cut, and the promotion says so. |
| + |
| +Tags are a naming convenience and an audit record, nothing more. No part |
| +of getting your app running reads one, so an app that never uses them |
| +loses nothing. |
| + |
| +Full flags for the commands on this page: |
| +[channels](https://carlosframework.com/docs/cli/#carlos-channels) and |
| +[rollback](https://carlosframework.com/docs/cli/#carlos-rollback). |
diff --git a/src/docs/config.md b/src/docs/config.md| new file mode 100644 |
| index 0000000..9de659f |
| --- /dev/null |
| +++ b/src/docs/config.md |
| @@ -0,0 +1,78 @@ |
| +# Env and secrets |
| + |
| +Your app's configuration lives in two stores. Both arrive at your process |
| +as ordinary environment variables, and from inside the app you cannot |
| +tell them apart. The difference is what happens to the value on the way |
| +there, and it is worth getting right the first time. |
| + |
| +Plain config goes in `carlos env`: |
| + |
| +```sh |
| +carlos env set -app hello LOG_LEVEL=debug |
| +``` |
| + |
| +Credentials go in `carlos secrets`. Same shape: |
| + |
| +```sh |
| +carlos secrets set -app hello STRIPE_KEY=sk_live_example |
| +``` |
| + |
| +## Which store a value belongs in |
| + |
| +Ask whether you would mind the value appearing in a screenshot. A log |
| +level, a feature flag, the public URL of a service you call — nobody |
| +cares, and `carlos env` keeps them readable, which is what you want when |
| +you are working out why staging behaves oddly. |
| + |
| +An API key, a database URL with a password in it, a signing secret, |
| +anything a third party issued you: `carlos secrets`. If in doubt, the |
| +sealed store costs you nothing except the ability to read the value back, |
| +so put it there. |
| + |
| +## What sealing actually does |
| + |
| +`carlos secrets set` seals the value on your machine, using the |
| +deployment's public key, before anything leaves your terminal. What gets |
| +stored is an envelope. The private half of that key lives on the box that |
| +runs your app and never leaves it, so the console holds a secret it |
| +cannot itself open. |
| + |
| +The consequence is the one thing people trip over: nothing can show you |
| +the value again. `carlos secrets list` prints key names and stops there. |
| +Lose the value and your only move is setting a new one. |
| + |
| +One practical wrinkle. The value is on your command line when you type |
| +it, so your shell history keeps a copy of it. Clear that out on a machine |
| +other people use. |
| + |
| +## Getting the new value into your app |
| + |
| +Changing config converges on its own. The write bumps a marker, every box |
| +serving the app notices within seconds, rewrites the environment file for |
| +each of the app's routes, and cycles the process so it comes back holding |
| +the new value. There is no separate deploy, and no restart for you to |
| +remember. |
| + |
| +Two things follow from the cycling. A config change is a small |
| +interruption for a running app, so batch related changes into one call — |
| +`carlos env set` takes several `KEY=value` pairs at once. And a change |
| +that appears not to have taken effect is more likely to be your app |
| +reading the variable once at startup than the platform failing to deliver |
| +it. |
| + |
| +To see what is set: |
| + |
| +```sh |
| +carlos env list -app hello |
| +``` |
| + |
| +That prints values as well as names, and the sealed store's `list` prints |
| +names alone. Both families also take `unset` with a key name, and the |
| +reference pages for |
| +[carlos env](https://carlosframework.com/docs/cli/#carlos-env) and |
| +[carlos secrets](https://carlosframework.com/docs/cli/#carlos-secrets) |
| +have their remaining sub-verbs. |
| + |
| +If you take one thing from this page: put the credential in the sealed |
| +store on the day you first set it. Moving it later means rotating it, |
| +because by then it has been sitting somewhere readable. |
diff --git a/src/docs/console.md b/src/docs/console.md| new file mode 100644 |
| index 0000000..7834a10 |
| --- /dev/null |
| +++ b/src/docs/console.md |
| @@ -0,0 +1,89 @@ |
| +# The console |
| + |
| +[console.carloku.com](https://console.carloku.com) is the same platform your |
| +terminal talks to, with a browser in front of it. You sign in with a passkey |
| +through Keymail, and the account you sign in to is the one |
| +`carlos auth login` connects a terminal to — the short code the CLI prints |
| +is approved in this browser. |
| + |
| +Most weeks you will barely open it. Deploys, config, logs and domains are all |
| +commands, and the console reads the records those commands write. Where it |
| +earns its place is the handful of jobs with no command behind them, and |
| +looking at an app somebody else on your team deployed. |
| + |
| +## What each page shows |
| + |
| +The bar across the top is per account: Apps, Fleet, Team and Settings, plus |
| +a switcher for moving between accounts when you belong to more than one. |
| + |
| +Apps is the list: each app's name, the address it answers on, a health chip, |
| +and the version live right now. A Trash link appears under it once something |
| +is in it. Opening an app gives you six tabs. |
| + |
| +Overview answers *is it up* in a sentence — the hostname, the running version, |
| +when it was published and by whom — then tiles for running copies, CPU and |
| +memory, web addresses and versions, and a left-to-right diagram of the |
| +channels a version travels through. [Channels](/docs/channels/) explains what |
| +that diagram is drawing. |
| + |
| +Versions is the release history. Every version you have shipped with its |
| +label, kind and build time and a promote control per row; underneath, the |
| +channels, what each holds, when a baked version goes live, and rollback. |
| +Canaries and semver tags sit at the bottom of the same page. |
| + |
| +Running is the copies themselves — address, health, version, channel, CPU, |
| +memory — and where the app is placed. A copy reading *asleep* is annotated |
| +there as waking on the next request, which [Restarting and |
| +sleeping](/docs/sleeping/) explains at more length. |
| + |
| +Logs is the window `carlos logs` prints, with a form for narrowing it. |
| +Activity is the other one: when, who, what, for every change made to this |
| +app. That record has no CLI verb, so the console is where you read it. |
| + |
| +Settings carries the app's name and rename, its domains with their DNS and |
| +TLS state alongside the attach form, its object store, its email |
| +configuration, a link through to the pipeline rules, and deletion. |
| + |
| +Back at account level, Fleet answers where this account's work actually runs: |
| +the shared platform boxes your apps are placed on, and any boxes of your own. |
| +Team and Settings are the subject of [Members and service |
| +credentials](/docs/members/). |
| + |
| +## Jobs that only happen here |
| + |
| +Approving a terminal is the first one you meet: `carlos auth login` prints a |
| +code, and nothing is granted until you type it into the console and confirm. |
| + |
| +The rest cluster around authority and permanence. Inviting a teammate, |
| +removing one and handing over ownership live on the Team page; minting, |
| +rotating and revoking a credential a server holds live on Account settings. |
| + |
| +Deletion is the other cluster. A deleted app goes to Trash and stays |
| +restorable for 30 days, keeping its releases, channels and history. |
| +Restoring gives all of that back with no routes, so the app comes back with |
| +nowhere to serve from until you make one: |
| + |
| +```sh |
| +carlos instances create -app hello -host hello.bdf.oncarlos.com |
| +``` |
| + |
| +That is the same command [Deploying](/docs/deploying/) names when a channel |
| +has no instance following it. The console's own confirmation currently tells |
| +you to run `carlos add` instead — don't. That verb edits a box's route |
| +registry from the box, and your app doesn't come with a box you can log in |
| +to. |
| + |
| +An owner can also destroy an app immediately from the same page; that asks |
| +for the app's name typed out and then for your passkey. |
| + |
| +And when the day's app-creation budget is spent, the console's Create app |
| +form is the way through — it has no limit of its own. [Limits and getting |
| +help](/docs/limits/) has the numbers. |
| + |
| +## Your own settings |
| + |
| +Your address in the header opens a page listing every terminal signed in as |
| +you: where it signed in from, when, when it was last used, and when it |
| +expires. Each row has a revoke control. A laptop that went missing is the |
| +reason this page exists — revoking there kills that terminal's access |
| +without touching anyone else's. |
diff --git a/src/docs/deploying.md b/src/docs/deploying.md| new file mode 100644 |
| index 0000000..9d95de7 |
| --- /dev/null |
| +++ b/src/docs/deploying.md |
| @@ -0,0 +1,89 @@ |
| +# Deploying |
| + |
| +Deploying is getting a build off your machine and onto the URL people |
| +use, and then knowing — not hoping — that the URL is serving it. One |
| +command covers all of that. |
| + |
| +```sh |
| +carlos deploy -app hello ./hello |
| +``` |
| + |
| +It prints where it is deploying to, then `shipped`, then `promoted`, and |
| +finally `live` against each hostname. That last line is the one that |
| +matters. The others say a write succeeded; only `live` says a visitor |
| +would get the new build. |
| + |
| +## What the one command is doing |
| + |
| +Three steps, and you can run them separately when you want the gap |
| +between them. |
| + |
| +Shipping hashes your artifact, stores it under its content address, and |
| +records a manifest that never changes afterwards. Nothing you serve |
| +changes yet. Promoting points a channel at that version — see |
| +[Channels](/docs/channels/) for what a channel is and when you would have |
| +more than one. Then the command watches: it polls the live hostname for |
| +the `X-Carlos-Version` header until that header names the version you |
| +shipped, over a 2xx. It gives up after two minutes by default, and |
| +`-timeout` moves that. |
| + |
| +The header is also how you check by hand later, and it is worth |
| +preferring over anything else you might reach for. A page that looks |
| +right can be a cached copy of the old build: |
| + |
| +```sh |
| +curl -sI https://hello.bdf.oncarlos.com | grep -i x-carlos-version |
| +``` |
| + |
| +## What you ship |
| + |
| +The version defaults to `git rev-parse --short HEAD`, so a deploy from a |
| +clean checkout is already labelled with the commit it came from. |
| +`-version` overrides it. |
| + |
| +Binaries are the common case, and `-target` says what you built for. It |
| +defaults to `linux-arm64`, which is what Carloku runs, so a Go build for |
| +this platform is: |
| + |
| +```sh |
| +GOOS=linux GOARCH=arm64 go build -o hello . |
| +``` |
| + |
| +A directory of files ships as a site, with `-kind static`: |
| + |
| +```sh |
| +carlos deploy -app hello-site -kind static ./public |
| +``` |
| + |
| +Run any of these once from your project and the CLI offers to remember |
| +the app and the artifact in `.carlos/config`. Say yes and the whole |
| +release becomes `carlos deploy` with no arguments, which is the form to |
| +put in a Makefile or a git hook. |
| + |
| +## When it doesn't go live |
| + |
| +A deploy that stops short usually stops for a reason it will tell you. |
| + |
| +If the channel you are promoting into needs approvals, the ship still |
| +lands and the promote becomes a proposal: the CLI says how many |
| +confirmations it needs and where to confirm them. If the channel has a |
| +bake window, the promote lands and the fleet adopts when the window |
| +expires — the command exits successfully, because the hold is policy |
| +rather than a failure. And if no instance follows the channel yet, it |
| +says so instead of watching a URL that nobody serves, and names |
| +`carlos instances create` as the step that gives the version somewhere to |
| +run. |
| + |
| +A watch that times out is a different situation. The version is published |
| +and the pointer has moved, so your app is either failing to start or |
| +failing its first request. Go to the logs: |
| + |
| +```sh |
| +carlos logs -app hello -since 10m |
| +``` |
| + |
| +Full flag lists for |
| +[deploy](https://carlosframework.com/docs/cli/#carlos-deploy), |
| +[ship](https://carlosframework.com/docs/cli/#carlos-ship) and |
| +[promote](https://carlosframework.com/docs/cli/#carlos-promote) are in |
| +the CLI reference, and `carlos deploy -h` prints them at your terminal. |
diff --git a/src/docs/docs.json b/src/docs/docs.json| new file mode 100644 |
| index 0000000..7708084 |
| --- /dev/null |
| +++ b/src/docs/docs.json |
| @@ -0,0 +1,5 @@ |
| +{ |
| + "//": "This directory's markdown is authored here, in place — there is no vendoring and no sync step (see eleventy.config.js). Each file is input for src/docs.njk (rendered page) and src/docsmd.njk (raw .md fetch), paginated over the docs collection by slug; it is not a page of its own, which is why permalink is false here rather than in each file's front matter.", |
| + "permalink": false, |
| + "eleventyExcludeFromCollections": false |
| +} |
diff --git a/src/docs/domains.md b/src/docs/domains.md| new file mode 100644 |
| index 0000000..26bc375 |
| --- /dev/null |
| +++ b/src/docs/domains.md |
| @@ -0,0 +1,70 @@ |
| +# Custom domains |
| + |
| +Your app already answers on a hostname the platform gave it, something |
| +like `hello.bdf.oncarlos.com`. Attaching your own domain is two moves: |
| +tell Carloku the hostname is yours, then point a DNS record at it. The |
| +platform hostname keeps working afterwards, so you can do all of this |
| +before you switch any traffic over. |
| + |
| +```sh |
| +carlos domains attach -app hello www.example.com |
| +``` |
| + |
| +Leave `-route` off when the app has one instance — the command works out |
| +what to point the hostname at. With several, name the one you mean. |
| + |
| +## The DNS record |
| + |
| +Attach finishes by printing the record to create and the hostname's |
| +current DNS state, so you need neither a second command nor a trip to the |
| +console to know what is left. Usually that is an A record at this |
| +deployment's edge addresses, named for you rather than left to look up. |
| +Where the deployment offers a steering hostname, a subdomain is offered a |
| +CNAME at that instead: visitors then reach the nearest healthy edge, and |
| +a failed edge drops out of the answer on its own. An apex domain — |
| +`example.com` with nothing in front of it — always takes the A record, |
| +because a CNAME cannot legally sit at the apex. |
| + |
| +Create the record with whoever runs your DNS. Checks run every few |
| +minutes, so there is nothing to trigger. |
| + |
| +## Watching it come live |
| + |
| +```sh |
| +carlos domains list -app hello |
| +``` |
| + |
| +Each row is one claim joined to what the fleet actually observes: which |
| +route it points at, whether the hostname resolves to the edge, when its |
| +certificate expires, and — while it isn't pointing yet — the same |
| +instruction attach printed. Certificates are minted and renewed for you; |
| +there is no step here you own. |
| + |
| +The claim itself reaches the boxes within seconds of the attach, so a row |
| +that still reads `pending` a minute later is unusual. The DNS column is |
| +the slow half. It stays unhelpful until your record has propagated, and |
| +nothing on this side can hurry that along. |
| + |
| +## Taking one off |
| + |
| +```sh |
| +carlos domains detach -app hello www.example.com |
| +``` |
| + |
| +The boxes serving the app notice within seconds and stop answering for |
| +the hostname, the same way they picked it up when you attached it. Your |
| +DNS record is yours to clean up afterwards. |
| + |
| +## Wildcards |
| + |
| +Two flags on |
| +[attach](https://carlosframework.com/docs/cli/#carlos-domains) cover the |
| +give-every-customer-a-subdomain case. `-wildcard` enables a delegated |
| +wildcard certificate for the hostname; `-catchall` derives a `*.<host>` |
| +alias onto the same route. Both need DNS delegation in place, and attach |
| +refuses with the reason when it isn't. Attaching one marketing domain |
| +needs neither. |
| + |
| +Neither flag has a default position either. Naming one changes it and |
| +leaving it off leaves it alone, so you can turn a wildcard on months |
| +later without restating anything else about the claim. |
diff --git a/src/docs/index.md b/src/docs/index.md| new file mode 100644 |
| index 0000000..25dcc3a |
| --- /dev/null |
| +++ b/src/docs/index.md |
| @@ -0,0 +1,13 @@ |
| +# Documentation |
| + |
| +Carloku is the hosted CARLOS platform. You deploy a binary, get a URL, and |
| +the platform handles certificates, restarts, and putting a quiet app to |
| +sleep until traffic comes back. |
| + |
| +These pages cover running your app day-to-day, and the account settings |
| +that go with it. |
| + |
| +Haven't deployed yet? [Get started](/get-started/) is where to begin. |
| + |
| +Platform internals, self-hosting, and the full `carlos` CLI reference live |
| +at [carlosframework.com/docs](https://carlosframework.com/docs). |
diff --git a/src/docs/limits.md b/src/docs/limits.md| new file mode 100644 |
| index 0000000..27f13d5 |
| --- /dev/null |
| +++ b/src/docs/limits.md |
| @@ -0,0 +1,76 @@ |
| +# Limits and getting help |
| + |
| +The numbers on this page exist to make a stolen token boring, not to ration |
| +ordinary work. You are unlikely to meet any of them by hand. An automated |
| +loop is another matter, so here they are, along with what the refusal looks |
| +like when one does bite. |
| + |
| +## Shipping |
| + |
| +Two uploads a minute, counted per terminal token. That is past what an honest |
| +build-test-ship loop can produce, since a build worth shipping takes longer |
| +than half a minute to make. |
| + |
| +A refusal is a 429 that names the wait in seconds and repeats it in a |
| +`Retry-After` header, so a script has a number to sleep on instead of a |
| +guess. The window is a minute for exactly that reason: the worst a refused |
| +caller is ever asked for is sixty seconds. Sleep, retry, carry on. |
| + |
| +Size is the other ship limit. One request carries 256 MiB across everything |
| +in it. A static release is a single archive; a binary release can carry up to |
| +eight targets at once, which is well past what a normal build produces. |
| + |
| +## Everything else you type |
| + |
| +Ordinary mutations — env, secrets, promotes, rollbacks, restarts, |
| +schedules — get thirty a minute, counted against your address and not your |
| +terminal, so opening a second shell does not double it. The instance verbs |
| +get sixty a minute on their own budget, because a burst of signups creating |
| +instances is the workload that API exists for. |
| + |
| +Claiming an app name is the tight one: ten attempts a day per address, and |
| +a try that gets refused still spends one of them. A claimed name persists |
| +whether or not you ever use it, which is why this ceiling runs over a day and |
| +the others run over a minute. When you hit it, the message points at the |
| +console's Create app form, which has no limit of its own. That is the |
| +intended way through. |
| + |
| +## Credentials a server holds |
| + |
| +A credential minted with `carlos services` draws on its own budgets, larger |
| +than a person's and entirely separate: ten ships a minute, a hundred and |
| +twenty other writes a minute, a hundred app claims a day. A CI fleet cutting |
| +releases back to back therefore cannot starve the person who minted it, and |
| +the person's noisy afternoon cannot stop CI. |
| + |
| +The counters key on the credential's name, so rotating its secret does not |
| +hand it a fresh budget. [Members and service |
| +credentials](/docs/members/) covers the rest of how they work. |
| + |
| +## Things that expire |
| + |
| +Logs go back seven days, with a size ceiling per app that drops the oldest |
| +hour first — [Logs](/docs/logs/) has the detail. A deleted app sits in Trash |
| +for thirty days before the platform destroys it. A terminal you have signed |
| +in stays valid for ninety days, and you can revoke it sooner from your own |
| +settings page. An invitation to join an account lasts seven days and works |
| +once. |
| + |
| +## When something is refused |
| + |
| +Read the message before anything else. The platform's refusals are written to |
| +be acted on: a rate limit names the seconds to wait, an app you cannot claim |
| +today names the console form that will let you, an attached domain that is |
| +not resolving prints the DNS record it wants. Very few need interpreting. |
| + |
| +After that, in rough order of how often it helps: `carlos <verb> -h` for the |
| +flags, [the CLI reference](https://carlosframework.com/docs/cli/) for the |
| +full page, and `carlos logs -app <app> -since 1h` for anything where your app |
| +started and then misbehaved. If a deploy looks stuck, check |
| +`X-Carlos-Version` on the live hostname, not the page in your browser — |
| +[Deploying](/docs/deploying/) explains why the header is the honest answer. |
| + |
| +If none of that gets you there, open an issue on |
| +[the carloku repo](https://github.com/carlosframework/carloku/issues). These |
| +pages are written and kept there, so a question worth asking is usually a |
| +page worth fixing, and both happen in the same place. |
diff --git a/src/docs/logs.md b/src/docs/logs.md| new file mode 100644 |
| index 0000000..a47f8bd |
| --- /dev/null |
| +++ b/src/docs/logs.md |
| @@ -0,0 +1,67 @@ |
| +# Logs |
| + |
| +`carlos logs` is one timeline rather than two. Your app's own stdout and |
| +stderr come back interleaved with the platform's account of what happened to |
| +it — which instance woke and on what version, when something restarted it, |
| +and when a wake failed, with the reason. |
| + |
| +```sh |
| +carlos logs -app hello -since 1h |
| +``` |
| + |
| +Lines print oldest to newest: a UTC timestamp, the stream, the instance, the |
| +message, and any structured fields your app attached, dimmed at the end. |
| + |
| +## Following |
| + |
| +`-f` leaves the command running and polls for new lines every two seconds. |
| + |
| +```sh |
| +carlos logs -app hello -f |
| +``` |
| + |
| +It runs close to live, not exactly live. Boxes ship what they have collected |
| +every ten seconds or so, so a follow arrives in small batches instead of |
| +scrolling a character at a time. Start one before a deploy and you will see |
| +the wake and the first requests. |
| + |
| +## Cutting it down |
| + |
| +An hour of a busy app is more than anyone reads. `-since` takes a duration |
| +back from now (`15m`, `24h`) or an RFC3339 timestamp, and `-until` closes the |
| +far end of the window. `-grep` matches an RE2 pattern against the message. |
| +`-host` narrows to a single instance when the app runs more than one. `-n` |
| +caps how many lines come back — a thousand by default, five thousand at most |
| +— and when the window has more than that, it tells you it is showing the |
| +newest. |
| + |
| +There is also `-stream`. `-stream app` drops the platform's commentary and |
| +leaves your own output; `-stream platform` does the opposite. |
| + |
| +## The half you can't get any other way |
| + |
| +Your app's output you could always have arranged for yourself. The platform |
| +stream is the part that needs a view from outside the process: `instance woke |
| +(version a1b2c3)`, `instance restarting (carlos restart)`, `instance |
| +stopping`, and — when a request got a 503 you cannot reproduce — a wake |
| +failure written in terms you can act on, like a socket left behind by a |
| +previous run, or nothing promoted on the channel this instance follows. |
| + |
| +A static site has no process and so has no app stream at all. Its platform |
| +events still show up, and they are the reason to run `carlos logs` against a |
| +site. |
| + |
| +## How far back it goes |
| + |
| +Seven days, and a size ceiling per app that drops the oldest hour first if |
| +an app is unusually chatty. If you need a longer record than that, have the |
| +app ship its own copy somewhere; the platform's retention is sized for |
| +debugging this week. |
| + |
| +An empty result is usually honest, and worth a second look. Platform |
| +events only happen when something happens — a wake, a restart, a failure — |
| +so a quiet hour of a quiet app really does have nothing in it. Widen to |
| +`-since 24h` before you conclude that logging is broken. |
| + |
| +The remaining flags, including `-level` and the repeatable `-field`, are in |
| +[carlos logs](https://carlosframework.com/docs/cli/#carlos-logs). |
diff --git a/src/docs/members.md b/src/docs/members.md| new file mode 100644 |
| index 0000000..f5a450d |
| --- /dev/null |
| +++ b/src/docs/members.md |
| @@ -0,0 +1,109 @@ |
| +# Members and service credentials |
| + |
| +Two kinds of thing act on your apps. A person is a Keymail address with a |
| +role in the account. A credential is something a server holds — a CI runner, |
| +a job, a router — with its reach decided when it was minted and no human |
| +attached to it. They are administered in different places, and the mistakes |
| +they invite are different too. |
| + |
| +## People |
| + |
| +Invites go out from the Team page in the console, addressed to a Keymail |
| +address. The invitation lands in that person's Keymail Apps inbox — the same |
| +sealed path a sign-in confirmation takes — so inviting somebody sends them no |
| +email. It works once and expires after seven days, and an owner can revoke a |
| +pending one before it is used. If the delivery itself fails the console hands |
| +you the link to pass on yourself; the invitation is real by then either way. |
| + |
| +There are two roles. A member does the day-to-day work on the account's apps |
| +— deploy, promote, roll back, config, domains, logs. An owner does all of |
| +that plus everything that changes who can act or ends something permanently: |
| +inviting, removing a member, handing ownership over, deleting an app or the |
| +account, and every credential in the next section. Handing over ownership is |
| +a single transaction that steps you down to member as it lifts them up, so |
| +there is no moment with two owners or none. |
| + |
| +Some of those actions ask for your passkey again even though you are already |
| +signed in — removing a member, transferring ownership, deleting the account, |
| +approving a terminal or revoking one, minting or rotating a credential. The |
| +gesture has to be within the last five minutes. A session cookie proves who |
| +you are and not that you are still at the keyboard, which is the whole |
| +question when the action is irreversible. |
| + |
| +None of this has a `carlos` verb. Membership is console work, deliberately: |
| +the passkey prompt is the point of it, and a terminal has nowhere to put one. |
| + |
| +## Credentials a server holds |
| + |
| +```sh |
| +carlos services create -role publish -app hello ci-runner |
| +``` |
| + |
| +The secret prints once, right there in your terminal, and only its hash is |
| +stored. There is no page and no command that will show it to you again — a |
| +lost secret means rotating, not re-running. Get it into the app the way you |
| +get any credential in: |
| + |
| +```sh |
| +carlos secrets set -app hello CARLOS_SERVICE_TOKEN=<the secret> |
| +``` |
| + |
| +Pick the narrowest role that does the job. `publish` is the deploy loop: |
| +claim an app, ship, promote, roll back, manage hostnames, retire. `operate` |
| +is runtime work with no release authority — restart, config, logs. `instance` |
| +is the older app-token shape, kept for what already depends on it. `admin` is |
| +the union of those plus ledgers, storage and placement. It also grants |
| +object-store access, and that mints a real IAM user — hand one out only when |
| +you mean to hand over. |
| + |
| +`-app hello` binds the credential to one app; it cannot claim new ones. |
| +Leave it off for an account-wide credential. Either way the reach is fixed at |
| +mint: if you join another account next month, the credential you minted does |
| +not follow you there. Its rate budgets are its own, separate from yours — see |
| +[Limits](/docs/limits/) — and it never expires, so revoking is the control. |
| + |
| +## Rotate replaces, revoke stops |
| + |
| +This is the distinction to have straight before you need it. |
| + |
| +```sh |
| +carlos services rotate ci-runner |
| +``` |
| + |
| +Rotation mints a fresh secret and prints it once, keeping the name, the role, |
| +the scope and the history. The old secret is refused on the credential's |
| +*next* call — work already in flight is unaffected. That makes rotation a |
| +hygiene move: something you schedule. |
| + |
| +```sh |
| +carlos services revoke ci-runner |
| +``` |
| + |
| +Revocation is the hard stop: the credential's next call fails whatever secret |
| +it is holding. It is idempotent, so revoking a name twice, or one that never |
| +existed, answers the same way. And unlike minting and rotating, it does not |
| +ask for your passkey — one step, at whatever hour you find out. |
| + |
| +If you suspect a secret has leaked, revoke. Then mint a replacement under a |
| +new name if the work still needs doing. |
| + |
| +To see what exists: |
| + |
| +```sh |
| +carlos services ls |
| +``` |
| + |
| +Live credentials only — name, role, scope, when it was minted, when its |
| +current secret began, when it was last used, and who minted it. A revoked one |
| +disappears from the list entirely, so this is inventory, not history. |
| + |
| +Every verb here is owner-only, and closed to service credentials themselves: |
| +no credential can mint, rotate or revoke any credential, not even its own. |
| +A leaked one therefore cannot quietly issue itself a quieter sibling. The |
| +console's Account settings page carries the same list, the same mint form, |
| +and rotate and revoke controls per row. |
| + |
| +Each verb takes `-account` when you belong to more than one; with a single |
| +account you can leave it off. |
| +[carlos services](https://carlosframework.com/docs/cli/#carlos-services) has |
| +the rest of the flags. |
diff --git a/src/docs/sleeping.md b/src/docs/sleeping.md| new file mode 100644 |
| index 0000000..505f9d2 |
| --- /dev/null |
| +++ b/src/docs/sleeping.md |
| @@ -0,0 +1,86 @@ |
| +# Restarting and sleeping |
| + |
| +Your app's process gets cycled for reasons that have nothing to do with new |
| +code. You ask for it, a config change forces it, or the platform puts a quiet |
| +app to sleep and starts it again when somebody turns up. This page covers the |
| +first and the last. |
| + |
| +```sh |
| +carlos restart -app hello |
| +``` |
| + |
| +The reply says *restart requested*, which is the accurate word for what |
| +happened: the console wrote one small object, and every box serving the app |
| +notices it within seconds and cycles its own routes. No version moves, no |
| +channel is consulted, nothing is pushed at a box. |
| + |
| +What cycling looks like depends on how the instance runs. A long-running one |
| +stops and comes straight back. An on-demand one stops within seconds and |
| +starts again on the next request that needs it — for a quiet app, that may |
| +be a while. An instance that is already asleep is left asleep and |
| +starts fresh whenever it next wakes. |
| + |
| +Reach for restart when a process is wedged and you have no change to make. A |
| +deploy already cycles the app, and so does a config change — see |
| +[Deploying](/docs/deploying/) and [Env and secrets](/docs/config/) — so |
| +neither of those needs a restart after it. |
| + |
| +## What hibernation is |
| + |
| +An app nobody has talked to for a while is checkpointed to its bucket and its |
| +process stopped. The idle window belongs to the deployment; fifteen minutes |
| +is the default. The app's state was living in the bucket the whole time, so |
| +there is nothing to lose in the transition — the process was only ever the |
| +part that reads it. |
| + |
| +```sh |
| +carlos instances list -app hello |
| +``` |
| + |
| +The health column reads `asleep` for those instances and `running` for the |
| +rest. For anything without steady traffic, `asleep` is the ordinary |
| +condition, not a fault to clear. |
| + |
| +## What a visitor sees |
| + |
| +A request for a sleeping app is held at the edge while the instance wakes, |
| +then handed to it. The visitor waits longer for that page and then gets it; |
| +there is no error page and nothing for them to retry. Requests that arrive |
| +together during a wake all join the same one instead of each starting their |
| +own. |
| + |
| +The exception is a wake that takes an unusual length of time. Past about |
| +thirty seconds the request is answered with a 503 and a `Retry-After`, and a |
| +wake that fails outright ends the same way. In |
| +both cases the reason is one line in the platform stream: |
| + |
| +```sh |
| +carlos logs -app hello -stream platform -since 15m |
| +``` |
| + |
| +## Traffic that doesn't wake anything |
| + |
| +The edge keeps its own cache of responses and checks it before it goes |
| +anywhere near the wake path, so a crawler asking for the same public page all |
| +afternoon can be answered without your app running at all. What makes a |
| +response eligible is your app saying so: a 2xx GET with `Cache-Control: |
| +max-age=…` and no `Set-Cookie`. Whatever number you set, the edge caps the |
| +entry at five minutes, so this cannot strand a page for long. Setting that |
| +header on your public pages is the single most useful thing you can do for an |
| +app that hibernates. |
| + |
| +A TLS handshake by itself never wakes anything either — only a request that |
| +would reach your app does. |
| + |
| +## Work that still happens |
| + |
| +A timetable set with [carlos |
| +schedule](https://carlosframework.com/docs/cli/#carlos-schedule) survives |
| +hibernation. Before an instance goes to sleep the platform records when its |
| +next job falls due, and wakes it when that time arrives; the job runs, and the |
| +usual idle window puts it back. You do not have to keep the app awake to keep |
| +its schedule. |
| + |
| +Flags for [carlos restart](https://carlosframework.com/docs/cli/#carlos-restart) |
| +and [carlos instances](https://carlosframework.com/docs/cli/#carlos-instances) |
| +are in the CLI reference. |
diff --git a/src/docs/storage.md b/src/docs/storage.md| new file mode 100644 |
| index 0000000..295fe12 |
| --- /dev/null |
| +++ b/src/docs/storage.md |
| @@ -0,0 +1,77 @@ |
| +# Object storage |
| + |
| +Uploads, generated files, anything your app wants to still have next week — |
| +none of it belongs on the box's local disk, because the process that wrote |
| +it there is not guaranteed to be the one serving your next request. Each app |
| +can have a bucket of its own instead, and the credentials for it arrive in |
| +your process as environment variables. |
| + |
| +```sh |
| +carlos store create -app hello |
| +``` |
| + |
| +That records the request and nothing more. No bucket exists yet: creating |
| +declares that the app wants one, and provisioning is a step on the Carloku |
| +side. `carlos store status` is where you watch for it landing. |
| + |
| +## What your app ends up with |
| + |
| +Five variables under a prefix, `CARLOS_STORE` unless you name another: |
| +`CARLOS_STORE_BUCKET`, `_REGION`, `_ACCESS_KEY` and `_SECRET_KEY`, plus |
| +`_ENDPOINT` where the backend has one. Your app reads them like any other |
| +config and talks to the bucket directly, with whichever S3 client its |
| +language already has. Nothing proxies that traffic. |
| + |
| +The bucket, region and endpoint are ordinary values — `carlos env list` will |
| +show them to you. The two keys are delivered through the sealed store, so |
| +they reach your process and stop being readable to anyone, including you. |
| +[Env and secrets](/docs/config/) explains why that is one-way. |
| + |
| +If your app already expects a particular set of names, say so at create |
| +time and skip the adapter code: |
| + |
| +```sh |
| +carlos store create -app hello -env-prefix BLOBS |
| +``` |
| + |
| +## Watching it arrive |
| + |
| +```sh |
| +carlos store status -app hello |
| +``` |
| + |
| +You get what you declared, what was granted, and what was provisioned — |
| +bucket, region, the user the credential belongs to, and the key id currently |
| +in use. Two of its lines are worth knowing before you meet them. One says a |
| +delivered variable has not reached your app's config yet. That is the |
| +ordinary state for a few seconds after any change, and a problem only if it |
| +persists. The other says a rotation was started and never finished. |
| + |
| +## Rotating the key |
| + |
| +```sh |
| +carlos store rotate -app hello |
| +``` |
| + |
| +A fresh credential is minted and delivered, and the previous one keeps |
| +working. Your app picks the new values up the way it picks up any config |
| +change: the box rewrites the environment for the app's routes and cycles the |
| +process, within seconds. Leaving the old key alive is the point of doing it |
| +in two halves — an upload that started under it gets to finish, and a second |
| +instance that has not converged yet keeps serving. |
| + |
| +Close the window when you are satisfied nothing is still using it: |
| + |
| +```sh |
| +carlos store rotate -finish -app hello |
| +``` |
| + |
| +Until you do, `status` keeps telling you a rotation is pending. |
| + |
| +One habit worth forming. The secret never appears at your terminal — rotate |
| +prints the new key's id and that is the whole of what you see — so if the |
| +value ever turns up somewhere it shouldn't, it came out of the app. Whatever |
| +your app dumps at startup is the first place to look. |
| + |
| +[carlos store](https://carlosframework.com/docs/cli/#carlos-store) has the |
| +sub-verbs and the rest of their flags. |
diff --git a/src/docsmd.njk b/src/docsmd.njk| new file mode 100644 |
| index 0000000..64ab68a |
| --- /dev/null |
| +++ b/src/docsmd.njk |
| @@ -0,0 +1,9 @@ |
| +--- |
| +pagination: |
| + data: collections.docs |
| + size: 1 |
| + alias: doc |
| +permalink: "/docs/{{ doc.slug }}.md" |
| +eleventyExcludeFromCollections: true |
| +--- |
| +{{ doc.page.rawInput | safe }} |
diff --git a/favicon.svg b/src/favicon.svg| similarity index 100% |
| rename from favicon.svg |
| rename to src/favicon.svg |
diff --git a/fonts/Geist-Variable.woff2 b/src/fonts/Geist-Variable.woff2| similarity index 100% |
| rename from fonts/Geist-Variable.woff2 |
| rename to src/fonts/Geist-Variable.woff2 |
diff --git a/fonts/GeistMono-Variable.woff2 b/src/fonts/GeistMono-Variable.woff2| similarity index 100% |
| rename from fonts/GeistMono-Variable.woff2 |
| rename to src/fonts/GeistMono-Variable.woff2 |
diff --git a/fonts/LICENSE-OFL.txt b/src/fonts/LICENSE-OFL.txt| similarity index 100% |
| rename from fonts/LICENSE-OFL.txt |
| rename to src/fonts/LICENSE-OFL.txt |
diff --git a/get-started/index.html b/src/get-started/index.html| similarity index 100% |
| rename from get-started/index.html |
| rename to src/get-started/index.html |
diff --git a/img/dashboard-dark.png b/src/img/dashboard-dark.png| similarity index 100% |
| rename from img/dashboard-dark.png |
| rename to src/img/dashboard-dark.png |
diff --git a/img/dashboard-light.png b/src/img/dashboard-light.png| similarity index 100% |
| rename from img/dashboard-light.png |
| rename to src/img/dashboard-light.png |
diff --git a/index.html b/src/index.html| similarity index 100% |
| rename from index.html |
| rename to src/index.html |
diff --git a/site.css b/src/site.css| similarity index 100% |
| rename from site.css |
| rename to src/site.css |
diff --git a/site.js b/src/site.js| similarity index 100% |
| rename from site.js |
| rename to src/site.js |