@edoxen/browser 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 (69) hide show
  1. package/README.md +45 -0
  2. package/dist/cli.js +4475 -0
  3. package/package.json +82 -0
  4. package/src/astro/components/ActionTypeBadge.astro +28 -0
  5. package/src/astro/components/AgendaTable.astro +49 -0
  6. package/src/astro/components/BodyBadge.astro +28 -0
  7. package/src/astro/components/CommitteeCard.astro +58 -0
  8. package/src/astro/components/CountryFlag.astro +34 -0
  9. package/src/astro/components/DecadeTimeline.astro +52 -0
  10. package/src/astro/components/DecisionList.astro +66 -0
  11. package/src/astro/components/EmptyState.astro +19 -0
  12. package/src/astro/components/LocaleTabs.astro +44 -0
  13. package/src/astro/components/MeetingList.astro +63 -0
  14. package/src/astro/components/MinutesSection.astro +59 -0
  15. package/src/astro/components/OfficerList.astro +49 -0
  16. package/src/astro/components/PageHero.astro +42 -0
  17. package/src/astro/components/PrevNextNav.astro +62 -0
  18. package/src/astro/components/ReferenceDocuments.astro +38 -0
  19. package/src/astro/components/UrnBar.astro +49 -0
  20. package/src/astro/layouts/BaseLayout.astro +79 -0
  21. package/src/astro/layouts/DecisionLayout.astro +21 -0
  22. package/src/astro/layouts/MeetingLayout.astro +21 -0
  23. package/src/astro/pages/404.astro +10 -0
  24. package/src/astro/pages/[lang]/about.astro +65 -0
  25. package/src/astro/pages/[lang]/decisions/[urn].astro +126 -0
  26. package/src/astro/pages/[lang]/decisions/index.astro +27 -0
  27. package/src/astro/pages/[lang]/index.astro +50 -0
  28. package/src/astro/pages/[lang]/meetings/[urn].astro +114 -0
  29. package/src/astro/pages/[lang]/meetings/index.astro +27 -0
  30. package/src/astro/pages/about.astro +65 -0
  31. package/src/astro/pages/decisions/[urn].astro +122 -0
  32. package/src/astro/pages/decisions/index.astro +67 -0
  33. package/src/astro/pages/index.astro +50 -0
  34. package/src/astro/pages/meetings/[urn].astro +110 -0
  35. package/src/astro/pages/meetings/index.astro +20 -0
  36. package/src/cli/index.ts +195 -0
  37. package/src/config/index.ts +33 -0
  38. package/src/config/paths.spec.ts +59 -0
  39. package/src/config/paths.ts +26 -0
  40. package/src/config/schema.spec.ts +263 -0
  41. package/src/config/schema.ts +132 -0
  42. package/src/config/tokens.spec.ts +57 -0
  43. package/src/config/tokens.ts +48 -0
  44. package/src/data/index.ts +36 -0
  45. package/src/data/lint.spec.ts +79 -0
  46. package/src/data/lint.ts +77 -0
  47. package/src/data/load.spec.ts +58 -0
  48. package/src/data/load.ts +50 -0
  49. package/src/data/prepare.spec.ts +137 -0
  50. package/src/data/prepare.ts +199 -0
  51. package/src/data/project.ts +11 -0
  52. package/src/data/validate.ts +38 -0
  53. package/src/errors.ts +25 -0
  54. package/src/i18n/index.spec.ts +121 -0
  55. package/src/i18n/index.ts +90 -0
  56. package/src/index.ts +74 -0
  57. package/src/integration.ts +233 -0
  58. package/src/islands/decade-scroller.ts +18 -0
  59. package/src/islands/print-button.ts +12 -0
  60. package/src/islands/search-filter-core.spec.ts +84 -0
  61. package/src/islands/search-filter-core.ts +89 -0
  62. package/src/islands/search-filter.ts +166 -0
  63. package/src/islands/section-tabs.ts +35 -0
  64. package/src/islands/theme-toggle.ts +33 -0
  65. package/src/islands/urn-copy.ts +29 -0
  66. package/src/seo/index.spec.ts +76 -0
  67. package/src/seo/index.ts +85 -0
  68. package/src/virtual.d.ts +11 -0
  69. package/styles/base.css +117 -0
@@ -0,0 +1,27 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import BaseLayout from '../../../layouts/BaseLayout.astro'
5
+ import MeetingList from '../../../components/MeetingList.astro'
6
+ import DecadeTimeline from '../../../components/DecadeTimeline.astro'
7
+
8
+ export function getStaticPaths() {
9
+ return cfg.locales
10
+ .filter((l) => (l.routePrefix ?? '') !== '')
11
+ .map((l) => ({ params: { lang: l.code } }))
12
+ }
13
+
14
+ const items = payloads.meetingsList.items
15
+ const decades = payloads.meetingsList.facets.decades
16
+ const lang = Astro.params.lang ?? cfg.site.locale
17
+ ---
18
+
19
+ <BaseLayout config={cfg} lang={lang} title={`Meetings — ${cfg.site.title}`}>
20
+ <h1>Meetings</h1>
21
+
22
+ <decade-scroller>
23
+ <DecadeTimeline decades={decades} />
24
+ </decade-scroller>
25
+
26
+ <MeetingList items={items} />
27
+ </BaseLayout>
@@ -0,0 +1,65 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import BaseLayout from '../layouts/BaseLayout.astro'
5
+
6
+ export function getStaticPaths() {
7
+ return cfg.locales
8
+ .filter((l) => (l.routePrefix ?? '') !== '')
9
+ .map((l) => ({ params: { lang: l.code } }))
10
+ }
11
+
12
+ const decisionCount = payloads.decisionsList.items.length
13
+ const meetingCount = payloads.meetingsList.items.length
14
+ ---
15
+
16
+ <BaseLayout config={cfg} title={`About — ${cfg.site.title}`}>
17
+ <h1>About</h1>
18
+
19
+ <section>
20
+ <h2>{cfg.site.title}</h2>
21
+ <p>{cfg.site.description}</p>
22
+ <dl class="edoxen-stats">
23
+ <div><dt>Decisions</dt><dd>{decisionCount}</dd></div>
24
+ <div><dt>Meetings</dt><dd>{meetingCount}</dd></div>
25
+ </dl>
26
+ </section>
27
+
28
+ <section>
29
+ <h2>Edoxen format</h2>
30
+ <p>
31
+ This site renders meeting and decision data using the
32
+ <a href="https://edoxen.github.io">Edoxen</a> information model — a
33
+ YAML-based schema for formal proceedings of standards bodies.
34
+ </p>
35
+ </section>
36
+
37
+ <section>
38
+ <h2>Using this site</h2>
39
+ <ul>
40
+ <li><strong>Decisions</strong> — browse the resolutions archive.</li>
41
+ <li><strong>Meetings</strong> — see meetings and their agendas, minutes, and adopted decisions.</li>
42
+ <li><strong>URNs</strong> — every entity has a stable URN for citation.</li>
43
+ </ul>
44
+ </section>
45
+ </BaseLayout>
46
+
47
+ <style>
48
+ .edoxen-stats {
49
+ display: flex;
50
+ flex-wrap: wrap;
51
+ gap: 1.5rem;
52
+ margin: 1rem 0 0;
53
+ }
54
+ .edoxen-stats dd {
55
+ margin: 0;
56
+ font-size: 1.5rem;
57
+ font-weight: 600;
58
+ }
59
+ .edoxen-stats dt {
60
+ font-size: 0.85rem;
61
+ color: var(--edoxen-color-muted);
62
+ text-transform: uppercase;
63
+ letter-spacing: 0.05em;
64
+ }
65
+ </style>
@@ -0,0 +1,122 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import DecisionLayout from '../../layouts/DecisionLayout.astro'
5
+ import UrnBar from '../../components/UrnBar.astro'
6
+ import BodyBadge from '../../components/BodyBadge.astro'
7
+ import LocaleTabs from '../../components/LocaleTabs.astro'
8
+ import { decisionJsonLd } from '../../../seo/index.js'
9
+ import { pickLocalizedValue, availableSpellings } from '../../../i18n/index.js'
10
+
11
+ export function getStaticPaths() {
12
+ return Object.keys(payloads.decisionByUrn).map((urn) => ({
13
+ params: { urn },
14
+ props: { decision: payloads.decisionByUrn[urn] },
15
+ }))
16
+ }
17
+
18
+ interface Props {
19
+ decision: typeof payloads.decisionByUrn[string]
20
+ }
21
+ const { decision } = Astro.props
22
+ const locale = cfg.site.locale
23
+ const title = pickLocalizedValue(decision.title, locale, decision.urn ?? '')
24
+ const subject = pickLocalizedValue(decision.subject, locale)
25
+ const considering = pickLocalizedValue(decision.considering, locale)
26
+ const actions = decision.actions ?? []
27
+ const considerations = decision.considerations ?? []
28
+ const approvals = decision.approvals ?? []
29
+ const titleSpellings = availableSpellings(decision.title)
30
+ const jsonld = decisionJsonLd(decision, { siteUrl: cfg.site.url, siteTitle: cfg.site.title, defaultLocale: locale })
31
+ ---
32
+
33
+ <DecisionLayout
34
+ config={cfg}
35
+ item={{
36
+ urn: decision.urn ?? '',
37
+ identifier: (decision.identifier[0]?.prefix ?? '') + '-' + (decision.identifier[0]?.number ?? ''),
38
+ title,
39
+ kind: decision.kind ?? '',
40
+ dates: (decision.dates ?? []).map((d) => ({ type: d.type ?? '', date: d.date ?? '' })),
41
+ bodyType: decision.body_type,
42
+ }}
43
+ >
44
+ <script type="application/ld+json" set:html={JSON.stringify(jsonld)} is:inline />
45
+ <UrnBar urn={decision.urn ?? ''} />
46
+
47
+ {decision.body_type && <p><BodyBadge code={decision.body_type} /></p>}
48
+
49
+ {titleSpellings.length > 1 && <LocaleTabs spellings={titleSpellings} active={locale} field="title" />}
50
+
51
+ {subject && (
52
+ <section>
53
+ <h2>Subject</h2>
54
+ <p>{subject}</p>
55
+ </section>
56
+ )}
57
+
58
+ {considering && (
59
+ <section>
60
+ <h2>Considering</h2>
61
+ <p>{considering}</p>
62
+ </section>
63
+ )}
64
+
65
+ {considerations.length > 0 && (
66
+ <section>
67
+ <h2>Considerations</h2>
68
+ <ul>
69
+ {considerations.map((c) => (
70
+ <li>{c.kind}{c.references && c.references.length > 0 ? `: ${c.references.map((r) => r.urn).join(', ')}` : ''}</li>
71
+ ))}
72
+ </ul>
73
+ </section>
74
+ )}
75
+
76
+ {actions.length > 0 && (
77
+ <section>
78
+ <h2>Actions</h2>
79
+ <ul>
80
+ {actions.map((a) => (
81
+ <li>
82
+ <strong>{a.type}</strong>
83
+ {a.message && <div>{pickLocalizedValue(a.message, locale)}</div>}
84
+ </li>
85
+ ))}
86
+ </ul>
87
+ </section>
88
+ )}
89
+
90
+ {approvals.length > 0 && (
91
+ <section>
92
+ <h2>Approvals</h2>
93
+ <ul>
94
+ {approvals.map((a) => (
95
+ <li>{a.degree}{a.body ? ` — ${a.body}` : ''}{a.date ? ` (${a.date})` : ''}</li>
96
+ ))}
97
+ </ul>
98
+ </section>
99
+ )}
100
+
101
+ {(decision.dates ?? []).length > 0 && (
102
+ <section>
103
+ <h2>Dates</h2>
104
+ <ul>
105
+ {(decision.dates ?? []).map((d) => (
106
+ <li>{d.type}: {d.date}</li>
107
+ ))}
108
+ </ul>
109
+ </section>
110
+ )}
111
+
112
+ {(decision.urls ?? []).length > 0 && (
113
+ <section>
114
+ <h2>Reference documents</h2>
115
+ <ul>
116
+ {(decision.urls ?? []).map((u) => (
117
+ <li><a href={u.ref}>{u.kind ?? 'document'}</a></li>
118
+ ))}
119
+ </ul>
120
+ </section>
121
+ )}
122
+ </DecisionLayout>
@@ -0,0 +1,67 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import BaseLayout from '../../layouts/BaseLayout.astro'
5
+ import DecisionList from '../../components/DecisionList.astro'
6
+
7
+ const items = payloads.decisionsList.items
8
+ const endpoint = `${cfg.site.basePath}data/decisions.json`.replace(/\/+/g, '/')
9
+ ---
10
+
11
+ <BaseLayout config={cfg} title={`Decisions — ${cfg.site.title}`}>
12
+ <h1>Decisions</h1>
13
+
14
+ <search-filter data-endpoint={endpoint} data-base-path="/decisions"></search-filter>
15
+ <noscript>
16
+ <DecisionList items={items} />
17
+ </noscript>
18
+ </BaseLayout>
19
+
20
+ <style>
21
+ :global(search-filter) {
22
+ display: block;
23
+ margin: 1rem 0;
24
+ }
25
+ :global(.edoxen-search-filter__form) {
26
+ display: flex;
27
+ flex-direction: column;
28
+ gap: 0.75rem;
29
+ }
30
+ :global(.edoxen-search-filter__facets) {
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ gap: 0.4rem;
34
+ }
35
+ :global(.edoxen-search-filter__facet) {
36
+ border: 1px solid var(--edoxen-color-border);
37
+ background: var(--edoxen-color-surface);
38
+ color: var(--edoxen-color-text);
39
+ padding: 0.2rem 0.6rem;
40
+ border-radius: var(--edoxen-radius-sm);
41
+ font-size: 0.8rem;
42
+ cursor: pointer;
43
+ }
44
+ :global(.edoxen-search-filter__facet[aria-pressed='true']) {
45
+ background: var(--edoxen-color-accent);
46
+ color: #fff;
47
+ border-color: var(--edoxen-color-accent);
48
+ }
49
+ :global(.edoxen-search-filter__results) {
50
+ list-style: none;
51
+ padding: 0;
52
+ margin: 0;
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 0.5rem;
56
+ }
57
+ :global(.edoxen-search-filter__result) {
58
+ padding: 0.6rem 0.85rem;
59
+ border: 1px solid var(--edoxen-color-border);
60
+ border-radius: var(--edoxen-radius-sm);
61
+ background: var(--edoxen-color-surface);
62
+ }
63
+ :global(.edoxen-search-filter__result a) {
64
+ font-weight: 500;
65
+ color: var(--edoxen-color-text);
66
+ }
67
+ </style>
@@ -0,0 +1,50 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import BaseLayout from '../layouts/BaseLayout.astro'
5
+
6
+ export function getStaticPaths() {
7
+ return cfg.locales
8
+ .filter((l) => (l.routePrefix ?? '') !== '')
9
+ .map((l) => ({ params: { lang: l.code } }))
10
+ }
11
+
12
+ const recentDecisions = payloads.decisionsList.items.slice(0, 5)
13
+ const recentMeetings = payloads.meetingsList.items.slice(0, 3)
14
+ const lang = Astro.params.lang
15
+ const prefix = lang && cfg.locales.find((l) => l.code === lang)?.routePrefix
16
+ const urlPrefix = prefix ?? ''
17
+ ---
18
+
19
+ <BaseLayout config={cfg}>
20
+ <section>
21
+ <h1>{cfg.site.title}</h1>
22
+ <p>{cfg.site.description}</p>
23
+ </section>
24
+
25
+ <section>
26
+ <h2>Latest decisions</h2>
27
+ <ul>
28
+ {recentDecisions.map((d) => (
29
+ <li>
30
+ <a href={`${urlPrefix}/decisions/${encodeURIComponent(d.urn)}`}>{d.title}</a>
31
+ {d.bodyType && <span class="edoxen-badge">{d.bodyType}</span>}
32
+ </li>
33
+ ))}
34
+ </ul>
35
+ </section>
36
+
37
+ {recentMeetings.length > 0 && (
38
+ <section>
39
+ <h2>Recent meetings</h2>
40
+ <ul>
41
+ {recentMeetings.map((m) => (
42
+ <li>
43
+ <a href={`${urlPrefix}/meetings/${encodeURIComponent(m.urn)}`}>{m.title}</a>
44
+ {m.startDate && <span> — {m.startDate}</span>}
45
+ </li>
46
+ ))}
47
+ </ul>
48
+ </section>
49
+ )}
50
+ </BaseLayout>
@@ -0,0 +1,110 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import MeetingLayout from '../../layouts/MeetingLayout.astro'
5
+ import UrnBar from '../../components/UrnBar.astro'
6
+ import BodyBadge from '../../components/BodyBadge.astro'
7
+ import AgendaTable from '../../components/AgendaTable.astro'
8
+ import MinutesSection from '../../components/MinutesSection.astro'
9
+ import { meetingJsonLd } from '../../../seo/index.js'
10
+ import { pickLocalizedValue } from '../../../i18n/index.js'
11
+
12
+ export function getStaticPaths() {
13
+ return Object.keys(payloads.meetingByUrn).map((urn) => ({
14
+ params: { urn },
15
+ props: { meeting: payloads.meetingByUrn[urn] },
16
+ }))
17
+ }
18
+
19
+ interface Props {
20
+ meeting: typeof payloads.meetingByUrn[string]
21
+ }
22
+ const { meeting } = Astro.props
23
+ const locale = cfg.site.locale
24
+ const title = pickLocalizedValue(meeting.title, locale, meeting.urn ?? '')
25
+ const officers = meeting.officers ?? []
26
+ const agenda = meeting.agenda
27
+ const minutes = meeting.minutes ?? []
28
+ const linkedDecisions = (meeting.decisions ?? []).map((id) =>
29
+ payloads.decisionsList.items.find((d) =>
30
+ d.identifier === `${id.prefix}-${id.number}`
31
+ )
32
+ ).filter((d): d is NonNullable<typeof d> => d !== undefined)
33
+ const jsonld = meetingJsonLd(meeting, { siteUrl: cfg.site.url, siteTitle: cfg.site.title, defaultLocale: locale })
34
+ ---
35
+
36
+ <MeetingLayout
37
+ config={cfg}
38
+ item={{
39
+ urn: meeting.urn ?? '',
40
+ identifier: (meeting.identifier[0]?.prefix ?? '') + '-' + (meeting.identifier[0]?.number ?? ''),
41
+ title,
42
+ startDate: meeting.date_range?.start,
43
+ endDate: meeting.date_range?.end,
44
+ bodyType: meeting.body_type,
45
+ city: meeting.city,
46
+ countryCode: meeting.country_code,
47
+ status: meeting.status,
48
+ }}
49
+ >
50
+ <script type="application/ld+json" set:html={JSON.stringify(jsonld)} is:inline />
51
+ <UrnBar urn={meeting.urn ?? ''} />
52
+
53
+ {meeting.body_type && <p><BodyBadge code={meeting.body_type} /></p>}
54
+
55
+ {meeting.city && (
56
+ <section>
57
+ <h2>Venue</h2>
58
+ <p>{meeting.city}{meeting.country_code ? `, ${meeting.country_code}` : ''}</p>
59
+ </section>
60
+ )}
61
+
62
+ {officers.length > 0 && (
63
+ <section>
64
+ <h2>Officers</h2>
65
+ <ul>
66
+ {officers.map((o) => (
67
+ <li>{o.role}{o.person?.name ? `: ${o.person.name}` : ''}</li>
68
+ ))}
69
+ </ul>
70
+ </section>
71
+ )}
72
+
73
+ {agenda && agenda.items && agenda.items.length > 0 && (
74
+ <section>
75
+ <h2>Agenda</h2>
76
+ <AgendaTable items={agenda.items} locale={locale} />
77
+ </section>
78
+ )}
79
+
80
+ {minutes.length > 0 && (
81
+ <section>
82
+ <h2>Minutes</h2>
83
+ {minutes.map((m) => (
84
+ <MinutesSection minutes={m} locale={locale} />
85
+ ))}
86
+ </section>
87
+ )}
88
+
89
+ {linkedDecisions.length > 0 && (
90
+ <section>
91
+ <h2>Adopted decisions</h2>
92
+ <ul>
93
+ {linkedDecisions.map((d) => (
94
+ <li><a href={`/decisions/${encodeURIComponent(d.urn)}`}>{d.title}</a></li>
95
+ ))}
96
+ </ul>
97
+ </section>
98
+ )}
99
+
100
+ {(meeting.source_urls ?? []).length > 0 && (
101
+ <section>
102
+ <h2>Source documents</h2>
103
+ <ul>
104
+ {(meeting.source_urls ?? []).map((s) => (
105
+ <li><a href={s.ref}>{s.format ?? 'document'}</a></li>
106
+ ))}
107
+ </ul>
108
+ </section>
109
+ )}
110
+ </MeetingLayout>
@@ -0,0 +1,20 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import BaseLayout from '../../layouts/BaseLayout.astro'
5
+ import MeetingList from '../../components/MeetingList.astro'
6
+ import DecadeTimeline from '../../components/DecadeTimeline.astro'
7
+
8
+ const items = payloads.meetingsList.items
9
+ const decades = payloads.meetingsList.facets.decades
10
+ ---
11
+
12
+ <BaseLayout config={cfg} title={`Meetings — ${cfg.site.title}`}>
13
+ <h1>Meetings</h1>
14
+
15
+ <decade-scroller>
16
+ <DecadeTimeline decades={decades} />
17
+ </decade-scroller>
18
+
19
+ <MeetingList items={items} />
20
+ </BaseLayout>
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util'
3
+ import { existsSync } from 'node:fs'
4
+ import { resolve, extname } from 'node:path'
5
+ import { pathToFileURL } from 'node:url'
6
+
7
+ import { EdoxenConfigSchema } from '../config/index.js'
8
+ import { loadAll, validateAll, lintProject, buildProjectFromLoaded } from '../data/index.js'
9
+ import { EdoxenBrowserError } from '../errors.js'
10
+
11
+ const HELP = `edoxen-browser — CLI for the @edoxen/browser package
12
+
13
+ Usage:
14
+ edoxen-browser <command> [options]
15
+
16
+ Commands:
17
+ validate Validate the data + config without building.
18
+ lint Run structural lint (duplicate URNs, broken relations).
19
+ check validate + lint in one shot.
20
+ config Print the resolved config as JSON.
21
+ build Run astro build (thin wrapper).
22
+ help Show this help.
23
+
24
+ Options:
25
+ --config <path> Path to edoxen.config.ts (default: ./edoxen.config.ts)
26
+ --cwd <path> Working directory (default: process.cwd())
27
+ --strict Treat warnings as errors (lint only).
28
+ -h, --help Show this help.
29
+ `
30
+
31
+ interface ParsedArgs {
32
+ command: string
33
+ configPath: string
34
+ cwd: string
35
+ strict: boolean
36
+ }
37
+
38
+ function parseCliArgs(argv: string[]): ParsedArgs {
39
+ const [command, ...rest] = argv.slice(2)
40
+ const { values } = parseArgs({
41
+ args: rest,
42
+ options: {
43
+ config: { type: 'string', default: './edoxen.config.ts' },
44
+ cwd: { type: 'string', default: process.cwd() },
45
+ strict: { type: 'boolean', default: false },
46
+ help: { type: 'boolean', short: 'h', default: false },
47
+ },
48
+ allowPositionals: true,
49
+ })
50
+ if (values.help || !command || command === 'help') {
51
+ process.stdout.write(HELP)
52
+ process.exit(0)
53
+ }
54
+ return {
55
+ command,
56
+ configPath: String(values.config),
57
+ cwd: String(values.cwd),
58
+ strict: Boolean(values.strict),
59
+ }
60
+ }
61
+
62
+ async function loadConfigFile(configPath: string, cwd: string): Promise<unknown> {
63
+ const absolute = resolve(cwd, configPath)
64
+ if (!existsSync(absolute)) {
65
+ throw new EdoxenBrowserError('config', `Config file not found: ${absolute}`, [])
66
+ }
67
+ const ext = extname(absolute)
68
+ if (ext === '.ts' || ext === '.tsx' || ext === '.mts') {
69
+ const { createJiti } = await import('jiti')
70
+ const jiti = createJiti(import.meta.url, { interopDefault: true })
71
+ const mod = (await jiti.import(absolute)) as { default?: unknown }
72
+ return mod.default
73
+ }
74
+ const url = pathToFileURL(absolute).href
75
+ const mod = (await import(url)) as { default?: unknown }
76
+ if (!mod.default) {
77
+ throw new EdoxenBrowserError('config', `Config file ${absolute} does not export a default config`, [])
78
+ }
79
+ return mod.default
80
+ }
81
+
82
+ async function loadValidatedConfig(args: ParsedArgs) {
83
+ const raw = await loadConfigFile(args.configPath, args.cwd)
84
+ const parseResult = EdoxenConfigSchema.safeParse(raw)
85
+ if (!parseResult.success) {
86
+ throw new EdoxenBrowserError(
87
+ 'config',
88
+ 'edoxen.config.ts failed schema validation',
89
+ parseResult.error.issues.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`),
90
+ )
91
+ }
92
+ const cfg = parseResult.data
93
+ return {
94
+ ...cfg,
95
+ data: {
96
+ decisions: resolve(args.cwd, cfg.data.decisions),
97
+ ...(cfg.data.meetings ? { meetings: resolve(args.cwd, cfg.data.meetings) } : {}),
98
+ ...(cfg.data.agendas ? { agendas: resolve(args.cwd, cfg.data.agendas) } : {}),
99
+ ...(cfg.data.minutes ? { minutes: resolve(args.cwd, cfg.data.minutes) } : {}),
100
+ ...(cfg.data.committee ? { committee: resolve(args.cwd, cfg.data.committee) } : {}),
101
+ },
102
+ }
103
+ }
104
+
105
+ async function runValidate(args: ParsedArgs): Promise<void> {
106
+ const cfg = await loadValidatedConfig(args)
107
+ const loaded = await loadAll(cfg.data)
108
+ if (!loaded.ok) {
109
+ throw new EdoxenBrowserError(
110
+ 'load',
111
+ 'Failed to load one or more data sources',
112
+ loaded.error.map((e) => `${e.source} (${e.path}): ${e.cause.message}`),
113
+ )
114
+ }
115
+ const report = await validateAll(loaded.value)
116
+ if (!report.valid) {
117
+ const details: string[] = []
118
+ if (report.decisions) details.push(...report.decisions.errors.map((e) => e.message))
119
+ if (report.meetings) details.push(...report.meetings.errors.map((e) => e.message))
120
+ throw new EdoxenBrowserError('validate', 'Schema validation failed', details)
121
+ }
122
+ process.stdout.write(`OK — schema validation passed for ${cfg.data.decisions}\n`)
123
+ }
124
+
125
+ async function runLint(args: ParsedArgs): Promise<void> {
126
+ const cfg = await loadValidatedConfig(args)
127
+ const loaded = await loadAll(cfg.data)
128
+ if (!loaded.ok) {
129
+ throw new EdoxenBrowserError(
130
+ 'load',
131
+ 'Failed to load data',
132
+ loaded.error.map((e) => `${e.source}: ${e.cause.message}`),
133
+ )
134
+ }
135
+ const project = buildProjectFromLoaded(loaded.value)
136
+ const report = lintProject(project)
137
+ if (report.findings.length === 0) {
138
+ process.stdout.write('OK — no lint findings\n')
139
+ return
140
+ }
141
+ for (const finding of report.findings) {
142
+ const prefix = finding.severity === 'error' ? 'ERROR' : 'WARN'
143
+ process.stdout.write(`${prefix} [${finding.code}] ${finding.message}\n`)
144
+ }
145
+ const hasErrors = args.strict
146
+ ? report.findings.length > 0
147
+ : report.findings.some((f) => f.severity === 'error')
148
+ if (hasErrors) {
149
+ throw new EdoxenBrowserError('lint', `Lint reported ${report.findings.length} finding(s)`)
150
+ }
151
+ }
152
+
153
+ async function runCheck(args: ParsedArgs): Promise<void> {
154
+ await runValidate(args)
155
+ await runLint(args)
156
+ }
157
+
158
+ async function runConfig(args: ParsedArgs): Promise<void> {
159
+ const cfg = await loadValidatedConfig(args)
160
+ process.stdout.write(`${JSON.stringify(cfg, null, 2)}\n`)
161
+ }
162
+
163
+ async function runBuild(args: ParsedArgs): Promise<void> {
164
+ await runValidate(args)
165
+ const { spawnSync } = await import('node:child_process')
166
+ const result = spawnSync('astro', ['build'], { stdio: 'inherit', cwd: args.cwd })
167
+ if (result.status !== 0) {
168
+ throw new EdoxenBrowserError('build', `astro build exited with status ${result.status ?? 'null'}`)
169
+ }
170
+ }
171
+
172
+ async function main(): Promise<void> {
173
+ const args = parseCliArgs(process.argv)
174
+ switch (args.command) {
175
+ case 'validate': return await runValidate(args)
176
+ case 'lint': return await runLint(args)
177
+ case 'check': return await runCheck(args)
178
+ case 'config': return await runConfig(args)
179
+ case 'build': return await runBuild(args)
180
+ default:
181
+ process.stderr.write(`Unknown command: ${args.command}\n\n${HELP}`)
182
+ process.exit(1)
183
+ }
184
+ }
185
+
186
+ main().catch((e: unknown) => {
187
+ if (e instanceof EdoxenBrowserError) {
188
+ process.stderr.write(`${e.toString()}\n`)
189
+ process.exit(1)
190
+ }
191
+ process.stderr.write(`Unexpected error: ${e instanceof Error ? e.stack : String(e)}\n`)
192
+ process.exit(1)
193
+ })
194
+
195
+ export const _cliExtAccept = extname
@@ -0,0 +1,33 @@
1
+ export {
2
+ EdoxenConfigSchema,
3
+ SiteSchema,
4
+ DataSchema,
5
+ OutputSchema,
6
+ BodySchema,
7
+ LocaleEntrySchema,
8
+ ThemeSchema,
9
+ NavItemSchema,
10
+ SocialItemSchema,
11
+ FeaturesSchema,
12
+ defineConfig,
13
+ type EdoxenConfig,
14
+ type EdoxenConfigInput,
15
+ type SiteConfig,
16
+ type DataConfig,
17
+ type OutputConfig,
18
+ type BodyEntry,
19
+ type LocaleEntry,
20
+ type ThemeConfig,
21
+ type NavItem,
22
+ type SocialItem,
23
+ type FeaturesConfig,
24
+ } from './schema.js'
25
+
26
+ export {
27
+ resolveDataPaths,
28
+ activeDataKeys,
29
+ type ResolvedDataPaths,
30
+ type DataPathKey,
31
+ } from './paths.js'
32
+
33
+ export { generateCssTokens } from './tokens.js'