@cratis/cast 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +12 -2
  2. package/badges/RegistryMember.ts +16 -0
  3. package/badges/ResolvedTier.ts +17 -0
  4. package/badges/Tier.ts +14 -0
  5. package/badges/TierSource.ts +14 -0
  6. package/badges/badgeSvg.ts +137 -0
  7. package/badges/constants.ts +17 -0
  8. package/badges/cratisMark.ts +15 -0
  9. package/badges/index.ts +12 -0
  10. package/badges/registry.ts +63 -0
  11. package/badges/resolveTier.ts +24 -0
  12. package/cast.schema.json +1113 -0
  13. package/config/CastConfig.ts +17 -0
  14. package/config/CastConfigError.ts +10 -0
  15. package/config/LiveStat.ts +17 -0
  16. package/config/ResolvedSection.ts +13 -0
  17. package/config/SectionId.ts +21 -0
  18. package/config/ThemeMode.ts +9 -0
  19. package/config/castSchema.ts +262 -0
  20. package/config/index.ts +12 -0
  21. package/config/parseCast.ts +58 -0
  22. package/config/resolveSections.ts +48 -0
  23. package/github/GitHubSnapshot.ts +66 -0
  24. package/github/fetchGitHub.ts +235 -0
  25. package/github/parse.ts +63 -0
  26. package/index.ts +88 -0
  27. package/integration/CastData.ts +20 -0
  28. package/integration/CastOptions.ts +17 -0
  29. package/integration/Snapshot.ts +12 -0
  30. package/integration/loadRegistry.ts +37 -0
  31. package/integration/resolveData.ts +138 -0
  32. package/integration/snapshotFile.ts +23 -0
  33. package/integration/virtualModule.ts +43 -0
  34. package/machine/castJson.ts +64 -0
  35. package/machine/llmsText.ts +69 -0
  36. package/machine/ogImageSvg.ts +79 -0
  37. package/machine/resumeJson.ts +87 -0
  38. package/package.json +75 -4
  39. package/registry/members.yaml +16 -0
  40. package/routes/404.astro +40 -0
  41. package/routes/apple-touch-icon.png.ts +17 -0
  42. package/routes/badge-card.svg.ts +13 -0
  43. package/routes/badge.svg.ts +11 -0
  44. package/routes/cast.json.ts +11 -0
  45. package/routes/cv.astro +371 -0
  46. package/routes/favicon.svg.ts +16 -0
  47. package/routes/index.astro +61 -0
  48. package/routes/llms.txt.ts +10 -0
  49. package/routes/og.png.ts +23 -0
  50. package/routes/resume.json.ts +13 -0
  51. package/routes/robots.txt.ts +10 -0
  52. package/site/components/Activity.astro +194 -0
  53. package/site/components/Agentic.astro +143 -0
  54. package/site/components/CareerReplay.astro +412 -0
  55. package/site/components/CastBadge.astro +132 -0
  56. package/site/components/CommandPalette.astro +116 -0
  57. package/site/components/Contact.astro +163 -0
  58. package/site/components/Cratis.astro +233 -0
  59. package/site/components/CratisMark.astro +18 -0
  60. package/site/components/EventModel.astro +262 -0
  61. package/site/components/Footer.astro +28 -0
  62. package/site/components/Hero.astro +208 -0
  63. package/site/components/Monogram.astro +25 -0
  64. package/site/components/Nav.astro +124 -0
  65. package/site/components/Now.astro +113 -0
  66. package/site/components/Projects.astro +188 -0
  67. package/site/components/PullRequestFeed.astro +86 -0
  68. package/site/components/SectionHeader.astro +27 -0
  69. package/site/components/Skills.astro +41 -0
  70. package/site/components/Stats.astro +101 -0
  71. package/site/components/Talks.astro +73 -0
  72. package/site/components/Terminal.astro +159 -0
  73. package/site/components/Writing.astro +126 -0
  74. package/site/data.ts +48 -0
  75. package/site/layouts/Base.astro +144 -0
  76. package/site/lib/accent.ts +52 -0
  77. package/site/lib/blog.ts +32 -0
  78. package/site/lib/career.ts +148 -0
  79. package/site/lib/clipboard.ts +12 -0
  80. package/site/lib/contributions.ts +63 -0
  81. package/site/lib/github.ts +55 -0
  82. package/site/lib/inline.ts +23 -0
  83. package/site/lib/lifecycle.ts +44 -0
  84. package/site/lib/motion.ts +43 -0
  85. package/site/lib/nav.ts +38 -0
  86. package/site/lib/reltime.ts +33 -0
  87. package/site/lib/safeJson.ts +15 -0
  88. package/site/lib/sections.ts +41 -0
  89. package/site/lib/theme.ts +28 -0
  90. package/site/scripts/ambient.ts +93 -0
  91. package/site/scripts/event-board.ts +403 -0
  92. package/site/scripts/event-model.ts +155 -0
  93. package/site/scripts/heatmap.ts +94 -0
  94. package/site/scripts/live.ts +145 -0
  95. package/site/scripts/palette.ts +296 -0
  96. package/site/scripts/replay.ts +251 -0
  97. package/site/scripts/terminal.ts +556 -0
  98. package/site/styles/global.css +508 -0
  99. package/virtual.d.ts +15 -0
  100. package/writing/WritingSnapshot.ts +20 -0
  101. package/writing/fetchWriting.ts +54 -0
  102. package/writing/parse.ts +72 -0
@@ -0,0 +1,43 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { CastData } from './CastData';
5
+
6
+ const moduleId = 'virtual:cast';
7
+ const resolvedModuleId = `\0${moduleId}`;
8
+
9
+ /** A minimal Vite plugin shape, so the engine does not depend on Vite's types directly. */
10
+ export type VirtualModulePlugin = {
11
+ name: string;
12
+ resolveId(id: string): string | undefined;
13
+ load(id: string): string | undefined;
14
+ };
15
+
16
+ /**
17
+ * Serves `virtual:cast`: the resolved data as named exports, plus `photo`, which is the imported local
18
+ * portrait (an Astro image) when cast.yaml points at a file.
19
+ */
20
+ export function castVirtualModule(data: () => CastData, photoFile: () => string | undefined): VirtualModulePlugin {
21
+ return {
22
+ name: 'cratis-cast:virtual',
23
+ resolveId(id) {
24
+ return id === moduleId ? resolvedModuleId : undefined;
25
+ },
26
+ load(id) {
27
+ if (id !== resolvedModuleId) return undefined;
28
+ const current = data();
29
+ const photo = photoFile();
30
+ return [
31
+ photo ? `import photo from ${JSON.stringify(photo)};` : 'const photo = undefined;',
32
+ `export const config = ${JSON.stringify(current.config)};`,
33
+ `export const sections = ${JSON.stringify(current.sections)};`,
34
+ `export const github = ${JSON.stringify(current.github)};`,
35
+ `export const writing = ${JSON.stringify(current.writing)};`,
36
+ `export const tier = ${JSON.stringify(current.tier)};`,
37
+ `export const photoUrl = ${JSON.stringify(current.photoUrl ?? null)} ?? undefined;`,
38
+ `export const build = ${JSON.stringify(current.build)};`,
39
+ 'export { photo };',
40
+ ].join('\n');
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,64 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { verificationUrl } from '../badges/constants';
5
+ import type { CastData } from '../integration/CastData';
6
+ import { plainText } from '../site/lib/inline';
7
+
8
+ /** Version of the /cast.json document. Bumped on breaking changes; additions keep the version. */
9
+ export const castJsonVersion = 1;
10
+
11
+ /**
12
+ * The machine-readable profile at /cast.json. Agents can read it; the directory shows its title and tagline.
13
+ * `verification.claimedTier` is what this site's build resolved. A site is static and controlled by its owner,
14
+ * so it is a claim: the directory page at `verification.url` is the source of truth, and the directory never
15
+ * reads tiers from here.
16
+ */
17
+ export function castJson(data: CastData) {
18
+ const { config, tier, github, build } = data;
19
+ const person = config.person;
20
+ return {
21
+ castJsonVersion,
22
+ generator: `@cratis/cast ${build.castVersion}`,
23
+ builtAt: build.builtAt,
24
+ handle: config.github.login,
25
+ name: person.name,
26
+ ...(person.fullName ? { fullName: person.fullName } : {}),
27
+ title: person.title,
28
+ ...(person.location ? { location: person.location } : {}),
29
+ tagline: plainText(person.tagline),
30
+ intro: plainText(person.intro),
31
+ site: config.site.url,
32
+ verification: {
33
+ url: verificationUrl(config.github.login),
34
+ claimedTier: tier.tier ?? null,
35
+ source: tier.source,
36
+ ...(tier.member?.since ? { since: tier.member.since } : {}),
37
+ ...(tier.member?.role ? { role: tier.member.role } : {}),
38
+ claimedAt: build.builtAt,
39
+ registry: build.registrySource,
40
+ note: 'Claimed by this site when it was built. The directory page at url is the source of truth.',
41
+ },
42
+ links: { github: `https://github.com/${config.github.login}`, ...person.links },
43
+ career: config.career.events.map(event => ({
44
+ seq: event.seq,
45
+ date: event.date,
46
+ ...(event.endDate ? { endDate: event.endDate } : {}),
47
+ type: event.type,
48
+ title: event.title,
49
+ ...(event.org ? { org: event.org } : {}),
50
+ ...(event.role ? { role: event.role } : {}),
51
+ ...(event.endsRole ? { endsRole: event.endsRole } : {}),
52
+ summary: plainText(event.summary),
53
+ ...(event.skills?.length ? { skills: event.skills } : {}),
54
+ })),
55
+ projects: config.projects.map(project => ({ name: project.name, url: project.url, pitch: plainText(project.pitch), stack: project.stack })),
56
+ stats: {
57
+ capturedAt: github.fetchedAt ?? null,
58
+ source: build.githubSource,
59
+ contributionsLastYear: github.contributions.total,
60
+ mergedPullRequests: github.mergedPullRequests.total,
61
+ mergedPullRequestsByOrg: github.mergedPullRequests.byOrg,
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,69 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { tierStyles } from '../badges/badgeSvg';
5
+ import { verificationUrl } from '../badges/constants';
6
+ import type { CastData } from '../integration/CastData';
7
+ import { formatMonth, project } from '../site/lib/career';
8
+ import { plainText } from '../site/lib/inline';
9
+
10
+ /** A short, factual Markdown summary for AI agents, following the llms.txt convention. */
11
+ export function llmsText(data: CastData): string {
12
+ const { config, tier, github } = data;
13
+ const person = config.person;
14
+ const site = config.site.url.replace(/\/$/, '');
15
+ const projection = project(config.career.events);
16
+ const lines: string[] = [];
17
+
18
+ lines.push(`# ${person.name}`, '');
19
+ lines.push(`> ${person.title}${person.location ? `, ${person.location}` : ''}. ${plainText(person.intro)}`, '');
20
+
21
+ if (tier.tier) {
22
+ const style = tierStyles[tier.tier];
23
+ lines.push(`Cratis Cast: ${style.label} (${style.description.toLowerCase()}), as claimed by this site's build from ${tier.source === 'registry' ? 'the Cast registry' : 'merged public pull requests in the Cratis organization'}. The authoritative check is ${verificationUrl(config.github.login)}`, '');
24
+ }
25
+
26
+ if (projection.currentRoles.length > 0) {
27
+ lines.push('## Now', '');
28
+ for (const role of projection.currentRoles) lines.push(`- ${role.title}, ${role.org} (since ${formatMonth(role.from)})`);
29
+ lines.push('');
30
+ }
31
+
32
+ if (person.about.length > 0) {
33
+ lines.push('## About', '', ...person.about.map(paragraph => plainText(paragraph)), '');
34
+ }
35
+
36
+ const roles = [...projection.roles].reverse();
37
+ if (roles.length > 0) {
38
+ lines.push('## Experience', '');
39
+ for (const role of roles) lines.push(`- ${formatMonth(role.from)} – ${role.to ? formatMonth(role.to) : 'present'}: ${role.title}, ${role.org}`);
40
+ lines.push('');
41
+ }
42
+
43
+ if (config.projects.length > 0) {
44
+ lines.push('## Projects', '');
45
+ for (const entry of config.projects) lines.push(`- [${entry.name}](${entry.url}): ${plainText(entry.pitch)}`);
46
+ lines.push('');
47
+ }
48
+
49
+ const skills = Object.entries(config.skills);
50
+ if (skills.length > 0) {
51
+ lines.push('## Skills', '');
52
+ for (const [group, list] of skills) lines.push(`- ${group}: ${list.join(', ')}`);
53
+ lines.push('');
54
+ }
55
+
56
+ if (github.fetchedAt) {
57
+ lines.push('## Public GitHub activity', '');
58
+ lines.push(`- ${github.contributions.total.toLocaleString('en-US')} contributions in the last year (captured ${github.fetchedAt.slice(0, 10)})`);
59
+ for (const [org, count] of Object.entries(github.mergedPullRequests.byOrg)) lines.push(`- ${count.toLocaleString('en-US')} merged public pull requests in ${org}`);
60
+ lines.push('');
61
+ }
62
+
63
+ lines.push('## Machine-readable', '');
64
+ lines.push(`- [Cast profile](${site}/cast.json): identity, verified tier, career events, projects`);
65
+ lines.push(`- [JSON Resume](${site}/resume.json)`);
66
+ lines.push(`- [CV](${site}/cv)`);
67
+ lines.push('');
68
+ return lines.join('\n');
69
+ }
@@ -0,0 +1,79 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { tierStyles } from '../badges/badgeSvg';
5
+ import { cratisMarkPaths } from '../badges/cratisMark';
6
+ import type { Tier } from '../badges/Tier';
7
+ import { plainText } from '../site/lib/inline';
8
+
9
+ /** Inputs for the social preview card. */
10
+ export type OgImageInput = {
11
+ name: string;
12
+ title: string;
13
+ tagline: string;
14
+ host: string;
15
+ accent: string;
16
+ tier?: Tier;
17
+ };
18
+
19
+ const escapeXml = (value: string): string =>
20
+ value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
21
+
22
+ /** Greedy word wrap by an approximate character budget. */
23
+ export function wrap(text: string, maximumCharacters: number, maximumLines: number): string[] {
24
+ const lines: string[] = [];
25
+ let current = '';
26
+ for (const word of text.split(/\s+/).filter(Boolean)) {
27
+ if ((current ? `${current} ${word}` : word).length > maximumCharacters && current) {
28
+ lines.push(current);
29
+ current = word;
30
+ } else {
31
+ current = current ? `${current} ${word}` : word;
32
+ }
33
+ }
34
+ if (current) lines.push(current);
35
+ if (lines.length > maximumLines) {
36
+ const kept = lines.slice(0, maximumLines);
37
+ kept[maximumLines - 1] = `${kept[maximumLines - 1]!.replace(/[.,;:]?$/, '')}…`;
38
+ return kept;
39
+ }
40
+ return lines;
41
+ }
42
+
43
+ const sans = "Inter, 'Helvetica Neue', Helvetica, Arial, 'DejaVu Sans', sans-serif";
44
+ const serif = "Fraunces, Georgia, 'DejaVu Serif', serif";
45
+ const mono = "'JetBrains Mono', Menlo, 'DejaVu Sans Mono', monospace";
46
+
47
+ /** The 1200 × 630 social preview card, rendered to PNG at build time. */
48
+ export function ogImageSvg(input: OgImageInput): string {
49
+ const style = input.tier ? tierStyles[input.tier] : undefined;
50
+ const tagline = wrap(plainText(input.tagline), 34, 2);
51
+ const title = wrap(input.title, 64, 2);
52
+ const mark = `<g transform="translate(80 72) scale(${(40 / 328).toFixed(5)})"><g transform="translate(0 328) scale(.1 -.1)" fill="#f5f3f0">${cratisMarkPaths.map(path => `<path d="${path}"/>`).join('')}</g></g>`;
53
+
54
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
55
+ <defs>
56
+ <radialGradient id="a" cx="0.92" cy="0.05" r="0.75"><stop offset="0" stop-color="${input.accent}" stop-opacity=".42"/><stop offset="1" stop-color="${input.accent}" stop-opacity="0"/></radialGradient>
57
+ <radialGradient id="b" cx="0.1" cy="1.05" r="0.6"><stop offset="0" stop-color="#2dd4bf" stop-opacity=".18"/><stop offset="1" stop-color="#2dd4bf" stop-opacity="0"/></radialGradient>
58
+ <pattern id="g" width="40" height="40" patternUnits="userSpaceOnUse"><path d="M40 0H0V40" fill="none" stroke="#ffffff" stroke-opacity=".04"/></pattern>
59
+ ${style ? `<linearGradient id="t" x1="0" x2="1"><stop offset="0" stop-color="${style.from}"/><stop offset="1" stop-color="${style.to}"/></linearGradient>` : ''}
60
+ </defs>
61
+ <rect width="1200" height="630" fill="#08080e"/>
62
+ <rect width="1200" height="630" fill="url(#g)"/>
63
+ <rect width="1200" height="630" fill="url(#a)"/>
64
+ <rect width="1200" height="630" fill="url(#b)"/>
65
+ ${mark}
66
+ <text x="136" y="101" font-family="${mono}" font-size="20" letter-spacing="3" fill="#a09fad">CRATIS CAST</text>
67
+ ${style ? `<g><rect x="${1120 - 52 - style.label.length * 15}" y="66" width="${52 + style.label.length * 15}" height="44" rx="12" fill="url(#t)"/><text x="${1120 - 26}" y="96" text-anchor="end" font-family="${sans}" font-weight="700" font-size="22" fill="${style.ink}">${escapeXml(style.label)}</text></g>` : ''}
68
+ <text x="80" y="250" font-family="${sans}" font-weight="800" font-size="84" letter-spacing="-3" fill="#f5f3f0">${escapeXml(input.name)}</text>
69
+ ${title.map((line, index) => `<text x="82" y="${306 + index * 36}" font-family="${sans}" font-size="28" fill="#a09fad">${escapeXml(line)}</text>`).join('\n')}
70
+ ${tagline.map((line, index) => `<text x="80" y="${440 + index * 62}" font-family="${serif}" font-style="italic" font-weight="600" font-size="54" fill="${input.accent}">${escapeXml(line)}</text>`).join('\n')}
71
+ <text x="80" y="582" font-family="${mono}" font-size="22" fill="#7c7b8b">${escapeXml(input.host)}</text>
72
+ <rect x="0" y="626" width="1200" height="4" fill="${input.accent}"/>
73
+ </svg>`;
74
+ }
75
+
76
+ /** The favicon: the person's initial in the italic serif on their accent. */
77
+ export function faviconSvg(initial: string, accent: string, ink: string): string {
78
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="${accent}"/><text x="32" y="45" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-style="italic" font-weight="700" font-size="40" fill="${ink}">${escapeXml(initial)}</text></svg>`;
79
+ }
@@ -0,0 +1,87 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { CastConfig } from '../config/CastConfig';
5
+ import { project } from '../site/lib/career';
6
+ import { plainText } from '../site/lib/inline';
7
+
8
+ const network: Record<string, string> = { github: 'GitHub', linkedin: 'LinkedIn', mastodon: 'Mastodon', bluesky: 'Bluesky', x: 'X', twitter: 'X', youtube: 'YouTube' };
9
+
10
+ /** 'YYYY-MM' stays; 'YYYY' stays: both are valid JSON Resume ISO 8601 partial dates. */
11
+ const isoDate = (date: string): string => date;
12
+
13
+ const username = (url: string): string => url.replace(/\/+$/, '').split('/').pop() ?? '';
14
+
15
+ /** Splits "Norwegian (native)" into language and fluency. */
16
+ const language = (entry: string) => {
17
+ const match = entry.match(/^(.*?)\s*\((.+)\)\s*$/);
18
+ return match ? { language: match[1]!, fluency: match[2]! } : { language: entry };
19
+ };
20
+
21
+ /** Maps cast.yaml to a JSON Resume document (https://jsonresume.org/schema). */
22
+ export function resumeJson(config: CastConfig, imageUrl?: string) {
23
+ const person = config.person;
24
+ const projection = project(config.career.events);
25
+ const volunteerTypes = new Set(['VolunteerTeamJoined']);
26
+ const [city] = (person.location ?? '').split(',').map(part => part.trim());
27
+
28
+ return {
29
+ $schema: 'https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json',
30
+ basics: {
31
+ name: person.fullName ?? person.name,
32
+ label: person.title,
33
+ ...(imageUrl ? { image: imageUrl } : {}),
34
+ ...(person.email ? { email: person.email } : {}),
35
+ url: config.site.url,
36
+ summary: plainText(person.about.length ? person.about.join('\n\n') : person.intro),
37
+ ...(person.location ? { location: { city, ...(person.country ? { countryCode: person.country } : {}) } } : {}),
38
+ profiles: [
39
+ { network: 'GitHub', username: config.github.login, url: `https://github.com/${config.github.login}` },
40
+ ...Object.entries(person.links)
41
+ .filter(([key]) => key !== 'github')
42
+ .map(([key, url]) => ({ network: network[key.toLowerCase()] ?? key, username: username(url), url })),
43
+ ],
44
+ },
45
+ work: [...projection.roles].reverse().map(role => ({
46
+ name: role.org,
47
+ position: role.title,
48
+ ...(role.orgUrl ? { url: role.orgUrl } : {}),
49
+ ...(role.location ? { location: role.location } : {}),
50
+ startDate: isoDate(role.from),
51
+ ...(role.to ? { endDate: isoDate(role.to) } : {}),
52
+ summary: plainText(role.summary),
53
+ ...(role.skills.length ? { highlights: role.skills } : {}),
54
+ })),
55
+ volunteer: config.career.events.filter(event => volunteerTypes.has(event.type)).map(event => ({
56
+ organization: event.org ?? '',
57
+ position: event.title,
58
+ ...(event.orgUrl ? { url: event.orgUrl } : {}),
59
+ startDate: isoDate(event.date),
60
+ ...(event.endDate ? { endDate: isoDate(event.endDate) } : {}),
61
+ summary: plainText(event.summary),
62
+ })),
63
+ education: config.education.map(entry => ({
64
+ institution: entry.school,
65
+ studyType: entry.degree,
66
+ startDate: entry.from,
67
+ ...(entry.to ? { endDate: entry.to } : {}),
68
+ })),
69
+ certificates: config.certifications.map(certificate => ({
70
+ name: certificate.title,
71
+ issuer: certificate.issuer,
72
+ date: certificate.year,
73
+ ...(certificate.url ? { url: certificate.url } : {}),
74
+ })),
75
+ skills: Object.entries(config.skills).map(([name, keywords]) => ({ name, keywords })),
76
+ languages: person.languages.map(language),
77
+ interests: person.interests.map(name => ({ name })),
78
+ projects: config.projects.map(entry => ({
79
+ name: entry.name,
80
+ url: entry.url,
81
+ description: plainText(entry.pitch),
82
+ ...(entry.features.length ? { highlights: entry.features } : {}),
83
+ ...(entry.stack.length ? { keywords: entry.stack } : {}),
84
+ })),
85
+ meta: { canonical: `${config.site.url.replace(/\/$/, '')}/resume.json`, version: 'v1.0.0' },
86
+ };
87
+ }
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
- "name": "@cratis/cast",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
2
+ "name": "@cratis/cast",
3
+ "version": "0.1.0",
4
+ "description": "Cast: the Cratis profile-site engine. One cast.yaml becomes a verified, event-sourced personal site with a printable CV, live GitHub numbers and a Cast badge.",
5
+ "keywords": [
6
+ "cratis",
7
+ "cast",
8
+ "astro",
9
+ "astro-integration",
10
+ "portfolio",
11
+ "cv",
12
+ "event-sourcing",
13
+ "event-modeling"
14
+ ],
15
+ "author": "Cratis",
16
+ "license": "MIT",
17
+ "homepage": "https://cast.cratis.io",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Cratis/Cast.git",
21
+ "directory": "Source/Cast"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "type": "module",
27
+ "engines": {
28
+ "node": ">=23.0.0"
29
+ },
30
+ "files": [
31
+ "**/*.ts",
32
+ "**/*.astro",
33
+ "**/*.css",
34
+ "**/*.svg",
35
+ "cast.schema.json",
36
+ "registry/members.yaml",
37
+ "!**/for_*/**",
38
+ "!vitest.config.ts",
39
+ "!scripts/**",
40
+ "README.md"
41
+ ],
42
+ "exports": {
43
+ ".": "./index.ts",
44
+ "./schema": "./config/index.ts",
45
+ "./config": "./config/index.ts",
46
+ "./badges": "./badges/index.ts",
47
+ "./github/*": "./github/*.ts",
48
+ "./cast.schema.json": "./cast.schema.json",
49
+ "./package.json": "./package.json",
50
+ "./*": "./*"
51
+ },
52
+ "scripts": {
53
+ "clean": "yarn g:clean",
54
+ "build": "yarn g:tsx scripts/build.ts && yarn g:typecheck",
55
+ "lint": "yarn g:lint",
56
+ "lint:ci": "yarn g:lint:ci",
57
+ "test": "yarn g:test",
58
+ "ci": "yarn g:ci"
59
+ },
60
+ "peerDependencies": {
61
+ "astro": "^7.2.0"
62
+ },
63
+ "dependencies": {
64
+ "@astrojs/sitemap": "^3.7.4",
65
+ "@fontsource-variable/fraunces": "^5.3.0",
66
+ "@fontsource-variable/inter": "^5.3.0",
67
+ "@fontsource-variable/jetbrains-mono": "^5.3.0",
68
+ "@tailwindcss/vite": "^4.3.3",
69
+ "sharp": "^0.35.5",
70
+ "tailwindcss": "^4.3.3",
71
+ "yaml": "^2.9.1",
72
+ "zod": "^4.6.7"
73
+ },
74
+ "devDependencies": {
75
+ "astro": "^7.2.0"
76
+ }
6
77
  }
@@ -0,0 +1,16 @@
1
+ # yaml-language-server: $schema=./members.schema.json
2
+ #
3
+ # The Cratis Cast registry. Every line here is reviewed by Cratis in a pull request.
4
+ # Core and Partner tiers come only from this file. Contributor is never listed here:
5
+ # it is computed from merged public pull requests in the Cratis organization.
6
+ # See README.md in this folder for what each tier means and how to join.
7
+
8
+ - github: woksin
9
+ tier: core
10
+ since: 2024-06
11
+ role: Co-founder & Chief Architect
12
+ site: https://www.woksin.com
13
+
14
+ - github: einari
15
+ tier: core
16
+ role: Co-founder & CTO
@@ -0,0 +1,40 @@
1
+ ---
2
+ // Copyright (c) Cratis. All rights reserved.
3
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
4
+
5
+ import Base from '../site/layouts/Base.astro';
6
+ import Nav from '../site/components/Nav.astro';
7
+ import Footer from '../site/components/Footer.astro';
8
+ import { person } from '../site/data';
9
+ ---
10
+
11
+ <Base title={`EventNotFound · ${person.name}`} description="This page is not in the event stream." noindex>
12
+ <Nav slot="nav" home={false} />
13
+ <section class="relative isolate flex min-h-[100svh] items-center overflow-hidden pt-[var(--nav-h)]">
14
+ <div class="pointer-events-none absolute inset-0 -z-10" aria-hidden="true">
15
+ <div class="absolute top-1/2 left-1/2 size-[40rem] -translate-x-1/2 -translate-y-1/2 rounded-full bg-accent/10 blur-[120px]"></div>
16
+ </div>
17
+ <div class="container-x py-24">
18
+ <div class="panel max-w-2xl overflow-hidden">
19
+ <div class="flex items-center justify-between border-b border-line px-6 py-3.5 font-mono text-xs">
20
+ <span class="note">Event</span>
21
+ <span class="text-fg-faint">stream position <span class="text-event">-1</span></span>
22
+ </div>
23
+ <div class="p-6 md:p-10">
24
+ <p class="font-mono text-sm text-fg-faint">HTTP 404</p>
25
+ <h1 class="mt-3 text-[clamp(2.5rem,7vw,4.75rem)] leading-[0.95] font-[800] tracking-[-0.045em] text-event-soft">EventNotFound</h1>
26
+ <p class="mt-5 max-w-[46ch] text-lg text-fg-muted">
27
+ Nothing was ever appended at this address, so there is nothing to project. The stream itself is fine.
28
+ </p>
29
+ <pre class="mt-8 overflow-x-auto rounded-xl border border-line bg-ink-950/80 p-4 font-mono text-[0.8125rem] leading-6 text-fg-muted"><span class="text-command">replay</span>(stream: <span class="text-view">"{person.handle}"</span>, from: <span class="text-event">-1</span>)
30
+ <span class="text-fg-faint">// → no events at position -1</span></pre>
31
+ <div class="mt-8 flex flex-wrap gap-3">
32
+ <a href="/" class="btn btn-primary">Back to the start of the stream</a>
33
+ <a href="/cv" class="btn">View CV</a>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </section>
39
+ <Footer slot="footer" />
40
+ </Base>
@@ -0,0 +1,17 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { APIRoute } from 'astro';
5
+ import sharp from 'sharp';
6
+ import { config } from 'virtual:cast';
7
+ import { faviconSvg } from '../machine/ogImageSvg';
8
+ import { accentTokens } from '../site/lib/accent';
9
+
10
+ export const prerender = true;
11
+
12
+ export const GET: APIRoute = async () => {
13
+ const initial = (config.person.name.trim()[0] ?? 'C').toUpperCase();
14
+ const svg = faviconSvg(initial, config.theme.accent, accentTokens(config.theme.accent).ink).replace('rx="14"', 'rx="0"');
15
+ const png = await sharp(Buffer.from(svg), { density: 300 }).resize(180, 180).png().toBuffer();
16
+ return new Response(new Uint8Array(png), { headers: { 'content-type': 'image/png' } });
17
+ };
@@ -0,0 +1,13 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { APIRoute } from 'astro';
5
+ import { config, tier } from 'virtual:cast';
6
+ import { badgeCardSvg } from '../badges/badgeSvg';
7
+
8
+ export const prerender = true;
9
+
10
+ export const GET: APIRoute = () =>
11
+ new Response(badgeCardSvg(tier.tier, { handle: config.person.handle, name: config.person.name, since: tier.member?.since }), {
12
+ headers: { 'content-type': 'image/svg+xml; charset=utf-8' },
13
+ });
@@ -0,0 +1,11 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { APIRoute } from 'astro';
5
+ import { tier } from 'virtual:cast';
6
+ import { badgeSvg } from '../badges/badgeSvg';
7
+
8
+ export const prerender = true;
9
+
10
+ /** This site's badge, as verified when it was built. The directory serves the authoritative one. */
11
+ export const GET: APIRoute = () => new Response(badgeSvg(tier.tier), { headers: { 'content-type': 'image/svg+xml; charset=utf-8' } });
@@ -0,0 +1,11 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { APIRoute } from 'astro';
5
+ import * as data from 'virtual:cast';
6
+ import { castJson } from '../machine/castJson';
7
+
8
+ export const prerender = true;
9
+
10
+ export const GET: APIRoute = () =>
11
+ new Response(JSON.stringify(castJson(data), null, 2), { headers: { 'content-type': 'application/json; charset=utf-8' } });