koguma 2.2.0 → 2.2.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/cli/scaffold.ts CHANGED
@@ -11,7 +11,11 @@ import {
11
11
  import { resolve } from 'path';
12
12
  import { ok, warn } from './log.ts';
13
13
  import { generateKogumaToml } from './config.ts';
14
- import { findMarkdownField, type ContentTypeInfo } from './content.ts';
14
+ import {
15
+ findMarkdownField,
16
+ findMarkdownFields,
17
+ type ContentTypeInfo
18
+ } from './content.ts';
15
19
  import matter from 'gray-matter';
16
20
 
17
21
  // ── Template types ─────────────────────────────────────────────────
@@ -315,29 +319,50 @@ export function generateExampleFile(
315
319
  ctId: string,
316
320
  fields: Record<string, { fieldType: string }>,
317
321
  singleton?: boolean
318
- ): { content: string; extension: string } {
322
+ ): {
323
+ content: string;
324
+ extension: string;
325
+ siblingFiles?: { fieldId: string; content: string }[];
326
+ } {
319
327
  const frontmatter: Record<string, unknown> = {};
320
- let hasMarkdown = false;
328
+ const mdFields = Object.entries(fields)
329
+ .filter(([, meta]) => meta.fieldType === 'markdown')
330
+ .map(([id]) => id);
331
+ const primaryMdField = mdFields[0] ?? null;
332
+ const extraMdFields = mdFields.slice(1);
321
333
 
322
334
  for (const [fieldId, meta] of Object.entries(fields)) {
323
335
  if (meta.fieldType === 'markdown') {
324
- hasMarkdown = true;
325
- continue; // markdown goes in body, not frontmatter
336
+ continue; // markdown goes in body or sibling files, not frontmatter
326
337
  }
327
338
  frontmatter[fieldId] = placeholderForFieldType(meta.fieldType);
328
339
  }
329
340
 
330
- if (hasMarkdown) {
341
+ // Build sibling example files for extra markdown fields
342
+ const siblingFiles: { fieldId: string; content: string }[] = [];
343
+ for (const fieldId of extraMdFields) {
344
+ siblingFiles.push({
345
+ fieldId,
346
+ content: `Write your ${fieldId} content here.`
347
+ });
348
+ }
349
+
350
+ if (primaryMdField) {
331
351
  const fm = matter.stringify('', frontmatter).trim();
332
352
  const bodyHint = singleton ? '' : `\nWrite your ${ctId} content here.\n`;
333
353
  return {
334
354
  content: `${fm}\n${bodyHint}`,
335
- extension: '.md'
355
+ extension: '.md',
356
+ ...(siblingFiles.length > 0 ? { siblingFiles } : {})
336
357
  };
337
358
  }
338
359
 
339
360
  const fm = matter.stringify('', frontmatter).trim();
340
- return { content: fm + '\n', extension: '.md' };
361
+ return {
362
+ content: fm + '\n',
363
+ extension: '.md',
364
+ ...(siblingFiles.length > 0 ? { siblingFiles } : {})
365
+ };
341
366
  }
342
367
 
343
368
  /**
@@ -367,7 +392,7 @@ export function scaffoldContentDirFromTemplate(
367
392
  fields[fid] = { fieldType: fieldTypeFromExpression(expr) };
368
393
  }
369
394
 
370
- const { content, extension } = generateExampleFile(
395
+ const { content, extension, siblingFiles } = generateExampleFile(
371
396
  ct.id,
372
397
  fields,
373
398
  ct.singleton
@@ -375,6 +400,14 @@ export function scaffoldContentDirFromTemplate(
375
400
  const filename = `_example${extension}`;
376
401
  writeFileSync(resolve(typeDir, filename), content);
377
402
  ok(`Created content/${ct.id}/${filename}`);
403
+
404
+ // Write sibling example files for extra markdown fields
405
+ if (siblingFiles) {
406
+ for (const { fieldId, content: siblingContent } of siblingFiles) {
407
+ const siblingName = `_example.${fieldId}.md`;
408
+ writeFileSync(resolve(typeDir, siblingName), siblingContent + '\n');
409
+ }
410
+ }
378
411
  } else if (!dirExisted) {
379
412
  ok(`Created content/${ct.id}/`);
380
413
  }
@@ -408,7 +441,7 @@ export function scaffoldContentDir(
408
441
  const isEmpty = dirExisted ? readdirSync(typeDir).length === 0 : true;
409
442
 
410
443
  if (isEmpty) {
411
- const { content, extension } = generateExampleFile(
444
+ const { content, extension, siblingFiles } = generateExampleFile(
412
445
  ct.id,
413
446
  ct.fieldMeta,
414
447
  ct.singleton
@@ -416,6 +449,14 @@ export function scaffoldContentDir(
416
449
  const filename = `_example${extension}`;
417
450
  writeFileSync(resolve(typeDir, filename), content);
418
451
  ok(`Created content/${ct.id}/${filename}`);
452
+
453
+ // Write sibling example files for extra markdown fields
454
+ if (siblingFiles) {
455
+ for (const { fieldId, content: siblingContent } of siblingFiles) {
456
+ const siblingName = `_example.${fieldId}.md`;
457
+ writeFileSync(resolve(typeDir, siblingName), siblingContent + '\n');
458
+ }
459
+ }
419
460
  } else if (!dirExisted) {
420
461
  ok(`Created content/${ct.id}/`);
421
462
  }
@@ -538,7 +579,7 @@ export function syncContentDirsWithConfig(
538
579
  mkdirSync(typeDir, { recursive: true });
539
580
  }
540
581
 
541
- const { content, extension } = generateExampleFile(
582
+ const { content, extension, siblingFiles } = generateExampleFile(
542
583
  ct.id,
543
584
  ct.fieldMeta,
544
585
  ct.singleton
@@ -551,6 +592,14 @@ export function syncContentDirsWithConfig(
551
592
  if (!dryRun) {
552
593
  writeFileSync(resolve(typeDir, filename), content);
553
594
  ok(`Created content/${ct.id}/${filename}`);
595
+
596
+ // Write sibling example files for extra markdown fields
597
+ if (siblingFiles) {
598
+ for (const { fieldId, content: siblingContent } of siblingFiles) {
599
+ const siblingName = `_example.${fieldId}.md`;
600
+ writeFileSync(resolve(typeDir, siblingName), siblingContent + '\n');
601
+ }
602
+ }
554
603
  }
555
604
  continue;
556
605
  }
@@ -559,7 +608,7 @@ export function syncContentDirsWithConfig(
559
608
 
560
609
  // If dir is empty, create _example
561
610
  if (files.length === 0) {
562
- const { content, extension } = generateExampleFile(
611
+ const { content, extension, siblingFiles } = generateExampleFile(
563
612
  ct.id,
564
613
  ct.fieldMeta,
565
614
  ct.singleton
@@ -572,6 +621,14 @@ export function syncContentDirsWithConfig(
572
621
  if (!dryRun) {
573
622
  writeFileSync(resolve(typeDir, filename), content);
574
623
  ok(`Created content/${ct.id}/${filename}`);
624
+
625
+ // Write sibling example files for extra markdown fields
626
+ if (siblingFiles) {
627
+ for (const { fieldId, content: siblingContent } of siblingFiles) {
628
+ const siblingName = `_example.${fieldId}.md`;
629
+ writeFileSync(resolve(typeDir, siblingName), siblingContent + '\n');
630
+ }
631
+ }
575
632
  }
576
633
  continue;
577
634
  }
package/cli/wrangler.ts CHANGED
@@ -320,12 +320,36 @@ export function wranglerDev(
320
320
  /^✘\s+/, // npm dep install error lines (let warn() handle these)
321
321
  /^▲\s+/, // npm dep install warning lines
322
322
  /^>\s+/, // npm progress lines
323
- /^\s*$/ // blank lines
323
+ /^\s*$/, // blank lines
324
+ /Reloading local server/, // wrangler dev reload spam
325
+ /Local server updated/, // wrangler dev reload (handled as status line)
326
+ /Unable to find and open the program executable/ // benign diagnostic
324
327
  ];
325
328
 
326
329
  const shouldSuppress = (line: string): boolean =>
327
330
  suppressPatterns.some(p => p.test(line));
328
331
 
332
+ // ── In-place status line for transient events ──
333
+ let reloadCount = 0;
334
+ let hasStatusLine = false;
335
+ const isTTY = process.stdout.isTTY;
336
+
337
+ /** Write a transient status that overwrites itself on the next call */
338
+ const writeStatus = (text: string) => {
339
+ if (isTTY) {
340
+ process.stdout.write(`\r\x1b[K ⟳ ${text}`);
341
+ hasStatusLine = true;
342
+ }
343
+ };
344
+
345
+ /** Clear the status line before printing a permanent line */
346
+ const clearStatus = () => {
347
+ if (hasStatusLine && isTTY) {
348
+ process.stdout.write('\r\x1b[K');
349
+ hasStatusLine = false;
350
+ }
351
+ };
352
+
329
353
  const handleOutput = (data: Buffer, isErr: boolean) => {
330
354
  const text = data.toString();
331
355
  for (const line of text.split('\n')) {
@@ -336,16 +360,29 @@ export function wranglerDev(
336
360
  if (trimmed.includes('Ready on http')) {
337
361
  const urlMatch = trimmed.match(/(https?:\/\/[^\s]+)/);
338
362
  const url = urlMatch?.[1] ?? 'http://localhost:8787';
363
+ clearStatus();
339
364
  ok(`Server ready → ${url}`);
340
365
  continue;
341
366
  }
342
367
 
368
+ // Reload events → transient status line (overwrites in place)
369
+ if (/Reloading local server/.test(trimmed)) {
370
+ reloadCount++;
371
+ writeStatus(`reload #${reloadCount}`);
372
+ continue;
373
+ }
374
+ if (/Local server updated/.test(trimmed)) {
375
+ writeStatus(`reload #${reloadCount} ✓`);
376
+ continue;
377
+ }
378
+
343
379
  if (shouldSuppress(line)) continue;
344
380
 
345
381
  if (trimmed.includes('Starting local server')) continue;
346
382
  if (trimmed.includes('Shutting down')) continue;
347
383
 
348
- // Forward everything else (request logs, errors, warnings)
384
+ // Permanent line clear status first
385
+ clearStatus();
349
386
  if (isErr) {
350
387
  warn(trimmed);
351
388
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koguma",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "🐻 A little CMS with big heart — schema-driven, runs on Cloudflare's free tier",
5
5
  "type": "module",
6
6
  "license": "MIT",