diff --git a/README.md b/README.md index c296cf2..28e0d3f 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Pass exactly one bare domain. The client supports Node.js `>=20.19.0`; CI verifi - [Agent skill](SKILL.md) - [Machine-readable guidance](llms.txt) - [Discovery examples](examples/) +- [Launch demo](examples/launch/README.md) ## Approved use cases @@ -58,4 +59,4 @@ The client in this repository is MIT-licensed. The hosted aggregate service at ` ## Contribution and release notes -See `CONTRIBUTING.md` for checks and contribution boundaries. CI runs smoke/contract checks on Node 20.x and 22.x. The published scoped npm release is `@replynodes/brand-kit@0.1.1`; v0.1.2 is a separate keyword patch. +See `CONTRIBUTING.md` for checks and contribution boundaries. CI runs smoke/contract checks on Node 20.x and 22.x. The published scoped npm release is `@replynodes/brand-kit@0.1.2`. diff --git a/examples/launch/README.md b/examples/launch/README.md new file mode 100644 index 0000000..ec3013b --- /dev/null +++ b/examples/launch/README.md @@ -0,0 +1,33 @@ +# Launch demo + +Run the published package against a public domain: + +```sh +npx --yes @replynodes/brand-kit@0.1.2 +``` + +The terminal smoke demo runs the same command in a temporary directory and checks the result: + +```sh +./examples/launch/terminal-demo.sh +./examples/launch/terminal-demo.sh vercel.com +``` + +It creates exactly six artifacts in `./brand/`: + +- `DESIGN.md` — concise human-readable design guidance. +- `brand.json` — the normalized domain and brand URL. +- `colors.json` — referenced color data. +- `fonts.json` — referenced font data. +- `logos.json` — referenced logo references. +- `tokens.css` — CSS custom properties for the available tokens. + +The launch is limited to public domains, Node.js `>=20.19.0`, and one HTTPS aggregate request. This is references-only: optional signals can be unavailable, and the client does not download logos, fonts, or other binary assets. It requires no signup, API key, cookies, or telemetry. An existing `./brand` directory is not overwritten. + +Any recorded output must come from the script or a live run; do not fabricate output. + +Suggested launch copy: + +> Generate a deterministic, agent-ready brand context package from a public domain with one command: `npx --yes @replynodes/brand-kit@0.1.2 example.com`. It writes six references-only brand artifacts for agents, design handoff, and CSS/token work. + +This is a demo only; do not submit it externally from this repository. diff --git a/examples/launch/terminal-demo.sh b/examples/launch/terminal-demo.sh new file mode 100755 index 0000000..2c90615 --- /dev/null +++ b/examples/launch/terminal-demo.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if (( $# > 1 )); then + printf 'Usage: %s [domain]\n' "$0" >&2 + exit 2 +fi + +domain=${1:-linear.app} +work_dir=$(mktemp -d) +npm_user_config="$work_dir/npmrc" +npm_cache="$work_dir/npm-cache" + +cleanup() { + rm -rf "$work_dir" +} +trap cleanup EXIT + +: > "$npm_user_config" + +printf 'Running in temporary directory: %s\n' "$work_dir" +printf '+ npx --yes @replynodes/brand-kit@0.1.2 %q\n' "$domain" + +set +e +( + cd "$work_dir" + npm_config_userconfig="$npm_user_config" \ + npm_config_cache="$npm_cache" \ + npm_config_audit=false \ + npm_config_fund=false \ + npm_config_update_notifier=false \ + npm_config_progress=false \ + timeout --signal=TERM --kill-after=5s 90s \ + npx --yes @replynodes/brand-kit@0.1.2 "$domain" +) +status=$? +set -e + +printf 'Command exit status: %d\n' "$status" +if (( status != 0 )); then + printf 'Error: launch command failed.\n' >&2 + exit "$status" +fi + +cd "$work_dir" +node --input-type=module - <<'NODE' +import { readdir, readFile } from 'node:fs/promises'; + +const expected = new Set([ + 'DESIGN.md', + 'brand.json', + 'colors.json', + 'fonts.json', + 'logos.json', + 'tokens.css', +]); +const entries = await readdir('./brand', { withFileTypes: true }); +const actual = entries.map((entry) => entry.name).sort(); +const expectedNames = [...expected].sort(); + +if (entries.length !== expected.size || actual.some((name, index) => name !== expectedNames[index])) { + throw new Error(`./brand must contain exactly: ${expectedNames.join(', ')}`); +} +if (entries.some((entry) => !entry.isFile())) { + throw new Error('./brand must contain only regular files'); +} + +const readJson = async (name) => { + try { + return JSON.parse(await readFile(`./brand/${name}`, 'utf8')); + } catch (error) { + throw new Error(`could not parse ./brand/${name}: ${error.message}`); + } +}; +const json = Object.fromEntries( + await Promise.all(['brand.json', 'colors.json', 'fonts.json', 'logos.json'].map(async (name) => [name, await readJson(name)])), +); + +for (const [name, value] of Object.entries(json)) { + if (value.schema_version !== '0.1') { + throw new Error(`./brand/${name} must have schema_version "0.1"`); + } +} +if (typeof json['brand.json'].domain !== 'string' || typeof json['brand.json'].url !== 'string') { + throw new Error('./brand/brand.json must have string domain and url'); +} +for (const name of ['colors.json', 'fonts.json', 'logos.json']) { + const key = name.slice(0, -5); + if (!Array.isArray(json[name][key])) { + throw new Error(`./brand/${name} must have an array at ${key}`); + } +} + +console.log('Verified ./brand: exactly six regular files and the required JSON shape.'); +NODE