kitfly 0.1.2
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 +60 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/VERSION +1 -0
- package/package.json +63 -0
- package/schemas/README.md +32 -0
- package/schemas/site.schema.json +5 -0
- package/schemas/theme.schema.json +5 -0
- package/schemas/v0/site.schema.json +172 -0
- package/schemas/v0/theme.schema.json +210 -0
- package/scripts/build-all.ts +121 -0
- package/scripts/build.ts +601 -0
- package/scripts/bundle.ts +781 -0
- package/scripts/dev.ts +777 -0
- package/scripts/generate-checksums.sh +78 -0
- package/scripts/release/export-release-key.sh +28 -0
- package/scripts/release/release-guard-tag-version.sh +79 -0
- package/scripts/release/sign-release-assets.sh +123 -0
- package/scripts/release/upload-release-assets.sh +76 -0
- package/scripts/release/upload-release-provenance.sh +52 -0
- package/scripts/release/verify-public-key.sh +48 -0
- package/scripts/release/verify-signatures.sh +117 -0
- package/scripts/version-sync.ts +82 -0
- package/src/__tests__/build.test.ts +240 -0
- package/src/__tests__/bundle.test.ts +786 -0
- package/src/__tests__/cli.test.ts +706 -0
- package/src/__tests__/crucible.test.ts +1043 -0
- package/src/__tests__/engine.test.ts +157 -0
- package/src/__tests__/init.test.ts +450 -0
- package/src/__tests__/pipeline.test.ts +1087 -0
- package/src/__tests__/productbook.test.ts +1206 -0
- package/src/__tests__/runbook.test.ts +974 -0
- package/src/__tests__/server-registry.test.ts +1251 -0
- package/src/__tests__/servicebook.test.ts +1248 -0
- package/src/__tests__/shared.test.ts +2005 -0
- package/src/__tests__/styles.test.ts +14 -0
- package/src/__tests__/theme-schema.test.ts +47 -0
- package/src/__tests__/theme.test.ts +554 -0
- package/src/cli.ts +582 -0
- package/src/commands/init.ts +92 -0
- package/src/commands/update.ts +444 -0
- package/src/engine.ts +20 -0
- package/src/logger.ts +15 -0
- package/src/migrations/0000_schema_versioning.ts +67 -0
- package/src/migrations/0001_server_port.ts +52 -0
- package/src/migrations/0002_brand_logo.ts +49 -0
- package/src/migrations/index.ts +26 -0
- package/src/migrations/schema.ts +24 -0
- package/src/server-registry.ts +405 -0
- package/src/shared.ts +1239 -0
- package/src/site/styles.css +931 -0
- package/src/site/template.html +193 -0
- package/src/templates/crucible.ts +1163 -0
- package/src/templates/driver.ts +876 -0
- package/src/templates/handbook.ts +339 -0
- package/src/templates/minimal.ts +139 -0
- package/src/templates/pipeline.ts +966 -0
- package/src/templates/productbook.ts +1032 -0
- package/src/templates/runbook.ts +829 -0
- package/src/templates/schema.ts +119 -0
- package/src/templates/servicebook.ts +1242 -0
- package/src/theme.ts +245 -0
package/scripts/build.ts
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build static site from markdown files
|
|
3
|
+
*
|
|
4
|
+
* Usage: bun run build [folder] [options]
|
|
5
|
+
*
|
|
6
|
+
* Options:
|
|
7
|
+
* -o, --out <dir> Output directory [env: KITFLY_BUILD_OUT] [default: dist]
|
|
8
|
+
* --raw Include raw markdown files [env: KITFLY_BUILD_RAW] [default: true]
|
|
9
|
+
* --no-raw Don't include raw markdown files
|
|
10
|
+
* --help Show help message
|
|
11
|
+
*
|
|
12
|
+
* Outputs to dist/ directory by default.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { copyFile, cp, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
16
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
17
|
+
import { marked, Renderer } from "marked";
|
|
18
|
+
import { ENGINE_ASSETS_DIR } from "../src/engine.ts";
|
|
19
|
+
import {
|
|
20
|
+
buildBreadcrumbsStatic,
|
|
21
|
+
buildFooter,
|
|
22
|
+
buildNavStatic,
|
|
23
|
+
buildPageMeta,
|
|
24
|
+
buildToc,
|
|
25
|
+
type ContentFile,
|
|
26
|
+
// Navigation/template building
|
|
27
|
+
collectFiles,
|
|
28
|
+
envBool,
|
|
29
|
+
// Config helpers
|
|
30
|
+
envString,
|
|
31
|
+
// Formatting
|
|
32
|
+
escapeHtml,
|
|
33
|
+
// File utilities
|
|
34
|
+
exists,
|
|
35
|
+
// Provenance
|
|
36
|
+
generateProvenance,
|
|
37
|
+
// YAML/Config parsing
|
|
38
|
+
loadSiteConfig,
|
|
39
|
+
type Provenance,
|
|
40
|
+
// Markdown utilities
|
|
41
|
+
parseFrontmatter,
|
|
42
|
+
resolveStylesPath,
|
|
43
|
+
resolveTemplatePath,
|
|
44
|
+
// Types
|
|
45
|
+
type SiteConfig,
|
|
46
|
+
slugify,
|
|
47
|
+
validatePath,
|
|
48
|
+
} from "../src/shared.ts";
|
|
49
|
+
import { generateThemeCSS, getPrismUrls, loadTheme, type Theme } from "../src/theme.ts";
|
|
50
|
+
|
|
51
|
+
// Defaults
|
|
52
|
+
const DEFAULT_OUT = "dist";
|
|
53
|
+
|
|
54
|
+
let ROOT = process.cwd();
|
|
55
|
+
let OUT_DIR = DEFAULT_OUT;
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// CLI argument parsing
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
interface ParsedArgs {
|
|
62
|
+
folder?: string;
|
|
63
|
+
out?: string;
|
|
64
|
+
raw?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
68
|
+
const result: ParsedArgs = {};
|
|
69
|
+
for (let i = 0; i < argv.length; i++) {
|
|
70
|
+
const arg = argv[i];
|
|
71
|
+
const next = argv[i + 1];
|
|
72
|
+
|
|
73
|
+
if ((arg === "--out" || arg === "-o") && next && !next.startsWith("-")) {
|
|
74
|
+
result.out = next;
|
|
75
|
+
i++;
|
|
76
|
+
} else if (arg === "--raw") {
|
|
77
|
+
result.raw = true;
|
|
78
|
+
} else if (arg === "--no-raw") {
|
|
79
|
+
result.raw = false;
|
|
80
|
+
} else if (!arg.startsWith("-") && !result.folder) {
|
|
81
|
+
result.folder = arg;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getConfig(): { folder?: string; out: string; raw: boolean } {
|
|
88
|
+
const args = parseArgs(process.argv.slice(2));
|
|
89
|
+
return {
|
|
90
|
+
folder: args.folder,
|
|
91
|
+
out: args.out ?? envString("KITFLY_BUILD_OUT", DEFAULT_OUT),
|
|
92
|
+
raw: args.raw ?? envBool("KITFLY_BUILD_RAW", true),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function resolveSiteAssetsDir(siteRoot: string): Promise<string | null> {
|
|
97
|
+
const overrideDir = join(siteRoot, "assets");
|
|
98
|
+
if (await exists(overrideDir)) return overrideDir;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function computePathPrefix(urlKey: string): string {
|
|
103
|
+
const clean = urlKey.replace(/^\/+/, "").replace(/\.html$/, "");
|
|
104
|
+
if (!clean) return "./";
|
|
105
|
+
const depth = Math.max(0, clean.split("/").length - 1);
|
|
106
|
+
return depth === 0 ? "./" : "../".repeat(depth);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function copyStaticAssetsFromDir(srcDir: string, destDir: string): Promise<void> {
|
|
110
|
+
try {
|
|
111
|
+
await mkdir(destDir, { recursive: true });
|
|
112
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const srcPath = join(srcDir, entry.name);
|
|
115
|
+
const destPath = join(destDir, entry.name);
|
|
116
|
+
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
// Skip hidden folders
|
|
119
|
+
if (entry.name.startsWith(".")) continue;
|
|
120
|
+
await copyStaticAssetsFromDir(srcPath, destPath);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!entry.isFile()) continue;
|
|
125
|
+
const ext = extname(entry.name).toLowerCase();
|
|
126
|
+
if (ext === ".md" || ext === ".yaml" || ext === ".yml") continue;
|
|
127
|
+
|
|
128
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
129
|
+
await copyFile(srcPath, destPath);
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Skip missing/unreadable directories
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Configure marked with custom renderer for mermaid support and heading IDs
|
|
137
|
+
const renderer = new Renderer();
|
|
138
|
+
const originalCode = renderer.code.bind(renderer);
|
|
139
|
+
renderer.code = (code: { type: "code"; raw: string; text: string; lang?: string }) => {
|
|
140
|
+
if (code.lang === "mermaid") {
|
|
141
|
+
// Store source in data attribute for theme toggle re-rendering
|
|
142
|
+
const escaped = code.text.replace(/"/g, """);
|
|
143
|
+
return `<pre class="mermaid" data-mermaid-source="${escaped}">${code.text}</pre>`;
|
|
144
|
+
}
|
|
145
|
+
return originalCode(code);
|
|
146
|
+
};
|
|
147
|
+
renderer.heading = ({ text, depth }: { text: string; depth: number }) => {
|
|
148
|
+
const plain = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
149
|
+
const id = slugify(plain);
|
|
150
|
+
const inner = marked.parseInline(text) as string;
|
|
151
|
+
return `<h${depth} id="${id}">${inner}</h${depth}>\n`;
|
|
152
|
+
};
|
|
153
|
+
marked.use({ renderer });
|
|
154
|
+
|
|
155
|
+
// Render a single file
|
|
156
|
+
async function renderFile(
|
|
157
|
+
filePath: string,
|
|
158
|
+
urlKey: string,
|
|
159
|
+
template: string,
|
|
160
|
+
files: ContentFile[],
|
|
161
|
+
provenance: Provenance,
|
|
162
|
+
config: SiteConfig,
|
|
163
|
+
theme: Theme,
|
|
164
|
+
): Promise<string> {
|
|
165
|
+
const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
|
|
166
|
+
const content = await readFile(filePath, "utf-8");
|
|
167
|
+
|
|
168
|
+
let title = basename(filePath, extname(filePath));
|
|
169
|
+
let htmlContent: string;
|
|
170
|
+
let pageMeta = "";
|
|
171
|
+
|
|
172
|
+
if (filePath.endsWith(".yaml")) {
|
|
173
|
+
htmlContent = `<h1>${title}</h1>\n<pre><code class="language-yaml">${escapeHtml(content)}</code></pre>`;
|
|
174
|
+
} else if (filePath.endsWith(".json")) {
|
|
175
|
+
// Render JSON as code block (pretty-printed)
|
|
176
|
+
let prettyJson = content;
|
|
177
|
+
try {
|
|
178
|
+
prettyJson = JSON.stringify(JSON.parse(content), null, 2);
|
|
179
|
+
} catch {
|
|
180
|
+
// Use original if not valid JSON
|
|
181
|
+
}
|
|
182
|
+
htmlContent = `<h1>${title}</h1>\n<pre><code class="language-json">${escapeHtml(prettyJson)}</code></pre>`;
|
|
183
|
+
} else {
|
|
184
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
185
|
+
if (frontmatter.title) {
|
|
186
|
+
title = frontmatter.title as string;
|
|
187
|
+
}
|
|
188
|
+
pageMeta = buildPageMeta(frontmatter);
|
|
189
|
+
htmlContent = marked.parse(body) as string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const pathPrefix = computePathPrefix(urlKey);
|
|
193
|
+
const nav = buildNavStatic(files, urlKey, config, pathPrefix);
|
|
194
|
+
const footer = buildFooter(provenance, config);
|
|
195
|
+
const breadcrumbs = buildBreadcrumbsStatic(urlKey, pathPrefix, files, config);
|
|
196
|
+
const toc = buildToc(htmlContent);
|
|
197
|
+
const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
|
|
198
|
+
const themeCSS = generateThemeCSS(theme);
|
|
199
|
+
const prismUrls = getPrismUrls(theme);
|
|
200
|
+
const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
|
|
201
|
+
|
|
202
|
+
return template
|
|
203
|
+
.replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
|
|
204
|
+
.replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
|
|
205
|
+
.replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
|
|
206
|
+
.replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
|
|
207
|
+
.replace(/\{\{BRAND_LOGO\}\}/g, config.brand.logo || "assets/brand/logo.png")
|
|
208
|
+
.replace(/\{\{BRAND_FAVICON\}\}/g, config.brand.favicon || "assets/brand/favicon.png")
|
|
209
|
+
.replace(/\{\{BRAND_LOGO_CLASS\}\}/g, logoClass)
|
|
210
|
+
.replace(/\{\{SITE_TITLE\}\}/g, config.title)
|
|
211
|
+
.replace("{{TITLE}}", title)
|
|
212
|
+
.replace("{{VERSION}}", uiVersion)
|
|
213
|
+
.replace("{{BRANCH}}", provenance.gitBranch)
|
|
214
|
+
.replace("{{BREADCRUMBS}}", breadcrumbs)
|
|
215
|
+
.replace("{{PAGE_META}}", pageMeta)
|
|
216
|
+
.replace("{{NAV}}", nav)
|
|
217
|
+
.replace("{{CONTENT}}", htmlContent)
|
|
218
|
+
.replace("{{TOC}}", toc)
|
|
219
|
+
.replace("{{FOOTER}}", footer)
|
|
220
|
+
.replace("{{THEME_CSS}}", themeCSS)
|
|
221
|
+
.replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
|
|
222
|
+
.replace("{{PRISM_DARK_URL}}", prismUrls.dark)
|
|
223
|
+
.replace("{{HOT_RELOAD_SCRIPT}}", "");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Render Getting Started page when no config
|
|
227
|
+
function renderGettingStarted(
|
|
228
|
+
template: string,
|
|
229
|
+
provenance: Provenance,
|
|
230
|
+
config: SiteConfig,
|
|
231
|
+
theme: Theme,
|
|
232
|
+
): string {
|
|
233
|
+
const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
|
|
234
|
+
const htmlContent = `
|
|
235
|
+
<h1>Getting Started</h1>
|
|
236
|
+
<p>Welcome! To configure your kitfly site, create a <code>site.yaml</code> file in the repository root:</p>
|
|
237
|
+
<pre><code class="language-yaml"># yaml-language-server: $schema=./schemas/v0/site.schema.json
|
|
238
|
+
schemaVersion: "0.1.0"
|
|
239
|
+
docroot: "."
|
|
240
|
+
title: "My Docs"
|
|
241
|
+
|
|
242
|
+
brand:
|
|
243
|
+
name: "My Brand"
|
|
244
|
+
url: "https://example.com"
|
|
245
|
+
external: true
|
|
246
|
+
|
|
247
|
+
sections:
|
|
248
|
+
- name: "Overview"
|
|
249
|
+
path: "."
|
|
250
|
+
files: ["README.md"]
|
|
251
|
+
- name: "Guides"
|
|
252
|
+
path: "guides"
|
|
253
|
+
</code></pre>
|
|
254
|
+
<p>Or create a <code>content/</code> directory with subdirectories for auto-discovery.</p>
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
|
|
258
|
+
const themeCSS = generateThemeCSS(theme);
|
|
259
|
+
const prismUrls = getPrismUrls(theme);
|
|
260
|
+
const pathPrefix = "./";
|
|
261
|
+
const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
|
|
262
|
+
|
|
263
|
+
return template
|
|
264
|
+
.replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
|
|
265
|
+
.replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
|
|
266
|
+
.replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
|
|
267
|
+
.replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
|
|
268
|
+
.replace(/\{\{BRAND_LOGO\}\}/g, config.brand.logo || "assets/brand/logo.png")
|
|
269
|
+
.replace(/\{\{BRAND_FAVICON\}\}/g, config.brand.favicon || "assets/brand/favicon.png")
|
|
270
|
+
.replace(/\{\{BRAND_LOGO_CLASS\}\}/g, logoClass)
|
|
271
|
+
.replace(/\{\{SITE_TITLE\}\}/g, config.title)
|
|
272
|
+
.replace("{{TITLE}}", "Getting Started")
|
|
273
|
+
.replace("{{VERSION}}", uiVersion)
|
|
274
|
+
.replace("{{BRANCH}}", provenance.gitBranch)
|
|
275
|
+
.replace("{{BREADCRUMBS}}", "")
|
|
276
|
+
.replace("{{PAGE_META}}", "")
|
|
277
|
+
.replace("{{NAV}}", "<ul></ul>")
|
|
278
|
+
.replace("{{CONTENT}}", htmlContent)
|
|
279
|
+
.replace("{{TOC}}", "")
|
|
280
|
+
.replace("{{FOOTER}}", buildFooter(provenance, config))
|
|
281
|
+
.replace("{{THEME_CSS}}", themeCSS)
|
|
282
|
+
.replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
|
|
283
|
+
.replace("{{PRISM_DARK_URL}}", prismUrls.dark)
|
|
284
|
+
.replace("{{HOT_RELOAD_SCRIPT}}", "");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Export for CLI usage
|
|
288
|
+
export interface BuildOptions {
|
|
289
|
+
folder?: string;
|
|
290
|
+
out?: string;
|
|
291
|
+
raw?: boolean; // Include raw markdown files (default: true)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let INCLUDE_RAW = true;
|
|
295
|
+
|
|
296
|
+
export async function build(options: BuildOptions = {}) {
|
|
297
|
+
if (options.folder) {
|
|
298
|
+
ROOT = resolve(process.cwd(), options.folder);
|
|
299
|
+
}
|
|
300
|
+
if (options.out) {
|
|
301
|
+
OUT_DIR = options.out;
|
|
302
|
+
}
|
|
303
|
+
if (options.raw === false) {
|
|
304
|
+
INCLUDE_RAW = false;
|
|
305
|
+
}
|
|
306
|
+
await buildSite();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Rename internal function
|
|
310
|
+
async function buildSite() {
|
|
311
|
+
const DIST = join(ROOT, OUT_DIR);
|
|
312
|
+
|
|
313
|
+
console.log("Building site...\n");
|
|
314
|
+
|
|
315
|
+
// Load configuration
|
|
316
|
+
const config = await loadSiteConfig(ROOT);
|
|
317
|
+
console.log(` ✓ Loaded config: "${config.title}" (${config.sections.length} sections)`);
|
|
318
|
+
|
|
319
|
+
// Load theme
|
|
320
|
+
const theme = await loadTheme(ROOT);
|
|
321
|
+
console.log(` ✓ Loaded theme: "${theme.name || "default"}"`);
|
|
322
|
+
|
|
323
|
+
// Create dist directory
|
|
324
|
+
await mkdir(DIST, { recursive: true });
|
|
325
|
+
|
|
326
|
+
// Generate provenance (build mode)
|
|
327
|
+
const provenance = await generateProvenance(ROOT, false, config.version);
|
|
328
|
+
await writeFile(join(DIST, "provenance.json"), JSON.stringify(provenance, null, 2));
|
|
329
|
+
console.log(
|
|
330
|
+
` ✓ provenance.json (${provenance.version ? `v${provenance.version}` : "unversioned"}, ${provenance.gitCommit})`,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// Read template
|
|
334
|
+
const template = await readFile(await resolveTemplatePath(ROOT), "utf-8");
|
|
335
|
+
|
|
336
|
+
// Copy CSS
|
|
337
|
+
const css = await readFile(await resolveStylesPath(ROOT), "utf-8");
|
|
338
|
+
await writeFile(join(DIST, "styles.css"), css);
|
|
339
|
+
console.log(" ✓ styles.css");
|
|
340
|
+
|
|
341
|
+
// Copy engine assets, then overlay site assets if present
|
|
342
|
+
try {
|
|
343
|
+
await stat(ENGINE_ASSETS_DIR);
|
|
344
|
+
await cp(ENGINE_ASSETS_DIR, join(DIST, "assets"), { recursive: true });
|
|
345
|
+
console.log(" ✓ assets/ (engine)");
|
|
346
|
+
} catch {
|
|
347
|
+
// No engine assets, skip
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const siteAssetsDir = await resolveSiteAssetsDir(ROOT);
|
|
351
|
+
if (siteAssetsDir) {
|
|
352
|
+
try {
|
|
353
|
+
await cp(siteAssetsDir, join(DIST, "assets"), { recursive: true });
|
|
354
|
+
console.log(" ✓ assets/ (site override)");
|
|
355
|
+
} catch {
|
|
356
|
+
// Skip
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Copy non-markdown assets referenced by docs (images, PDFs, etc.)
|
|
361
|
+
for (const section of config.sections) {
|
|
362
|
+
const sectionSrc = validatePath(ROOT, config.docroot, section.path);
|
|
363
|
+
if (!sectionSrc) continue;
|
|
364
|
+
const sectionDest = section.path === "." ? DIST : join(DIST, section.path);
|
|
365
|
+
await copyStaticAssetsFromDir(sectionSrc, sectionDest);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Collect and render all files
|
|
369
|
+
const files = await collectFiles(ROOT, config);
|
|
370
|
+
|
|
371
|
+
if (files.length === 0) {
|
|
372
|
+
// No content - render Getting Started page
|
|
373
|
+
const html = renderGettingStarted(template, provenance, config, theme);
|
|
374
|
+
await writeFile(join(DIST, "index.html"), html);
|
|
375
|
+
console.log(" ✓ index.html (Getting Started)");
|
|
376
|
+
console.log(`\n\x1b[33mNo content found. Create site.yaml or content/ directory.\x1b[0m`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const file of files) {
|
|
381
|
+
const html = await renderFile(
|
|
382
|
+
file.path,
|
|
383
|
+
file.urlPath,
|
|
384
|
+
template,
|
|
385
|
+
files,
|
|
386
|
+
provenance,
|
|
387
|
+
config,
|
|
388
|
+
theme,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Create output path
|
|
392
|
+
const outPath = join(DIST, `${file.urlPath}.html`);
|
|
393
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
394
|
+
await writeFile(outPath, html);
|
|
395
|
+
|
|
396
|
+
console.log(` ✓ ${file.urlPath}.html`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Create index.html
|
|
400
|
+
if (config.home) {
|
|
401
|
+
// Render dedicated home page
|
|
402
|
+
const homePath = validatePath(ROOT, config.docroot, config.home);
|
|
403
|
+
if (homePath) {
|
|
404
|
+
try {
|
|
405
|
+
await stat(homePath);
|
|
406
|
+
const homeHtml = await renderFile(homePath, "", template, files, provenance, config, theme);
|
|
407
|
+
await writeFile(join(DIST, "index.html"), homeHtml);
|
|
408
|
+
console.log(` ✓ index.html (from ${config.home})`);
|
|
409
|
+
} catch {
|
|
410
|
+
console.warn(` ⚠ Home page ${config.home} not found, using first file`);
|
|
411
|
+
const firstFile = files[0];
|
|
412
|
+
const indexHtml = await renderFile(
|
|
413
|
+
firstFile.path,
|
|
414
|
+
"",
|
|
415
|
+
template,
|
|
416
|
+
files,
|
|
417
|
+
provenance,
|
|
418
|
+
config,
|
|
419
|
+
theme,
|
|
420
|
+
);
|
|
421
|
+
await writeFile(join(DIST, "index.html"), indexHtml);
|
|
422
|
+
console.log(" ✓ index.html");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
// Fallback: copy first file as index
|
|
427
|
+
const firstFile = files[0];
|
|
428
|
+
const indexHtml = await renderFile(
|
|
429
|
+
firstFile.path,
|
|
430
|
+
"",
|
|
431
|
+
template,
|
|
432
|
+
files,
|
|
433
|
+
provenance,
|
|
434
|
+
config,
|
|
435
|
+
theme,
|
|
436
|
+
);
|
|
437
|
+
await writeFile(join(DIST, "index.html"), indexHtml);
|
|
438
|
+
console.log(" ✓ index.html");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Create section redirect files (for breadcrumb navigation)
|
|
442
|
+
const sectionFirstFile: Map<string, string> = new Map();
|
|
443
|
+
for (const file of files) {
|
|
444
|
+
if (!sectionFirstFile.has(file.section)) {
|
|
445
|
+
const parts = file.urlPath.split("/");
|
|
446
|
+
if (parts.length > 1) {
|
|
447
|
+
const sectionPath = parts.slice(0, -1).join("/");
|
|
448
|
+
sectionFirstFile.set(sectionPath, file.urlPath);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (const [sectionPath, firstFilePath] of sectionFirstFile) {
|
|
454
|
+
const targetName = firstFilePath.split("/").pop() || firstFilePath;
|
|
455
|
+
const targetHref = `./${targetName}.html`;
|
|
456
|
+
const redirectHtml = `<!DOCTYPE html>
|
|
457
|
+
<html>
|
|
458
|
+
<head>
|
|
459
|
+
<meta charset="UTF-8">
|
|
460
|
+
<meta http-equiv="refresh" content="0; url=${targetHref}">
|
|
461
|
+
<title>Redirecting...</title>
|
|
462
|
+
</head>
|
|
463
|
+
<body>
|
|
464
|
+
<p>Redirecting to <a href="${targetHref}">${targetName}</a>...</p>
|
|
465
|
+
</body>
|
|
466
|
+
</html>`;
|
|
467
|
+
const redirectPath = join(DIST, sectionPath, "index.html");
|
|
468
|
+
await mkdir(dirname(redirectPath), { recursive: true });
|
|
469
|
+
await writeFile(redirectPath, redirectHtml);
|
|
470
|
+
console.log(` ✓ ${sectionPath}/index.html (redirect)`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Generate AI accessibility files
|
|
474
|
+
await generateAIAccessibility(DIST, files, config, provenance);
|
|
475
|
+
|
|
476
|
+
console.log(`\n\x1b[32mBuild complete! Output in ${OUT_DIR}/\x1b[0m`);
|
|
477
|
+
console.log(`\nTo view locally: open ${OUT_DIR}/index.html`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Generate AI accessibility files: content-index.json, llms.txt, and optionally _raw/
|
|
481
|
+
async function generateAIAccessibility(
|
|
482
|
+
dist: string,
|
|
483
|
+
files: ContentFile[],
|
|
484
|
+
config: SiteConfig,
|
|
485
|
+
provenance: Provenance,
|
|
486
|
+
) {
|
|
487
|
+
// 1. Generate content-index.json
|
|
488
|
+
const contentIndex = {
|
|
489
|
+
version: provenance.version,
|
|
490
|
+
generated: new Date().toISOString(),
|
|
491
|
+
title: config.title,
|
|
492
|
+
baseUrl: "/",
|
|
493
|
+
rawMarkdownPath: INCLUDE_RAW ? "/_raw" : null,
|
|
494
|
+
pages: await Promise.all(
|
|
495
|
+
files.map(async (file) => {
|
|
496
|
+
let title = basename(file.path).replace(/\.(md|yaml|json)$/, "");
|
|
497
|
+
let description: string | undefined;
|
|
498
|
+
|
|
499
|
+
// Try to extract title and description from frontmatter
|
|
500
|
+
if (file.path.endsWith(".md")) {
|
|
501
|
+
try {
|
|
502
|
+
const content = await readFile(file.path, "utf-8");
|
|
503
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
504
|
+
if (frontmatter.title) title = frontmatter.title as string;
|
|
505
|
+
if (frontmatter.description) description = frontmatter.description as string;
|
|
506
|
+
} catch {
|
|
507
|
+
// Use defaults
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
path: `/${file.urlPath}`,
|
|
513
|
+
htmlPath: `/${file.urlPath}.html`,
|
|
514
|
+
rawPath: INCLUDE_RAW ? `/_raw/${file.urlPath}.md` : undefined,
|
|
515
|
+
title,
|
|
516
|
+
section: file.section,
|
|
517
|
+
source: file.path.replace(`${ROOT}/`, ""),
|
|
518
|
+
description,
|
|
519
|
+
};
|
|
520
|
+
}),
|
|
521
|
+
),
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
await writeFile(join(dist, "content-index.json"), JSON.stringify(contentIndex, null, 2));
|
|
525
|
+
console.log(" ✓ content-index.json (AI accessibility)");
|
|
526
|
+
|
|
527
|
+
// 2. Generate llms.txt
|
|
528
|
+
const llmsTxt = `# llms.txt - AI agent guidance for ${config.title}
|
|
529
|
+
# Learn more: https://llmstxt.org/
|
|
530
|
+
|
|
531
|
+
# Site metadata
|
|
532
|
+
name: ${config.title}
|
|
533
|
+
version: ${provenance.version}
|
|
534
|
+
generated: ${new Date().toISOString()}
|
|
535
|
+
|
|
536
|
+
# Content discovery
|
|
537
|
+
content-index: /content-index.json
|
|
538
|
+
${INCLUDE_RAW ? "raw-markdown: /_raw/{path}.md" : "# raw-markdown: disabled"}
|
|
539
|
+
|
|
540
|
+
# Preferred format for content consumption
|
|
541
|
+
preferred-format: markdown
|
|
542
|
+
|
|
543
|
+
# Site structure
|
|
544
|
+
sections: ${config.sections.map((s) => s.name).join(", ")}
|
|
545
|
+
total-pages: ${files.length}
|
|
546
|
+
`;
|
|
547
|
+
|
|
548
|
+
await writeFile(join(dist, "llms.txt"), llmsTxt);
|
|
549
|
+
console.log(" ✓ llms.txt (AI accessibility)");
|
|
550
|
+
|
|
551
|
+
// 3. Copy raw markdown files to _raw/ if enabled
|
|
552
|
+
if (INCLUDE_RAW) {
|
|
553
|
+
const rawDir = join(dist, "_raw");
|
|
554
|
+
await mkdir(rawDir, { recursive: true });
|
|
555
|
+
|
|
556
|
+
for (const file of files) {
|
|
557
|
+
if (file.path.endsWith(".md")) {
|
|
558
|
+
try {
|
|
559
|
+
const content = await readFile(file.path, "utf-8");
|
|
560
|
+
const rawPath = join(rawDir, `${file.urlPath}.md`);
|
|
561
|
+
await mkdir(dirname(rawPath), { recursive: true });
|
|
562
|
+
await writeFile(rawPath, content);
|
|
563
|
+
} catch {
|
|
564
|
+
// Skip if can't read
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
console.log(" ✓ _raw/ (raw markdown for AI agents)");
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Run directly if executed as script
|
|
573
|
+
if (import.meta.main) {
|
|
574
|
+
// Check for help flag
|
|
575
|
+
if (process.argv.includes("--help")) {
|
|
576
|
+
console.log(`
|
|
577
|
+
Usage: bun run build [folder] [options]
|
|
578
|
+
|
|
579
|
+
Options:
|
|
580
|
+
-o, --out <dir> Output directory [env: KITFLY_BUILD_OUT] [default: ${DEFAULT_OUT}]
|
|
581
|
+
--raw Include raw markdown files [env: KITFLY_BUILD_RAW] [default: true]
|
|
582
|
+
--no-raw Don't include raw markdown files
|
|
583
|
+
--help Show this help message
|
|
584
|
+
|
|
585
|
+
Examples:
|
|
586
|
+
bun run build
|
|
587
|
+
bun run build ./docs
|
|
588
|
+
bun run build --out ./public
|
|
589
|
+
bun run build ./docs --out ./public --no-raw
|
|
590
|
+
KITFLY_BUILD_OUT=public bun run build
|
|
591
|
+
`);
|
|
592
|
+
process.exit(0);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const cfg = getConfig();
|
|
596
|
+
build({
|
|
597
|
+
folder: cfg.folder,
|
|
598
|
+
out: cfg.out,
|
|
599
|
+
raw: cfg.raw,
|
|
600
|
+
}).catch(console.error);
|
|
601
|
+
}
|