@raystack/chronicle 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/cli/index.js +260 -81
  2. package/package.json +8 -6
  3. package/src/cli/commands/build.ts +5 -8
  4. package/src/cli/commands/dev.ts +5 -6
  5. package/src/cli/commands/init.test.ts +77 -0
  6. package/src/cli/commands/init.ts +73 -40
  7. package/src/cli/commands/serve.ts +6 -9
  8. package/src/cli/commands/start.ts +5 -5
  9. package/src/cli/utils/config.ts +6 -12
  10. package/src/cli/utils/scaffold.test.ts +179 -0
  11. package/src/cli/utils/scaffold.ts +70 -9
  12. package/src/components/api/field-row.tsx +1 -1
  13. package/src/components/api/field-section.tsx +2 -2
  14. package/src/components/mdx/index.tsx +1 -1
  15. package/src/components/mdx/mermaid.tsx +24 -21
  16. package/src/components/ui/breadcrumbs.tsx +4 -2
  17. package/src/components/ui/client-theme-switcher.tsx +21 -4
  18. package/src/components/ui/search.module.css +16 -41
  19. package/src/components/ui/search.tsx +30 -41
  20. package/src/lib/config.test.ts +493 -0
  21. package/src/lib/config.ts +123 -22
  22. package/src/lib/head.tsx +23 -5
  23. package/src/lib/llms.test.ts +94 -0
  24. package/src/lib/llms.ts +41 -0
  25. package/src/lib/navigation.test.ts +94 -0
  26. package/src/lib/navigation.ts +51 -0
  27. package/src/lib/page-context.tsx +79 -32
  28. package/src/lib/route-resolver.test.ts +173 -0
  29. package/src/lib/route-resolver.ts +73 -0
  30. package/src/lib/source.ts +94 -1
  31. package/src/lib/version-source.test.ts +163 -0
  32. package/src/lib/version-source.ts +101 -0
  33. package/src/pages/ApiPage.tsx +1 -1
  34. package/src/pages/DocsLayout.tsx +24 -3
  35. package/src/pages/DocsPage.tsx +7 -7
  36. package/src/pages/LandingPage.module.css +56 -0
  37. package/src/pages/LandingPage.tsx +39 -0
  38. package/src/pages/NotFound.module.css +3 -0
  39. package/src/pages/NotFound.tsx +9 -12
  40. package/src/server/App.tsx +21 -23
  41. package/src/server/api/{page/[...slug].ts → page.ts} +7 -3
  42. package/src/server/api/search.ts +51 -24
  43. package/src/server/api/specs.ts +17 -5
  44. package/src/server/entry-client.tsx +42 -14
  45. package/src/server/entry-server.tsx +35 -13
  46. package/src/server/plugins/telemetry.ts +47 -7
  47. package/src/server/routes/[...slug].md.ts +0 -6
  48. package/src/server/routes/[version]/llms.txt.ts +26 -0
  49. package/src/server/routes/llms.txt.ts +10 -13
  50. package/src/server/routes/og.tsx +2 -2
  51. package/src/server/routes/sitemap.xml.ts +14 -6
  52. package/src/server/vite-config.ts +5 -5
  53. package/src/themes/default/ContentDirButtons.tsx +66 -0
  54. package/src/themes/default/Layout.module.css +187 -40
  55. package/src/themes/default/Layout.tsx +166 -65
  56. package/src/themes/default/OpenInAI.tsx +112 -0
  57. package/src/themes/default/Page.module.css +30 -0
  58. package/src/themes/default/Page.tsx +1 -3
  59. package/src/themes/default/SidebarLogo.tsx +26 -0
  60. package/src/themes/default/Toc.module.css +102 -25
  61. package/src/themes/default/Toc.tsx +56 -10
  62. package/src/themes/default/VersionSwitcher.tsx +59 -0
  63. package/src/themes/paper/ContentDirDropdown.tsx +47 -0
  64. package/src/themes/paper/Layout.module.css +7 -0
  65. package/src/themes/paper/Layout.tsx +20 -13
  66. package/src/themes/paper/VersionSwitcher.tsx +60 -0
  67. package/src/types/config.ts +146 -23
  68. package/src/types/content.ts +11 -1
  69. package/src/types/theme.ts +1 -0
  70. package/src/components/ui/footer.module.css +0 -27
  71. package/src/components/ui/footer.tsx +0 -30
  72. package/src/server/api/metrics.ts +0 -23
  73. package/src/server/api/page/index.ts +0 -1
  74. package/src/server/telemetry.ts +0 -49
package/dist/cli/index.js CHANGED
@@ -91,8 +91,9 @@ async function readChronicleConfig(projectRoot, configPath) {
91
91
  }
92
92
  }
93
93
  async function createViteConfig(options) {
94
- const { packageRoot, projectRoot, contentDir, configPath, preset } = options;
94
+ const { packageRoot, projectRoot, configPath, preset } = options;
95
95
  const rawConfig = await readChronicleConfig(projectRoot, configPath);
96
+ const contentMirror = path4.resolve(packageRoot, ".content");
96
97
  return {
97
98
  root: packageRoot,
98
99
  configFile: false,
@@ -134,9 +135,9 @@ async function createViteConfig(options) {
134
135
  ],
135
136
  resolve: {
136
137
  alias: {
137
- "@": path4.resolve(packageRoot, "src")
138
+ "@": path4.resolve(packageRoot, "src"),
139
+ tslib: "tslib/tslib.es6.js"
138
140
  },
139
- conditions: ["module-sync", "import", "node"],
140
141
  dedupe: [
141
142
  "react",
142
143
  "react-dom",
@@ -147,11 +148,11 @@ async function createViteConfig(options) {
147
148
  },
148
149
  server: {
149
150
  fs: {
150
- allow: [packageRoot, projectRoot, contentDir]
151
+ allow: [packageRoot, projectRoot, contentMirror]
151
152
  }
152
153
  },
153
154
  define: {
154
- __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
155
+ __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentMirror),
155
156
  __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot),
156
157
  __CHRONICLE_CONFIG_RAW__: JSON.stringify(rawConfig)
157
158
  },
@@ -199,6 +200,7 @@ import chalk from "chalk";
199
200
  import { parse } from "yaml";
200
201
 
201
202
  // src/types/config.ts
203
+ import uniqBy from "lodash/uniqBy.js";
202
204
  import { z } from "zod";
203
205
  var logoSchema = z.object({
204
206
  light: z.string().optional(),
@@ -237,16 +239,10 @@ var apiSchema = z.object({
237
239
  name: z.string(),
238
240
  spec: z.string(),
239
241
  basePath: z.string(),
242
+ icon: z.string().optional(),
240
243
  server: apiServerSchema,
241
244
  auth: apiAuthSchema.optional()
242
245
  });
243
- var footerSchema = z.object({
244
- copyright: z.string().optional(),
245
- links: z.array(navLinkSchema).optional()
246
- });
247
- var llmsSchema = z.object({
248
- enabled: z.boolean().optional()
249
- });
250
246
  var googleAnalyticsSchema = z.object({
251
247
  measurementId: z.string()
252
248
  });
@@ -256,23 +252,115 @@ var analyticsSchema = z.object({
256
252
  });
257
253
  var telemetrySchema = z.object({
258
254
  enabled: z.boolean().optional(),
259
- serviceName: z.string().optional()
255
+ serviceName: z.string().optional(),
256
+ port: z.number().int().min(1).max(65535).default(9090)
260
257
  });
261
- var chronicleConfigSchema = z.object({
258
+ var siteSchema = z.object({
262
259
  title: z.string(),
263
- description: z.string().optional(),
260
+ description: z.string().optional()
261
+ });
262
+ var DIR_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
263
+ var dirNameSchema = z.string().min(1).refine((s) => DIR_NAME_PATTERN.test(s) && s !== "." && s !== "..", {
264
+ message: 'dir must start with a letter or digit and contain only letters, digits, ".", "_", or "-"'
265
+ });
266
+ var contentEntrySchema = z.object({
267
+ dir: dirNameSchema,
268
+ label: z.string().min(1),
269
+ icon: z.string().optional()
270
+ });
271
+ var badgeVariantSchema = z.enum([
272
+ "accent",
273
+ "warning",
274
+ "danger",
275
+ "success",
276
+ "neutral",
277
+ "gradient"
278
+ ]);
279
+ var badgeSchema = z.object({
280
+ label: z.string().min(1),
281
+ variant: badgeVariantSchema.default("accent")
282
+ });
283
+ var latestSchema = z.object({
284
+ label: z.string().min(1),
285
+ landing: z.boolean().optional()
286
+ });
287
+ var versionSchema = z.object({
288
+ dir: dirNameSchema,
289
+ label: z.string().min(1),
290
+ badge: badgeSchema.optional(),
291
+ landing: z.boolean().optional(),
292
+ content: z.array(contentEntrySchema).min(1),
293
+ api: z.array(apiSchema).optional()
294
+ });
295
+ var allUnique = (items, key) => uniqBy(items, key).length === items.length;
296
+ var RESERVED_ROUTE_SEGMENTS = [
297
+ "api",
298
+ "apis",
299
+ "og",
300
+ "llms.txt",
301
+ "robots.txt",
302
+ "sitemap.xml"
303
+ ];
304
+ var chronicleConfigSchema = z.object({
305
+ site: siteSchema,
264
306
  url: z.string().optional(),
265
- content: z.string().optional(),
307
+ content: z.array(contentEntrySchema).min(1),
308
+ latest: latestSchema.optional(),
309
+ versions: z.array(versionSchema).optional(),
266
310
  preset: z.string().optional(),
267
311
  logo: logoSchema.optional(),
268
312
  theme: themeSchema.optional(),
269
313
  navigation: navigationSchema.optional(),
270
314
  search: searchSchema.optional(),
271
- footer: footerSchema.optional(),
272
315
  api: z.array(apiSchema).optional(),
273
- llms: llmsSchema.optional(),
274
316
  analytics: analyticsSchema.optional(),
275
317
  telemetry: telemetrySchema.optional()
318
+ }).strict().refine((cfg) => allUnique(cfg.content, (c) => c.dir), {
319
+ message: "content[].dir must be unique",
320
+ path: ["content"]
321
+ }).refine((cfg) => !cfg.versions || allUnique(cfg.versions, (v) => v.dir), {
322
+ message: "versions[].dir must be unique",
323
+ path: ["versions"]
324
+ }).refine((cfg) => !cfg.versions || cfg.versions.every((v) => allUnique(v.content, (c) => c.dir)), {
325
+ message: "versions[].content[].dir must be unique within each version",
326
+ path: ["versions"]
327
+ }).refine((cfg) => !cfg.versions || cfg.versions.length === 0 || !!cfg.latest, {
328
+ message: "latest is required when versions are declared",
329
+ path: ["latest"]
330
+ }).refine((cfg) => {
331
+ if (!cfg.versions)
332
+ return true;
333
+ const contentDirs = new Set(cfg.content.map((c) => c.dir));
334
+ return !cfg.versions.some((v) => contentDirs.has(v.dir));
335
+ }, {
336
+ message: "versions[].dir must not overlap with content[].dir — the URL segment would be shadowed",
337
+ path: ["versions"]
338
+ }).superRefine((cfg, ctx) => {
339
+ const reserved = new Set(RESERVED_ROUTE_SEGMENTS);
340
+ const message = `dir must not be a reserved route segment: ${RESERVED_ROUTE_SEGMENTS.join(", ")}`;
341
+ cfg.content.forEach((c, i) => {
342
+ if (reserved.has(c.dir)) {
343
+ ctx.addIssue({ code: "custom", message, path: ["content", i, "dir"] });
344
+ }
345
+ });
346
+ cfg.versions?.forEach((v, vi) => {
347
+ if (reserved.has(v.dir)) {
348
+ ctx.addIssue({
349
+ code: "custom",
350
+ message,
351
+ path: ["versions", vi, "dir"]
352
+ });
353
+ }
354
+ v.content.forEach((c, ci) => {
355
+ if (reserved.has(c.dir)) {
356
+ ctx.addIssue({
357
+ code: "custom",
358
+ message,
359
+ path: ["versions", vi, "content", ci, "dir"]
360
+ });
361
+ }
362
+ });
363
+ });
276
364
  });
277
365
  // src/cli/utils/config.ts
278
366
  function resolveConfigPath(configPath) {
@@ -298,20 +386,13 @@ function validateConfig(raw, configPath) {
298
386
  if (!result.success) {
299
387
  console.log(chalk.red(`Error: Invalid chronicle.yaml at '${configPath}'`));
300
388
  for (const issue of result.error.issues) {
301
- const path2 = issue.path.join(".");
302
- console.log(chalk.gray(` ${path2 ? `${path2}: ` : ""}${issue.message}`));
389
+ const issuePath = issue.path.join(".");
390
+ console.log(chalk.gray(` ${issuePath ? `${issuePath}: ` : ""}${issue.message}`));
303
391
  }
304
392
  process.exit(1);
305
393
  }
306
394
  return result.data;
307
395
  }
308
- function resolveContentDir(config2, configPath, contentFlag) {
309
- if (contentFlag)
310
- return path.resolve(contentFlag);
311
- if (config2.content)
312
- return path.resolve(path.dirname(configPath), config2.content);
313
- return path.resolve("content");
314
- }
315
396
  function resolvePreset(config2, presetFlag) {
316
397
  return presetFlag ?? config2.preset;
317
398
  }
@@ -319,9 +400,9 @@ async function loadCLIConfig(configPath, options) {
319
400
  const resolvedConfigPath = resolveConfigPath(configPath) ?? path.join(process.cwd(), "chronicle.yaml");
320
401
  const raw = await readConfig(resolvedConfigPath);
321
402
  const config2 = validateConfig(raw, resolvedConfigPath);
322
- const contentDir = resolveContentDir(config2, resolvedConfigPath, options?.content);
403
+ const projectRoot = path.dirname(resolvedConfigPath);
323
404
  const preset = resolvePreset(config2, options?.preset);
324
- return { config: config2, configPath: resolvedConfigPath, contentDir, preset };
405
+ return { config: config2, configPath: resolvedConfigPath, projectRoot, preset };
325
406
  }
326
407
 
327
408
  // src/cli/utils/resolve.ts
@@ -332,36 +413,116 @@ var PACKAGE_ROOT = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)),
332
413
  // src/cli/utils/scaffold.ts
333
414
  import fs2 from "node:fs/promises";
334
415
  import path3 from "node:path";
335
- async function linkContent(contentDir) {
336
- const linkPath = path3.join(PACKAGE_ROOT, ".content");
337
- const target = path3.resolve(contentDir);
416
+
417
+ // src/lib/config.ts
418
+ import { parse as parse2 } from "yaml";
419
+ var defaultConfig = chronicleConfigSchema.parse({
420
+ site: { title: "Documentation" },
421
+ content: [{ dir: "docs", label: "Docs" }],
422
+ theme: { name: "default" },
423
+ search: { enabled: true, placeholder: "Search..." }
424
+ });
425
+ function getLatestContentRoots(config2) {
426
+ return config2.content.map((c) => ({
427
+ versionDir: null,
428
+ versionLabel: config2.latest?.label ?? null,
429
+ contentDir: c.dir,
430
+ contentLabel: c.label,
431
+ contentIcon: c.icon,
432
+ fsPath: `content/${c.dir}`,
433
+ urlPrefix: `/${c.dir}`
434
+ }));
435
+ }
436
+ function getVersionContentRoots(config2, versionDir) {
437
+ const version = config2.versions?.find((v) => v.dir === versionDir);
438
+ if (!version)
439
+ return [];
440
+ return version.content.map((c) => ({
441
+ versionDir: version.dir,
442
+ versionLabel: version.label,
443
+ contentDir: c.dir,
444
+ contentLabel: c.label,
445
+ contentIcon: c.icon,
446
+ fsPath: `versions/${version.dir}/${c.dir}`,
447
+ urlPrefix: `/${version.dir}/${c.dir}`
448
+ }));
449
+ }
450
+
451
+ // src/cli/utils/scaffold.ts
452
+ async function buildContentMirror(mirrorRoot, projectRoot, config2) {
453
+ await removeMirror(mirrorRoot);
454
+ await fs2.mkdir(mirrorRoot, { recursive: true });
455
+ for (const root of getLatestContentRoots(config2)) {
456
+ const source = path3.resolve(projectRoot, root.fsPath);
457
+ const dest = path3.join(mirrorRoot, root.contentDir);
458
+ await mirrorTree(source, dest);
459
+ }
460
+ for (const version of config2.versions ?? []) {
461
+ const versionMirror = path3.join(mirrorRoot, version.dir);
462
+ await fs2.mkdir(versionMirror, { recursive: true });
463
+ for (const root of getVersionContentRoots(config2, version.dir)) {
464
+ const source = path3.resolve(projectRoot, root.fsPath);
465
+ const dest = path3.join(versionMirror, root.contentDir);
466
+ await mirrorTree(source, dest);
467
+ }
468
+ }
469
+ }
470
+ function linkContent(projectRoot, config2) {
471
+ return buildContentMirror(path3.join(PACKAGE_ROOT, ".content"), projectRoot, config2);
472
+ }
473
+ async function mirrorTree(source, dest) {
474
+ let entries;
338
475
  try {
339
- const existing = await fs2.readlink(linkPath);
340
- if (existing === target)
476
+ entries = await fs2.readdir(source, { withFileTypes: true });
477
+ } catch (error) {
478
+ const err = error;
479
+ if (err.code === "ENOENT") {
480
+ throw new Error(`Content directory not found: ${source}`);
481
+ }
482
+ throw error;
483
+ }
484
+ await fs2.mkdir(dest, { recursive: true });
485
+ for (const entry of entries) {
486
+ const sourcePath = path3.join(source, entry.name);
487
+ const destPath = path3.join(dest, entry.name);
488
+ if (entry.isDirectory()) {
489
+ await mirrorTree(sourcePath, destPath);
490
+ } else if (entry.isFile() || entry.isSymbolicLink()) {
491
+ await fs2.symlink(sourcePath, destPath);
492
+ }
493
+ }
494
+ }
495
+ async function removeMirror(mirrorRoot) {
496
+ try {
497
+ const stat = await fs2.lstat(mirrorRoot);
498
+ if (stat.isSymbolicLink() || stat.isFile()) {
499
+ await fs2.unlink(mirrorRoot);
500
+ } else if (stat.isDirectory()) {
501
+ await fs2.rm(mirrorRoot, { recursive: true, force: true });
502
+ }
503
+ } catch (error) {
504
+ if (error.code === "ENOENT")
341
505
  return;
342
- await fs2.unlink(linkPath);
343
- } catch {}
344
- await fs2.symlink(target, linkPath);
506
+ throw error;
507
+ }
345
508
  }
346
509
 
347
510
  // src/cli/commands/build.ts
348
- var buildCommand = new Command("build").description("Build for production").option("--content <path>", "Content directory").option("--config <path>", "Path to chronicle.yaml").option("--preset <preset>", "Deploy preset (vercel, cloudflare, node-server)").action(async (options) => {
349
- const { contentDir, configPath, preset } = await loadCLIConfig(options.config, {
350
- content: options.content,
511
+ var buildCommand = new Command("build").description("Build for production").option("--config <path>", "Path to chronicle.yaml").option("--preset <preset>", "Deploy preset (vercel, cloudflare, node-server)").action(async (options) => {
512
+ const { config: config2, projectRoot, configPath, preset } = await loadCLIConfig(options.config, {
351
513
  preset: options.preset
352
514
  });
353
- await linkContent(contentDir);
515
+ await linkContent(projectRoot, config2);
354
516
  console.log(chalk2.cyan("Building for production..."));
355
517
  const { createBuilder } = await import("vite");
356
518
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_vite_config(), exports_vite_config));
357
- const config2 = await createViteConfig2({
519
+ const viteConfig = await createViteConfig2({
358
520
  packageRoot: PACKAGE_ROOT,
359
- projectRoot: process.cwd(),
360
- contentDir,
521
+ projectRoot,
361
522
  configPath,
362
523
  preset
363
524
  });
364
- const builder = await createBuilder({ ...config2, builder: {} });
525
+ const builder = await createBuilder({ ...viteConfig, builder: {} });
365
526
  await builder.buildApp();
366
527
  console.log(chalk2.green("Build complete"));
367
528
  console.log(chalk2.cyan("Run `chronicle start` to start the server"));
@@ -370,17 +531,17 @@ var buildCommand = new Command("build").description("Build for production").opti
370
531
  // src/cli/commands/dev.ts
371
532
  import chalk3 from "chalk";
372
533
  import { Command as Command2 } from "commander";
373
- var devCommand = new Command2("dev").description("Start development server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").action(async (options) => {
374
- const { contentDir, configPath } = await loadCLIConfig(options.config, { content: options.content });
534
+ var devCommand = new Command2("dev").description("Start development server").option("-p, --port <port>", "Port number", "3000").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").action(async (options) => {
535
+ const { config: config2, projectRoot, configPath } = await loadCLIConfig(options.config);
375
536
  const port = parseInt(options.port, 10);
376
- await linkContent(contentDir);
537
+ await linkContent(projectRoot, config2);
377
538
  console.log(chalk3.cyan("Starting dev server..."));
378
539
  const { createServer } = await import("vite");
379
540
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_vite_config(), exports_vite_config));
380
- const config2 = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir, configPath });
541
+ const viteConfig = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot, configPath });
381
542
  const server = await createServer({
382
- ...config2,
383
- server: { ...config2.server, port, host: options.host }
543
+ ...viteConfig,
544
+ server: { ...viteConfig.server, port, host: options.host }
384
545
  });
385
546
  await server.listen();
386
547
  server.printUrls();
@@ -392,9 +553,12 @@ import path5 from "node:path";
392
553
  import chalk4 from "chalk";
393
554
  import { Command as Command3 } from "commander";
394
555
  import { stringify } from "yaml";
395
- var defaultConfig = {
396
- title: "My Documentation",
397
- description: "Documentation powered by Chronicle",
556
+ var defaultInitConfig = {
557
+ site: {
558
+ title: "My Documentation",
559
+ description: "Documentation powered by Chronicle"
560
+ },
561
+ content: [{ dir: "docs", label: "Docs" }],
398
562
  theme: { name: "default" },
399
563
  search: { enabled: true, placeholder: "Search documentation..." }
400
564
  };
@@ -408,44 +572,61 @@ order: 1
408
572
 
409
573
  This is your documentation home page.
410
574
  `;
411
- var initCommand = new Command3("init").description("Initialize a new Chronicle project").option("-c, --content <path>", "Content directory name", "content").action((options) => {
412
- const projectDir = process.cwd();
413
- const contentDir = path5.join(projectDir, options.content);
575
+ var GITIGNORE_ENTRIES = ["node_modules", "dist", ".output"];
576
+ function runInit(projectDir) {
577
+ const events = [];
578
+ const defaultDir = defaultInitConfig.content[0].dir;
579
+ const contentDir = path5.join(projectDir, "content", defaultDir);
414
580
  if (!fs4.existsSync(contentDir)) {
415
581
  fs4.mkdirSync(contentDir, { recursive: true });
416
- console.log(chalk4.green("✓"), "Created", contentDir);
582
+ events.push({ type: "created", path: contentDir });
417
583
  }
418
584
  const configPath = path5.join(projectDir, "chronicle.yaml");
419
585
  if (!fs4.existsSync(configPath)) {
420
- fs4.writeFileSync(configPath, stringify(defaultConfig));
421
- console.log(chalk4.green("✓"), "Created", configPath);
586
+ fs4.writeFileSync(configPath, stringify(defaultInitConfig));
587
+ events.push({ type: "created", path: configPath });
422
588
  } else {
423
- console.log(chalk4.yellow(""), configPath, "already exists");
589
+ events.push({ type: "skipped", path: configPath, detail: "already exists" });
424
590
  }
425
591
  const contentFiles = fs4.readdirSync(contentDir);
426
592
  if (contentFiles.length === 0) {
427
593
  const indexPath = path5.join(contentDir, "index.mdx");
428
594
  fs4.writeFileSync(indexPath, sampleMdx);
429
- console.log(chalk4.green("✓"), "Created", indexPath);
595
+ events.push({ type: "created", path: indexPath });
430
596
  }
431
597
  const gitignorePath = path5.join(projectDir, ".gitignore");
432
- const gitignoreEntries = ["node_modules", "dist", ".output"];
433
598
  if (fs4.existsSync(gitignorePath)) {
434
599
  const existing = fs4.readFileSync(gitignorePath, "utf-8");
435
- const missing = gitignoreEntries.filter((e) => !existing.includes(e));
600
+ const existingLines = new Set(existing.split(/\r?\n/).map((l) => l.trim()).filter(Boolean));
601
+ const missing = GITIGNORE_ENTRIES.filter((e) => !existingLines.has(e));
436
602
  if (missing.length > 0) {
437
603
  fs4.appendFileSync(gitignorePath, `
438
604
  ${missing.join(`
439
605
  `)}
440
606
  `);
441
- console.log(chalk4.green(""), "Added", missing.join(", "), "to .gitignore");
607
+ events.push({ type: "updated", path: gitignorePath, detail: missing.join(", ") });
442
608
  }
443
609
  } else {
444
- fs4.writeFileSync(gitignorePath, `${gitignoreEntries.join(`
610
+ fs4.writeFileSync(gitignorePath, `${GITIGNORE_ENTRIES.join(`
445
611
  `)}
446
612
  `);
447
- console.log(chalk4.green(""), "Created .gitignore");
613
+ events.push({ type: "created", path: gitignorePath });
614
+ }
615
+ return events;
616
+ }
617
+ function formatEvent(e) {
618
+ if (e.type === "skipped") {
619
+ return `${chalk4.yellow("⚠")} ${e.path}${e.detail ? ` ${e.detail}` : ""}`;
448
620
  }
621
+ if (e.type === "updated") {
622
+ return `${chalk4.green("✓")} Updated ${e.path}${e.detail ? ` (+${e.detail})` : ""}`;
623
+ }
624
+ return `${chalk4.green("✓")} Created ${e.path}`;
625
+ }
626
+ var initCommand = new Command3("init").description("Initialize a new Chronicle project").action(() => {
627
+ const events = runInit(process.cwd());
628
+ for (const e of events)
629
+ console.log(formatEvent(e));
449
630
  console.log(chalk4.green(`
450
631
  ✓ Chronicle initialized!`));
451
632
  console.log(`
@@ -455,27 +636,25 @@ Run`, chalk4.cyan("chronicle dev"), "to start development server");
455
636
  // src/cli/commands/serve.ts
456
637
  import chalk5 from "chalk";
457
638
  import { Command as Command4 } from "commander";
458
- var serveCommand = new Command4("serve").description("Build and start production server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").option("--preset <preset>", "Deploy preset (vercel, cloudflare, node-server)").action(async (options) => {
459
- const { contentDir, configPath, preset } = await loadCLIConfig(options.config, {
460
- content: options.content,
639
+ var serveCommand = new Command4("serve").description("Build and start production server").option("-p, --port <port>", "Port number", "3000").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").option("--preset <preset>", "Deploy preset (vercel, cloudflare, node-server)").action(async (options) => {
640
+ const { config: config2, projectRoot, configPath, preset } = await loadCLIConfig(options.config, {
461
641
  preset: options.preset
462
642
  });
463
643
  const port = parseInt(options.port, 10);
464
- await linkContent(contentDir);
644
+ await linkContent(projectRoot, config2);
465
645
  const { build, preview } = await import("vite");
466
646
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_vite_config(), exports_vite_config));
467
- const config2 = await createViteConfig2({
647
+ const viteConfig = await createViteConfig2({
468
648
  packageRoot: PACKAGE_ROOT,
469
- projectRoot: process.cwd(),
470
- contentDir,
649
+ projectRoot,
471
650
  configPath,
472
651
  preset
473
652
  });
474
653
  console.log(chalk5.cyan("Building for production..."));
475
- await build(config2);
654
+ await build(viteConfig);
476
655
  console.log(chalk5.cyan("Starting production server..."));
477
656
  const server = await preview({
478
- ...config2,
657
+ ...viteConfig,
479
658
  preview: { port, host: options.host }
480
659
  });
481
660
  server.printUrls();
@@ -484,16 +663,16 @@ var serveCommand = new Command4("serve").description("Build and start production
484
663
  // src/cli/commands/start.ts
485
664
  import chalk6 from "chalk";
486
665
  import { Command as Command5 } from "commander";
487
- var startCommand = new Command5("start").description("Start production server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").option("--host <host>", "Host address", "localhost").action(async (options) => {
488
- const { contentDir, configPath } = await loadCLIConfig(undefined, { content: options.content });
666
+ var startCommand = new Command5("start").description("Start production server").option("-p, --port <port>", "Port number", "3000").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").action(async (options) => {
667
+ const { config: config2, projectRoot, configPath } = await loadCLIConfig(options.config);
489
668
  const port = parseInt(options.port, 10);
490
- await linkContent(contentDir);
669
+ await linkContent(projectRoot, config2);
491
670
  console.log(chalk6.cyan("Starting production server..."));
492
671
  const { preview } = await import("vite");
493
672
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_vite_config(), exports_vite_config));
494
- const config2 = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir, configPath });
673
+ const viteConfig = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot, configPath });
495
674
  const server = await preview({
496
- ...config2,
675
+ ...viteConfig,
497
676
  preview: { port, host: options.host }
498
677
  });
499
678
  server.printUrls();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raystack/chronicle",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -16,7 +16,8 @@
16
16
  },
17
17
  "scripts": {
18
18
  "build:cli": "bun build-cli.ts",
19
- "lint": "biome lint src/"
19
+ "lint": "biome lint src/",
20
+ "test": "bun test"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@biomejs/biome": "^2.3.13",
@@ -41,20 +42,20 @@
41
42
  "@opentelemetry/resources": "^2.6.1",
42
43
  "@opentelemetry/sdk-metrics": "^2.6.1",
43
44
  "@opentelemetry/semantic-conventions": "^1.40.0",
44
- "@raystack/apsara": "0.55.1",
45
+ "@raystack/apsara": "1.0.0-rc.4",
45
46
  "@shikijs/rehype": "^4.0.2",
46
47
  "@vitejs/plugin-react": "^6.0.1",
48
+ "beautiful-mermaid": "^1.1.3",
47
49
  "chalk": "^5.6.2",
48
50
  "class-variance-authority": "^0.7.1",
49
51
  "codemirror": "^6.0.2",
50
52
  "commander": "^14.0.2",
51
- "fumadocs-core": "16.6.15",
52
- "fumadocs-mdx": "^14.2.6",
53
+ "fumadocs-core": "16.8.1",
54
+ "fumadocs-mdx": "14.3.1",
53
55
  "glob": "^11.0.0",
54
56
  "gray-matter": "^4.0.3",
55
57
  "h3": "^2.0.1-rc.16",
56
58
  "lodash": "^4.17.23",
57
- "mermaid": "^11.13.0",
58
59
  "minisearch": "^7.2.0",
59
60
  "nitro": "3.0.260311-beta",
60
61
  "openapi-types": "^12.1.3",
@@ -68,6 +69,7 @@
68
69
  "remark-parse": "^11.0.0",
69
70
  "satori": "^0.25.0",
70
71
  "slugify": "^1.6.6",
72
+ "std-env": "^4.1.0",
71
73
  "unified": "^11.0.5",
72
74
  "unist-util-visit": "^5.1.0",
73
75
  "vite": "8.0.3",
@@ -6,33 +6,30 @@ import { linkContent } from '@/cli/utils/scaffold';
6
6
 
7
7
  export const buildCommand = new Command('build')
8
8
  .description('Build for production')
9
- .option('--content <path>', 'Content directory')
10
9
  .option('--config <path>', 'Path to chronicle.yaml')
11
10
  .option(
12
11
  '--preset <preset>',
13
12
  'Deploy preset (vercel, cloudflare, node-server)'
14
13
  )
15
14
  .action(async options => {
16
- const { contentDir, configPath, preset } = await loadCLIConfig(options.config, {
17
- content: options.content,
15
+ const { config, projectRoot, configPath, preset } = await loadCLIConfig(options.config, {
18
16
  preset: options.preset,
19
17
  });
20
- await linkContent(contentDir);
18
+ await linkContent(projectRoot, config);
21
19
 
22
20
  console.log(chalk.cyan('Building for production...'));
23
21
 
24
22
  const { createBuilder } = await import('vite');
25
23
  const { createViteConfig } = await import('@/server/vite-config');
26
24
 
27
- const config = await createViteConfig({
25
+ const viteConfig = await createViteConfig({
28
26
  packageRoot: PACKAGE_ROOT,
29
- projectRoot: process.cwd(),
30
- contentDir,
27
+ projectRoot,
31
28
  configPath,
32
29
  preset
33
30
  });
34
31
 
35
- const builder = await createBuilder({ ...config, builder: {} });
32
+ const builder = await createBuilder({ ...viteConfig, builder: {} });
36
33
  await builder.buildApp();
37
34
 
38
35
  console.log(chalk.green('Build complete'));
@@ -7,24 +7,23 @@ import { linkContent } from '@/cli/utils/scaffold';
7
7
  export const devCommand = new Command('dev')
8
8
  .description('Start development server')
9
9
  .option('-p, --port <port>', 'Port number', '3000')
10
- .option('--content <path>', 'Content directory')
11
10
  .option('--config <path>', 'Path to chronicle.yaml')
12
11
  .option('--host <host>', 'Host address', 'localhost')
13
12
  .action(async options => {
14
- const { contentDir, configPath } = await loadCLIConfig(options.config, { content: options.content });
13
+ const { config, projectRoot, configPath } = await loadCLIConfig(options.config);
15
14
  const port = parseInt(options.port, 10);
16
15
 
17
- await linkContent(contentDir);
16
+ await linkContent(projectRoot, config);
18
17
 
19
18
  console.log(chalk.cyan('Starting dev server...'));
20
19
 
21
20
  const { createServer } = await import('vite');
22
21
  const { createViteConfig } = await import('@/server/vite-config');
23
22
 
24
- const config = await createViteConfig({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir, configPath });
23
+ const viteConfig = await createViteConfig({ packageRoot: PACKAGE_ROOT, projectRoot, configPath });
25
24
  const server = await createServer({
26
- ...config,
27
- server: { ...config.server, port, host: options.host }
25
+ ...viteConfig,
26
+ server: { ...viteConfig.server, port, host: options.host }
28
27
  });
29
28
 
30
29
  await server.listen();