README, docs and site: one story (the whole job, no avoidable rejections, the desktop app, your AI, install)

README is a short front page (about 120 lines) with one real visual per section; every former section moved
unchanged into docs/*.md with a docs/README.md index (journey, rejections, getting-started, commands, growth,
studios, store-art, listing-languages, api, skills, mcp, safety, faq, project). The site tells the same story on one
page: the teaser in the hero, real CLI output as text, the desktop app as a section with light/dark captures of eight
screens and the per-system downloads (desktop/latest.json); /desktop/ redirects to /#download. New capture tools:
tools/site-desktop-shots.ts (fixture-mode desktop frames) and tools/site-term-shots.ts (real CLI output as text and
WebP). tests/docs-links.test.ts checks README, docs and site links resolve.
This commit is contained in:
CRTkafa 2026-10-13 03:29:59 +08:00
parent deb218943c
commit 710c3a53d0
63 changed files with 2742 additions and 1782 deletions

View file

@ -48,9 +48,9 @@ describe('minisign', () => {
const conf = JSON.parse(readFileSync(join(import.meta.dir, '../apps/desktop/src-tauri/tauri.conf.json'), 'utf8'));
expect(PUBLIC_KEY).toBe(conf.plugins.updater.pubkey);
expect(parsePublicKey(PUBLIC_KEY).keyId).toMatch(/^[0-9A-F]{16}$/);
// the downloads page prints the minisign key line for people who verify by hand
// the home page's download section prints the minisign key line for people who verify by hand
const line = Buffer.from(PUBLIC_KEY, 'base64').toString().split('\n')[1]!;
expect(readFileSync(join(import.meta.dir, '../site/desktop/index.html'), 'utf8')).toContain(`<code class="dl-key">${line}</code>`);
expect(readFileSync(join(import.meta.dir, '../site/index.html'), 'utf8')).toContain(`<code class="dl-key">${line}</code>`);
});
});

View file

@ -23,10 +23,11 @@ describe("first rule: do it, don't ask", () => {
test('reaches every generated adapter, README and the lead of store-pitfalls', () => {
for (const f of ['AGENTS.md', 'GEMINI.md', '.github/copilot-instructions.md', '.cursor/rules/storebound.mdc'])
expect(read(f).toLowerCase()).toContain(RULE);
// the first section of README carries the rule; the full text lives in docs/journey.md
const readme = read('README.md');
const at = readme.indexOf('## How agents use storebound');
expect(at).toBeGreaterThan(0);
expect(readme.slice(0, at).split('\n## ').length).toBe(1); // the first section of README
const first = readme.split('\n## ')[1] ?? '';
expect(first.toLowerCase()).toContain(RULE);
expect(read('docs/journey.md')).toContain('## How agents use storebound');
const pit = read('skills/store-pitfalls/SKILL.md');
expect(/^## (.*)$/m.exec(pit)?.[1]?.toLowerCase()).toContain(RULE);
for (const s of ['console-autofill', 'play-first-release', 'appstore-first-release', 'store-declarations'])
@ -43,7 +44,8 @@ describe("first rule: do it, don't ask", () => {
/\b(do|fill|finish) (that|this) (part )?by hand\b/i,
/\bfor the owner to copy\b/i,
];
const files = ['docs/agents-core.md', 'README.md',
const files = ['docs/agents-core.md', 'README.md', // the README's former sections, now in docs/
...['journey', 'getting-started', 'commands', 'rejections', 'growth', 'studios', 'store-art', 'faq', 'safety', 'mcp', 'skills', 'listing-languages', 'api', 'project'].map(n => `docs/${n}.md`),
...readdirSync(join(root, 'skills')).map(n => `skills/${n}/SKILL.md`).filter(f => { try { read(f); return true; } catch { return false; } })];
test('no skill or rule hands navigation, exports, uploads or forms to the owner', () => {

80
tests/docs-links.test.ts Normal file
View file

@ -0,0 +1,80 @@
// Links in README.md, docs/*.md and the site resolve: relative links and images point at files that exist, #anchors
// at headings (GitHub / Forgejo slugs) or element ids, and links into this repo on git.crt.fyi
// (/raw/branch/main/<path>, /src/branch/main/<path>) at files in the checkout. Other external URLs are not fetched.
import { describe, expect, test } from 'bun:test';
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
const ROOT = join(import.meta.dir, '..');
const REPO = /^https:\/\/git\.crt\.fyi\/CRT\/storebound\/(?:raw|src)\/branch\/main\/([^?#]+)(#.*)?$/;
/** GitHub-style heading slug: lower case, punctuation dropped, spaces to hyphens. */
const slug = (h: string): string => h.trim().toLowerCase().replace(/<[^>]+>/g, '').replace(/[`*_]/g, '')
.replace(/[^\p{L}\p{N}\s-]/gu, '').replace(/\s/g, '-');
function anchorsOf(file: string): Set<string> {
const src = readFileSync(file, 'utf8');
if (file.endsWith('.html')) return new Set([...src.matchAll(/\sid="([^"]+)"/g)].map(m => m[1]!));
const out = new Set<string>();
const seen = new Map<string, number>();
let fence = false;
for (const line of src.split('\n')) {
if (/^```/.test(line)) fence = !fence;
const m = !fence && /^#{1,6} (.+)$/.exec(line);
if (!m) continue;
const s = slug(m[1]!);
const n = seen.get(s) ?? 0;
seen.set(s, n + 1);
out.add(n ? `${s}-${n}` : s);
}
return out;
}
function linksOf(file: string): string[] {
let src = readFileSync(file, 'utf8');
if (file.endsWith('.md')) src = src.replace(/^```[\s\S]*?^```/gm, '').replace(/`[^`\n]*`/g, '');
const md = [...src.matchAll(/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)].map(m => m[1]!);
const html = [...src.matchAll(/\s(?:href|src|srcset|poster)="([^"]+)"/g)].flatMap(m => m[1]!.split(',').map(s => s.trim().split(/\s+/)[0]!));
return [...md, ...html].filter(Boolean);
}
function problems(file: string): string[] {
const out: string[] = [];
const base = dirname(file);
for (const link of linksOf(file)) {
const repo = REPO.exec(link);
let target: string, anchor: string | undefined;
if (repo) { target = join(ROOT, decodeURIComponent(repo[1]!)); anchor = repo[2]?.slice(1); }
else if (/^[a-z]+:/i.test(link) || link.startsWith('//')) continue;
// served by the desktop release server, not the site (tools/vds/apache-storebound-desktop.conf)
else if (/(^|\/)latest\.json$/.test(link)) continue;
else {
const [p, a] = link.split('#') as [string, string | undefined];
target = p ? join(base, decodeURIComponent(p.split('?')[0]!)) : file;
anchor = a;
}
const rel = relative(ROOT, file);
if (!existsSync(target)) { out.push(`${rel}: ${link} (no such file)`); continue; }
if (anchor && statSync(target).isFile() && /\.(md|html)$/.test(target) && !anchorsOf(target).has(anchor))
out.push(`${rel}: ${link} (no #${anchor})`);
}
return out;
}
const docs = readdirSync(join(ROOT, 'docs')).filter(n => n.endsWith('.md')).map(n => join(ROOT, 'docs', n));
describe('links resolve', () => {
test('README.md and docs/*.md', () => {
expect([join(ROOT, 'README.md'), ...docs].flatMap(problems)).toEqual([]);
});
test('site pages', () => {
const pages = ['site/index.html', 'site/desktop/index.html'].map(p => join(ROOT, p)).filter(existsSync);
expect(pages.flatMap(problems)).toEqual([]);
});
test('the checker catches a missing file, a missing anchor and a stale repo link', () => {
expect(slug('Install for your AI tool')).toBe('install-for-your-ai-tool');
expect(slug('Credentials (never in the repo)')).toBe('credentials-never-in-the-repo');
expect(anchorsOf(join(ROOT, 'docs/getting-started.md')).has('install-for-your-ai-tool')).toBe(true);
expect(REPO.exec('https://git.crt.fyi/CRT/storebound/raw/branch/main/site/img/showcase.webp')?.[1]).toBe('site/img/showcase.webp');
});
});