@erclx/aitk 0.8.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 (359) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/claude/.claude-plugin/plugin.json +19 -0
  4. package/claude/skills/bash-script/SKILL.md +96 -0
  5. package/claude/skills/bash-script/references/patterns.md +349 -0
  6. package/claude/skills/ci-workflow/SKILL.md +52 -0
  7. package/claude/skills/ci-workflow/references/workflows.md +74 -0
  8. package/claude/skills/claude-address-review/SKILL.md +107 -0
  9. package/claude/skills/claude-autoship/SKILL.md +137 -0
  10. package/claude/skills/claude-design-extract/SKILL.md +69 -0
  11. package/claude/skills/claude-design-propose/SKILL.md +75 -0
  12. package/claude/skills/claude-diagram/SKILL.md +126 -0
  13. package/claude/skills/claude-docs/SKILL.md +204 -0
  14. package/claude/skills/claude-feature/REQUIREMENT.md +35 -0
  15. package/claude/skills/claude-feature/SKILL.md +148 -0
  16. package/claude/skills/claude-groundwork/SKILL.md +148 -0
  17. package/claude/skills/claude-groundwork/references/folder-format.md +107 -0
  18. package/claude/skills/claude-memory-capture/SKILL.md +80 -0
  19. package/claude/skills/claude-memory-review/REQUIREMENT.md +37 -0
  20. package/claude/skills/claude-memory-review/SKILL.md +206 -0
  21. package/claude/skills/claude-orchestrate/SKILL.md +125 -0
  22. package/claude/skills/claude-pr-review/SKILL.md +150 -0
  23. package/claude/skills/claude-review/SKILL.md +136 -0
  24. package/claude/skills/claude-roadmap/SKILL.md +61 -0
  25. package/claude/skills/claude-roadmap/references/roadmap.md +52 -0
  26. package/claude/skills/claude-screencast/SKILL.md +160 -0
  27. package/claude/skills/claude-seed-sync/SKILL.md +141 -0
  28. package/claude/skills/claude-slides-draft/SKILL.md +62 -0
  29. package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
  30. package/claude/skills/claude-standards-audit/SKILL.md +79 -0
  31. package/claude/skills/claude-standards-audit/references/branch.md +49 -0
  32. package/claude/skills/claude-standards-audit/references/pr.md +124 -0
  33. package/claude/skills/claude-tasks/SKILL.md +123 -0
  34. package/claude/skills/claude-ui-test/SKILL.md +75 -0
  35. package/claude/skills/claude-ux-audit/SKILL.md +78 -0
  36. package/claude/skills/claude-worktree/SKILL.md +79 -0
  37. package/claude/skills/cli-script/SKILL.md +47 -0
  38. package/claude/skills/cli-script/references/template.md +43 -0
  39. package/claude/skills/create-rule/SKILL.md +68 -0
  40. package/claude/skills/create-skill/REQUIREMENT.md +30 -0
  41. package/claude/skills/create-skill/SKILL.md +24 -0
  42. package/claude/skills/create-snippet/SKILL.md +28 -0
  43. package/claude/skills/create-snippet/references/snippets.md +57 -0
  44. package/claude/skills/create-standard/SKILL.md +31 -0
  45. package/claude/skills/create-standard/references/standard.md +52 -0
  46. package/claude/skills/docs-sync/SKILL.md +87 -0
  47. package/claude/skills/git-branch/REQUIREMENT.md +38 -0
  48. package/claude/skills/git-branch/SKILL.md +60 -0
  49. package/claude/skills/git-branch/references/branch.md +49 -0
  50. package/claude/skills/git-commit/REQUIREMENT.md +33 -0
  51. package/claude/skills/git-commit/SKILL.md +51 -0
  52. package/claude/skills/git-commit/references/commit.md +63 -0
  53. package/claude/skills/git-followup/REQUIREMENT.md +38 -0
  54. package/claude/skills/git-followup/SKILL.md +35 -0
  55. package/claude/skills/git-issue/REQUIREMENT.md +38 -0
  56. package/claude/skills/git-issue/SKILL.md +69 -0
  57. package/claude/skills/git-issue/references/issue.md +84 -0
  58. package/claude/skills/git-pr/REQUIREMENT.md +37 -0
  59. package/claude/skills/git-pr/SKILL.md +119 -0
  60. package/claude/skills/git-pr/references/branch.md +49 -0
  61. package/claude/skills/git-pr/references/pr.md +124 -0
  62. package/claude/skills/git-ship/REQUIREMENT.md +34 -0
  63. package/claude/skills/git-ship/SKILL.md +39 -0
  64. package/claude/skills/git-split/REQUIREMENT.md +38 -0
  65. package/claude/skills/git-split/SKILL.md +160 -0
  66. package/claude/skills/git-split/references/branch.md +49 -0
  67. package/claude/skills/git-split/references/pr.md +124 -0
  68. package/claude/skills/git-stage/REQUIREMENT.md +36 -0
  69. package/claude/skills/git-stage/SKILL.md +74 -0
  70. package/claude/skills/git-stage/references/commit.md +63 -0
  71. package/claude/skills/git-worktree/REQUIREMENT.md +37 -0
  72. package/claude/skills/git-worktree/SKILL.md +120 -0
  73. package/claude/skills/migration-claude-md/SKILL.md +74 -0
  74. package/claude/skills/migration-context/SKILL.md +93 -0
  75. package/claude/skills/migration-standards/SKILL.md +75 -0
  76. package/claude/skills/project-commands/SKILL.md +73 -0
  77. package/claude/skills/session-resume/SKILL.md +36 -0
  78. package/claude/skills/setup-gov/SKILL.md +71 -0
  79. package/claude/skills/setup-indexes/SKILL.md +148 -0
  80. package/claude/skills/setup-init/SKILL.md +114 -0
  81. package/claude/skills/setup-plugins/SKILL.md +81 -0
  82. package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
  83. package/claude/skills/setup-verify/SKILL.md +51 -0
  84. package/claude/skills/systematic-debugging/SKILL.md +70 -0
  85. package/claude/skills/toolkit-cli/SKILL.md +38 -0
  86. package/claude/skills/toolkit-feedback/SKILL.md +80 -0
  87. package/claude/skills/toolkit-operator/SKILL.md +45 -0
  88. package/claude/skills/toolkit-triage/SKILL.md +63 -0
  89. package/claude/skills/youtube-transcripts/SKILL.md +34 -0
  90. package/docs/agents.md +371 -0
  91. package/docs/ai-workflow.md +157 -0
  92. package/docs/index.md +20 -0
  93. package/docs/operating-model.md +106 -0
  94. package/docs/target-projects.md +183 -0
  95. package/docs/visual-design-workflow.md +136 -0
  96. package/docs/zshrc-aliases.md +65 -0
  97. package/governance/rules/claude/500-prose.md +12 -0
  98. package/governance/rules/claude/510-context.md +16 -0
  99. package/governance/rules/claude/520-wireframes.md +12 -0
  100. package/governance/rules/claude/530-requirements.md +11 -0
  101. package/governance/rules/claude/540-architecture.md +11 -0
  102. package/governance/rules/claude/550-design.md +11 -0
  103. package/governance/rules/claude/555-tasks.md +12 -0
  104. package/governance/rules/claude/560-diagrams.md +11 -0
  105. package/governance/rules/claude/570-skill.md +18 -0
  106. package/governance/rules/claude/580-readme.md +11 -0
  107. package/governance/rules/claude/590-rule-authoring.md +12 -0
  108. package/governance/rules/core/000-constitution.md +30 -0
  109. package/governance/rules/core/010-testing.md +35 -0
  110. package/governance/rules/core/020-concurrency.md +22 -0
  111. package/governance/rules/core/030-error-handling.md +31 -0
  112. package/governance/rules/core/040-performance.md +20 -0
  113. package/governance/rules/core/050-logging.md +20 -0
  114. package/governance/rules/core/060-naming.md +19 -0
  115. package/governance/rules/core/070-planning.md +13 -0
  116. package/governance/rules/core/080-config-comments.md +22 -0
  117. package/governance/rules/framework/200-react.md +51 -0
  118. package/governance/rules/framework/210-astro.md +41 -0
  119. package/governance/rules/framework/220-fastapi.md +43 -0
  120. package/governance/rules/framework/230-nextjs.md +48 -0
  121. package/governance/rules/framework/250-tailwind.md +32 -0
  122. package/governance/rules/framework/260-shadcn.md +34 -0
  123. package/governance/rules/lang/100-typescript.md +40 -0
  124. package/governance/rules/lang/110-python.md +42 -0
  125. package/governance/rules/lib/300-testing-ts.md +38 -0
  126. package/governance/rules/lib/310-zod.md +25 -0
  127. package/governance/rules/lib/320-tanstack-query.md +32 -0
  128. package/governance/rules/lib/330-testing-py.md +44 -0
  129. package/governance/rules/lib/340-pydantic.md +38 -0
  130. package/governance/rules/lib/350-security-web.md +31 -0
  131. package/governance/rules/ui/400-ui.md +34 -0
  132. package/governance/rules/ui/410-a11y.md +44 -0
  133. package/governance/rules/ui/420-forms.md +34 -0
  134. package/governance/rules/ui/430-ux-completeness.md +62 -0
  135. package/governance/stacks/astro.toml +2 -0
  136. package/governance/stacks/base.toml +2 -0
  137. package/governance/stacks/node.toml +2 -0
  138. package/governance/stacks/python-fastapi.toml +2 -0
  139. package/governance/stacks/python.toml +2 -0
  140. package/governance/stacks/react.toml +2 -0
  141. package/package.json +67 -0
  142. package/scripts/config.sh +11 -0
  143. package/scripts/core/bootstrap.sh +81 -0
  144. package/scripts/core/check-plugin-boundary.sh +45 -0
  145. package/scripts/core/check-skill-paths.sh +25 -0
  146. package/scripts/core/clean.sh +36 -0
  147. package/scripts/core/install-check.sh +93 -0
  148. package/scripts/core/regen-claude-copies.sh +26 -0
  149. package/scripts/core/regen-indexes.sh +10 -0
  150. package/scripts/core/regen-skill-references.sh +27 -0
  151. package/scripts/core/snapshot.sh +132 -0
  152. package/scripts/core/update.sh +35 -0
  153. package/scripts/core/verify.sh +201 -0
  154. package/scripts/docs/list.sh +129 -0
  155. package/scripts/gov/list.sh +234 -0
  156. package/scripts/lib/frontmatter.sh +30 -0
  157. package/scripts/lib/gov.sh +14 -0
  158. package/scripts/lib/sandbox-fixtures.sh +109 -0
  159. package/scripts/lib/sandbox-git.sh +73 -0
  160. package/scripts/lib/tooling.sh +24 -0
  161. package/scripts/lib/ui.sh +173 -0
  162. package/scripts/lib/worktree.sh +20 -0
  163. package/scripts/manage-sandbox.sh +453 -0
  164. package/scripts/snippets/create.sh +156 -0
  165. package/scripts/standards/list.sh +91 -0
  166. package/scripts/tooling/create.sh +109 -0
  167. package/scripts/tooling/ref.sh +141 -0
  168. package/scripts/tooling/verify.sh +179 -0
  169. package/snippets/claude/decision-memo.md +39 -0
  170. package/snippets/claude/feature-recap.md +19 -0
  171. package/snippets/claude/figma-steps.md +18 -0
  172. package/snippets/claude/orchestrator-resume.md +23 -0
  173. package/snippets/claude/orchestrator-sweep.md +10 -0
  174. package/snippets/compact-summary.md +5 -0
  175. package/snippets/create-snippet.md +7 -0
  176. package/snippets/decision-help.md +6 -0
  177. package/snippets/meta-prompt.md +14 -0
  178. package/snippets/research-prompt.md +7 -0
  179. package/snippets/session-notes.md +11 -0
  180. package/snippets/snippets.toml +6 -0
  181. package/snippets/step-by-step.md +10 -0
  182. package/snippets/web-research.md +21 -0
  183. package/src/claude/gitignore.ts +51 -0
  184. package/src/claude/seeds-list.ts +47 -0
  185. package/src/claude/seeds.ts +124 -0
  186. package/src/claude/settings.ts +151 -0
  187. package/src/cli-run.ts +43 -0
  188. package/src/cli.ts +111 -0
  189. package/src/commands/claude.ts +349 -0
  190. package/src/commands/design.ts +39 -0
  191. package/src/commands/docs.ts +59 -0
  192. package/src/commands/feedback-format.ts +23 -0
  193. package/src/commands/feedback.ts +103 -0
  194. package/src/commands/gov.ts +268 -0
  195. package/src/commands/indexes.ts +182 -0
  196. package/src/commands/init.ts +117 -0
  197. package/src/commands/pass-through.ts +39 -0
  198. package/src/commands/sandbox.ts +315 -0
  199. package/src/commands/slides.ts +106 -0
  200. package/src/commands/snippets.ts +236 -0
  201. package/src/commands/standards.ts +89 -0
  202. package/src/commands/sync.ts +211 -0
  203. package/src/commands/tasks.ts +185 -0
  204. package/src/commands/tooling.ts +432 -0
  205. package/src/commands/transcripts.ts +48 -0
  206. package/src/commands/wiki.ts +101 -0
  207. package/src/copy.ts +30 -0
  208. package/src/design/parse.ts +79 -0
  209. package/src/design/render.ts +170 -0
  210. package/src/docs/read.ts +59 -0
  211. package/src/exec.ts +17 -0
  212. package/src/frontmatter.ts +13 -0
  213. package/src/github-format.ts +13 -0
  214. package/src/github.ts +20 -0
  215. package/src/gov/adapter.ts +73 -0
  216. package/src/gov/install.ts +103 -0
  217. package/src/gov/payload.ts +39 -0
  218. package/src/gov/stacks.ts +124 -0
  219. package/src/indexes/frontmatter.ts +46 -0
  220. package/src/indexes/regen.ts +84 -0
  221. package/src/indexes/render.ts +181 -0
  222. package/src/indexes/walk.ts +111 -0
  223. package/src/init/flags.ts +66 -0
  224. package/src/init/plan.ts +128 -0
  225. package/src/init/run.ts +46 -0
  226. package/src/init/steps.ts +91 -0
  227. package/src/sandbox/coverage.ts +115 -0
  228. package/src/sandbox/expect.ts +412 -0
  229. package/src/slides/layouts.ts +391 -0
  230. package/src/slides/open.ts +18 -0
  231. package/src/slides/parse.ts +84 -0
  232. package/src/slides/render.ts +88 -0
  233. package/src/slides/styles.ts +44 -0
  234. package/src/snippets/adapter.ts +34 -0
  235. package/src/snippets/categories.ts +59 -0
  236. package/src/snippets/install.ts +125 -0
  237. package/src/snippets/list.ts +32 -0
  238. package/src/snippets/presets.ts +50 -0
  239. package/src/standards/adapter.ts +51 -0
  240. package/src/standards/index-refresh.ts +44 -0
  241. package/src/standards/install.ts +52 -0
  242. package/src/sync/check.ts +269 -0
  243. package/src/sync/engine.ts +416 -0
  244. package/src/sync/git.ts +204 -0
  245. package/src/sync/stamp.ts +202 -0
  246. package/src/sync/target.ts +79 -0
  247. package/src/sync/workflow.ts +201 -0
  248. package/src/target.ts +43 -0
  249. package/src/tasks/archive.ts +304 -0
  250. package/src/tooling/gitignore.ts +122 -0
  251. package/src/tooling/inject.ts +214 -0
  252. package/src/tooling/list.ts +39 -0
  253. package/src/tooling/manifest.ts +178 -0
  254. package/src/tooling/package.ts +166 -0
  255. package/src/tooling/scan.ts +186 -0
  256. package/src/transcripts/fetch.ts +156 -0
  257. package/src/transcripts/metadata.ts +54 -0
  258. package/src/transcripts/vtt.ts +114 -0
  259. package/src/ui.ts +187 -0
  260. package/src/wiki/init.ts +85 -0
  261. package/standards/architecture.md +41 -0
  262. package/standards/bundled/branch.md +49 -0
  263. package/standards/bundled/commit.md +63 -0
  264. package/standards/bundled/issue.md +84 -0
  265. package/standards/bundled/pr.md +124 -0
  266. package/standards/bundled/roadmap.md +52 -0
  267. package/standards/bundled/snippets.md +57 -0
  268. package/standards/bundled/standard.md +52 -0
  269. package/standards/context.md +132 -0
  270. package/standards/design.md +31 -0
  271. package/standards/diagrams.md +55 -0
  272. package/standards/index.md +21 -0
  273. package/standards/prose.md +122 -0
  274. package/standards/readme.md +163 -0
  275. package/standards/requirements.md +61 -0
  276. package/standards/rule.md +79 -0
  277. package/standards/skill.md +257 -0
  278. package/standards/tasks.md +140 -0
  279. package/standards/versioning.md +47 -0
  280. package/standards/wireframes.md +67 -0
  281. package/tooling/astro/configs/astro.config.mjs +17 -0
  282. package/tooling/astro/configs/eslint.config.js +79 -0
  283. package/tooling/astro/configs/playwright.config.ts +25 -0
  284. package/tooling/astro/configs/tsconfig.json +12 -0
  285. package/tooling/astro/configs/vitest.config.ts +22 -0
  286. package/tooling/astro/manifest.toml +32 -0
  287. package/tooling/astro/reference.md +60 -0
  288. package/tooling/base/configs/.editorconfig +5 -0
  289. package/tooling/base/configs/.github/pull_request_template.md +18 -0
  290. package/tooling/base/configs/.github/workflows/verify.yml +35 -0
  291. package/tooling/base/configs/.husky/commit-msg +1 -0
  292. package/tooling/base/configs/.husky/post-merge +61 -0
  293. package/tooling/base/configs/.husky/post-rewrite +21 -0
  294. package/tooling/base/configs/.husky/pre-commit +1 -0
  295. package/tooling/base/configs/.husky/pre-push +1 -0
  296. package/tooling/base/configs/.prettierrc +12 -0
  297. package/tooling/base/configs/.shellcheckrc +1 -0
  298. package/tooling/base/configs/.vscode/extensions.json +9 -0
  299. package/tooling/base/configs/.vscode/settings.json +3 -0
  300. package/tooling/base/configs/commitlint.config.js +11 -0
  301. package/tooling/base/configs/scripts/clean.sh +45 -0
  302. package/tooling/base/configs/scripts/update.sh +49 -0
  303. package/tooling/base/configs/scripts/verify.sh +64 -0
  304. package/tooling/base/manifest.toml +32 -0
  305. package/tooling/base/reference.md +114 -0
  306. package/tooling/base/seeds/.claude/context/ci.md +27 -0
  307. package/tooling/base/seeds/.claude/context/development.md +32 -0
  308. package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
  309. package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
  310. package/tooling/base/seeds/.lintstagedrc +8 -0
  311. package/tooling/base/seeds/.prettierignore +0 -0
  312. package/tooling/base/seeds/cspell.json +20 -0
  313. package/tooling/claude/manifest.toml +11 -0
  314. package/tooling/claude/reference.md +62 -0
  315. package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
  316. package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
  317. package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
  318. package/tooling/claude/seeds/.claude/context/index.md +8 -0
  319. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +43 -0
  320. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +45 -0
  321. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +61 -0
  322. package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +60 -0
  323. package/tooling/claude/seeds/.claude/settings.json +39 -0
  324. package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
  325. package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
  326. package/tooling/claude/seeds/CLAUDE.md +87 -0
  327. package/tooling/claude/user/settings.template.json +10 -0
  328. package/tooling/claude/user/statusline-command.sh +53 -0
  329. package/tooling/python/configs/.coveragerc +14 -0
  330. package/tooling/python/configs/.python-version +1 -0
  331. package/tooling/python/configs/mypy.ini +6 -0
  332. package/tooling/python/configs/pytest.ini +4 -0
  333. package/tooling/python/configs/ruff.toml +15 -0
  334. package/tooling/python/configs/scripts/verify.sh +77 -0
  335. package/tooling/python/manifest.toml +16 -0
  336. package/tooling/python/reference.md +66 -0
  337. package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
  338. package/tooling/python/seeds/tests/test_smoke.py +2 -0
  339. package/tooling/vite-react/configs/playwright.config.ts +25 -0
  340. package/tooling/vite-react/configs/tsconfig.json +35 -0
  341. package/tooling/vite-react/configs/vite.config.ts +14 -0
  342. package/tooling/vite-react/configs/vitest.config.ts +26 -0
  343. package/tooling/vite-react/manifest.toml +21 -0
  344. package/tooling/vite-react/reference.md +55 -0
  345. package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
  346. package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
  347. package/tooling/web/configs/.github/workflows/verify.yml +134 -0
  348. package/tooling/web/configs/.vscode/extensions.json +13 -0
  349. package/tooling/web/configs/.vscode/settings.json +10 -0
  350. package/tooling/web/configs/e2e/home.spec.ts +6 -0
  351. package/tooling/web/configs/e2e/screenshot.ts +41 -0
  352. package/tooling/web/configs/eslint.config.js +82 -0
  353. package/tooling/web/configs/scripts/screenshot.sh +28 -0
  354. package/tooling/web/configs/scripts/verify.sh +80 -0
  355. package/tooling/web/configs/src/test/setup.ts +8 -0
  356. package/tooling/web/manifest.toml +57 -0
  357. package/tooling/web/reference.md +95 -0
  358. package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
  359. package/tsconfig.json +14 -0
@@ -0,0 +1,170 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import type { DesignDoc, Row } from '@/design/parse'
4
+ import { parseDesignDoc } from '@/design/parse'
5
+
6
+ export interface RenderResult {
7
+ htmlPath: string
8
+ cssPath: string
9
+ }
10
+
11
+ export function renderDesignDoc(
12
+ sourcePath: string,
13
+ outDir: string,
14
+ ): RenderResult {
15
+ const doc = parseDesignDoc(sourcePath)
16
+ mkdirSync(outDir, { recursive: true })
17
+ const cssPath = join(outDir, 'design.css')
18
+ const htmlPath = join(outDir, 'index.html')
19
+ writeFileSync(cssPath, buildCss(doc))
20
+ writeFileSync(htmlPath, buildHtml(doc))
21
+ return { htmlPath, cssPath }
22
+ }
23
+
24
+ function slug(s: string): string {
25
+ return s
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, '-')
28
+ .replace(/(^-|-$)/g, '')
29
+ }
30
+
31
+ function buildCss(doc: DesignDoc): string {
32
+ const lines: string[] = [':root {']
33
+ for (const row of doc.color) {
34
+ if (row['Value']) {
35
+ lines.push(` --color-${slug(row['Role'])}: ${row['Value']};`)
36
+ }
37
+ }
38
+ for (const row of doc.spacing) {
39
+ if (row['Value']) {
40
+ lines.push(` --space-${slug(row['Step'])}: ${row['Value']};`)
41
+ }
42
+ }
43
+ for (const row of doc.typography) {
44
+ if (row['Size']) {
45
+ lines.push(` --type-${slug(row['Role'])}-size: ${row['Size']};`)
46
+ }
47
+ if (row['Line height']) {
48
+ lines.push(` --type-${slug(row['Role'])}-lh: ${row['Line height']};`)
49
+ }
50
+ }
51
+ for (const row of doc.borders) {
52
+ if (row['Radius']) {
53
+ lines.push(` --radius-${slug(row['Role'])}: ${row['Radius']};`)
54
+ }
55
+ }
56
+ lines.push('}')
57
+ return lines.join('\n') + '\n'
58
+ }
59
+
60
+ function escape(s: string): string {
61
+ return s
62
+ .replace(/&/g, '&')
63
+ .replace(/</g, '&lt;')
64
+ .replace(/>/g, '&gt;')
65
+ .replace(/"/g, '&quot;')
66
+ }
67
+
68
+ function buildHtml(doc: DesignDoc): string {
69
+ const sections = [
70
+ sectionPersonality(doc.personality),
71
+ sectionColor(doc.color),
72
+ sectionTypography(doc.typography),
73
+ sectionSpacing(doc.spacing),
74
+ sectionBorders(doc.borders),
75
+ sectionLine('Motion', doc.motion),
76
+ sectionLine('Iconography', doc.iconography),
77
+ ]
78
+ return `<!doctype html>
79
+ <html lang="en">
80
+ <head>
81
+ <meta charset="utf-8">
82
+ <title>Design tokens</title>
83
+ <link rel="stylesheet" href="design.css">
84
+ <style>
85
+ body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 960px; color: #222; }
86
+ h1 { margin-top: 0; }
87
+ h2 { margin-top: 2rem; border-bottom: 1px solid #ddd; padding-bottom: 0.25rem; }
88
+ table { border-collapse: collapse; width: 100%; margin-top: 0.5rem; }
89
+ th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid #eee; font-size: 14px; }
90
+ th { background: #f7f7f7; font-weight: 600; }
91
+ .swatch { display: inline-block; width: 1.5rem; height: 1.5rem; border-radius: 4px; border: 1px solid #ddd; vertical-align: middle; margin-right: 0.5rem; }
92
+ .bar { display: inline-block; height: 1rem; background: #888; border-radius: 2px; vertical-align: middle; }
93
+ .note { color: #666; font-size: 13px; margin-top: 0.5rem; }
94
+ .empty { color: #999; font-style: italic; }
95
+ </style>
96
+ </head>
97
+ <body>
98
+ <h1>Design tokens</h1>
99
+ <p class="note">Generated from <code>.claude/DESIGN.md</code> by <code>aitk design render</code>. Token preview only, not a screen mock.</p>
100
+ ${sections.join('\n')}
101
+ </body>
102
+ </html>
103
+ `
104
+ }
105
+
106
+ function sectionPersonality(text: string): string {
107
+ if (!text) return ''
108
+ return `<h2>Personality</h2>\n<p>${escape(text)}</p>`
109
+ }
110
+
111
+ function sectionColor(rows: Row[]): string {
112
+ if (!rows.length) return ''
113
+ const body = rows
114
+ .map((r) => {
115
+ const swatch = r['Value']
116
+ ? `<span class="swatch" style="background:${escape(r['Value'])}"></span>`
117
+ : '<span class="swatch"></span>'
118
+ const value = r['Value'] || '<span class="empty">unset</span>'
119
+ return `<tr><td>${swatch}${escape(r['Role'] ?? '')}</td><td>${escape(r['Intent'] ?? '')}</td><td><code>${value}</code></td></tr>`
120
+ })
121
+ .join('\n')
122
+ return `<h2>Color</h2>\n<table><thead><tr><th>Role</th><th>Intent</th><th>Value</th></tr></thead><tbody>${body}</tbody></table>`
123
+ }
124
+
125
+ function sectionTypography(rows: Row[]): string {
126
+ if (!rows.length) return ''
127
+ const body = rows
128
+ .map((r) => {
129
+ const family = r['Family'] || 'system-ui'
130
+ const weight = r['Weight'] || '400'
131
+ const size = r['Size'] || '16px'
132
+ const lh = r['Line height'] || '1.4'
133
+ const sample = `<span style="font-family:${escape(family)};font-weight:${escape(weight)};font-size:${escape(size)};line-height:${escape(lh)}">The quick brown fox</span>`
134
+ return `<tr><td>${escape(r['Role'] ?? '')}</td><td>${escape(family)}</td><td>${escape(weight)}</td><td>${escape(size)}</td><td>${escape(lh)}</td><td>${sample}</td></tr>`
135
+ })
136
+ .join('\n')
137
+ return `<h2>Typography</h2>\n<table><thead><tr><th>Role</th><th>Family</th><th>Weight</th><th>Size</th><th>Line height</th><th>Sample</th></tr></thead><tbody>${body}</tbody></table>`
138
+ }
139
+
140
+ function sectionSpacing(rows: Row[]): string {
141
+ if (!rows.length) return ''
142
+ const body = rows
143
+ .map((r) => {
144
+ const value = r['Value'] || ''
145
+ const bar = value
146
+ ? `<span class="bar" style="width:${escape(value)}"></span>`
147
+ : '<span class="empty">unset</span>'
148
+ return `<tr><td>${escape(r['Step'] ?? '')}</td><td>${escape(r['Multiplier'] ?? '')}</td><td><code>${escape(value || 'unset')}</code></td><td>${bar}</td></tr>`
149
+ })
150
+ .join('\n')
151
+ return `<h2>Spacing</h2>\n<table><thead><tr><th>Step</th><th>Multiplier</th><th>Value</th><th>Sample</th></tr></thead><tbody>${body}</tbody></table>`
152
+ }
153
+
154
+ function sectionBorders(rows: Row[]): string {
155
+ if (!rows.length) return ''
156
+ const body = rows
157
+ .map((r) => {
158
+ const radius = r['Radius'] || '0'
159
+ const width = r['Width'] || '1px'
160
+ const sample = `<span style="display:inline-block;width:2rem;height:1.5rem;background:#eee;border:${escape(width)} solid #888;border-radius:${escape(radius)};vertical-align:middle"></span>`
161
+ return `<tr><td>${escape(r['Role'] ?? '')}</td><td><code>${escape(radius)}</code></td><td><code>${escape(width)}</code></td><td>${escape(r['When used'] ?? '')}</td><td>${sample}</td></tr>`
162
+ })
163
+ .join('\n')
164
+ return `<h2>Borders</h2>\n<table><thead><tr><th>Role</th><th>Radius</th><th>Width</th><th>When used</th><th>Sample</th></tr></thead><tbody>${body}</tbody></table>`
165
+ }
166
+
167
+ function sectionLine(title: string, text: string): string {
168
+ if (!text) return ''
169
+ return `<h2>${title}</h2>\n<p>${escape(text)}</p>`
170
+ }
@@ -0,0 +1,59 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { basename, join } from 'node:path'
3
+ import { stripFrontmatter } from '@/frontmatter'
4
+
5
+ const INDEX_TOPIC = 'index'
6
+
7
+ /**
8
+ * The two roots a topic resolves against, in precedence order. `docs/` holds
9
+ * consumer-facing reference and `.claude/context/` holds per-domain internal
10
+ * narrative, so a name present in both resolves to the consumer-facing copy.
11
+ */
12
+ const ROOTS: readonly string[] = ['docs', join('.claude', 'context')]
13
+
14
+ export interface ResolvedTopic {
15
+ readonly path: string
16
+ readonly rel: string
17
+ }
18
+
19
+ export function resolveTopic(
20
+ root: string,
21
+ topic: string,
22
+ ): ResolvedTopic | undefined {
23
+ for (const dir of ROOTS) {
24
+ const rel = join(dir, `${topic}.md`)
25
+ const path = join(root, rel)
26
+ if (existsSync(path)) return { path, rel }
27
+ }
28
+
29
+ return undefined
30
+ }
31
+
32
+ /**
33
+ * Names every topic a `get` could resolve, listed per root in the order the
34
+ * roots are searched. Shown when a topic misses, so it doubles as the answer to
35
+ * what the caller should have typed.
36
+ */
37
+ export function listTopics(root: string): string[] {
38
+ const topics: string[] = []
39
+
40
+ for (const dir of ROOTS) {
41
+ const cwd = join(root, dir)
42
+ if (!existsSync(cwd)) continue
43
+
44
+ const names = [
45
+ ...new Bun.Glob('*.md').scanSync({ cwd, onlyFiles: true, dot: true }),
46
+ ]
47
+ .map((name) => basename(name, '.md'))
48
+ .filter((name) => name !== INDEX_TOPIC)
49
+ .sort()
50
+
51
+ topics.push(...names)
52
+ }
53
+
54
+ return topics
55
+ }
56
+
57
+ export function readTopic(topic: ResolvedTopic): string {
58
+ return stripFrontmatter(readFileSync(topic.path, 'utf8'))
59
+ }
package/src/exec.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { join, resolve } from 'node:path'
2
+ import { execa } from 'execa'
3
+
4
+ export const PROJECT_ROOT = resolve(import.meta.dir, '..')
5
+
6
+ export async function execScript(
7
+ script: string,
8
+ args: string[],
9
+ ): Promise<void> {
10
+ const scriptPath = join(PROJECT_ROOT, 'scripts', script)
11
+ const result = await execa(scriptPath, args, {
12
+ stdio: 'inherit',
13
+ env: { ...process.env, PROJECT_ROOT },
14
+ reject: false,
15
+ })
16
+ process.exit(result.exitCode ?? 1)
17
+ }
@@ -0,0 +1,13 @@
1
+ const FRONTMATTER = /^---\n[\s\S]*?\n---\n?/
2
+
3
+ /**
4
+ * Drops a leading `---` block. Only a block starting on the first line counts,
5
+ * so a horizontal rule further down the document is left in place.
6
+ *
7
+ * The bash this replaced treated the first `---` on any line as an opening
8
+ * fence, which silently swallowed everything between two horizontal rules in a
9
+ * body. Anchoring to the first line is the correct reading.
10
+ */
11
+ export function stripFrontmatter(source: string): string {
12
+ return source.replace(FRONTMATTER, '')
13
+ }
@@ -0,0 +1,13 @@
1
+ export interface CreateIssueOptions {
2
+ title: string
3
+ body: string
4
+ labels?: string[]
5
+ }
6
+
7
+ export function buildIssueArgs(opts: CreateIssueOptions): string[] {
8
+ const args = ['issue', 'create', '--title', opts.title, '--body', opts.body]
9
+ for (const label of opts.labels ?? []) {
10
+ args.push('--label', label)
11
+ }
12
+ return args
13
+ }
package/src/github.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { execa } from 'execa'
2
+ import { PROJECT_ROOT } from '@/exec'
3
+ import { buildIssueArgs, type CreateIssueOptions } from '@/github-format'
4
+
5
+ const GH_TIMEOUT_MS = 30_000
6
+
7
+ export async function createGithubIssue(
8
+ opts: CreateIssueOptions,
9
+ ): Promise<string | null> {
10
+ if (Bun.which('gh') === null) return null
11
+ try {
12
+ const result = await execa('gh', buildIssueArgs(opts), {
13
+ cwd: PROJECT_ROOT,
14
+ timeout: GH_TIMEOUT_MS,
15
+ })
16
+ return result.stdout.trim() || null
17
+ } catch {
18
+ return null
19
+ }
20
+ }
@@ -0,0 +1,73 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { basename, join, relative, resolve } from 'node:path'
3
+ import type { InstalledFile, RetiredSurface, SyncAdapter } from '@/sync/engine'
4
+
5
+ const RETIRED_GOV_FILE = join('.claude', 'GOV.md')
6
+
7
+ export function rulesSourceDir(root: string): string {
8
+ return join(root, 'governance', 'rules')
9
+ }
10
+
11
+ /**
12
+ * Maps a rule name to its source file. The bash used
13
+ * `find governance/rules -name "<rule>.md" | head -n 1` once per installed
14
+ * rule, which walked the whole tree per file and returned whichever match
15
+ * the filesystem happened to yield first. Indexing once makes the walk single
16
+ * pass and the winner deterministic when a name appears in two subdirectories.
17
+ */
18
+ export function indexSourceRules(root: string): Map<string, string> {
19
+ const dir = rulesSourceDir(root)
20
+ const index = new Map<string, string>()
21
+ if (!existsSync(dir)) return index
22
+
23
+ const paths = [
24
+ ...new Bun.Glob('**/*.md').scanSync({
25
+ cwd: dir,
26
+ onlyFiles: true,
27
+ dot: true,
28
+ }),
29
+ ].sort()
30
+
31
+ for (const path of paths) {
32
+ const name = basename(path, '.md')
33
+ if (!index.has(name)) index.set(name, resolve(dir, path))
34
+ }
35
+
36
+ return index
37
+ }
38
+
39
+ /**
40
+ * Matches installed rules to sources by rule name rather than by relative
41
+ * path, so a rule that moved between subdirectories in the toolkit still
42
+ * syncs into the subdirectory the target already uses.
43
+ */
44
+ export function createGovAdapter(root: string): SyncAdapter {
45
+ const index = indexSourceRules(root)
46
+
47
+ return {
48
+ banner: 'aitk gov sync',
49
+ label: 'rules',
50
+ missingMessage:
51
+ "No governance surfaces found in target. Run 'aitk gov install' first.",
52
+ unit: 'changes',
53
+ installedRoot: (target: string) => join(target, '.claude', 'rules'),
54
+ locateSource: (file: InstalledFile) =>
55
+ index.get(basename(file.path, '.md')),
56
+ collectRetired: (target: string) => collectRetiredGov(target),
57
+ stamp: { domain: 'governance', toolkitRoot: root },
58
+ }
59
+ }
60
+
61
+ function collectRetiredGov(target: string): RetiredSurface[] {
62
+ const path = join(target, RETIRED_GOV_FILE)
63
+ if (!existsSync(path)) return []
64
+
65
+ const rel = relative(target, path)
66
+ return [
67
+ {
68
+ path,
69
+ rel,
70
+ notice: `${rel} (retired surface, scheduled for removal)`,
71
+ },
72
+ ]
73
+ }
@@ -0,0 +1,103 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { dirname, join, relative } from 'node:path'
3
+ import { copyPreservingMode } from '@/copy'
4
+
5
+ export interface RuleSource {
6
+ readonly rule: string
7
+ readonly src: string
8
+ readonly subdir: string
9
+ }
10
+
11
+ export interface RuleLookup {
12
+ readonly found: readonly RuleSource[]
13
+ readonly missing: readonly string[]
14
+ }
15
+
16
+ export function rulesSourceDir(root: string): string {
17
+ return join(root, 'governance', 'rules')
18
+ }
19
+
20
+ export function installedRulesDir(target: string): string {
21
+ return join(target, '.claude', 'rules')
22
+ }
23
+
24
+ /**
25
+ * Mirrors `rule_subdir` in `scripts/lib/gov.sh`, which stays in bash for the
26
+ * sandbox loops. A rule sitting directly under `governance/rules/` has no
27
+ * subdirectory and installs flat.
28
+ */
29
+ export function ruleSubdir(src: string, rulesRoot: string): string {
30
+ const subdir = dirname(relative(rulesRoot, src))
31
+ return subdir === '.' ? '' : subdir
32
+ }
33
+
34
+ /**
35
+ * Finds each rule's source file by name across the `governance/rules/`
36
+ * subfolders. A rule with no source is reported rather than dropped, matching
37
+ * the `(source not found, skipping)` warning the bash printed.
38
+ */
39
+ export function lookupRules(
40
+ root: string,
41
+ rules: readonly string[],
42
+ ): RuleLookup {
43
+ const rulesRoot = rulesSourceDir(root)
44
+ const byName = new Map<string, string>()
45
+
46
+ const relPaths = existsSync(rulesRoot)
47
+ ? [
48
+ ...new Bun.Glob('**/*.md').scanSync({
49
+ cwd: rulesRoot,
50
+ onlyFiles: true,
51
+ }),
52
+ ].sort()
53
+ : []
54
+
55
+ for (const rel of relPaths) {
56
+ const name = rel.slice(rel.lastIndexOf('/') + 1, -'.md'.length)
57
+ if (!byName.has(name)) byName.set(name, join(rulesRoot, rel))
58
+ }
59
+
60
+ const found: RuleSource[] = []
61
+ const missing: string[] = []
62
+
63
+ for (const rule of rules) {
64
+ const src = byName.get(rule)
65
+ if (src === undefined) {
66
+ missing.push(rule)
67
+ continue
68
+ }
69
+ found.push({ rule, src, subdir: ruleSubdir(src, rulesRoot) })
70
+ }
71
+
72
+ return { found, missing }
73
+ }
74
+
75
+ /**
76
+ * Copies each rule into the subdirectory it was authored in, so the installed
77
+ * tree keeps the band structure `governance/rules/` carries. Returns the
78
+ * target-relative paths the timeline prints.
79
+ */
80
+ export async function installRules(
81
+ sources: readonly RuleSource[],
82
+ target: string,
83
+ ): Promise<string[]> {
84
+ const rulesDir = installedRulesDir(target)
85
+ const installed: string[] = []
86
+
87
+ for (const source of sources) {
88
+ const dest = join(rulesDir, source.subdir, `${source.rule}.md`)
89
+ await copyPreservingMode(source.src, dest)
90
+ installed.push(relative(target, dest))
91
+ }
92
+
93
+ return installed
94
+ }
95
+
96
+ /**
97
+ * The bash checked `<target>/standards`, but `aitk standards install` writes
98
+ * `.claude/standards/`, which is also the path the rules reference and the
99
+ * path the warning names. The guard therefore fired on every modern install.
100
+ */
101
+ export function hasStandards(target: string): boolean {
102
+ return existsSync(join(target, '.claude', 'standards'))
103
+ }
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { basename, resolve } from 'node:path'
3
+ import { stripFrontmatter } from '@/frontmatter'
4
+
5
+ /**
6
+ * Lists installed rules in the order the payload concatenates them. Sorting by
7
+ * path keeps the numeric filename prefixes ordered inside each subdirectory,
8
+ * which is how the rule bands are meant to read.
9
+ */
10
+ export function listRuleFiles(rulesDir: string): string[] {
11
+ return [
12
+ ...new Bun.Glob('**/*.md').scanSync({
13
+ cwd: rulesDir,
14
+ onlyFiles: true,
15
+ dot: true,
16
+ }),
17
+ ]
18
+ .sort()
19
+ .map((path) => resolve(rulesDir, path))
20
+ }
21
+
22
+ /**
23
+ * Wraps each rule in a named tag so a model reading the payload can tell the
24
+ * rules apart without the file boundaries. Bodies are trimmed of blank lines
25
+ * at both ends and separated by exactly one blank line.
26
+ */
27
+ export function buildRulesPayload(files: readonly string[]): string {
28
+ const blocks = files.map((file) => {
29
+ const name = basename(file, '.md')
30
+ const body = trimBlankLines(stripFrontmatter(readFileSync(file, 'utf8')))
31
+ return `<rule name="${name}">\n${body}\n</rule>`
32
+ })
33
+
34
+ return `${blocks.join('\n\n')}\n`
35
+ }
36
+
37
+ function trimBlankLines(source: string): string {
38
+ return source.replace(/^(?:[ \t]*\n)+/, '').replace(/(?:\n[ \t]*)+$/, '')
39
+ }
@@ -0,0 +1,124 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export interface GovStack {
5
+ readonly name: string
6
+ readonly parent?: string
7
+ readonly rules: readonly string[]
8
+ }
9
+
10
+ export type RuleResolution =
11
+ | { readonly ok: true; readonly rules: string[] }
12
+ | { readonly ok: false; readonly missingStack: string }
13
+
14
+ function stacksDir(root: string): string {
15
+ return join(root, 'governance', 'stacks')
16
+ }
17
+
18
+ export function govStackPath(root: string, stack: string): string {
19
+ return join(stacksDir(root), `${stack}.toml`)
20
+ }
21
+
22
+ export function govStackExists(root: string, stack: string): boolean {
23
+ return existsSync(govStackPath(root, stack))
24
+ }
25
+
26
+ /**
27
+ * Lists stack names the way `find -name "*.toml" -exec basename {} .toml` did.
28
+ */
29
+ export function listGovStacks(root: string): string[] {
30
+ const dir = stacksDir(root)
31
+ if (!existsSync(dir)) return []
32
+
33
+ return [...new Bun.Glob('*.toml').scanSync({ cwd: dir, onlyFiles: true })]
34
+ .map((entry) => entry.slice(0, -'.toml'.length))
35
+ .sort()
36
+ }
37
+
38
+ /**
39
+ * Reads one stack file. `Bun.TOML.parse` replaces a `BASH_REMATCH` loop that
40
+ * consumed each rules line with `sed` as it walked, which is the hand-rolled
41
+ * parser shape the tooling manifest already moved off.
42
+ */
43
+ export function loadGovStack(
44
+ root: string,
45
+ stack: string,
46
+ ): GovStack | undefined {
47
+ const path = govStackPath(root, stack)
48
+ if (!existsSync(path)) return undefined
49
+
50
+ const parsed = Bun.TOML.parse(readFileSync(path, 'utf8')) as Record<
51
+ string,
52
+ unknown
53
+ >
54
+ const parent = typeof parsed.extends === 'string' ? parsed.extends : ''
55
+
56
+ return {
57
+ name: stack,
58
+ parent: parent === '' ? undefined : parent,
59
+ rules: Array.isArray(parsed.rules)
60
+ ? parsed.rules.filter(
61
+ (rule): rule is string => typeof rule === 'string' && rule !== '',
62
+ )
63
+ : [],
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Walks `extends` ancestors first, then the stack's own rules, deduped by
69
+ * first appearance. Tooling's `resolveChain` returns full manifests nearest
70
+ * first and carries `skipStack` truncation, so the two walks stay separate
71
+ * rather than fitting one shape to both.
72
+ */
73
+ export function resolveRules(root: string, stack: string): RuleResolution {
74
+ const rules: string[] = []
75
+ const seen = new Set<string>()
76
+ const visited = new Set<string>()
77
+
78
+ const walk = (current: string): string | undefined => {
79
+ if (visited.has(current)) return undefined
80
+ visited.add(current)
81
+
82
+ const loaded = loadGovStack(root, current)
83
+ if (!loaded) return current
84
+
85
+ if (loaded.parent !== undefined) {
86
+ const missing = walk(loaded.parent)
87
+ if (missing !== undefined) return missing
88
+ }
89
+
90
+ for (const rule of loaded.rules) {
91
+ if (seen.has(rule)) continue
92
+ seen.add(rule)
93
+ rules.push(rule)
94
+ }
95
+
96
+ return undefined
97
+ }
98
+
99
+ const missingStack = walk(stack)
100
+ if (missingStack !== undefined) return { ok: false, missingStack }
101
+
102
+ return { ok: true, rules }
103
+ }
104
+
105
+ /**
106
+ * Layers `--add` names on top of a resolved stack. The bash trimmed a single
107
+ * leading and trailing space per entry; trimming fully is the same result for
108
+ * every input that parsed before.
109
+ */
110
+ export function mergeExtraRules(
111
+ rules: readonly string[],
112
+ add: string,
113
+ ): string[] {
114
+ const merged = [...rules]
115
+
116
+ for (const raw of add.split(',')) {
117
+ const extra = raw.trim()
118
+ if (extra === '') continue
119
+ if (merged.includes(extra)) continue
120
+ merged.push(extra)
121
+ }
122
+
123
+ return merged
124
+ }
@@ -0,0 +1,46 @@
1
+ const FRONTMATTER = /^---\n([\s\S]*?)\n---/
2
+
3
+ export interface Frontmatter {
4
+ readonly raw: string
5
+ readonly fields: Readonly<Record<string, unknown>>
6
+ }
7
+
8
+ /**
9
+ * Splits a leading `---` block off a markdown source.
10
+ *
11
+ * `raw` keeps the delimiters so an index can be rewritten with its own
12
+ * frontmatter untouched, including comments and key order that a parse and
13
+ * re-emit would lose.
14
+ */
15
+ export function parseFrontmatter(source: string): Frontmatter | undefined {
16
+ const match = source.match(FRONTMATTER)
17
+ if (!match) return undefined
18
+
19
+ const parsed = Bun.YAML.parse(match[1])
20
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
21
+ return undefined
22
+ }
23
+
24
+ return {
25
+ raw: match[0],
26
+ fields: parsed as Record<string, unknown>,
27
+ }
28
+ }
29
+
30
+ export function readField(
31
+ frontmatter: Frontmatter | undefined,
32
+ key: string,
33
+ ): string | undefined {
34
+ const value = frontmatter?.fields[key]
35
+ if (typeof value === 'string') return value
36
+ if (typeof value === 'number' || typeof value === 'boolean') {
37
+ return String(value)
38
+ }
39
+ return undefined
40
+ }
41
+
42
+ export async function readFrontmatter(
43
+ path: string,
44
+ ): Promise<Frontmatter | undefined> {
45
+ return parseFrontmatter(await Bun.file(path).text())
46
+ }