kitfly 0.1.2 → 0.2.1

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 (209) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +63 -16
  3. package/VERSION +1 -1
  4. package/dist/_raw/content/deployment/preflight.md +134 -0
  5. package/dist/_raw/content/deployment/recipes/aws-s3.md +128 -0
  6. package/dist/_raw/content/deployment/recipes/cloudflare-pages.md +73 -0
  7. package/dist/_raw/content/deployment/recipes/cloudflare-r2.md +156 -0
  8. package/dist/_raw/content/deployment/recipes/fly-io.md +57 -0
  9. package/dist/_raw/content/deployment/recipes/github-pages.md +112 -0
  10. package/dist/_raw/content/deployment/recipes/netlify.md +99 -0
  11. package/dist/_raw/content/deployment/recipes/vercel.md +88 -0
  12. package/dist/_raw/content/deployment/secrets-and-env-vars.md +75 -0
  13. package/dist/_raw/content/deployment.md +128 -0
  14. package/dist/_raw/content/guide/approaches.md +182 -0
  15. package/dist/_raw/content/guide/features.md +121 -0
  16. package/dist/_raw/content/guide/getting-started.md +112 -0
  17. package/dist/_raw/content/guide/kitfly-overview.md +209 -0
  18. package/dist/_raw/content/reference/configuration.md +259 -0
  19. package/dist/_raw/content/reference/design-catalog.md +167 -0
  20. package/dist/_raw/content/reference/environment-variables.md +66 -0
  21. package/dist/_raw/content/reference/glossary.md +92 -0
  22. package/dist/_raw/content/reference/key-concepts.md +118 -0
  23. package/dist/_raw/content/reference/plugins.md +220 -0
  24. package/dist/_raw/content/reference/slides-authoring-guidelines.md +129 -0
  25. package/dist/_raw/content/reference/structure.md +166 -0
  26. package/dist/_raw/content/reference.md +20 -0
  27. package/dist/_raw/content/templates/crucible.md +192 -0
  28. package/dist/_raw/content/templates/handbook.md +83 -0
  29. package/dist/_raw/content/templates/minimal.md +138 -0
  30. package/dist/_raw/content/templates/overview.md +187 -0
  31. package/dist/_raw/content/templates/pipeline.md +151 -0
  32. package/dist/_raw/content/templates/productbook.md +187 -0
  33. package/dist/_raw/content/templates/runbook.md +193 -0
  34. package/dist/_raw/content/templates/servicebook.md +163 -0
  35. package/dist/_raw/docs/decisions/ADR-0001-minimalist-site-code.md +118 -0
  36. package/dist/_raw/docs/decisions/ADR-0002-ai-accessibility.md +153 -0
  37. package/dist/_raw/docs/decisions/ADR-0003-single-file-bundle.md +93 -0
  38. package/dist/_raw/docs/decisions/ADR-0004-bun-runtime.md +98 -0
  39. package/dist/_raw/docs/decisions/ADR-0005-plugin-contract-and-distribution.md +110 -0
  40. package/dist/_raw/docs/decisions/DDR-0001-viewport-locked-layout.md +111 -0
  41. package/dist/_raw/docs/decisions/DDR-0002-theme-system.md +131 -0
  42. package/dist/_raw/docs/decisions/DDR-0003-bounded-logo-slot.md +106 -0
  43. package/dist/_raw/docs/decisions/DDR-0004-slides-rendering-model.md +113 -0
  44. package/dist/_raw/docs/decisions/DDR-0005-deterministic-layout-boundary.md +107 -0
  45. package/dist/_raw/docs/userguide/cli/build.md +85 -0
  46. package/dist/_raw/docs/userguide/cli/bundle.md +81 -0
  47. package/dist/_raw/docs/userguide/cli/dev.md +92 -0
  48. package/dist/_raw/docs/userguide/cli/init.md +116 -0
  49. package/dist/_raw/docs/userguide/cli/servers.md +69 -0
  50. package/dist/_raw/docs/userguide/cli/stop.md +76 -0
  51. package/dist/_raw/docs/userguide/cli/update.md +78 -0
  52. package/dist/_raw/docs/userguide/cli/version.md +65 -0
  53. package/dist/_raw/docs/userguide/cli.md +34 -0
  54. package/dist/_raw/docs/userguide/sharing.md +94 -0
  55. package/dist/_raw/schemas/plugin-schemas-notes.md +71 -0
  56. package/dist/_raw/schemas.md +42 -0
  57. package/dist/assets/brand/kitfly-favicon-32.png +0 -0
  58. package/dist/assets/brand/kitfly-icon-64.png +0 -0
  59. package/dist/assets/brand/kitfly-logo-128.png +0 -0
  60. package/dist/assets/brand/kitfly-logo-512.png +0 -0
  61. package/dist/assets/brand/kitfly-logo.svg +12132 -0
  62. package/dist/assets/brand/kitfly-neon-128.png +0 -0
  63. package/dist/assets/brand/kitfly-neon-192.png +0 -0
  64. package/dist/assets/brand/kitfly-neon-256.png +0 -0
  65. package/dist/assets/brand/kitfly-neon.png +0 -0
  66. package/dist/assets/brand/palette.md +75 -0
  67. package/dist/content/deployment/index.html +11 -0
  68. package/dist/content/deployment/preflight.html +418 -0
  69. package/dist/content/deployment/recipes/aws-s3.html +421 -0
  70. package/dist/content/deployment/recipes/cloudflare-pages.html +372 -0
  71. package/dist/content/deployment/recipes/cloudflare-r2.html +443 -0
  72. package/dist/content/deployment/recipes/fly-io.html +356 -0
  73. package/dist/content/deployment/recipes/github-pages.html +414 -0
  74. package/dist/content/deployment/recipes/index.html +11 -0
  75. package/dist/content/deployment/recipes/netlify.html +394 -0
  76. package/dist/content/deployment/recipes/vercel.html +382 -0
  77. package/dist/content/deployment/secrets-and-env-vars.html +380 -0
  78. package/dist/content/deployment.html +426 -0
  79. package/dist/content/guide/approaches.html +501 -0
  80. package/dist/content/guide/features.html +436 -0
  81. package/dist/content/guide/getting-started.html +403 -0
  82. package/dist/content/guide/index.html +11 -0
  83. package/dist/content/guide/kitfly-overview.html +544 -0
  84. package/dist/content/index.html +11 -0
  85. package/dist/content/reference/configuration.html +580 -0
  86. package/dist/content/reference/design-catalog.html +449 -0
  87. package/dist/content/reference/environment-variables.html +367 -0
  88. package/dist/content/reference/glossary.html +368 -0
  89. package/dist/content/reference/index.html +11 -0
  90. package/dist/content/reference/key-concepts.html +399 -0
  91. package/dist/content/reference/plugins.html +491 -0
  92. package/dist/content/reference/slides-authoring-guidelines.html +418 -0
  93. package/dist/content/reference/structure.html +463 -0
  94. package/dist/content/reference.html +335 -0
  95. package/dist/content/templates/crucible.html +546 -0
  96. package/dist/content/templates/handbook.html +405 -0
  97. package/dist/content/templates/index.html +11 -0
  98. package/dist/content/templates/minimal.html +447 -0
  99. package/dist/content/templates/overview.html +558 -0
  100. package/dist/content/templates/pipeline.html +494 -0
  101. package/dist/content/templates/productbook.html +540 -0
  102. package/dist/content/templates/runbook.html +543 -0
  103. package/dist/content/templates/servicebook.html +523 -0
  104. package/dist/content-index.json +549 -0
  105. package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +491 -0
  106. package/dist/docs/decisions/ADR-0002-ai-accessibility.html +434 -0
  107. package/dist/docs/decisions/ADR-0003-single-file-bundle.html +412 -0
  108. package/dist/docs/decisions/ADR-0004-bun-runtime.html +409 -0
  109. package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +402 -0
  110. package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +459 -0
  111. package/dist/docs/decisions/DDR-0002-theme-system.html +452 -0
  112. package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +423 -0
  113. package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +399 -0
  114. package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +422 -0
  115. package/dist/docs/decisions/index.html +11 -0
  116. package/dist/docs/userguide/cli/build.html +408 -0
  117. package/dist/docs/userguide/cli/bundle.html +419 -0
  118. package/dist/docs/userguide/cli/dev.html +428 -0
  119. package/dist/docs/userguide/cli/index.html +11 -0
  120. package/dist/docs/userguide/cli/init.html +436 -0
  121. package/dist/docs/userguide/cli/servers.html +393 -0
  122. package/dist/docs/userguide/cli/stop.html +408 -0
  123. package/dist/docs/userguide/cli/update.html +406 -0
  124. package/dist/docs/userguide/cli/version.html +406 -0
  125. package/dist/docs/userguide/cli.html +386 -0
  126. package/dist/docs/userguide/index.html +11 -0
  127. package/dist/docs/userguide/sharing.html +465 -0
  128. package/dist/index.html +387 -0
  129. package/dist/llms.txt +18 -0
  130. package/dist/provenance.json +7 -0
  131. package/dist/schemas/index.html +11 -0
  132. package/dist/schemas/plugin-registry.schema.html +327 -0
  133. package/dist/schemas/plugin-schemas-notes.html +364 -0
  134. package/dist/schemas/plugin.schema.html +327 -0
  135. package/dist/schemas/plugins.schema.html +327 -0
  136. package/dist/schemas/v0/common.schema.html +386 -0
  137. package/dist/schemas/v0/index.html +11 -0
  138. package/dist/schemas/v0/plugin-registry.schema.html +547 -0
  139. package/dist/schemas/v0/plugin.schema.html +497 -0
  140. package/dist/schemas/v0/plugins.schema.html +406 -0
  141. package/dist/schemas/v0/site.schema.html +541 -0
  142. package/dist/schemas/v0/theme.schema.html +615 -0
  143. package/dist/schemas.html +351 -0
  144. package/dist/styles.css +1262 -0
  145. package/package.json +4 -2
  146. package/plugins-dist/callouts.css +32 -0
  147. package/plugins-dist/callouts.js +46 -0
  148. package/plugins-dist/slides-visuals.css +390 -0
  149. package/plugins-dist/slides-visuals.js +689 -0
  150. package/registry/plugins.yaml +35 -0
  151. package/schemas/README.md +10 -0
  152. package/schemas/plugin-registry.schema.json +5 -0
  153. package/schemas/plugin-schemas-notes.md +71 -0
  154. package/schemas/plugin.schema.json +5 -0
  155. package/schemas/plugins.schema.json +5 -0
  156. package/schemas/v0/common.schema.json +64 -0
  157. package/schemas/v0/plugin-registry.schema.json +225 -0
  158. package/schemas/v0/plugin.schema.json +175 -0
  159. package/schemas/v0/plugins.schema.json +84 -0
  160. package/schemas/v0/site.schema.json +56 -9
  161. package/schemas/v0/theme.schema.json +105 -22
  162. package/scripts/build.ts +158 -3
  163. package/scripts/bundle.ts +261 -95
  164. package/scripts/dev.ts +301 -11
  165. package/src/__tests__/build.test.ts +220 -1
  166. package/src/__tests__/bundle.test.ts +31 -0
  167. package/src/__tests__/cli.test.ts +14 -3
  168. package/src/__tests__/dev-plugin-errors.test.ts +20 -0
  169. package/src/__tests__/fixtures/fences/slides-visuals/invalid/bad-list-indent.md +5 -0
  170. package/src/__tests__/fixtures/fences/slides-visuals/invalid/blank-line.md +5 -0
  171. package/src/__tests__/fixtures/fences/slides-visuals/invalid/compare-object-items.md +9 -0
  172. package/src/__tests__/fixtures/fences/slides-visuals/invalid/flow-branching-no-source.md +5 -0
  173. package/src/__tests__/fixtures/fences/slides-visuals/invalid/flow-converging-no-target.md +6 -0
  174. package/src/__tests__/fixtures/fences/slides-visuals/invalid/indented-fence.md +4 -0
  175. package/src/__tests__/fixtures/fences/slides-visuals/invalid/staircase-empty-steps.md +3 -0
  176. package/src/__tests__/fixtures/fences/slides-visuals/invalid/stat-grid-missing-fields.md +5 -0
  177. package/src/__tests__/fixtures/fences/slides-visuals/invalid/timeline-horizontal-no-events.md +2 -0
  178. package/src/__tests__/fixtures/fences/slides-visuals/invalid/unknown-type.md +3 -0
  179. package/src/__tests__/fixtures/fences/slides-visuals/valid/compare.md +10 -0
  180. package/src/__tests__/fixtures/fences/slides-visuals/valid/comparison-table.md +14 -0
  181. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-branching-no-split.md +7 -0
  182. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-branching.md +8 -0
  183. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-converging-no-merge.md +7 -0
  184. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-converging.md +8 -0
  185. package/src/__tests__/fixtures/fences/slides-visuals/valid/funnel.md +7 -0
  186. package/src/__tests__/fixtures/fences/slides-visuals/valid/kpi.md +5 -0
  187. package/src/__tests__/fixtures/fences/slides-visuals/valid/layer-cake.md +6 -0
  188. package/src/__tests__/fixtures/fences/slides-visuals/valid/pyramid.md +6 -0
  189. package/src/__tests__/fixtures/fences/slides-visuals/valid/quadrant-grid.md +8 -0
  190. package/src/__tests__/fixtures/fences/slides-visuals/valid/scorecard.md +13 -0
  191. package/src/__tests__/fixtures/fences/slides-visuals/valid/staircase-down.md +7 -0
  192. package/src/__tests__/fixtures/fences/slides-visuals/valid/staircase.md +8 -0
  193. package/src/__tests__/fixtures/fences/slides-visuals/valid/stat-grid.md +8 -0
  194. package/src/__tests__/fixtures/fences/slides-visuals/valid/timeline-horizontal.md +9 -0
  195. package/src/__tests__/fixtures/fences/slides-visuals/valid/timeline-vertical.md +10 -0
  196. package/src/__tests__/init.test.ts +35 -0
  197. package/src/__tests__/plugin-loader.test.ts +221 -0
  198. package/src/__tests__/shared.test.ts +451 -0
  199. package/src/__tests__/slides-visuals-fence-contract.test.ts +28 -0
  200. package/src/__tests__/slides-visuals-runtime-regressions.bun.test.ts +147 -0
  201. package/src/__tests__/styles.test.ts +35 -0
  202. package/src/cli.ts +9 -4
  203. package/src/plugin-loader.ts +245 -0
  204. package/src/shared.ts +650 -7
  205. package/src/site/styles.css +331 -0
  206. package/src/site/template.html +66 -5
  207. package/src/templates/deck.ts +186 -0
  208. package/src/templates/driver.ts +11 -1
  209. package/src/templates/minimal.ts +1 -0
@@ -12,3 +12,38 @@ describe("sidebar folder indicator CSS", () => {
12
12
  expect(css).toContain("transform: rotate(90deg)");
13
13
  });
14
14
  });
15
+
16
+ describe("slide layout primitives CSS", () => {
17
+ it("includes core block-flow/grid/stack primitives", async () => {
18
+ const css = await readFile(join(process.cwd(), "src/site/styles.css"), "utf-8");
19
+ expect(css).toContain(".block-flow");
20
+ expect(css).toContain(".block-grid");
21
+ expect(css).toContain(".block-stack");
22
+ expect(css).toContain(".block-label");
23
+ expect(css).toContain(".block-flow:not(.vertical) .block:not(:last-child)::after");
24
+ expect(css).toContain(".block-flow.vertical .block:not(:last-child)::after");
25
+ expect(css).toContain(".block-grid.cols-3");
26
+ expect(css).toContain(".block-grid.cols-4");
27
+ });
28
+
29
+ it("includes shape modifiers for block visuals", async () => {
30
+ const css = await readFile(join(process.cwd(), "src/site/styles.css"), "utf-8");
31
+ expect(css).toContain(".block.circle");
32
+ expect(css).toContain(".block.pill");
33
+ expect(css).toContain(".block.diamond");
34
+ expect(css).toContain(".block.chevron");
35
+ expect(css).toContain(".block.hexagon");
36
+ expect(css).toContain(".block.triangle");
37
+ expect(css).toContain(".block.block-arrow");
38
+ });
39
+
40
+ it("keeps non-active layout-class slides hidden", async () => {
41
+ const css = await readFile(join(process.cwd(), "src/site/styles.css"), "utf-8");
42
+ expect(css).toContain(".slide {");
43
+ expect(css).toContain("display: none;");
44
+ expect(css).toContain(".slide.active.centered");
45
+ expect(css).toContain(".slide.active.two-column");
46
+ expect(css).not.toContain(".slide.centered {\n min-height: 100%;\n display: flex;");
47
+ expect(css).not.toContain(".slide.two-column {\n display: grid;");
48
+ });
49
+ });
package/src/cli.ts CHANGED
@@ -70,7 +70,7 @@ kitfly v${VERSION} - Turn your writing into a website
70
70
  Usage:
71
71
  kitfly dev [folder] Start dev server with hot reload
72
72
  kitfly build [folder] Build static site to dist/
73
- kitfly bundle [folder] Build single-file HTML bundle
73
+ kitfly bundle [folder] Build single-file HTML bundle to bundles/
74
74
  kitfly init [name] Create new project from template
75
75
  kitfly update [version] Update standalone site code
76
76
  kitfly servers List running dev servers
@@ -86,11 +86,15 @@ Dev options:
86
86
  --json Output JSON (implies --daemon)
87
87
  --no-open Don't open browser
88
88
 
89
- Build/bundle options:
89
+ Build options:
90
90
  --out <dir> Output directory [env: KITFLY_BUILD_OUT] (default: dist)
91
- --name <file> Bundle filename (default: bundle.html)
92
91
  --no-raw Don't include raw markdown
93
92
 
93
+ Bundle options:
94
+ --out <dir> Output directory [env: KITFLY_BUNDLE_OUT] (default: bundles)
95
+ --name <file> Bundle filename (default: bundle.html)
96
+ --no-raw Don't include raw markdown [env: KITFLY_BUNDLE_RAW]
97
+
94
98
  Stop options:
95
99
  --force Skip graceful shutdown, kill immediately
96
100
 
@@ -117,6 +121,7 @@ Examples:
117
121
  kitfly logs 3340 --follow
118
122
  kitfly logs --clean
119
123
  kitfly build ./docs --out ./public
124
+ kitfly bundle ./docs --out ./bundles --name docs.html
120
125
  kitfly init my-handbook
121
126
  kitfly update --check
122
127
 
@@ -359,7 +364,7 @@ async function main() {
359
364
 
360
365
  case "bundle": {
361
366
  const folder = positional[0] || ".";
362
- const out = (flags.out as string) || "dist";
367
+ const out = (flags.out as string) || "bundles";
363
368
  const name = (flags.name as string) || "bundle.html";
364
369
  const raw = flags.raw !== false; // --no-raw disables raw markdown
365
370
  const { bundleSite } = await import("../scripts/bundle.ts");
@@ -0,0 +1,245 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { ENGINE_ROOT } from "./engine.ts";
5
+ import { exists, parseYaml, validatePath } from "./shared.ts";
6
+
7
+ export class PluginNetworkError extends Error {}
8
+ export class PluginIntegrityError extends Error {}
9
+ export class PluginPolicyError extends Error {}
10
+ export class PluginConfigError extends Error {}
11
+
12
+ export type PluginInjections = {
13
+ head: string;
14
+ bodyEnd: string;
15
+ };
16
+
17
+ export type KitflyMode = "docs" | "slides";
18
+
19
+ type RegistryAssetChecksums = {
20
+ js?: string;
21
+ css?: string;
22
+ };
23
+
24
+ type RegistryAssets = {
25
+ js?: string;
26
+ css?: string;
27
+ assetSha256: RegistryAssetChecksums;
28
+ };
29
+
30
+ type RegistryPlugin = {
31
+ name: string;
32
+ description: string;
33
+ version: string;
34
+ contract: string;
35
+ kitfly: string;
36
+ license: string;
37
+ verified: boolean;
38
+ modes?: KitflyMode[];
39
+ assets: RegistryAssets;
40
+ };
41
+
42
+ type PluginRegistry = {
43
+ version: number;
44
+ updated: string;
45
+ baseUrl: string;
46
+ plugins: Record<string, RegistryPlugin>;
47
+ };
48
+
49
+ type CanonicalPluginRef = {
50
+ id: string;
51
+ version: string;
52
+ };
53
+
54
+ const PLUGIN_ID_RE = /^[a-z][a-z0-9-]*$/;
55
+ const SEMVER_RE =
56
+ /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
57
+
58
+ function parseCanonicalRef(spec: string): CanonicalPluginRef | null {
59
+ const at = spec.lastIndexOf("@");
60
+ if (at <= 0) return null;
61
+ const id = spec.slice(0, at).trim();
62
+ const version = spec.slice(at + 1).trim();
63
+ if (!id || !version) return null;
64
+ if (!PLUGIN_ID_RE.test(id) || id.length > 40) return null;
65
+ if (!SEMVER_RE.test(version)) return null;
66
+ return { id, version };
67
+ }
68
+
69
+ function sha256Hex(data: Uint8Array): string {
70
+ return createHash("sha256").update(data).digest("hex");
71
+ }
72
+
73
+ function requiredSha256Hex(expected: string, context: string): string {
74
+ if (!expected.startsWith("sha256:")) {
75
+ throw new PluginConfigError(`${context}: expected sha256:<hex>, got ${expected}`);
76
+ }
77
+ const hex = expected.slice("sha256:".length);
78
+ if (!/^[0-9a-f]{64}$/i.test(hex)) {
79
+ throw new PluginConfigError(`${context}: invalid sha256 hex`);
80
+ }
81
+ return hex.toLowerCase();
82
+ }
83
+
84
+ function templateBaseUrl(url: string, baseUrl: string): string {
85
+ const token = "${" + "baseUrl}";
86
+ return url.replaceAll(token, baseUrl);
87
+ }
88
+
89
+ async function fetchTextOrThrow(url: string): Promise<string> {
90
+ if (!/^https?:\/\//.test(url)) {
91
+ throw new PluginConfigError(`Unsupported URL scheme: ${url}`);
92
+ }
93
+ let res: Response;
94
+ try {
95
+ res = await fetch(url);
96
+ } catch (e) {
97
+ throw new PluginNetworkError(`Failed to fetch ${url}: ${String(e)}`);
98
+ }
99
+ if (!res.ok)
100
+ throw new PluginNetworkError(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
101
+ return res.text();
102
+ }
103
+
104
+ async function readLocalTextOrThrow(root: string, relOrAbs: string): Promise<string> {
105
+ const rel = relOrAbs.replace(/^\//, "");
106
+ const fsPath = validatePath(root, ".", rel, true);
107
+ if (!fsPath) throw new PluginConfigError(`Invalid local plugin asset path: ${relOrAbs}`);
108
+ return readFile(fsPath, "utf-8");
109
+ }
110
+
111
+ async function fetchWithSha256Cache(
112
+ cacheFile: string,
113
+ url: string,
114
+ expectedSha256Hex: string,
115
+ assetRoot: string,
116
+ ): Promise<string> {
117
+ if (!/^https?:\/\//.test(url)) {
118
+ const text = await readLocalTextOrThrow(assetRoot, url);
119
+ const actual = sha256Hex(new TextEncoder().encode(text));
120
+ if (actual !== expectedSha256Hex) {
121
+ throw new PluginIntegrityError(
122
+ `CHECKSUM MISMATCH for ${url}\n expected: ${expectedSha256Hex}\n got: ${actual}`,
123
+ );
124
+ }
125
+ return text;
126
+ }
127
+
128
+ if (await exists(cacheFile)) {
129
+ const cached = await readFile(cacheFile);
130
+ if (sha256Hex(cached) === expectedSha256Hex) return cached.toString("utf-8");
131
+ }
132
+
133
+ const text = await fetchTextOrThrow(url);
134
+ const bytes = new TextEncoder().encode(text);
135
+ const actual = sha256Hex(bytes);
136
+ if (actual !== expectedSha256Hex) {
137
+ throw new PluginIntegrityError(
138
+ `CHECKSUM MISMATCH for ${url}\n expected: ${expectedSha256Hex}\n got: ${actual}`,
139
+ );
140
+ }
141
+
142
+ await mkdir(dirname(cacheFile), { recursive: true });
143
+ await writeFile(cacheFile, text, "utf-8");
144
+ return text;
145
+ }
146
+
147
+ export async function loadPluginRegistry(registryPath: string): Promise<PluginRegistry> {
148
+ const raw = await readFile(registryPath, "utf-8");
149
+ const parsed = parseYaml(raw) as unknown as PluginRegistry;
150
+ if (!parsed || typeof parsed !== "object") throw new PluginConfigError("Invalid plugin registry");
151
+ if (typeof parsed.baseUrl !== "string")
152
+ throw new PluginConfigError("Invalid plugin registry shape");
153
+ if (!parsed.plugins || typeof parsed.plugins !== "object") {
154
+ throw new PluginConfigError("Invalid plugin registry shape");
155
+ }
156
+ return parsed;
157
+ }
158
+
159
+ export async function loadPluginInjections(opts: {
160
+ root: string;
161
+ mode?: KitflyMode;
162
+ registryPath?: string;
163
+ configPath?: string;
164
+ cacheDir?: string;
165
+ allowUntrusted?: boolean;
166
+ }): Promise<PluginInjections> {
167
+ const configPath = opts.configPath ?? join(opts.root, "kitfly.plugins.yaml");
168
+ const cacheDir = opts.cacheDir ?? join(opts.root, "node_modules", ".kitfly-plugins");
169
+ const allowUntrusted = opts.allowUntrusted ?? false;
170
+ const mode: KitflyMode = opts.mode ?? "docs";
171
+
172
+ if (!(await exists(configPath))) return { head: "", bodyEnd: "" };
173
+
174
+ const engineRegistryPath = join(ENGINE_ROOT, "registry", "plugins.yaml");
175
+ const siteRegistryPath = join(opts.root, "registry", "plugins.yaml");
176
+ const hasSiteRegistry = await exists(siteRegistryPath);
177
+ const registryPath =
178
+ opts.registryPath ?? (hasSiteRegistry ? siteRegistryPath : engineRegistryPath);
179
+ const assetRoot = hasSiteRegistry ? opts.root : ENGINE_ROOT;
180
+
181
+ if (!(await exists(registryPath)))
182
+ throw new PluginConfigError(`Missing registry: ${registryPath}`);
183
+
184
+ const registry = await loadPluginRegistry(registryPath);
185
+ const config = parseYaml(await readFile(configPath, "utf-8")) as Record<string, unknown>;
186
+ const plugins = Array.isArray(config.plugins) ? config.plugins : [];
187
+
188
+ let head = "";
189
+ let bodyEnd = "";
190
+
191
+ for (const entry of plugins) {
192
+ if (typeof entry !== "string") {
193
+ if (allowUntrusted) {
194
+ throw new PluginPolicyError("Third-party plugin objects are not supported yet");
195
+ }
196
+ throw new PluginPolicyError("Third-party plugins require --allow-untrusted");
197
+ }
198
+
199
+ const ref = parseCanonicalRef(entry);
200
+ if (!ref) throw new PluginConfigError(`Invalid plugin ref: ${entry}`);
201
+ const reg = registry.plugins?.[ref.id];
202
+ if (!reg) throw new PluginConfigError(`Plugin not in registry: ${ref.id}`);
203
+ if (reg.version !== ref.version) {
204
+ throw new PluginConfigError(
205
+ `Plugin ${ref.id} version mismatch: ${ref.version} != ${reg.version}`,
206
+ );
207
+ }
208
+
209
+ if (Array.isArray(reg.modes)) {
210
+ if (reg.modes.length === 0) continue;
211
+ if (!reg.modes.includes(mode)) continue;
212
+ }
213
+
214
+ const assets = reg.assets;
215
+ const pluginCacheDir = join(cacheDir, `${ref.id}@${ref.version}`);
216
+
217
+ if (assets.css) {
218
+ const url = templateBaseUrl(assets.css, registry.baseUrl);
219
+ const expected = requiredSha256Hex(
220
+ assets.assetSha256.css ?? "",
221
+ `${ref.id}: assetSha256.css`,
222
+ );
223
+ const css = await fetchWithSha256Cache(
224
+ join(pluginCacheDir, basename(url)),
225
+ url,
226
+ expected,
227
+ assetRoot,
228
+ );
229
+ head += `\n<style data-kitfly-plugin="${ref.id}@${ref.version}">\n${css}\n</style>\n`;
230
+ }
231
+ if (assets.js) {
232
+ const url = templateBaseUrl(assets.js, registry.baseUrl);
233
+ const expected = requiredSha256Hex(assets.assetSha256.js ?? "", `${ref.id}: assetSha256.js`);
234
+ const js = await fetchWithSha256Cache(
235
+ join(pluginCacheDir, basename(url)),
236
+ url,
237
+ expected,
238
+ assetRoot,
239
+ );
240
+ bodyEnd += `\n<script data-kitfly-plugin="${ref.id}@${ref.version}">\n${js}\n</script>\n`;
241
+ }
242
+ }
243
+
244
+ return { head, bodyEnd };
245
+ }