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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
type ChartsLiteHooks = {
|
|
4
|
+
parseSpec: (text: string) => {
|
|
5
|
+
kind: string;
|
|
6
|
+
labels: string[];
|
|
7
|
+
data: number[];
|
|
8
|
+
title: string;
|
|
9
|
+
color: string;
|
|
10
|
+
height: number;
|
|
11
|
+
} | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async function loadHooks(): Promise<ChartsLiteHooks> {
|
|
15
|
+
// @ts-expect-error JS plugin helper file
|
|
16
|
+
await import("../../plugins-dist/slides-charts-lite-runtime.js");
|
|
17
|
+
const hooks = (globalThis as any).__kitflyChartsLiteTest as ChartsLiteHooks | undefined;
|
|
18
|
+
if (!hooks) throw new Error("slides-charts-lite test hooks not found on globalThis");
|
|
19
|
+
return hooks;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test("slides-charts-lite: parses valid chart block", async () => {
|
|
23
|
+
const hooks = await loadHooks();
|
|
24
|
+
const spec = hooks.parseSpec(`kind: bar\nlabels: ["Q1","Q2"]\ndata: [10, 20]\nheight: 400`);
|
|
25
|
+
expect(spec).toEqual({
|
|
26
|
+
kind: "bar",
|
|
27
|
+
labels: ["Q1", "Q2"],
|
|
28
|
+
data: [10, 20],
|
|
29
|
+
title: "",
|
|
30
|
+
color: "primary",
|
|
31
|
+
height: 400,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("slides-charts-lite: rejects mismatched labels/data", async () => {
|
|
36
|
+
const hooks = await loadHooks();
|
|
37
|
+
const spec = hooks.parseSpec(`kind: line\nlabels: ["A","B"]\ndata: [1]`);
|
|
38
|
+
expect(spec).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("slides-charts-lite: rejects unknown kind", async () => {
|
|
42
|
+
const hooks = await loadHooks();
|
|
43
|
+
const spec = hooks.parseSpec(`kind: scatter\nlabels: ["A"]\ndata: [1]`);
|
|
44
|
+
expect(spec).toBeNull();
|
|
45
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -12,6 +12,9 @@ import { dirname, join, resolve } from "node:path";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { loadSiteConfig } from "./shared.ts";
|
|
14
14
|
|
|
15
|
+
// Exit cleanly when piped output is closed early (e.g., `kitfly docs show x | less` then quit)
|
|
16
|
+
process.on("SIGPIPE", () => process.exit(0));
|
|
17
|
+
|
|
15
18
|
// Resolve paths relative to CLI location (works in binary too)
|
|
16
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
20
|
const ROOT = join(__dirname, "..");
|
|
@@ -76,23 +79,27 @@ Usage:
|
|
|
76
79
|
kitfly servers List running dev servers
|
|
77
80
|
kitfly stop <port|all> Stop dev server(s)
|
|
78
81
|
kitfly logs <port> View daemon server logs
|
|
82
|
+
kitfly docs [list|show] Browse embedded documentation
|
|
79
83
|
kitfly version Show version (use 'version extended' for details)
|
|
80
84
|
kitfly help Show this help
|
|
81
85
|
|
|
82
86
|
Dev options:
|
|
83
87
|
--port <n> Server port [env: KITFLY_DEV_PORT] (default: 3333)
|
|
84
88
|
--host <h> Server host [env: KITFLY_DEV_HOST] (default: localhost)
|
|
89
|
+
--profile <p> Active content profile [env: KITFLY_PROFILE]
|
|
85
90
|
--daemon, -d Run in background, return immediately
|
|
86
91
|
--json Output JSON (implies --daemon)
|
|
87
92
|
--no-open Don't open browser
|
|
88
93
|
|
|
89
94
|
Build options:
|
|
90
95
|
--out <dir> Output directory [env: KITFLY_BUILD_OUT] (default: dist)
|
|
96
|
+
--profile <p> Active content profile [env: KITFLY_PROFILE]
|
|
91
97
|
--no-raw Don't include raw markdown
|
|
92
98
|
|
|
93
99
|
Bundle options:
|
|
94
100
|
--out <dir> Output directory [env: KITFLY_BUNDLE_OUT] (default: bundles)
|
|
95
101
|
--name <file> Bundle filename (default: bundle.html)
|
|
102
|
+
--profile <p> Active content profile [env: KITFLY_PROFILE]
|
|
96
103
|
--no-raw Don't include raw markdown [env: KITFLY_BUNDLE_RAW]
|
|
97
104
|
|
|
98
105
|
Stop options:
|
|
@@ -204,6 +211,7 @@ async function main() {
|
|
|
204
211
|
}
|
|
205
212
|
|
|
206
213
|
const host = (flags.host as string) || "localhost";
|
|
214
|
+
const profile = (flags.profile as string | undefined) ?? process.env.KITFLY_PROFILE;
|
|
207
215
|
|
|
208
216
|
// Warn if binding to all interfaces
|
|
209
217
|
if (host === "0.0.0.0" || host === "::") {
|
|
@@ -271,7 +279,7 @@ async function main() {
|
|
|
271
279
|
|
|
272
280
|
if (daemon) {
|
|
273
281
|
// Daemon mode: spawn detached process using shell redirection
|
|
274
|
-
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
282
|
+
const { mkdir, writeFile, open: fsOpen } = await import("node:fs/promises");
|
|
275
283
|
const logsDir = join(getKitflyHome(), "logs");
|
|
276
284
|
await mkdir(logsDir, { recursive: true });
|
|
277
285
|
|
|
@@ -283,18 +291,53 @@ async function main() {
|
|
|
283
291
|
|
|
284
292
|
// Build command with shell redirection for logging
|
|
285
293
|
// Pass --log-format structured so dev.ts enables structured request logging
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
const profileArg = profile ? ` --profile "${profile}"` : "";
|
|
295
|
+
|
|
296
|
+
// Open log file as a write handle to pass as stdout/stderr for the child
|
|
297
|
+
const logFd = await fsOpen(logPath, "a");
|
|
298
|
+
|
|
299
|
+
let proc: ReturnType<typeof Bun.spawn>;
|
|
300
|
+
if (process.platform === "win32") {
|
|
301
|
+
// On Windows, use Bun.spawn with detached:true and stdio redirected to log file.
|
|
302
|
+
// nohup and sh -c are not available; Bun's detached mode achieves the same.
|
|
303
|
+
const args = [
|
|
304
|
+
"bun",
|
|
305
|
+
"run",
|
|
306
|
+
devScript,
|
|
307
|
+
folder,
|
|
308
|
+
"--port",
|
|
309
|
+
String(port),
|
|
310
|
+
"--host",
|
|
311
|
+
host,
|
|
312
|
+
...(profile ? ["--profile", profile] : []),
|
|
313
|
+
"--no-open",
|
|
314
|
+
"--log-format",
|
|
315
|
+
"structured",
|
|
316
|
+
];
|
|
317
|
+
proc = Bun.spawn(args, {
|
|
318
|
+
cwd: process.cwd(),
|
|
319
|
+
stdout: logFd.fd,
|
|
320
|
+
stderr: logFd.fd,
|
|
321
|
+
stdin: "ignore",
|
|
322
|
+
detached: true,
|
|
323
|
+
});
|
|
324
|
+
proc.unref();
|
|
325
|
+
await logFd.close();
|
|
326
|
+
// Give Windows a moment to spawn the child before proc.exited resolves
|
|
327
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
328
|
+
} else {
|
|
329
|
+
// Unix: nohup via sh -c keeps the process alive after terminal close
|
|
330
|
+
await logFd.close();
|
|
331
|
+
const shellCmd = `nohup bun run "${devScript}" "${folder}" --port ${port} --host "${host}"${profileArg} --no-open --log-format structured > "${logPath}" 2>&1 &`;
|
|
332
|
+
proc = Bun.spawn(["sh", "-c", shellCmd], {
|
|
333
|
+
cwd: process.cwd(),
|
|
334
|
+
stdout: "ignore",
|
|
335
|
+
stderr: "ignore",
|
|
336
|
+
stdin: "ignore",
|
|
337
|
+
});
|
|
338
|
+
// Wait for shell to spawn the background process
|
|
339
|
+
await proc.exited;
|
|
340
|
+
}
|
|
298
341
|
|
|
299
342
|
// Give server a moment to start
|
|
300
343
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -348,7 +391,7 @@ async function main() {
|
|
|
348
391
|
} else {
|
|
349
392
|
// Foreground mode: run directly
|
|
350
393
|
const { dev } = await import("../scripts/dev.ts");
|
|
351
|
-
await dev({ folder, port, host, open });
|
|
394
|
+
await dev({ folder, port, host, open, profile });
|
|
352
395
|
}
|
|
353
396
|
break;
|
|
354
397
|
}
|
|
@@ -357,8 +400,9 @@ async function main() {
|
|
|
357
400
|
const folder = positional[0] || ".";
|
|
358
401
|
const out = (flags.out as string) || "dist";
|
|
359
402
|
const raw = flags.raw !== false; // --no-raw disables raw markdown
|
|
403
|
+
const profile = (flags.profile as string | undefined) ?? process.env.KITFLY_PROFILE;
|
|
360
404
|
const { build } = await import("../scripts/build.ts");
|
|
361
|
-
await build({ folder, out, raw });
|
|
405
|
+
await build({ folder, out, raw, profile });
|
|
362
406
|
break;
|
|
363
407
|
}
|
|
364
408
|
|
|
@@ -367,8 +411,9 @@ async function main() {
|
|
|
367
411
|
const out = (flags.out as string) || "bundles";
|
|
368
412
|
const name = (flags.name as string) || "bundle.html";
|
|
369
413
|
const raw = flags.raw !== false; // --no-raw disables raw markdown
|
|
414
|
+
const profile = (flags.profile as string | undefined) ?? process.env.KITFLY_PROFILE;
|
|
370
415
|
const { bundleSite } = await import("../scripts/bundle.ts");
|
|
371
|
-
await bundleSite({ folder, out, name, raw });
|
|
416
|
+
await bundleSite({ folder, out, name, raw, profile });
|
|
372
417
|
break;
|
|
373
418
|
}
|
|
374
419
|
|
|
@@ -531,12 +576,47 @@ async function main() {
|
|
|
531
576
|
const follow = flags.follow === true || flags.f === true;
|
|
532
577
|
|
|
533
578
|
if (follow) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
579
|
+
if (process.platform !== "win32") {
|
|
580
|
+
// Unix: tail -f is available and efficient
|
|
581
|
+
const proc = Bun.spawn(["tail", "-f", logFile], {
|
|
582
|
+
stdout: "inherit",
|
|
583
|
+
stderr: "inherit",
|
|
584
|
+
});
|
|
585
|
+
await proc.exited;
|
|
586
|
+
} else {
|
|
587
|
+
// Windows: tail -f is not available; poll the file for new content
|
|
588
|
+
const { watch } = await import("node:fs");
|
|
589
|
+
const { open: fsOpen } = await import("node:fs/promises");
|
|
590
|
+
let fd: import("node:fs/promises").FileHandle;
|
|
591
|
+
try {
|
|
592
|
+
fd = await fsOpen(logFile, "r");
|
|
593
|
+
} catch {
|
|
594
|
+
console.error(`No log file found for port ${logPort}`);
|
|
595
|
+
console.error(` Expected: ${logFile}`);
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
// Print existing content first
|
|
599
|
+
const existing = await fd.readFile("utf-8");
|
|
600
|
+
if (existing.length > 0) process.stdout.write(existing);
|
|
601
|
+
let offset = Buffer.byteLength(existing, "utf-8");
|
|
602
|
+
// Watch for changes and stream new bytes
|
|
603
|
+
const watcher = watch(logFile, async () => {
|
|
604
|
+
const buf = Buffer.alloc(65536);
|
|
605
|
+
const { bytesRead } = await fd.read(buf, 0, buf.length, offset);
|
|
606
|
+
if (bytesRead > 0) {
|
|
607
|
+
offset += bytesRead;
|
|
608
|
+
process.stdout.write(buf.subarray(0, bytesRead));
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
// Keep running until Ctrl+C
|
|
612
|
+
await new Promise<void>((resolve) => {
|
|
613
|
+
process.on("SIGINT", () => {
|
|
614
|
+
watcher.close();
|
|
615
|
+
void fd.close();
|
|
616
|
+
resolve();
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
}
|
|
540
620
|
} else {
|
|
541
621
|
const { readFile } = await import("node:fs/promises");
|
|
542
622
|
try {
|
|
@@ -555,6 +635,28 @@ async function main() {
|
|
|
555
635
|
break;
|
|
556
636
|
}
|
|
557
637
|
|
|
638
|
+
case "docs": {
|
|
639
|
+
const sub = positional[0];
|
|
640
|
+
const { docsList, docsShow } = await import("./commands/docs.ts");
|
|
641
|
+
if (!sub || sub === "list") {
|
|
642
|
+
docsList();
|
|
643
|
+
} else if (sub === "show") {
|
|
644
|
+
const slug = positional[1];
|
|
645
|
+
if (!slug) {
|
|
646
|
+
console.error("Error: Slug required.\n");
|
|
647
|
+
console.error("Usage: kitfly docs show <slug>");
|
|
648
|
+
console.error(" kitfly docs list");
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
docsShow(slug);
|
|
652
|
+
} else {
|
|
653
|
+
console.error(`Unknown docs subcommand: "${sub}"\n`);
|
|
654
|
+
console.error("Usage: kitfly docs [list|show <slug>]");
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
|
|
558
660
|
case "version":
|
|
559
661
|
case "-v":
|
|
560
662
|
case "--version": {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kitfly docs — browse embedded documentation from the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* kitfly docs [list] List available doc slugs with titles
|
|
6
|
+
* kitfly docs show <slug> Output raw markdown to stdout
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EMBEDDED_DOCS } from "../generated/embedded-docs.ts";
|
|
10
|
+
|
|
11
|
+
// Build a Map from the generated tuple array for O(1) lookup
|
|
12
|
+
const docsMap = new Map<string, { title: string; content: string }>();
|
|
13
|
+
for (const [slug, title, content] of EMBEDDED_DOCS) {
|
|
14
|
+
docsMap.set(slug, { title, content });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function docsList(): void {
|
|
18
|
+
if (docsMap.size === 0) {
|
|
19
|
+
console.log("No embedded documentation available.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const slugs = [...docsMap.keys()].sort();
|
|
24
|
+
const maxSlug = Math.max(...slugs.map((s) => s.length));
|
|
25
|
+
|
|
26
|
+
console.log(`Embedded documentation (${slugs.length} topics):\n`);
|
|
27
|
+
|
|
28
|
+
for (const slug of slugs) {
|
|
29
|
+
const entry = docsMap.get(slug);
|
|
30
|
+
if (entry) console.log(` ${slug.padEnd(maxSlug + 2)}${entry.title}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`\nUsage: kitfly docs show <slug>`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function docsShow(slug: string): void {
|
|
37
|
+
const entry = docsMap.get(slug);
|
|
38
|
+
|
|
39
|
+
if (!entry) {
|
|
40
|
+
const suggestions = findSimilar(slug, [...docsMap.keys()], 3);
|
|
41
|
+
console.error(`Not found: "${slug}"`);
|
|
42
|
+
if (suggestions.length > 0) {
|
|
43
|
+
console.error(`Did you mean: ${suggestions.join(", ")}?`);
|
|
44
|
+
}
|
|
45
|
+
console.error(`\nUse 'kitfly docs list' to see available topics.`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(entry.content);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Find similar slugs by prefix or substring match.
|
|
54
|
+
* Returns up to `max` results, prioritizing prefix matches.
|
|
55
|
+
*/
|
|
56
|
+
export function findSimilar(query: string, slugs: string[], max: number): string[] {
|
|
57
|
+
const q = query.toLowerCase();
|
|
58
|
+
const prefixMatches: string[] = [];
|
|
59
|
+
const substringMatches: string[] = [];
|
|
60
|
+
|
|
61
|
+
for (const slug of slugs) {
|
|
62
|
+
const s = slug.toLowerCase();
|
|
63
|
+
if (s.startsWith(q) || q.startsWith(s)) {
|
|
64
|
+
prefixMatches.push(slug);
|
|
65
|
+
} else if (s.includes(q) || q.includes(s)) {
|
|
66
|
+
substringMatches.push(slug);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [...prefixMatches, ...substringMatches].slice(0, max);
|
|
71
|
+
}
|