kitfly 0.2.1 → 0.2.3
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.
- package/CHANGELOG.md +56 -0
- package/README.md +25 -10
- package/VERSION +1 -1
- package/dist/_raw/content/guide/branding.md +146 -0
- package/dist/_raw/content/guide/data-driven-content.md +204 -0
- package/dist/_raw/content/reference/configuration.md +145 -7
- package/dist/_raw/content/reference/environment-variables.md +26 -1
- package/dist/_raw/content/reference/glossary.md +25 -1
- package/dist/_raw/content/reference/key-concepts.md +30 -2
- package/dist/_raw/content/reference/plugins.md +14 -0
- package/dist/_raw/docs/decisions/ADR-0006-data-driven-content.md +350 -0
- package/dist/content/deployment/preflight.html +10 -6
- package/dist/content/deployment/recipes/aws-s3.html +10 -6
- package/dist/content/deployment/recipes/cloudflare-pages.html +10 -6
- package/dist/content/deployment/recipes/cloudflare-r2.html +10 -6
- package/dist/content/deployment/recipes/fly-io.html +10 -6
- package/dist/content/deployment/recipes/github-pages.html +10 -6
- package/dist/content/deployment/recipes/netlify.html +10 -6
- package/dist/content/deployment/recipes/vercel.html +10 -6
- package/dist/content/deployment/secrets-and-env-vars.html +10 -6
- package/dist/content/deployment.html +10 -6
- package/dist/content/guide/approaches.html +10 -6
- package/dist/content/guide/branding.html +510 -0
- package/dist/content/guide/data-driven-content.html +543 -0
- package/dist/content/guide/features.html +10 -6
- package/dist/content/guide/getting-started.html +10 -6
- package/dist/content/guide/kitfly-overview.html +10 -6
- package/dist/content/reference/configuration.html +135 -9
- package/dist/content/reference/design-catalog.html +10 -6
- package/dist/content/reference/environment-variables.html +50 -8
- package/dist/content/reference/glossary.html +24 -8
- package/dist/content/reference/key-concepts.html +33 -9
- package/dist/content/reference/plugins.html +22 -7
- package/dist/content/reference/slides-authoring-guidelines.html +10 -6
- package/dist/content/reference/structure.html +10 -6
- package/dist/content/reference.html +10 -6
- package/dist/content/templates/crucible.html +10 -6
- package/dist/content/templates/handbook.html +10 -6
- package/dist/content/templates/minimal.html +10 -6
- package/dist/content/templates/overview.html +10 -6
- package/dist/content/templates/pipeline.html +10 -6
- package/dist/content/templates/productbook.html +10 -6
- package/dist/content/templates/runbook.html +10 -6
- package/dist/content/templates/servicebook.html +10 -6
- package/dist/content-index.json +29 -2
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +10 -6
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +10 -6
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +10 -6
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +10 -6
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +10 -6
- package/dist/docs/decisions/ADR-0006-data-driven-content.html +752 -0
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +10 -6
- package/dist/docs/decisions/DDR-0002-theme-system.html +10 -6
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +10 -6
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +10 -6
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +10 -6
- package/dist/docs/userguide/cli/build.html +10 -6
- package/dist/docs/userguide/cli/bundle.html +10 -6
- package/dist/docs/userguide/cli/dev.html +10 -6
- package/dist/docs/userguide/cli/init.html +10 -6
- package/dist/docs/userguide/cli/servers.html +10 -6
- package/dist/docs/userguide/cli/stop.html +10 -6
- package/dist/docs/userguide/cli/update.html +10 -6
- package/dist/docs/userguide/cli/version.html +10 -6
- package/dist/docs/userguide/cli.html +10 -6
- package/dist/docs/userguide/sharing.html +10 -6
- package/dist/index.html +10 -6
- package/dist/llms.txt +3 -3
- package/dist/provenance.json +4 -4
- package/dist/schemas/plugin-registry.schema.html +10 -6
- package/dist/schemas/plugin-schemas-notes.html +10 -6
- package/dist/schemas/plugin.schema.html +10 -6
- package/dist/schemas/plugins.schema.html +10 -6
- package/dist/schemas/v0/common.schema.html +14 -10
- package/dist/schemas/v0/plugin-registry.schema.html +13 -9
- package/dist/schemas/v0/plugin.schema.html +13 -9
- package/dist/schemas/v0/plugins.schema.html +13 -9
- package/dist/schemas/v0/site.schema.html +67 -7
- package/dist/schemas/v0/theme.schema.html +21 -17
- package/dist/schemas.html +10 -6
- package/dist/styles.css +39 -4
- package/package.json +1 -1
- package/plugins-dist/latex-runtime.js +140 -0
- package/plugins-dist/latex.js +178 -0
- package/plugins-dist/slides-charts-lite-runtime.js +179 -0
- package/plugins-dist/slides-charts-lite.js +198 -0
- package/registry/plugins.yaml +25 -0
- package/schemas/v0/site.schema.json +56 -0
- package/scripts/build.ts +191 -69
- package/scripts/bundle.ts +118 -10
- package/scripts/dev.ts +245 -166
- package/src/__tests__/brief.test.ts +151 -0
- package/src/__tests__/build.test.ts +169 -1
- package/src/__tests__/bundle.test.ts +134 -0
- package/src/__tests__/init.test.ts +51 -2
- package/src/__tests__/latex-runtime.bun.test.ts +35 -0
- package/src/__tests__/shared.test.ts +598 -1
- package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
- package/src/cli.ts +11 -4
- package/src/commands/init.ts +1 -1
- package/src/shared.ts +725 -18
- package/src/site/styles.css +39 -4
- package/src/site/template.html +5 -2
- package/src/templates/brief.ts +486 -0
- package/src/templates/deck.ts +59 -0
- package/src/templates/driver.ts +46 -13
- package/src/templates/handbook.ts +32 -0
- package/src/templates/runbook.ts +32 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Brief template definition
|
|
3
|
+
*
|
|
4
|
+
* Covers: src/templates/brief.ts
|
|
5
|
+
* Strategy: import the template directly, verify structure and generated content
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { brief } from "../templates/brief.ts";
|
|
10
|
+
import type { BrandingConfig, TemplateContext, TemplateFile } from "../templates/schema.ts";
|
|
11
|
+
|
|
12
|
+
function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
|
|
13
|
+
const branding: BrandingConfig = {
|
|
14
|
+
siteName: "Test Brief",
|
|
15
|
+
brandName: "Acme Corp",
|
|
16
|
+
brandUrl: "https://acme.example.com",
|
|
17
|
+
primaryColor: "#2563eb",
|
|
18
|
+
footerText: "Footer text",
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
name: "test-brief",
|
|
22
|
+
branding,
|
|
23
|
+
template: brief,
|
|
24
|
+
year: 2026,
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveContent(file: TemplateFile, ctx: TemplateContext): string {
|
|
30
|
+
return typeof file.content === "function" ? file.content(ctx) : file.content;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findFile(path: string): TemplateFile {
|
|
34
|
+
const f = brief.files.find((entry) => entry.path === path);
|
|
35
|
+
if (!f) throw new Error(`Template file not found: ${path}`);
|
|
36
|
+
return f;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("brief template definition", () => {
|
|
40
|
+
it("has correct identity metadata", () => {
|
|
41
|
+
expect(brief.id).toBe("brief");
|
|
42
|
+
expect(brief.name).toBe("Brief");
|
|
43
|
+
expect(brief.version).toBe(1);
|
|
44
|
+
expect(brief.extends).toBe("minimal");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("defines exactly four sections", () => {
|
|
48
|
+
expect(brief.sections).toHaveLength(4);
|
|
49
|
+
expect(brief.sections.map((s) => s.name)).toEqual([
|
|
50
|
+
"Product",
|
|
51
|
+
"Use Cases",
|
|
52
|
+
"Getting Started",
|
|
53
|
+
"Reference",
|
|
54
|
+
]);
|
|
55
|
+
expect(brief.sections.map((s) => s.path)).toEqual([
|
|
56
|
+
"content/product",
|
|
57
|
+
"content/use-cases",
|
|
58
|
+
"content/getting-started",
|
|
59
|
+
"content/reference",
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("defines unique file paths", () => {
|
|
64
|
+
const paths = brief.files.map((f) => f.path);
|
|
65
|
+
expect(new Set(paths).size).toBe(paths.length);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("brief site.yaml", () => {
|
|
70
|
+
const ctx = makeCtx();
|
|
71
|
+
const file = findFile("site.yaml");
|
|
72
|
+
|
|
73
|
+
it("includes branding and all four section paths", () => {
|
|
74
|
+
const content = resolveContent(file, ctx);
|
|
75
|
+
expect(content).toContain('title: "Test Brief"');
|
|
76
|
+
expect(content).toContain('name: "Acme Corp"');
|
|
77
|
+
expect(content).toContain('url: "https://acme.example.com"');
|
|
78
|
+
expect(content).toContain('"content/product"');
|
|
79
|
+
expect(content).toContain('"content/use-cases"');
|
|
80
|
+
expect(content).toContain('"content/getting-started"');
|
|
81
|
+
expect(content).toContain('"content/reference"');
|
|
82
|
+
expect(content).toContain('home: "index.md"');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("brief starter files", () => {
|
|
87
|
+
const ctx = makeCtx();
|
|
88
|
+
const starterFiles = [
|
|
89
|
+
"index.md",
|
|
90
|
+
"content/product/overview.md",
|
|
91
|
+
"content/product/capabilities.md",
|
|
92
|
+
"content/use-cases/overview.md",
|
|
93
|
+
"content/use-cases/example-use-case.md",
|
|
94
|
+
"content/getting-started/overview.md",
|
|
95
|
+
"content/getting-started/requirements.md",
|
|
96
|
+
"content/reference/architecture.md",
|
|
97
|
+
"content/reference/faq.md",
|
|
98
|
+
"content/reference/contacts.md",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
it("ships all 10 starter files", () => {
|
|
102
|
+
expect(starterFiles).toHaveLength(10);
|
|
103
|
+
for (const path of starterFiles) {
|
|
104
|
+
expect(brief.files.some((f) => f.path === path)).toBe(true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("includes frontmatter in markdown starter files", () => {
|
|
109
|
+
for (const path of starterFiles) {
|
|
110
|
+
const content = resolveContent(findFile(path), ctx);
|
|
111
|
+
expect(content).toMatch(/^---\n/);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("includes customize markers in starter files", () => {
|
|
116
|
+
for (const path of starterFiles) {
|
|
117
|
+
const content = resolveContent(findFile(path), ctx);
|
|
118
|
+
expect(content).toContain("<!-- ← CUSTOMIZE");
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("uses professional framing in index and use cases", () => {
|
|
123
|
+
const index = resolveContent(findFile("index.md"), ctx);
|
|
124
|
+
expect(index).toContain("At a Glance");
|
|
125
|
+
expect(index).toContain("[Use Cases](/content/use-cases/overview)");
|
|
126
|
+
|
|
127
|
+
const useCases = resolveContent(findFile("content/use-cases/example-use-case.md"), ctx);
|
|
128
|
+
expect(useCases).toContain("## Scenario");
|
|
129
|
+
expect(useCases).toContain("## Outcome");
|
|
130
|
+
expect(useCases).toContain("| Metric | Before | After |");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("brief CUSTOMIZING.md", () => {
|
|
135
|
+
const ctx = makeCtx();
|
|
136
|
+
const file = findFile("CUSTOMIZING.md");
|
|
137
|
+
|
|
138
|
+
it("documents template identity and external-audience tone", () => {
|
|
139
|
+
const content = resolveContent(file, ctx);
|
|
140
|
+
expect(content).toContain("template: brief");
|
|
141
|
+
expect(content).toContain("clients, prospects, and partners");
|
|
142
|
+
expect(content).toContain("Informational and professional");
|
|
143
|
+
expect(content).toContain("Not sales copy");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("guides use-case duplication workflow", () => {
|
|
147
|
+
const content = resolveContent(file, ctx);
|
|
148
|
+
expect(content).toContain("Duplicate `content/use-cases/example-use-case.md`");
|
|
149
|
+
expect(content).toContain("problem -> solution -> outcome");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -35,7 +35,8 @@ async function writeSiteYaml(dir: string, extra: Record<string, unknown> = {}):
|
|
|
35
35
|
const mode = extra.mode ? `mode: ${extra.mode}\n` : "";
|
|
36
36
|
const aspect = extra.aspect ? `aspect: ${extra.aspect}\n` : "";
|
|
37
37
|
const home = extra.home ? `home: ${extra.home}\n` : "";
|
|
38
|
-
const
|
|
38
|
+
const footer = extra.footer ? `footer:\n${extra.footer}\n` : "";
|
|
39
|
+
const yaml = `title: ${title}\n${version}${mode}${aspect}brand:\n${brand}\n${home}${footer}sections:\n${sections}\n`;
|
|
39
40
|
await writeFile(join(dir, "site.yaml"), yaml);
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -216,6 +217,45 @@ describe("build", () => {
|
|
|
216
217
|
expect(html).toContain("unversioned");
|
|
217
218
|
});
|
|
218
219
|
|
|
220
|
+
it("renders footer logo with static path prefix in built output", async () => {
|
|
221
|
+
const siteDir = await makeTempDir();
|
|
222
|
+
const outDir = "out";
|
|
223
|
+
await writeSiteYaml(siteDir, {
|
|
224
|
+
footer: ' logo: "assets/brand/footer-logo.png"\n logoAlt: "Footer Brand"\n logoHeight: 24',
|
|
225
|
+
});
|
|
226
|
+
await writeMd(siteDir, "docs/page.md", "# Page");
|
|
227
|
+
|
|
228
|
+
await build({ folder: siteDir, out: outDir });
|
|
229
|
+
|
|
230
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
231
|
+
expect(html).toContain('class="footer-logo-img"');
|
|
232
|
+
expect(html).toContain('src="./assets/brand/footer-logo.png"');
|
|
233
|
+
expect(html).toContain('alt="Footer Brand"');
|
|
234
|
+
expect(html).toContain("max-height: 24px");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("renders light/dark variants for header and footer logos when configured", async () => {
|
|
238
|
+
const siteDir = await makeTempDir();
|
|
239
|
+
const outDir = "out";
|
|
240
|
+
await writeSiteYaml(siteDir, {
|
|
241
|
+
brand:
|
|
242
|
+
' name: Test\n url: /\n logo: "assets/brand/logo.png"\n logoDark: "assets/brand/logo-dark.png"',
|
|
243
|
+
footer:
|
|
244
|
+
' logo: "assets/brand/footer-logo.png"\n logoDark: "assets/brand/footer-logo-dark.png"',
|
|
245
|
+
});
|
|
246
|
+
await writeMd(siteDir, "docs/page.md", "# Page");
|
|
247
|
+
|
|
248
|
+
await build({ folder: siteDir, out: outDir });
|
|
249
|
+
|
|
250
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
251
|
+
expect(html).toContain("logo-img logo-light");
|
|
252
|
+
expect(html).toContain("logo-img logo-dark");
|
|
253
|
+
expect(html).toContain("./assets/brand/logo-dark.png");
|
|
254
|
+
expect(html).toContain("footer-logo-img logo-light");
|
|
255
|
+
expect(html).toContain("footer-logo-img logo-dark");
|
|
256
|
+
expect(html).toContain("./assets/brand/footer-logo-dark.png");
|
|
257
|
+
});
|
|
258
|
+
|
|
219
259
|
it("builds a single-page hash-routed deck when mode is slides", async () => {
|
|
220
260
|
const siteDir = await makeTempDir();
|
|
221
261
|
const outDir = "out";
|
|
@@ -332,6 +372,87 @@ plugins:
|
|
|
332
372
|
expect(html).toContain(js);
|
|
333
373
|
});
|
|
334
374
|
|
|
375
|
+
it("preserves dollar-sign replacement patterns in injected plugin JS", async () => {
|
|
376
|
+
const siteDir = await makeTempDir();
|
|
377
|
+
const outDir = "out";
|
|
378
|
+
await writeSiteYaml(siteDir);
|
|
379
|
+
await writeMd(siteDir, "docs/page.md", "# Test");
|
|
380
|
+
|
|
381
|
+
const js = "const expr='x'; const sample = `$${expr}$`; const marker = '$`';";
|
|
382
|
+
await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
|
|
383
|
+
await writeFile(join(siteDir, "plugins-dist", "literal.js"), js, "utf-8");
|
|
384
|
+
|
|
385
|
+
await mkdir(join(siteDir, "registry"), { recursive: true });
|
|
386
|
+
await writeFile(
|
|
387
|
+
join(siteDir, "registry", "plugins.yaml"),
|
|
388
|
+
`version: 1
|
|
389
|
+
updated: "2026-02-16"
|
|
390
|
+
baseUrl: ""
|
|
391
|
+
plugins:
|
|
392
|
+
literal:
|
|
393
|
+
name: "Literal"
|
|
394
|
+
description: "Replacement pattern regression"
|
|
395
|
+
version: "1.0.0"
|
|
396
|
+
contract: "1"
|
|
397
|
+
kitfly: ">=0.2.0 <1.0.0"
|
|
398
|
+
license: MIT
|
|
399
|
+
verified: true
|
|
400
|
+
assets:
|
|
401
|
+
js: "plugins-dist/literal.js"
|
|
402
|
+
assetSha256:
|
|
403
|
+
js: "sha256:${sha256Hex(js)}"
|
|
404
|
+
`,
|
|
405
|
+
"utf-8",
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - literal@1.0.0\n", "utf-8");
|
|
409
|
+
|
|
410
|
+
await build({ folder: siteDir, out: outDir });
|
|
411
|
+
|
|
412
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
413
|
+
expect(html).toContain("const sample = `$${expr}$`;");
|
|
414
|
+
expect(html).toContain("const marker = '$`';");
|
|
415
|
+
expect(html).not.toContain("<!DOCTYPE html><html");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("preserves dollar-sign replacement patterns in markdown content", async () => {
|
|
419
|
+
const siteDir = await makeTempDir();
|
|
420
|
+
const outDir = "out";
|
|
421
|
+
await writeSiteYaml(siteDir);
|
|
422
|
+
await writeMd(
|
|
423
|
+
siteDir,
|
|
424
|
+
"docs/dollars.md",
|
|
425
|
+
"# Dollars\n\nLiteral dollars: $$ and $` and $' and $&\n\nMath style: $$x^2$$",
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
await build({ folder: siteDir, out: outDir });
|
|
429
|
+
|
|
430
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
431
|
+
expect(html).toContain("Literal dollars: $$ and $` and $' and $&");
|
|
432
|
+
expect(html).toContain("Math style: $$x^2$$");
|
|
433
|
+
expect(html.match(/<!DOCTYPE html>/g)?.length ?? 0).toBe(1);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("injects latex plugin in docs mode", async () => {
|
|
437
|
+
const siteDir = await makeTempDir();
|
|
438
|
+
const outDir = "out";
|
|
439
|
+
await writeSiteYaml(siteDir);
|
|
440
|
+
await writeMd(
|
|
441
|
+
siteDir,
|
|
442
|
+
"docs/math.md",
|
|
443
|
+
"# Math\n\nInline: $x^2$.\n\n$$\n\\\\int_0^1 x^2 dx\n$$\n\n```math\n\\\\sum_{i=1}^{n} i\n```",
|
|
444
|
+
);
|
|
445
|
+
await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - latex@0.2.2\n", "utf-8");
|
|
446
|
+
|
|
447
|
+
await build({ folder: siteDir, out: outDir });
|
|
448
|
+
|
|
449
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
450
|
+
expect(html).toContain('data-kitfly-plugin="latex@0.2.2"');
|
|
451
|
+
expect(html).toContain("const KATEX_JS_URL =");
|
|
452
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js");
|
|
453
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css");
|
|
454
|
+
});
|
|
455
|
+
|
|
335
456
|
it("injects slides-only plugins when mode=slides", async () => {
|
|
336
457
|
const siteDir = await makeTempDir();
|
|
337
458
|
const outDir = "out";
|
|
@@ -395,6 +516,53 @@ plugins:
|
|
|
395
516
|
expect(html).toContain(js);
|
|
396
517
|
});
|
|
397
518
|
|
|
519
|
+
it("injects slides-charts-lite plugin in slides mode", async () => {
|
|
520
|
+
const siteDir = await makeTempDir();
|
|
521
|
+
const outDir = "out";
|
|
522
|
+
await writeSiteYaml(siteDir, { mode: "slides" });
|
|
523
|
+
await writeMd(
|
|
524
|
+
siteDir,
|
|
525
|
+
"docs/deck.md",
|
|
526
|
+
`# Chart
|
|
527
|
+
|
|
528
|
+
\`\`\`chart
|
|
529
|
+
kind: bar
|
|
530
|
+
title: Revenue
|
|
531
|
+
labels: ["Q1", "Q2"]
|
|
532
|
+
data: [10, 12]
|
|
533
|
+
\`\`\`
|
|
534
|
+
`,
|
|
535
|
+
);
|
|
536
|
+
await writeFile(
|
|
537
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
538
|
+
"plugins:\n - slides-charts-lite@0.2.2\n",
|
|
539
|
+
"utf-8",
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
await build({ folder: siteDir, out: outDir });
|
|
543
|
+
|
|
544
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
545
|
+
expect(html).toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
|
|
546
|
+
expect(html).toContain("kitfly-chart-wrapper");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("does not inject slides-charts-lite in docs mode", async () => {
|
|
550
|
+
const siteDir = await makeTempDir();
|
|
551
|
+
const outDir = "out";
|
|
552
|
+
await writeSiteYaml(siteDir, { mode: "docs" });
|
|
553
|
+
await writeMd(siteDir, "docs/page.md", "# Page");
|
|
554
|
+
await writeFile(
|
|
555
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
556
|
+
"plugins:\n - slides-charts-lite@0.2.2\n",
|
|
557
|
+
"utf-8",
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
await build({ folder: siteDir, out: outDir });
|
|
561
|
+
|
|
562
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
563
|
+
expect(html).not.toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
|
|
564
|
+
});
|
|
565
|
+
|
|
398
566
|
it("ignores unknown slides-visuals block types while enforcing known contracts", async () => {
|
|
399
567
|
const siteDir = await makeTempDir();
|
|
400
568
|
const outDir = "out";
|
|
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
|
|
5
5
|
import {
|
|
6
6
|
buildBundleNav,
|
|
7
7
|
buildBundleSidebarHeader,
|
|
8
|
+
bundleSite,
|
|
8
9
|
fileToDataUri,
|
|
9
10
|
imageMime,
|
|
10
11
|
inlineLocalImages,
|
|
@@ -815,3 +816,136 @@ describe("buildBundleSidebarHeader", () => {
|
|
|
815
816
|
expect(html).toContain(`src="${logo}"`);
|
|
816
817
|
});
|
|
817
818
|
});
|
|
819
|
+
|
|
820
|
+
describe("bundleSite plugin integration", () => {
|
|
821
|
+
it("inlines latex plugin script when enabled", async () => {
|
|
822
|
+
const siteDir = await makeTempDir();
|
|
823
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
824
|
+
await writeFile(
|
|
825
|
+
join(siteDir, "site.yaml"),
|
|
826
|
+
'title: "Bundle Test"\nbrand:\n name: "Test"\n url: "/"\nsections:\n - name: Docs\n path: docs\n',
|
|
827
|
+
"utf-8",
|
|
828
|
+
);
|
|
829
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Bundle Math\n\n$E=mc^2$", "utf-8");
|
|
830
|
+
await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - latex@0.2.2\n", "utf-8");
|
|
831
|
+
|
|
832
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
833
|
+
|
|
834
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
835
|
+
expect(html).toContain('data-kitfly-plugin="latex@0.2.2"');
|
|
836
|
+
expect(html).toContain("const KATEX_JS_URL =");
|
|
837
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js");
|
|
838
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css");
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it("inlines slides-charts-lite plugin in slides mode", async () => {
|
|
842
|
+
const siteDir = await makeTempDir();
|
|
843
|
+
await mkdir(join(siteDir, "slides"), { recursive: true });
|
|
844
|
+
await writeFile(
|
|
845
|
+
join(siteDir, "site.yaml"),
|
|
846
|
+
'title: "Bundle Charts Test"\nmode: "slides"\nbrand:\n name: "Test"\n url: "/"\nsections:\n - name: Slides\n path: slides\n',
|
|
847
|
+
"utf-8",
|
|
848
|
+
);
|
|
849
|
+
await writeFile(
|
|
850
|
+
join(siteDir, "slides", "deck.md"),
|
|
851
|
+
'# Deck\n\n```chart\nkind: line\nlabels: ["W1", "W2"]\ndata: [2, 3]\n```',
|
|
852
|
+
"utf-8",
|
|
853
|
+
);
|
|
854
|
+
await writeFile(
|
|
855
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
856
|
+
"plugins:\n - slides-charts-lite@0.2.2\n",
|
|
857
|
+
"utf-8",
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
861
|
+
|
|
862
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
863
|
+
expect(html).toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
|
|
864
|
+
expect(html).toContain("kitfly-chart-wrapper");
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it("inlines footer logo image when configured", async () => {
|
|
868
|
+
const siteDir = await makeTempDir();
|
|
869
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
870
|
+
await mkdir(join(siteDir, "assets", "brand"), { recursive: true });
|
|
871
|
+
await writeFile(
|
|
872
|
+
join(siteDir, "site.yaml"),
|
|
873
|
+
'title: "Bundle Footer Test"\nbrand:\n name: "Test"\n url: "/"\nfooter:\n logo: "assets/brand/footer-logo.png"\n logoAlt: "Footer Brand"\n logoHeight: 24\nsections:\n - name: Docs\n path: docs\n',
|
|
874
|
+
"utf-8",
|
|
875
|
+
);
|
|
876
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Footer Logo");
|
|
877
|
+
await writeFile(
|
|
878
|
+
join(siteDir, "assets", "brand", "footer-logo.png"),
|
|
879
|
+
Buffer.from(
|
|
880
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
|
|
881
|
+
"base64",
|
|
882
|
+
),
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
886
|
+
|
|
887
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
888
|
+
expect(html).toContain('class="footer-logo-img"');
|
|
889
|
+
expect(html).toContain('src="data:image/png;base64,');
|
|
890
|
+
expect(html).not.toContain('src="assets/brand/footer-logo.png"');
|
|
891
|
+
expect(html).toContain('alt="Footer Brand"');
|
|
892
|
+
expect(html).toContain("max-height: 24px");
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it("inlines footer logo from site-root-relative path outside assets", async () => {
|
|
896
|
+
const siteDir = await makeTempDir();
|
|
897
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
898
|
+
await mkdir(join(siteDir, "logos"), { recursive: true });
|
|
899
|
+
await writeFile(
|
|
900
|
+
join(siteDir, "site.yaml"),
|
|
901
|
+
'title: "Bundle Footer Root Path Test"\nbrand:\n name: "Test"\n url: "/"\nfooter:\n logo: "logos/footer.png"\n logoAlt: "Footer Root Logo"\nsections:\n - name: Docs\n path: docs\n',
|
|
902
|
+
"utf-8",
|
|
903
|
+
);
|
|
904
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Footer Root Path");
|
|
905
|
+
await writeFile(
|
|
906
|
+
join(siteDir, "logos", "footer.png"),
|
|
907
|
+
Buffer.from(
|
|
908
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
|
|
909
|
+
"base64",
|
|
910
|
+
),
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
914
|
+
|
|
915
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
916
|
+
expect(html).toContain('class="footer-logo-img"');
|
|
917
|
+
expect(html).toContain('src="data:image/png;base64,');
|
|
918
|
+
expect(html).not.toContain('src="logos/footer.png"');
|
|
919
|
+
expect(html).toContain('alt="Footer Root Logo"');
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it("inlines light/dark variants for header and footer logos", async () => {
|
|
923
|
+
const siteDir = await makeTempDir();
|
|
924
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
925
|
+
await mkdir(join(siteDir, "assets", "brand"), { recursive: true });
|
|
926
|
+
await writeFile(
|
|
927
|
+
join(siteDir, "site.yaml"),
|
|
928
|
+
'title: "Bundle Dark Logos Test"\nbrand:\n name: "Test"\n url: "/"\n logo: "assets/brand/logo.png"\n logoDark: "assets/brand/logo-dark.png"\nfooter:\n logo: "assets/brand/footer-logo.png"\n logoDark: "assets/brand/footer-logo-dark.png"\nsections:\n - name: Docs\n path: docs\n',
|
|
929
|
+
"utf-8",
|
|
930
|
+
);
|
|
931
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Dark Logos");
|
|
932
|
+
const pixelPng = Buffer.from(
|
|
933
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
|
|
934
|
+
"base64",
|
|
935
|
+
);
|
|
936
|
+
await writeFile(join(siteDir, "assets", "brand", "logo.png"), pixelPng);
|
|
937
|
+
await writeFile(join(siteDir, "assets", "brand", "logo-dark.png"), pixelPng);
|
|
938
|
+
await writeFile(join(siteDir, "assets", "brand", "footer-logo.png"), pixelPng);
|
|
939
|
+
await writeFile(join(siteDir, "assets", "brand", "footer-logo-dark.png"), pixelPng);
|
|
940
|
+
|
|
941
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
942
|
+
|
|
943
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
944
|
+
expect(html).toContain('class="logo-img logo-light"');
|
|
945
|
+
expect(html).toContain('class="logo-img logo-dark"');
|
|
946
|
+
expect(html).toContain('class="footer-logo-img logo-light"');
|
|
947
|
+
expect(html).toContain('class="footer-logo-img logo-dark"');
|
|
948
|
+
expect(html).not.toContain('src="assets/brand/logo-dark.png"');
|
|
949
|
+
expect(html).not.toContain('src="assets/brand/footer-logo-dark.png"');
|
|
950
|
+
});
|
|
951
|
+
});
|
|
@@ -9,8 +9,8 @@ import { existsSync } from "node:fs";
|
|
|
9
9
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
13
|
-
import { init } from "../commands/init.ts";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { init, listAvailableTemplates } from "../commands/init.ts";
|
|
14
14
|
import { defaultBranding, getTemplate, listTemplates, runTemplate } from "../templates/driver.ts";
|
|
15
15
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -53,6 +53,7 @@ describe("template registry", () => {
|
|
|
53
53
|
expect(ids).toContain("minimal");
|
|
54
54
|
expect(ids).toContain("deck");
|
|
55
55
|
expect(ids).toContain("handbook");
|
|
56
|
+
expect(ids).toContain("brief");
|
|
56
57
|
expect(templates.length).toBeGreaterThanOrEqual(2);
|
|
57
58
|
});
|
|
58
59
|
|
|
@@ -66,6 +67,17 @@ describe("template registry", () => {
|
|
|
66
67
|
it("returns undefined for unknown template", () => {
|
|
67
68
|
expect(getTemplate("nonexistent")).toBeUndefined();
|
|
68
69
|
});
|
|
70
|
+
|
|
71
|
+
it("listAvailableTemplates output includes brief", () => {
|
|
72
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
73
|
+
try {
|
|
74
|
+
listAvailableTemplates();
|
|
75
|
+
const output = spy.mock.calls.map((call) => String(call[0] ?? "")).join("\n");
|
|
76
|
+
expect(output).toContain("brief");
|
|
77
|
+
} finally {
|
|
78
|
+
spy.mockRestore();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
69
81
|
});
|
|
70
82
|
|
|
71
83
|
// ---------------------------------------------------------------------------
|
|
@@ -358,6 +370,16 @@ describe("init() entry point", () => {
|
|
|
358
370
|
expect(generatedExists(projectName, "CUSTOMIZING.md")).toBe(true);
|
|
359
371
|
});
|
|
360
372
|
|
|
373
|
+
it("creates a brief site when template flag is set", async () => {
|
|
374
|
+
await init(projectName, { template: "brief", git: false });
|
|
375
|
+
|
|
376
|
+
expect(generatedExists(projectName, "content/product/overview.md")).toBe(true);
|
|
377
|
+
expect(generatedExists(projectName, "content/use-cases/example-use-case.md")).toBe(true);
|
|
378
|
+
expect(generatedExists(projectName, "content/getting-started/requirements.md")).toBe(true);
|
|
379
|
+
expect(generatedExists(projectName, "content/reference/contacts.md")).toBe(true);
|
|
380
|
+
expect(generatedExists(projectName, "CUSTOMIZING.md")).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
|
|
361
383
|
it("passes brand overrides through to template context", async () => {
|
|
362
384
|
await init(projectName, {
|
|
363
385
|
git: false,
|
|
@@ -439,6 +461,33 @@ describe("standalone mode", () => {
|
|
|
439
461
|
});
|
|
440
462
|
});
|
|
441
463
|
|
|
464
|
+
describe("ai-assist mode", () => {
|
|
465
|
+
const projectName = "test-brief-ai";
|
|
466
|
+
|
|
467
|
+
it("brief template writes AGENTS.md and brief-specific role files", async () => {
|
|
468
|
+
await runTemplate({
|
|
469
|
+
name: projectName,
|
|
470
|
+
template: "brief",
|
|
471
|
+
git: false,
|
|
472
|
+
aiAssist: true,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
expect(generatedExists(projectName, "AGENTS.md")).toBe(true);
|
|
476
|
+
expect(generatedExists(projectName, "config/agentic/roles/devlead.yaml")).toBe(true);
|
|
477
|
+
expect(generatedExists(projectName, "config/agentic/roles/infoarch.yaml")).toBe(true);
|
|
478
|
+
expect(generatedExists(projectName, "config/agentic/roles/qa.yaml")).toBe(true);
|
|
479
|
+
expect(generatedExists(projectName, "config/agentic/roles/prodstrat.yaml")).toBe(true);
|
|
480
|
+
expect(generatedExists(projectName, "config/agentic/roles/advisor.yaml")).toBe(true);
|
|
481
|
+
expect(generatedExists(projectName, "config/agentic/roles/analyst.yaml")).toBe(false);
|
|
482
|
+
expect(generatedExists(projectName, "config/agentic/roles/prodmktg.yaml")).toBe(false);
|
|
483
|
+
|
|
484
|
+
const agents = await readGenerated(projectName, "AGENTS.md");
|
|
485
|
+
expect(agents).toContain("external audience brief");
|
|
486
|
+
expect(agents).toContain("informational and professional");
|
|
487
|
+
expect(agents).toContain("problem -> solution -> outcome");
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
442
491
|
// ---------------------------------------------------------------------------
|
|
443
492
|
// Default Branding Derivation
|
|
444
493
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
type LatexHooks = {
|
|
4
|
+
splitMath: (text: string) => Array<{ text?: string; math?: string; display?: boolean }>;
|
|
5
|
+
isCurrency: (expr: string) => boolean;
|
|
6
|
+
shouldTreatAsLiteralInline: (expr: string, text: string, closingIndex: number) => boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
async function loadHooks(): Promise<LatexHooks> {
|
|
10
|
+
// @ts-expect-error JS plugin helper file
|
|
11
|
+
await import("../../plugins-dist/latex-runtime.js");
|
|
12
|
+
const hooks = (globalThis as any).__kitflyLatexTest as LatexHooks | undefined;
|
|
13
|
+
if (!hooks) throw new Error("latex test hooks not found on globalThis");
|
|
14
|
+
return hooks;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("latex: parses inline math", async () => {
|
|
18
|
+
const hooks = await loadHooks();
|
|
19
|
+
expect(hooks.splitMath("A $x^2$ value")).toEqual([
|
|
20
|
+
{ text: "A " },
|
|
21
|
+
{ math: "x^2", display: false },
|
|
22
|
+
{ text: " value" },
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("latex: treats $5-$10 as literal currency range", async () => {
|
|
27
|
+
const hooks = await loadHooks();
|
|
28
|
+
expect(hooks.splitMath("$5-$10")).toEqual([{ text: "$5-$10" }]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("latex: currency helper matches dashed ranges", async () => {
|
|
32
|
+
const hooks = await loadHooks();
|
|
33
|
+
expect(hooks.isCurrency("5-10")).toBe(true);
|
|
34
|
+
expect(hooks.isCurrency("5 to 10")).toBe(true);
|
|
35
|
+
});
|