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:
parent
deb218943c
commit
710c3a53d0
63 changed files with 2742 additions and 1782 deletions
|
|
@ -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>`);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
80
tests/docs-links.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue