kitfly 0.2.1 → 0.2.4
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 +79 -0
- package/README.md +38 -21
- 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/gantt-widget.md +468 -0
- 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 +170 -1
- package/dist/_raw/docs/decisions/ADR-0006-data-driven-content.md +350 -0
- package/dist/content/deployment/preflight.html +11 -8
- package/dist/content/deployment/recipes/aws-s3.html +11 -8
- package/dist/content/deployment/recipes/cloudflare-pages.html +11 -8
- package/dist/content/deployment/recipes/cloudflare-r2.html +11 -8
- package/dist/content/deployment/recipes/fly-io.html +11 -8
- package/dist/content/deployment/recipes/github-pages.html +11 -8
- package/dist/content/deployment/recipes/netlify.html +11 -8
- package/dist/content/deployment/recipes/vercel.html +11 -8
- package/dist/content/deployment/secrets-and-env-vars.html +11 -8
- package/dist/content/deployment.html +11 -8
- package/dist/content/guide/approaches.html +11 -8
- package/dist/content/guide/branding.html +509 -0
- package/dist/content/guide/data-driven-content.html +542 -0
- package/dist/content/guide/features.html +11 -8
- package/dist/content/guide/getting-started.html +11 -8
- package/dist/content/guide/kitfly-overview.html +11 -8
- package/dist/content/reference/configuration.html +136 -11
- package/dist/content/reference/design-catalog.html +11 -8
- package/dist/content/reference/environment-variables.html +51 -10
- package/dist/content/reference/gantt-widget.html +899 -0
- package/dist/content/reference/glossary.html +25 -10
- package/dist/content/reference/key-concepts.html +34 -11
- package/dist/content/reference/plugins.html +261 -10
- package/dist/content/reference/slides-authoring-guidelines.html +11 -8
- package/dist/content/reference/structure.html +11 -8
- package/dist/content/reference.html +11 -8
- package/dist/content/templates/crucible.html +11 -8
- package/dist/content/templates/handbook.html +11 -8
- package/dist/content/templates/minimal.html +11 -8
- package/dist/content/templates/overview.html +11 -8
- package/dist/content/templates/pipeline.html +11 -8
- package/dist/content/templates/productbook.html +11 -8
- package/dist/content/templates/runbook.html +11 -8
- package/dist/content/templates/servicebook.html +11 -8
- package/dist/content-index.json +37 -2
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +11 -8
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +11 -8
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +11 -8
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +11 -8
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +11 -8
- package/dist/docs/decisions/ADR-0006-data-driven-content.html +751 -0
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +11 -8
- package/dist/docs/decisions/DDR-0002-theme-system.html +11 -8
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +11 -8
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +11 -8
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +11 -8
- package/dist/docs/userguide/cli/build.html +11 -8
- package/dist/docs/userguide/cli/bundle.html +11 -8
- package/dist/docs/userguide/cli/dev.html +11 -8
- package/dist/docs/userguide/cli/init.html +11 -8
- package/dist/docs/userguide/cli/servers.html +11 -8
- package/dist/docs/userguide/cli/stop.html +11 -8
- package/dist/docs/userguide/cli/update.html +11 -8
- package/dist/docs/userguide/cli/version.html +11 -8
- package/dist/docs/userguide/cli.html +11 -8
- package/dist/docs/userguide/sharing.html +11 -8
- package/dist/index.html +11 -8
- package/dist/llms.txt +3 -3
- package/dist/provenance.json +4 -5
- package/dist/reports/license-inventory.csv +199 -0
- package/dist/schemas/plugin-registry.schema.html +11 -8
- package/dist/schemas/plugin-schemas-notes.html +11 -8
- package/dist/schemas/plugin.schema.html +11 -8
- package/dist/schemas/plugins.schema.html +11 -8
- package/dist/schemas/v0/common.schema.html +15 -12
- package/dist/schemas/v0/plugin-registry.schema.html +14 -11
- package/dist/schemas/v0/plugin.schema.html +14 -11
- package/dist/schemas/v0/plugins.schema.html +14 -11
- package/dist/schemas/v0/site.schema.html +68 -9
- package/dist/schemas/v0/theme.schema.html +22 -19
- package/dist/schemas.html +11 -8
- 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/planning-visuals.css +261 -0
- package/plugins-dist/planning-visuals.js +669 -0
- package/plugins-dist/slides-charts-lite-runtime.js +179 -0
- package/plugins-dist/slides-charts-lite.js +198 -0
- package/registry/plugins.yaml +40 -1
- package/schemas/v0/site.schema.json +56 -0
- package/scripts/build-all.ts +5 -0
- package/scripts/build.ts +264 -80
- package/scripts/bundle.ts +188 -17
- package/scripts/dev.ts +294 -171
- package/scripts/embed-docs.ts +119 -0
- package/src/__tests__/brief.test.ts +151 -0
- package/src/__tests__/build.test.ts +293 -1
- package/src/__tests__/bundle.test.ts +195 -0
- package/src/__tests__/docs.test.ts +117 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/bad-month-format.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/marker-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/milestone-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/missing-tracks.md +5 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/track-reversed.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-basic.md +15 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-no-milestones.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/month-basic.md +16 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/no-milestones.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/week-basic.md +20 -0
- package/src/__tests__/init.test.ts +51 -2
- package/src/__tests__/latex-runtime.bun.test.ts +35 -0
- package/src/__tests__/planning-visuals-fence-contract.test.ts +28 -0
- package/src/__tests__/planning-visuals-runtime-regressions.bun.test.ts +68 -0
- package/src/__tests__/planning-visuals-runtime.bun.test.ts +192 -0
- package/src/__tests__/shared.test.ts +719 -1
- package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
- package/src/cli.ts +124 -22
- package/src/commands/docs.ts +71 -0
- package/src/commands/init.ts +1 -1
- package/src/generated/embedded-docs.ts +2384 -0
- package/src/server-registry.ts +50 -10
- package/src/shared.ts +1174 -43
- 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
package/scripts/dev.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Options:
|
|
7
7
|
* -p, --port <number> Port to serve on [env: KITFLY_DEV_PORT] [default: 3333]
|
|
8
8
|
* -H, --host <string> Host to bind to [env: KITFLY_DEV_HOST] [default: localhost]
|
|
9
|
+
* --profile <name> Active content profile [env: KITFLY_PROFILE]
|
|
9
10
|
* -o, --open Open browser on start [env: KITFLY_DEV_OPEN] [default: true]
|
|
10
11
|
* --no-open Don't open browser
|
|
11
12
|
* --help Show help message
|
|
@@ -28,14 +29,18 @@ import {
|
|
|
28
29
|
import {
|
|
29
30
|
buildBreadcrumbsSimple,
|
|
30
31
|
buildFooter,
|
|
32
|
+
buildLogoImgHtml,
|
|
31
33
|
buildNavSimple,
|
|
32
34
|
buildPageMeta,
|
|
33
|
-
|
|
35
|
+
buildSlideNavHierarchical,
|
|
34
36
|
buildToc,
|
|
37
|
+
// Types
|
|
38
|
+
type ContentFile,
|
|
35
39
|
// Network utilities
|
|
36
40
|
checkPortOrExit,
|
|
37
41
|
// Navigation/template building
|
|
38
42
|
collectFiles,
|
|
43
|
+
collectPlanningVisualsContainmentWarnings,
|
|
39
44
|
collectSlides,
|
|
40
45
|
envBool,
|
|
41
46
|
envInt,
|
|
@@ -43,23 +48,29 @@ import {
|
|
|
43
48
|
envString,
|
|
44
49
|
// Formatting
|
|
45
50
|
escapeHtml,
|
|
51
|
+
filterByProfile,
|
|
52
|
+
filterUnknownPlanningVisualsTypeDiagnostics,
|
|
46
53
|
filterUnknownSlidesVisualsTypeDiagnostics,
|
|
47
54
|
// Provenance
|
|
48
55
|
generateProvenance,
|
|
56
|
+
loadDataBindings,
|
|
49
57
|
// YAML/Config parsing
|
|
50
58
|
loadSiteConfig,
|
|
59
|
+
mergeFrontmatterWithBody,
|
|
51
60
|
type Provenance,
|
|
61
|
+
pagePathForData,
|
|
52
62
|
// Markdown utilities
|
|
53
63
|
parseFrontmatter,
|
|
54
64
|
parseYaml,
|
|
65
|
+
resolveBindings,
|
|
55
66
|
resolveStylesPath,
|
|
56
67
|
resolveTemplatePath,
|
|
57
68
|
rewriteRelativeAssetUrls,
|
|
58
|
-
|
|
69
|
+
runPrebuildHooks,
|
|
59
70
|
type SiteConfig,
|
|
60
71
|
slugify,
|
|
61
|
-
toUrlPath,
|
|
62
72
|
validatePath,
|
|
73
|
+
validatePlanningVisualsFences,
|
|
63
74
|
validateSlidesVisualsFences,
|
|
64
75
|
} from "../src/shared.ts";
|
|
65
76
|
import { generateThemeCSS, getPrismUrls, loadTheme, type Theme } from "../src/theme.ts";
|
|
@@ -73,6 +84,7 @@ let HOST = DEFAULT_HOST;
|
|
|
73
84
|
let ROOT = process.cwd();
|
|
74
85
|
let OPEN_BROWSER = true;
|
|
75
86
|
let LOG_FORMAT = ""; // "structured" when invoked by CLI daemon
|
|
87
|
+
let ACTIVE_PROFILE: string | undefined;
|
|
76
88
|
|
|
77
89
|
// Structured logger for daemon mode — set during main() init.
|
|
78
90
|
// When null, all output goes through console.log (standalone mode).
|
|
@@ -82,6 +94,32 @@ let daemonLog: {
|
|
|
82
94
|
error: (msg: string) => void;
|
|
83
95
|
} | null = null;
|
|
84
96
|
|
|
97
|
+
async function applyDataBindingsToMarkdown(
|
|
98
|
+
rawMarkdown: string,
|
|
99
|
+
filePath: string,
|
|
100
|
+
config: SiteConfig,
|
|
101
|
+
): Promise<{ frontmatter: Record<string, unknown>; body: string }> {
|
|
102
|
+
const parsed = parseFrontmatter(rawMarkdown);
|
|
103
|
+
const dataRef = typeof parsed.frontmatter.data === "string" ? parsed.frontmatter.data.trim() : "";
|
|
104
|
+
if (!dataRef) return parsed;
|
|
105
|
+
|
|
106
|
+
const pagePath = pagePathForData(ROOT, config.docroot, filePath);
|
|
107
|
+
const bindings = await loadDataBindings(dataRef, pagePath, ROOT, config.docroot, config.dataroot);
|
|
108
|
+
return {
|
|
109
|
+
frontmatter: parsed.frontmatter,
|
|
110
|
+
body: resolveBindings(parsed.body, bindings, pagePath),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function applyDataBindingsForSlides(
|
|
115
|
+
rawMarkdown: string,
|
|
116
|
+
filePath: string,
|
|
117
|
+
config: SiteConfig,
|
|
118
|
+
): Promise<string> {
|
|
119
|
+
const resolved = await applyDataBindingsToMarkdown(rawMarkdown, filePath, config);
|
|
120
|
+
return mergeFrontmatterWithBody(rawMarkdown, resolved.body);
|
|
121
|
+
}
|
|
122
|
+
|
|
85
123
|
function isPluginLoaderError(error: unknown): error is Error {
|
|
86
124
|
return (
|
|
87
125
|
error instanceof PluginConfigError ||
|
|
@@ -158,6 +196,7 @@ interface ParsedArgs {
|
|
|
158
196
|
open?: boolean;
|
|
159
197
|
folder?: string;
|
|
160
198
|
logFormat?: string;
|
|
199
|
+
profile?: string;
|
|
161
200
|
}
|
|
162
201
|
|
|
163
202
|
function parseArgs(argv: string[]): ParsedArgs {
|
|
@@ -175,6 +214,9 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
175
214
|
} else if (arg === "--log-format") {
|
|
176
215
|
result.logFormat = next;
|
|
177
216
|
i++;
|
|
217
|
+
} else if (arg === "--profile" && next && !next.startsWith("-")) {
|
|
218
|
+
result.profile = next;
|
|
219
|
+
i++;
|
|
178
220
|
} else if (arg === "--open" || arg === "-o") {
|
|
179
221
|
result.open = true;
|
|
180
222
|
} else if (arg === "--no-open") {
|
|
@@ -192,6 +234,7 @@ function getConfig(): {
|
|
|
192
234
|
open: boolean;
|
|
193
235
|
folder?: string;
|
|
194
236
|
logFormat?: string;
|
|
237
|
+
profile?: string;
|
|
195
238
|
} {
|
|
196
239
|
const args = parseArgs(process.argv.slice(2));
|
|
197
240
|
return {
|
|
@@ -200,9 +243,15 @@ function getConfig(): {
|
|
|
200
243
|
open: args.open ?? envBool("KITFLY_DEV_OPEN", true),
|
|
201
244
|
folder: args.folder,
|
|
202
245
|
logFormat: args.logFormat,
|
|
246
|
+
profile: args.profile ?? process.env.KITFLY_PROFILE,
|
|
203
247
|
};
|
|
204
248
|
}
|
|
205
249
|
|
|
250
|
+
async function getFilteredFiles(config: SiteConfig): Promise<ContentFile[]> {
|
|
251
|
+
const files = await collectFiles(ROOT, config);
|
|
252
|
+
return filterByProfile(files, ACTIVE_PROFILE, config.profiles);
|
|
253
|
+
}
|
|
254
|
+
|
|
206
255
|
function getContentType(filePath: string): string {
|
|
207
256
|
const ext = extname(filePath).toLowerCase();
|
|
208
257
|
switch (ext) {
|
|
@@ -256,15 +305,23 @@ const clients: Set<ReadableStreamDefaultController> = new Set();
|
|
|
256
305
|
|
|
257
306
|
let pluginCache: { key: string; head: string; bodyEnd: string } | null = null;
|
|
258
307
|
|
|
259
|
-
async function
|
|
308
|
+
async function getFenceValidationFlags(): Promise<{
|
|
309
|
+
slidesVisuals: boolean;
|
|
310
|
+
planningVisuals: boolean;
|
|
311
|
+
}> {
|
|
260
312
|
const configPath = join(ROOT, "kitfly.plugins.yaml");
|
|
261
313
|
try {
|
|
262
314
|
const raw = await readFile(configPath, "utf-8");
|
|
263
315
|
const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
|
|
264
316
|
const plugins = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
|
|
265
|
-
return
|
|
317
|
+
return {
|
|
318
|
+
slidesVisuals: plugins.some((p) => typeof p === "string" && p.startsWith("slides-visuals@")),
|
|
319
|
+
planningVisuals: plugins.some(
|
|
320
|
+
(p) => typeof p === "string" && p.startsWith("planning-visuals@"),
|
|
321
|
+
),
|
|
322
|
+
};
|
|
266
323
|
} catch {
|
|
267
|
-
return false;
|
|
324
|
+
return { slidesVisuals: false, planningVisuals: false };
|
|
268
325
|
}
|
|
269
326
|
}
|
|
270
327
|
|
|
@@ -349,24 +406,41 @@ async function renderPage(
|
|
|
349
406
|
}
|
|
350
407
|
htmlContent = `<h1>${title}</h1>\n<pre><code class="language-json">${escapeHtml(prettyJson)}</code></pre>`;
|
|
351
408
|
} else {
|
|
352
|
-
const { frontmatter, body } =
|
|
409
|
+
const { frontmatter, body } = await applyDataBindingsToMarkdown(content, filePath, config);
|
|
353
410
|
if (frontmatter.title) {
|
|
354
411
|
title = frontmatter.title as string;
|
|
355
412
|
}
|
|
356
413
|
pageMeta = buildPageMeta(frontmatter);
|
|
414
|
+
const fenceValidation = await getFenceValidationFlags();
|
|
415
|
+
if (fenceValidation.planningVisuals) {
|
|
416
|
+
const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
|
|
417
|
+
validatePlanningVisualsFences(body),
|
|
418
|
+
);
|
|
419
|
+
if (diagnostics.length) {
|
|
420
|
+
const msg = diagnostics
|
|
421
|
+
.slice(0, 12)
|
|
422
|
+
.map((d) => ` - ${filePath}:${d.line} ${d.message}`)
|
|
423
|
+
.join("\n");
|
|
424
|
+
throw new Error(`planning-visuals fence contract violations:\n${msg}`);
|
|
425
|
+
}
|
|
426
|
+
const warnings = collectPlanningVisualsContainmentWarnings(body);
|
|
427
|
+
for (const warning of warnings.slice(0, 12)) {
|
|
428
|
+
logWarn(`${filePath}:${warning.line} ${warning.message}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
357
431
|
htmlContent = marked.parse(body) as string;
|
|
358
432
|
}
|
|
359
433
|
|
|
360
|
-
const files = await
|
|
434
|
+
const files = await getFilteredFiles(config);
|
|
361
435
|
const currentUrlPath = urlPath.slice(1).replace(/\.html$/, "");
|
|
436
|
+
const pathPrefix = "/";
|
|
362
437
|
const nav = buildNavSimple(files, config, currentUrlPath);
|
|
363
|
-
const footer = buildFooter(provenance, config);
|
|
438
|
+
const footer = buildFooter(provenance, config, pathPrefix);
|
|
364
439
|
const breadcrumbs = buildBreadcrumbsSimple(urlPath, files, config);
|
|
365
440
|
const toc = buildToc(htmlContent);
|
|
366
441
|
const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
|
|
367
442
|
const themeCSS = generateThemeCSS(theme);
|
|
368
443
|
const prismUrls = getPrismUrls(theme);
|
|
369
|
-
const pathPrefix = "/";
|
|
370
444
|
const plugins = await getPluginInjectionsCached(config.mode === "slides" ? "slides" : "docs");
|
|
371
445
|
|
|
372
446
|
const hotReloadScript = `
|
|
@@ -378,33 +452,51 @@ async function renderPage(
|
|
|
378
452
|
|
|
379
453
|
const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
|
|
380
454
|
const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
|
|
455
|
+
const mobileLogoHtml = buildLogoImgHtml({
|
|
456
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
457
|
+
logoDark: config.brand.logoDark,
|
|
458
|
+
alt: config.brand.name,
|
|
459
|
+
className: `logo-img ${logoClass}`,
|
|
460
|
+
pathPrefix,
|
|
461
|
+
onerrorFallback: true,
|
|
462
|
+
});
|
|
463
|
+
const sidebarLogoHtml = buildLogoImgHtml({
|
|
464
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
465
|
+
logoDark: config.brand.logoDark,
|
|
466
|
+
alt: config.brand.name,
|
|
467
|
+
className: "logo-img",
|
|
468
|
+
pathPrefix,
|
|
469
|
+
onerrorFallback: true,
|
|
470
|
+
});
|
|
381
471
|
|
|
382
472
|
return template
|
|
383
473
|
.replace("{{BODY_CLASS}}", "mode-docs")
|
|
384
|
-
.replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
|
|
385
|
-
.replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
|
|
386
|
-
.replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
|
|
387
|
-
.replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
|
|
388
|
-
.replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
|
|
389
|
-
.replace(
|
|
390
|
-
.replace(
|
|
391
|
-
.replace(/\{\{
|
|
392
|
-
.replace(/\{\{
|
|
393
|
-
.replace(
|
|
394
|
-
.replace(
|
|
395
|
-
.replace("{{
|
|
396
|
-
.replace("{{
|
|
397
|
-
.replace("{{
|
|
398
|
-
.replace("{{
|
|
399
|
-
.replace("{{
|
|
400
|
-
.replace("{{
|
|
401
|
-
.replace("{{
|
|
402
|
-
.replace("{{
|
|
403
|
-
.replace("{{
|
|
404
|
-
.replace("{{
|
|
405
|
-
.replace("{{
|
|
406
|
-
.replace("{{
|
|
407
|
-
.replace("{{
|
|
474
|
+
.replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
|
|
475
|
+
.replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
|
|
476
|
+
.replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
|
|
477
|
+
.replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
|
|
478
|
+
.replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
|
|
479
|
+
.replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
|
|
480
|
+
.replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
|
|
481
|
+
.replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
|
|
482
|
+
.replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
|
|
483
|
+
.replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
|
|
484
|
+
.replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
|
|
485
|
+
.replace("{{TITLE}}", () => title)
|
|
486
|
+
.replace("{{VERSION}}", () => uiVersion)
|
|
487
|
+
.replace("{{BRANCH}}", () => provenance.gitBranch)
|
|
488
|
+
.replace("{{BREADCRUMBS}}", () => breadcrumbs)
|
|
489
|
+
.replace("{{PAGE_META}}", () => pageMeta)
|
|
490
|
+
.replace("{{NAV}}", () => nav)
|
|
491
|
+
.replace("{{CONTENT}}", () => htmlContent)
|
|
492
|
+
.replace("{{TOC}}", () => toc)
|
|
493
|
+
.replace("{{FOOTER}}", () => footer)
|
|
494
|
+
.replace("{{THEME_CSS}}", () => themeCSS)
|
|
495
|
+
.replace("{{PLUGIN_HEAD}}", () => plugins.head)
|
|
496
|
+
.replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
|
|
497
|
+
.replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
|
|
498
|
+
.replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
|
|
499
|
+
.replace("{{HOT_RELOAD_SCRIPT}}", () => hotReloadScript);
|
|
408
500
|
}
|
|
409
501
|
|
|
410
502
|
async function renderSlidesPage(
|
|
@@ -414,20 +506,22 @@ async function renderSlidesPage(
|
|
|
414
506
|
): Promise<string> {
|
|
415
507
|
const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
|
|
416
508
|
const template = await readFile(await resolveTemplatePath(ROOT), "utf-8");
|
|
417
|
-
const files = await
|
|
418
|
-
const slides = await collectSlides(files
|
|
509
|
+
const files = await getFilteredFiles(config);
|
|
510
|
+
const slides = await collectSlides(files, {
|
|
511
|
+
markdownTransform: (raw, file) => applyDataBindingsForSlides(raw, file.path, config),
|
|
512
|
+
});
|
|
419
513
|
|
|
420
514
|
if (slides.length === 0) {
|
|
421
515
|
return renderGettingStarted(provenance, config, theme);
|
|
422
516
|
}
|
|
423
517
|
const pathPrefix = "/";
|
|
424
|
-
const
|
|
518
|
+
const fenceValidation = await getFenceValidationFlags();
|
|
425
519
|
|
|
426
520
|
const sections = await Promise.all(
|
|
427
521
|
slides.map(async (slide, i) => {
|
|
428
522
|
let inner = "";
|
|
429
523
|
if (slide.kind === "markdown") {
|
|
430
|
-
if (
|
|
524
|
+
if (fenceValidation.slidesVisuals) {
|
|
431
525
|
const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
|
|
432
526
|
validateSlidesVisualsFences(slide.body),
|
|
433
527
|
);
|
|
@@ -439,6 +533,22 @@ async function renderSlidesPage(
|
|
|
439
533
|
throw new Error(`slides-visuals fence contract violations:\n${msg}`);
|
|
440
534
|
}
|
|
441
535
|
}
|
|
536
|
+
if (fenceValidation.planningVisuals) {
|
|
537
|
+
const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
|
|
538
|
+
validatePlanningVisualsFences(slide.body),
|
|
539
|
+
);
|
|
540
|
+
if (diagnostics.length) {
|
|
541
|
+
const msg = diagnostics
|
|
542
|
+
.slice(0, 12)
|
|
543
|
+
.map((d) => ` - ${slide.sourcePath}:${d.line} ${d.message}`)
|
|
544
|
+
.join("\n");
|
|
545
|
+
throw new Error(`planning-visuals fence contract violations:\n${msg}`);
|
|
546
|
+
}
|
|
547
|
+
const warnings = collectPlanningVisualsContainmentWarnings(slide.body);
|
|
548
|
+
for (const warning of warnings.slice(0, 12)) {
|
|
549
|
+
logWarn(`${slide.sourcePath}:${warning.line} ${warning.message}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
442
552
|
inner = marked.parse(slide.body) as string;
|
|
443
553
|
} else if (slide.kind === "yaml") {
|
|
444
554
|
inner = `<pre><code class="language-yaml">${escapeHtml(slide.body)}</code></pre>`;
|
|
@@ -476,8 +586,8 @@ async function renderSlidesPage(
|
|
|
476
586
|
</div>
|
|
477
587
|
</div>`;
|
|
478
588
|
|
|
479
|
-
const nav =
|
|
480
|
-
const footer = buildFooter(provenance, config);
|
|
589
|
+
const nav = buildSlideNavHierarchical(slides, config, "slide-1");
|
|
590
|
+
const footer = buildFooter(provenance, config, pathPrefix);
|
|
481
591
|
const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
|
|
482
592
|
const themeCSS = generateThemeCSS(theme);
|
|
483
593
|
const prismUrls = getPrismUrls(theme);
|
|
@@ -490,33 +600,51 @@ async function renderSlidesPage(
|
|
|
490
600
|
const plugins = await getPluginInjectionsCached("slides");
|
|
491
601
|
const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
|
|
492
602
|
const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
|
|
603
|
+
const mobileLogoHtml = buildLogoImgHtml({
|
|
604
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
605
|
+
logoDark: config.brand.logoDark,
|
|
606
|
+
alt: config.brand.name,
|
|
607
|
+
className: `logo-img ${logoClass}`,
|
|
608
|
+
pathPrefix,
|
|
609
|
+
onerrorFallback: true,
|
|
610
|
+
});
|
|
611
|
+
const sidebarLogoHtml = buildLogoImgHtml({
|
|
612
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
613
|
+
logoDark: config.brand.logoDark,
|
|
614
|
+
alt: config.brand.name,
|
|
615
|
+
className: "logo-img",
|
|
616
|
+
pathPrefix,
|
|
617
|
+
onerrorFallback: true,
|
|
618
|
+
});
|
|
493
619
|
|
|
494
620
|
return template
|
|
495
621
|
.replace("{{BODY_CLASS}}", "mode-slides")
|
|
496
|
-
.replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
|
|
497
|
-
.replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
|
|
498
|
-
.replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
|
|
499
|
-
.replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
|
|
500
|
-
.replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
|
|
501
|
-
.replace(
|
|
502
|
-
.replace(
|
|
503
|
-
.replace(/\{\{
|
|
504
|
-
.replace(/\{\{
|
|
505
|
-
.replace(
|
|
506
|
-
.replace(
|
|
507
|
-
.replace("{{
|
|
622
|
+
.replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
|
|
623
|
+
.replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
|
|
624
|
+
.replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
|
|
625
|
+
.replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
|
|
626
|
+
.replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
|
|
627
|
+
.replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
|
|
628
|
+
.replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
|
|
629
|
+
.replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
|
|
630
|
+
.replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
|
|
631
|
+
.replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
|
|
632
|
+
.replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
|
|
633
|
+
.replace("{{TITLE}}", () => config.title)
|
|
634
|
+
.replace("{{VERSION}}", () => uiVersion)
|
|
635
|
+
.replace("{{BRANCH}}", () => provenance.gitBranch)
|
|
508
636
|
.replace("{{BREADCRUMBS}}", "")
|
|
509
637
|
.replace("{{PAGE_META}}", "")
|
|
510
|
-
.replace("{{NAV}}", nav)
|
|
511
|
-
.replace("{{CONTENT}}", htmlContent)
|
|
638
|
+
.replace("{{NAV}}", () => nav)
|
|
639
|
+
.replace("{{CONTENT}}", () => htmlContent)
|
|
512
640
|
.replace("{{TOC}}", "")
|
|
513
|
-
.replace("{{FOOTER}}", footer)
|
|
514
|
-
.replace("{{THEME_CSS}}", themeCSS)
|
|
515
|
-
.replace("{{PLUGIN_HEAD}}", plugins.head)
|
|
516
|
-
.replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
|
|
517
|
-
.replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
|
|
518
|
-
.replace("{{PRISM_DARK_URL}}", prismUrls.dark)
|
|
519
|
-
.replace("{{HOT_RELOAD_SCRIPT}}", hotReloadScript);
|
|
641
|
+
.replace("{{FOOTER}}", () => footer)
|
|
642
|
+
.replace("{{THEME_CSS}}", () => themeCSS)
|
|
643
|
+
.replace("{{PLUGIN_HEAD}}", () => plugins.head)
|
|
644
|
+
.replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
|
|
645
|
+
.replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
|
|
646
|
+
.replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
|
|
647
|
+
.replace("{{HOT_RELOAD_SCRIPT}}", () => hotReloadScript);
|
|
520
648
|
}
|
|
521
649
|
|
|
522
650
|
// Render Getting Started page when no config
|
|
@@ -565,33 +693,51 @@ sections:
|
|
|
565
693
|
|
|
566
694
|
const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
|
|
567
695
|
const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
|
|
696
|
+
const mobileLogoHtml = buildLogoImgHtml({
|
|
697
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
698
|
+
logoDark: config.brand.logoDark,
|
|
699
|
+
alt: config.brand.name,
|
|
700
|
+
className: `logo-img ${logoClass}`,
|
|
701
|
+
pathPrefix,
|
|
702
|
+
onerrorFallback: true,
|
|
703
|
+
});
|
|
704
|
+
const sidebarLogoHtml = buildLogoImgHtml({
|
|
705
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
706
|
+
logoDark: config.brand.logoDark,
|
|
707
|
+
alt: config.brand.name,
|
|
708
|
+
className: "logo-img",
|
|
709
|
+
pathPrefix,
|
|
710
|
+
onerrorFallback: true,
|
|
711
|
+
});
|
|
568
712
|
|
|
569
713
|
return template
|
|
570
714
|
.replace("{{BODY_CLASS}}", "mode-docs")
|
|
571
|
-
.replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
|
|
572
|
-
.replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
|
|
573
|
-
.replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
|
|
574
|
-
.replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
|
|
575
|
-
.replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
|
|
576
|
-
.replace(
|
|
577
|
-
.replace(
|
|
578
|
-
.replace(/\{\{
|
|
579
|
-
.replace(/\{\{
|
|
715
|
+
.replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
|
|
716
|
+
.replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
|
|
717
|
+
.replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
|
|
718
|
+
.replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
|
|
719
|
+
.replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
|
|
720
|
+
.replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
|
|
721
|
+
.replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
|
|
722
|
+
.replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
|
|
723
|
+
.replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
|
|
724
|
+
.replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
|
|
725
|
+
.replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
|
|
580
726
|
.replace("{{TITLE}}", "Getting Started")
|
|
581
|
-
.replace("{{VERSION}}", uiVersion)
|
|
582
|
-
.replace("{{BRANCH}}", provenance.gitBranch)
|
|
727
|
+
.replace("{{VERSION}}", () => uiVersion)
|
|
728
|
+
.replace("{{BRANCH}}", () => provenance.gitBranch)
|
|
583
729
|
.replace("{{BREADCRUMBS}}", "")
|
|
584
730
|
.replace("{{PAGE_META}}", "")
|
|
585
731
|
.replace("{{NAV}}", "<ul></ul>")
|
|
586
|
-
.replace("{{CONTENT}}", htmlContent)
|
|
732
|
+
.replace("{{CONTENT}}", () => htmlContent)
|
|
587
733
|
.replace("{{TOC}}", "")
|
|
588
|
-
.replace("{{FOOTER}}", buildFooter(provenance, config))
|
|
589
|
-
.replace("{{THEME_CSS}}", themeCSS)
|
|
590
|
-
.replace("{{PLUGIN_HEAD}}", plugins.head)
|
|
591
|
-
.replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
|
|
592
|
-
.replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
|
|
593
|
-
.replace("{{PRISM_DARK_URL}}", prismUrls.dark)
|
|
594
|
-
.replace("{{HOT_RELOAD_SCRIPT}}", hotReloadScript);
|
|
734
|
+
.replace("{{FOOTER}}", () => buildFooter(provenance, config, pathPrefix))
|
|
735
|
+
.replace("{{THEME_CSS}}", () => themeCSS)
|
|
736
|
+
.replace("{{PLUGIN_HEAD}}", () => plugins.head)
|
|
737
|
+
.replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
|
|
738
|
+
.replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
|
|
739
|
+
.replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
|
|
740
|
+
.replace("{{HOT_RELOAD_SCRIPT}}", () => hotReloadScript);
|
|
595
741
|
}
|
|
596
742
|
|
|
597
743
|
async function tryServeFile(filePath: string): Promise<Response | null> {
|
|
@@ -626,9 +772,11 @@ async function tryServeContentAsset(
|
|
|
626
772
|
}
|
|
627
773
|
|
|
628
774
|
// Find file for a URL path
|
|
629
|
-
async function findFile(
|
|
630
|
-
|
|
631
|
-
|
|
775
|
+
async function findFile(
|
|
776
|
+
urlPath: string,
|
|
777
|
+
config: SiteConfig,
|
|
778
|
+
files: ContentFile[],
|
|
779
|
+
): Promise<string | null> {
|
|
632
780
|
// Remove leading slash and .html extension (for compatibility with built links)
|
|
633
781
|
const path = urlPath.slice(1).replace(/\.html$/, "") || "";
|
|
634
782
|
|
|
@@ -636,84 +784,20 @@ async function findFile(urlPath: string, config: SiteConfig): Promise<string | n
|
|
|
636
784
|
if (!path) {
|
|
637
785
|
if (config.home) {
|
|
638
786
|
const homePath = validatePath(ROOT, config.docroot, config.home, true);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
await stat(homePath);
|
|
642
|
-
return homePath;
|
|
643
|
-
} catch {
|
|
644
|
-
// Home file not found, fall through
|
|
645
|
-
}
|
|
646
|
-
}
|
|
787
|
+
const homeFile = homePath ? files.find((file) => file.path === homePath) : undefined;
|
|
788
|
+
if (homeFile) return homeFile.path;
|
|
647
789
|
}
|
|
648
790
|
// Fallback to first file
|
|
649
|
-
const files = await collectFiles(ROOT, config);
|
|
650
791
|
return files.length > 0 ? files[0].path : null;
|
|
651
792
|
}
|
|
652
793
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
for (const file of section.files) {
|
|
661
|
-
const name = file.replace(/\.(md|yaml|json)$/, "").toLowerCase();
|
|
662
|
-
if (name === path) {
|
|
663
|
-
const filePath = join(sectionPath, file);
|
|
664
|
-
try {
|
|
665
|
-
await stat(filePath);
|
|
666
|
-
return filePath;
|
|
667
|
-
} catch {
|
|
668
|
-
// Continue
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
} else {
|
|
673
|
-
// Check directory for matching file (supports nested paths)
|
|
674
|
-
const urlBase = toUrlPath(ROOT, sectionPath);
|
|
675
|
-
if (path.startsWith(`${urlBase}/`) || path === urlBase) {
|
|
676
|
-
const relPath = path === urlBase ? "" : path.slice(urlBase.length + 1);
|
|
677
|
-
// Guard against path traversal
|
|
678
|
-
if (relPath.includes("..")) continue;
|
|
679
|
-
const extensions = [".md", ".yaml", ".json"];
|
|
680
|
-
|
|
681
|
-
if (relPath === "") {
|
|
682
|
-
// Section root URL — try index file
|
|
683
|
-
for (const ext of extensions) {
|
|
684
|
-
const filePath = join(sectionPath, `index${ext}`);
|
|
685
|
-
try {
|
|
686
|
-
await stat(filePath);
|
|
687
|
-
return filePath;
|
|
688
|
-
} catch {
|
|
689
|
-
// Continue
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
} else {
|
|
693
|
-
// Try direct file match at nested path
|
|
694
|
-
for (const ext of extensions) {
|
|
695
|
-
const filePath = join(sectionPath, relPath + ext);
|
|
696
|
-
try {
|
|
697
|
-
await stat(filePath);
|
|
698
|
-
return filePath;
|
|
699
|
-
} catch {
|
|
700
|
-
// Continue
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
// Try as directory with index file
|
|
704
|
-
for (const ext of extensions) {
|
|
705
|
-
const filePath = join(sectionPath, relPath, `index${ext}`);
|
|
706
|
-
try {
|
|
707
|
-
await stat(filePath);
|
|
708
|
-
return filePath;
|
|
709
|
-
} catch {
|
|
710
|
-
// Continue
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
794
|
+
const directMatch = files.find((file) => file.urlPath === path);
|
|
795
|
+
if (directMatch) return directMatch.path;
|
|
796
|
+
|
|
797
|
+
const sectionIndex = files.find(
|
|
798
|
+
(file) => file.urlPath === `${path}/index` || file.urlPath === path,
|
|
799
|
+
);
|
|
800
|
+
if (sectionIndex) return sectionIndex.path;
|
|
717
801
|
|
|
718
802
|
return null;
|
|
719
803
|
}
|
|
@@ -750,17 +834,35 @@ function startWatcher(config: SiteConfig) {
|
|
|
750
834
|
for (const dir of watchDirs) {
|
|
751
835
|
try {
|
|
752
836
|
watch(dir, { recursive: true }, (_event, filename) => {
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
837
|
+
if (!filename) return;
|
|
838
|
+
void (async () => {
|
|
839
|
+
try {
|
|
840
|
+
let hooksRan = 0;
|
|
841
|
+
if (config.prebuild?.length) {
|
|
842
|
+
hooksRan = await runPrebuildHooks(
|
|
843
|
+
config.prebuild,
|
|
844
|
+
ROOT,
|
|
845
|
+
"dev",
|
|
846
|
+
ACTIVE_PROFILE,
|
|
847
|
+
config.dataroot || "data",
|
|
848
|
+
filename,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const shouldReload =
|
|
852
|
+
hooksRan > 0 ||
|
|
853
|
+
filename.endsWith(".md") ||
|
|
854
|
+
filename.endsWith(".yaml") ||
|
|
855
|
+
filename.endsWith(".json") ||
|
|
856
|
+
filename.endsWith(".html") ||
|
|
857
|
+
filename.endsWith(".css");
|
|
858
|
+
if (shouldReload) {
|
|
859
|
+
logInfo(`File changed: ${filename}`);
|
|
860
|
+
notifyReload();
|
|
861
|
+
}
|
|
862
|
+
} catch (error) {
|
|
863
|
+
logWarn(error instanceof Error ? error.message : String(error));
|
|
864
|
+
}
|
|
865
|
+
})();
|
|
764
866
|
});
|
|
765
867
|
} catch {
|
|
766
868
|
// Directory doesn't exist, skip
|
|
@@ -784,6 +886,10 @@ async function main() {
|
|
|
784
886
|
// Load configuration
|
|
785
887
|
const config = await loadSiteConfig(ROOT);
|
|
786
888
|
logInfo(`Loaded config: "${config.title}" (${config.sections.length} sections)`);
|
|
889
|
+
if (config.prebuild?.length) {
|
|
890
|
+
await runPrebuildHooks(config.prebuild, ROOT, "dev", ACTIVE_PROFILE, config.dataroot || "data");
|
|
891
|
+
logInfo(`Ran prebuild hooks (${config.prebuild.length})`);
|
|
892
|
+
}
|
|
787
893
|
|
|
788
894
|
// Apply server config from site.yaml if CLI didn't override
|
|
789
895
|
if (config.server?.port && PORT === DEFAULT_PORT) {
|
|
@@ -858,7 +964,7 @@ async function main() {
|
|
|
858
964
|
if (assetResponse) return assetResponse;
|
|
859
965
|
|
|
860
966
|
// Check for content
|
|
861
|
-
const files = await
|
|
967
|
+
const files = await getFilteredFiles(config);
|
|
862
968
|
if (files.length === 0) {
|
|
863
969
|
// No content - render Getting Started page
|
|
864
970
|
const html = await renderGettingStarted(provenance, config, theme);
|
|
@@ -876,7 +982,7 @@ async function main() {
|
|
|
876
982
|
}
|
|
877
983
|
|
|
878
984
|
// Find and render markdown/yaml file
|
|
879
|
-
const filePath = await findFile(url.pathname, config);
|
|
985
|
+
const filePath = await findFile(url.pathname, config, files);
|
|
880
986
|
if (filePath) {
|
|
881
987
|
// If this is an index/readme file and the URL lacks a trailing slash,
|
|
882
988
|
// redirect so relative links resolve correctly (BUG-003)
|
|
@@ -998,9 +1104,22 @@ async function main() {
|
|
|
998
1104
|
}
|
|
999
1105
|
}
|
|
1000
1106
|
|
|
1001
|
-
// Open browser (
|
|
1107
|
+
// Open browser (cross-platform)
|
|
1002
1108
|
if (OPEN_BROWSER) {
|
|
1003
|
-
|
|
1109
|
+
try {
|
|
1110
|
+
if (process.platform === "win32") {
|
|
1111
|
+
// cmd.exe built-in: start
|
|
1112
|
+
// Empty title argument avoids treating URL as window title.
|
|
1113
|
+
Bun.spawn(["cmd", "/c", "start", "", serverUrl]);
|
|
1114
|
+
} else if (process.platform === "darwin") {
|
|
1115
|
+
Bun.spawn(["open", serverUrl]);
|
|
1116
|
+
} else {
|
|
1117
|
+
// Most Linux distros
|
|
1118
|
+
Bun.spawn(["xdg-open", serverUrl]);
|
|
1119
|
+
}
|
|
1120
|
+
} catch {
|
|
1121
|
+
// Non-fatal: server is already running; user can open manually.
|
|
1122
|
+
}
|
|
1004
1123
|
}
|
|
1005
1124
|
}
|
|
1006
1125
|
|
|
@@ -1011,6 +1130,7 @@ export interface DevOptions {
|
|
|
1011
1130
|
host?: string;
|
|
1012
1131
|
open?: boolean;
|
|
1013
1132
|
logFormat?: string;
|
|
1133
|
+
profile?: string;
|
|
1014
1134
|
}
|
|
1015
1135
|
|
|
1016
1136
|
export async function dev(options: DevOptions = {}) {
|
|
@@ -1029,6 +1149,7 @@ export async function dev(options: DevOptions = {}) {
|
|
|
1029
1149
|
if (options.logFormat) {
|
|
1030
1150
|
LOG_FORMAT = options.logFormat;
|
|
1031
1151
|
}
|
|
1152
|
+
ACTIVE_PROFILE = options.profile;
|
|
1032
1153
|
await main();
|
|
1033
1154
|
}
|
|
1034
1155
|
|
|
@@ -1042,6 +1163,7 @@ Usage: bun run dev [folder] [options]
|
|
|
1042
1163
|
Options:
|
|
1043
1164
|
-p, --port <number> Port to serve on [env: KITFLY_DEV_PORT] [default: ${DEFAULT_PORT}]
|
|
1044
1165
|
-H, --host <string> Host to bind to [env: KITFLY_DEV_HOST] [default: ${DEFAULT_HOST}]
|
|
1166
|
+
--profile <name> Active content profile [env: KITFLY_PROFILE]
|
|
1045
1167
|
-o, --open Open browser on start [env: KITFLY_DEV_OPEN] [default: true]
|
|
1046
1168
|
--no-open Don't open browser
|
|
1047
1169
|
--help Show this help message
|
|
@@ -1063,5 +1185,6 @@ Examples:
|
|
|
1063
1185
|
host: cfg.host,
|
|
1064
1186
|
open: cfg.open,
|
|
1065
1187
|
logFormat: cfg.logFormat,
|
|
1188
|
+
profile: cfg.profile,
|
|
1066
1189
|
}).catch(console.error);
|
|
1067
1190
|
}
|