Initial Commit

This commit is contained in:
kossLAN 2026-06-06 23:08:48 -04:00
commit 0534f88f70
No known key found for this signature in database
14 changed files with 5590 additions and 0 deletions

View file

@ -0,0 +1,74 @@
name: Release Site
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
wry_ref:
description: "Branch, tag, or commit to build from in the Wry repository"
required: false
default: "workspace-refactor"
jobs:
release:
runs-on: docker
steps:
- name: Checkout docs
uses: https://data.forgejo.org/actions/checkout@v6
with:
fetch-depth: 0
- name: Checkout Wry
shell: bash
run: |
set -euo pipefail
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.WRY_DEPLOY_KEY }}" > ~/.ssh/wry_deploy_key
chmod 600 ~/.ssh/wry_deploy_key
ssh-keyscan git.wry.land >> ~/.ssh/known_hosts
WRY_REF="${{ inputs.wry_ref }}"
if [ -z "$WRY_REF" ]; then
WRY_REF="workspace-refactor"
fi
GIT_SSH_COMMAND="ssh -i ~/.ssh/wry_deploy_key -o IdentitiesOnly=yes" \
git clone --depth 1 --branch "$WRY_REF" \
ssh://forgejo@git.wry.land/kossLAN/wry.git _wry
- name: Setup Node
uses: https://data.forgejo.org/actions/setup-node@v6
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
shell: bash
run: |
set -euo pipefail
corepack enable
pnpm install --frozen-lockfile
- name: Build site
shell: bash
run: |
set -euo pipefail
WRY_REPO_PATH="$PWD/_wry" pnpm run build
- name: Package release assets
shell: bash
run: |
set -euo pipefail
TAG="${{ forge.ref_name }}" pnpm run release:pack
- name: Upload Forgejo release
uses: https://data.forgejo.org/actions/forgejo-release@v2.13.0
with:
direction: upload
tag: ${{ forge.ref_name }}
release-dir: dist/release
release-notes-file: dist/release/RELEASE_NOTES.md
override: true

42
.gitignore vendored Normal file
View file

@ -0,0 +1,42 @@
# Dependencies
node_modules/
# Astro build and type caches
.astro/
dist/
# Local package-manager and tool caches
.cache/
.pnpm-store/
# Environment overrides
.env
.env.*
!.env.example
# Logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
# Nix shell helpers
.direnv/
result
result-*
# Release and sync scratch space
_wry/
.release-tmp/
# Generated by scripts/sync-specs.mjs
public/spec/
src/content/docs/index.md
src/content/docs/reference/
# OS/editor noise
.DS_Store
Thumbs.db
*.swp
*.swo

30
README.md Normal file
View file

@ -0,0 +1,30 @@
# Wry Docs
Static documentation site for the generated Wry TOML configuration specs.
Generated docs live under `src/content/docs/` and copied raw specs live under
`public/spec/`. They are intentionally ignored and recreated by
`pnpm run sync:specs`.
## Local development
The generator reads Wry specs from `../wry` by default. Override that with
`WRY_REPO_PATH` when needed.
```shell
nix develop
pnpm install
WRY_REPO_PATH=/home/koss/git/wry pnpm run dev
```
## Release build
```shell
WRY_REPO_PATH=/home/koss/git/wry pnpm run build
TAG=v0.0.0-test pnpm run release:pack
```
The release assets are written to `dist/release/`.
Forgejo Actions builds the same bundle on `v*` tags and uploads it to the
matching Forgejo release.

31
astro.config.mjs Normal file
View file

@ -0,0 +1,31 @@
import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
export default defineConfig({
integrations: [
starlight({
title: "Wry Config Docs",
description: "Generated documentation for the Wry TOML configuration format.",
customCss: ["./src/styles/custom.css"],
sidebar: [
{
label: "Start",
items: [
{ label: "Overview", slug: "" },
{ label: "Raw Generated Specs", slug: "reference/raw-specs" }
]
},
{
label: "Reference",
items: [
{ label: "Top-Level Config", slug: "reference/config" },
{
label: "Configuration Types",
items: [{ autogenerate: { directory: "reference/types" } }]
}
]
}
]
})
]
});

27
flake.lock generated Normal file
View file

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1780243769,
"narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "331800de5053fcebacf6813adb5db9c9dca22a0c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

41
flake.nix Normal file
View file

@ -0,0 +1,41 @@
{
description = "Development shell for Wry docs";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { nixpkgs, ... }:
let
systems = [
"x86_64-linux"
"aarch64-linux"
];
forAllSystems = nixpkgs.lib.genAttrs systems;
in
{
devShells = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
default = pkgs.mkShell {
packages = [
pkgs.corepack
pkgs.nodejs_24
];
shellHook = ''
export COREPACK_HOME="$PWD/.cache/corepack"
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
mkdir -p "$COREPACK_HOME" "$PWD/.cache/corepack-bin"
corepack enable --install-directory "$PWD/.cache/corepack-bin" >/dev/null
corepack install >/dev/null
export PATH="$PWD/.cache/corepack-bin:$PATH"
'';
};
});
};
}

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "wry-docs",
"version": "0.0.0",
"private": true,
"type": "module",
"packageManager": "pnpm@11.5.2",
"scripts": {
"dev": "pnpm run sync:specs && astro dev",
"sync:specs": "node scripts/sync-specs.mjs",
"build": "pnpm run sync:specs && astro build",
"check": "pnpm run sync:specs && astro check",
"release:pack": "node scripts/pack-release.mjs"
},
"dependencies": {
"@astrojs/starlight": "^0.39.3",
"astro": "^6.4.4"
},
"devDependencies": {
"@astrojs/check": "^0.9.9",
"tar": "^7.5.16",
"typescript": "^6.0.3",
"yazl": "^3.3.1"
}
}

4761
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

3
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,3 @@
allowBuilds:
esbuild: true
sharp: true

144
scripts/pack-release.mjs Normal file
View file

@ -0,0 +1,144 @@
import { createHash } from "node:crypto";
import { createReadStream, createWriteStream } from "node:fs";
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import * as tar from "tar";
import { ZipFile } from "yazl";
const root = process.cwd();
const distDir = path.join(root, "dist");
const releaseDir = path.join(distDir, "release");
function releaseTag() {
const value = process.env.TAG ?? process.env.FORGEJO_REF_NAME ?? process.env.GITHUB_REF_NAME ?? "";
return value.replace(/^refs\/tags\//, "") || "dev";
}
function cleanTag(tag) {
return tag.replace(/[^A-Za-z0-9._-]+/g, "-");
}
async function ensureBuilt() {
const index = path.join(distDir, "index.html");
try {
const info = await stat(index);
if (!info.isFile()) throw new Error();
} catch {
throw new Error("dist/index.html is missing. Run `pnpm run build` before `pnpm run release:pack`.");
}
}
async function distEntries() {
const entries = await readdir(distDir);
return entries.filter((entry) => entry !== "release");
}
async function addZipPath(zip, source, name) {
const info = await stat(source);
if (info.isDirectory()) {
const children = await readdir(source);
if (children.length === 0) {
zip.addEmptyDirectory(name);
return;
}
for (const child of children) {
await addZipPath(zip, path.join(source, child), `${name}/${child}`);
}
return;
}
zip.addFile(source, name);
}
async function writeZip(file, entries, bundleName) {
await new Promise((resolve, reject) => {
const output = createWriteStream(file);
const zip = new ZipFile();
output.on("close", resolve);
output.on("error", reject);
zip.outputStream.on("error", reject);
zip.outputStream.pipe(output);
Promise.all(entries.map((entry) => addZipPath(zip, path.join(distDir, entry), `${bundleName}/${entry}`)))
.then(() => zip.end())
.catch(reject);
});
}
async function sha256(file) {
const hash = createHash("sha256");
await new Promise((resolve, reject) => {
createReadStream(file)
.on("data", (chunk) => hash.update(chunk))
.on("error", reject)
.on("end", resolve);
});
return hash.digest("hex");
}
async function readBuildInfo() {
try {
return JSON.parse(await readFile(path.join(distDir, "spec", "build-info.json"), "utf8"));
} catch {
return {};
}
}
async function main() {
await ensureBuilt();
const tag = cleanTag(releaseTag());
const bundleName = `wry-docs-${tag}`;
const entries = await distEntries();
await rm(releaseDir, { recursive: true, force: true });
await mkdir(releaseDir, { recursive: true });
const tarball = path.join(releaseDir, `${bundleName}.tar.gz`);
const zipfile = path.join(releaseDir, `${bundleName}.zip`);
await tar.c(
{
cwd: distDir,
file: tarball,
gzip: true,
portable: true,
prefix: `${bundleName}/`
},
entries
);
await writeZip(zipfile, entries, bundleName);
const assets = [tarball, zipfile];
const sums = [];
for (const file of assets) {
sums.push(`${await sha256(file)} ${path.basename(file)}`);
}
await writeFile(path.join(releaseDir, "SHA256SUMS"), `${sums.join("\n")}\n`);
const info = await readBuildInfo();
const notes = [
`# Wry Docs ${tag}`,
"",
"Static documentation bundle generated from Wry's TOML configuration specs.",
"",
"## Source",
"",
`- Wry branch: \`${info.sourceBranch ?? "unknown"}\``,
`- Wry commit: \`${info.sourceCommit ?? "unknown"}\``,
`- Generated at: \`${info.generatedAt ?? "unknown"}\``,
"",
"## Assets",
"",
`- \`${path.basename(tarball)}\``,
`- \`${path.basename(zipfile)}\``,
"- `SHA256SUMS`",
""
].join("\n");
await writeFile(path.join(releaseDir, "RELEASE_NOTES.md"), notes);
console.log(`Wrote release assets to ${releaseDir}`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});

353
scripts/sync-specs.mjs Normal file
View file

@ -0,0 +1,353 @@
import { execFileSync } from "node:child_process";
import { mkdir, readFile, rm, writeFile, copyFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
const root = process.cwd();
const wryRoot = path.resolve(process.env.WRY_REPO_PATH ?? path.join(root, "..", "wry"));
const specDir = path.join(wryRoot, "crates", "toml-spec", "spec");
const schemaPath = path.join(specDir, "spec.generated.json");
const docsDir = path.join(root, "src", "content", "docs");
const referenceDir = path.join(docsDir, "reference");
const typesDir = path.join(referenceDir, "types");
const publicSpecDir = path.join(root, "public", "spec");
const copiedSpecs = ["spec.generated.json", "spec.generated.md", "spec.yaml"];
function frontmatter({ title, description }) {
return [
"---",
`title: ${JSON.stringify(title)}`,
description ? `description: ${JSON.stringify(description)}` : undefined,
"---",
""
]
.filter(Boolean)
.join("\n");
}
function slugify(name) {
return name
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
.replace(/[^A-Za-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.toLowerCase();
}
function trimDescription(description) {
return typeof description === "string" ? description.trim() : "";
}
function escapePipe(value) {
return String(value).replaceAll("|", "\\|").replaceAll("\n", "<br>");
}
function inlineCode(value) {
return `\`${String(value).replaceAll("`", "\\`")}\``;
}
function refName(ref) {
const prefix = "#/$defs/";
return typeof ref === "string" && ref.startsWith(prefix) ? ref.slice(prefix.length) : null;
}
function linkRef(ref) {
const name = refName(ref);
if (!name) {
return inlineCode(ref);
}
return `[${name}](/reference/types/${slugify(name)}/)`;
}
function schemaType(schema) {
if (!schema || typeof schema !== "object") {
return "unknown";
}
if (schema.$ref) {
return linkRef(schema.$ref);
}
if (schema.const !== undefined) {
return `constant ${inlineCode(schema.const)}`;
}
if (Array.isArray(schema.enum)) {
return "enum";
}
if (schema.type === "array") {
return `array of ${schemaType(schema.items)}`;
}
if (schema.type) {
return Array.isArray(schema.type) ? schema.type.map(inlineCode).join(" or ") : inlineCode(schema.type);
}
if (schema.anyOf) {
return "one of several forms";
}
if (schema.oneOf) {
return "one of several forms";
}
if (schema.allOf) {
return "combination";
}
if (schema.properties) {
return inlineCode("object");
}
return "unspecified";
}
function describeConstraints(schema) {
const rows = [];
if (schema.pattern) rows.push(["Pattern", inlineCode(schema.pattern)]);
if (schema.minimum !== undefined) rows.push(["Minimum", inlineCode(schema.minimum)]);
if (schema.maximum !== undefined) rows.push(["Maximum", inlineCode(schema.maximum)]);
if (schema.exclusiveMinimum !== undefined) rows.push(["Exclusive minimum", inlineCode(schema.exclusiveMinimum)]);
if (schema.exclusiveMaximum !== undefined) rows.push(["Exclusive maximum", inlineCode(schema.exclusiveMaximum)]);
if (schema.minLength !== undefined) rows.push(["Minimum length", inlineCode(schema.minLength)]);
if (schema.maxLength !== undefined) rows.push(["Maximum length", inlineCode(schema.maxLength)]);
if (schema.minItems !== undefined) rows.push(["Minimum items", inlineCode(schema.minItems)]);
if (schema.maxItems !== undefined) rows.push(["Maximum items", inlineCode(schema.maxItems)]);
return rows;
}
function heading(level, text) {
return `${"#".repeat(Math.min(level, 6))} ${text}`;
}
function variantLabel(schema, index) {
const tag = schema?.properties?.type?.const;
if (tag !== undefined) {
return `${inlineCode(`type = "${tag}"`)}`;
}
if (schema?.const !== undefined) {
return inlineCode(schema.const);
}
if (schema?.$ref) {
return linkRef(schema.$ref);
}
if (schema?.type === "array") {
return "array";
}
if (schema?.type) {
return Array.isArray(schema.type) ? schema.type.join(" or ") : schema.type;
}
return `form ${index + 1}`;
}
function renderSchema(schema, level = 2, seen = new Set()) {
if (!schema || typeof schema !== "object") {
return "";
}
const out = [];
const description = trimDescription(schema.description);
if (description) {
out.push(description, "");
}
const constraints = describeConstraints(schema);
if (schema.$ref || schema.type || schema.const !== undefined || constraints.length || Array.isArray(schema.enum)) {
out.push('<div class="schema-meta">');
out.push(`<div><strong>Type:</strong> ${schemaType(schema)}</div>`);
for (const [label, value] of constraints) {
out.push(`<div><strong>${label}:</strong> ${value}</div>`);
}
out.push("</div>", "");
}
if (Array.isArray(schema.enum)) {
out.push(heading(level, "Allowed Values"), "");
for (const value of schema.enum) {
out.push(`- ${inlineCode(value)}`);
}
out.push("");
}
const union = schema.anyOf ?? schema.oneOf;
if (Array.isArray(union)) {
out.push(heading(level, "Accepted Forms"), "");
union.forEach((variant, index) => {
out.push(heading(level + 1, variantLabel(variant, index)), "");
out.push(renderSchema(variant, level + 2, seen));
});
}
if (Array.isArray(schema.allOf)) {
out.push(heading(level, "Combined Schemas"), "");
schema.allOf.forEach((variant, index) => {
out.push(heading(level + 1, `Part ${index + 1}`), "");
out.push(renderSchema(variant, level + 2, seen));
});
}
if (schema.type === "array" && schema.items) {
out.push(heading(level, "Items"), "");
if (schema.items.$ref) {
out.push(`Each item should be ${linkRef(schema.items.$ref)}.`, "");
} else {
out.push(renderSchema(schema.items, level + 1, seen));
}
}
if (schema.properties && !seen.has(schema)) {
seen.add(schema);
const required = new Set(schema.required ?? []);
const entries = Object.entries(schema.properties).filter(([name]) => name !== "type" || schema.properties.type.const === undefined);
if (entries.length) {
out.push(heading(level, "Fields"), "");
for (const [name, property] of entries) {
out.push('<div class="schema-field">');
out.push(`<p><strong>${inlineCode(name)}</strong> ${required.has(name) ? "(required)" : "(optional)"}</p>`);
out.push(`<p><strong>Type:</strong> ${schemaType(property)}</p>`);
const propertyDescription = trimDescription(property.description);
if (propertyDescription) {
out.push("", propertyDescription);
}
const propertyConstraints = describeConstraints(property);
if (propertyConstraints.length) {
out.push("", "| Constraint | Value |", "| --- | --- |");
for (const [label, value] of propertyConstraints) {
out.push(`| ${escapePipe(label)} | ${escapePipe(value)} |`);
}
}
if (property.items?.$ref) {
out.push("", `Each item should be ${linkRef(property.items.$ref)}.`);
}
if ((property.anyOf || property.oneOf || property.properties) && !property.$ref) {
out.push("", renderSchema(property, level + 1, seen));
}
out.push("</div>", "");
}
}
}
return out.join("\n").replace(/\n{4,}/g, "\n\n\n");
}
function tryGit(args) {
try {
return execFileSync("git", ["-C", wryRoot, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
} catch {
return "";
}
}
function buildInfo() {
const commit = tryGit(["rev-parse", "--short=12", "HEAD"]);
const branch = tryGit(["branch", "--show-current"]);
return {
sourcePath: wryRoot,
sourceCommit: commit || null,
sourceBranch: branch || null,
generatedAt: new Date().toISOString()
};
}
function sourceSummary(info) {
const rows = [];
if (info.sourceBranch) rows.push(`- Source branch: \`${info.sourceBranch}\``);
if (info.sourceCommit) rows.push(`- Source commit: \`${info.sourceCommit}\``);
rows.push(`- Generated at: \`${info.generatedAt}\``);
return rows.join("\n");
}
async function main() {
const rawSchema = await readFile(schemaPath, "utf8");
const schema = JSON.parse(rawSchema);
const defs = schema.$defs ?? {};
const typeEntries = Object.keys(defs).sort((a, b) => a.localeCompare(b));
const info = buildInfo();
await rm(typesDir, { recursive: true, force: true });
await mkdir(typesDir, { recursive: true });
await mkdir(publicSpecDir, { recursive: true });
for (const name of copiedSpecs) {
await copyFile(path.join(specDir, name), path.join(publicSpecDir, name));
}
await writeFile(path.join(publicSpecDir, "build-info.json"), `${JSON.stringify(info, null, 2)}\n`);
const index = `${frontmatter({
title: "Wry Config Docs",
description: "Generated documentation for the Wry TOML configuration format."
})}
# Wry Config Docs
This site is generated from Wry's TOML configuration specs.
${sourceSummary(info)}
## Reference
- [Top-Level Config](/reference/config/) starts at the schema root.
- [Configuration Types](/reference/types/) lists every generated type.
- [Raw Generated Specs](/reference/raw-specs/) exposes the source artifacts copied from Wry.
`;
await writeFile(path.join(docsDir, "index.md"), index);
const configSchema = defs.Config;
if (!configSchema) {
throw new Error("spec.generated.json does not contain $defs.Config");
}
const configPage = `${frontmatter({
title: "Top-Level Config",
description: "The root Wry TOML configuration schema."
})}
# Top-Level Config
This page is generated from \`$defs.Config\` in \`spec.generated.json\`.
${renderSchema(configSchema, 2)}
`;
await writeFile(path.join(referenceDir, "config.md"), configPage);
const rawPage = `${frontmatter({
title: "Raw Generated Specs",
description: "Downloadable generated Wry configuration spec artifacts."
})}
# Raw Generated Specs
These files are copied directly from Wry during \`pnpm run sync:specs\`.
<ul class="raw-links">
<li><a href="/spec/spec.generated.json">spec.generated.json</a><br />Machine-readable JSON Schema.</li>
<li><a href="/spec/spec.generated.md">spec.generated.md</a><br />Generated Markdown reference from Wry.</li>
<li><a href="/spec/spec.yaml">spec.yaml</a><br />Canonical TOML spec input.</li>
<li><a href="/spec/build-info.json">build-info.json</a><br />Source checkout metadata for this generated build.</li>
</ul>
`;
await writeFile(path.join(referenceDir, "raw-specs.md"), rawPage);
const typeIndex = `${frontmatter({
title: "Configuration Types",
description: "Alphabetized generated type reference."
})}
# Configuration Types
This index is generated from the \`$defs\` section of \`spec.generated.json\`.
<ul class="schema-list">
${typeEntries.map((name) => ` <li><a href="/reference/types/${slugify(name)}/">${name}</a></li>`).join("\n")}
</ul>
`;
await writeFile(path.join(typesDir, "index.md"), typeIndex);
for (const name of typeEntries) {
const type = defs[name];
const page = `${frontmatter({
title: name,
description: trimDescription(type.description).split("\n")[0] || `Generated schema reference for ${name}.`
})}
# \`${name}\`
Generated from \`$defs.${name}\` in \`spec.generated.json\`.
${renderSchema(type, 2)}
`;
await writeFile(path.join(typesDir, `${slugify(name)}.md`), page);
}
console.log(`Generated ${typeEntries.length} type pages from ${schemaPath}`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});

10
src/content.config.ts Normal file
View file

@ -0,0 +1,10 @@
import { defineCollection } from "astro:content";
import { docsLoader } from "@astrojs/starlight/loaders";
import { docsSchema } from "@astrojs/starlight/schema";
export const collections = {
docs: defineCollection({
loader: docsLoader(),
schema: docsSchema()
})
};

44
src/styles/custom.css Normal file
View file

@ -0,0 +1,44 @@
:root {
--sl-content-width: 56rem;
}
.schema-meta {
display: grid;
gap: 0.35rem;
margin: 1rem 0;
padding: 0.85rem 1rem;
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.375rem;
background: var(--sl-color-gray-6);
}
.schema-field {
margin: 1.25rem 0;
padding-left: 1rem;
border-left: 3px solid var(--sl-color-gray-5);
}
.schema-field > p:first-child {
margin-top: 0;
}
.schema-list {
columns: 2 18rem;
}
.schema-list li {
break-inside: avoid;
}
.raw-links {
display: grid;
gap: 0.75rem;
padding: 0;
list-style: none;
}
.raw-links li {
padding: 0.85rem 1rem;
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.375rem;
}

6
tsconfig.json Normal file
View file

@ -0,0 +1,6 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"forceConsistentCasingInFileNames": true
}
}