@hutusi/amytis 1.7.0 → 1.9.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 (83) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/CHANGELOG.md +63 -0
  3. package/CLAUDE.md +9 -18
  4. package/GEMINI.md +6 -0
  5. package/README.md +44 -0
  6. package/TODO.md +15 -3
  7. package/bun.lock +5 -3
  8. package/content/about.mdx +64 -10
  9. package/content/about.zh.mdx +66 -9
  10. package/content/books/sample-book/index.mdx +3 -3
  11. package/content/flows/2026/02/05.md +0 -1
  12. package/content/flows/2026/02/10.mdx +2 -1
  13. package/content/flows/2026/02/15.md +2 -1
  14. package/content/flows/2026/02/18.mdx +2 -1
  15. package/content/flows/2026/02/20.md +0 -1
  16. package/content/notes/algorithms-and-data-structures.mdx +51 -0
  17. package/content/notes/digital-garden-philosophy.mdx +36 -0
  18. package/content/notes/react-server-components.mdx +49 -0
  19. package/content/notes/tailwind-v4.mdx +45 -0
  20. package/content/notes/zettelkasten-method.mdx +33 -0
  21. package/content/series/digital-garden/01-philosophy.mdx +25 -12
  22. package/docs/ARCHITECTURE.md +9 -1
  23. package/docs/CONTRIBUTING.md +26 -0
  24. package/docs/DIGITAL_GARDEN.md +72 -0
  25. package/imports/README.md +45 -0
  26. package/package.json +12 -5
  27. package/scripts/generate-knowledge-graph.ts +162 -0
  28. package/scripts/import-book.ts +176 -0
  29. package/scripts/new-flow-from-chat.ts +238 -0
  30. package/scripts/new-flow.ts +0 -5
  31. package/scripts/new-note.ts +53 -0
  32. package/scripts/sync-book-chapters.ts +210 -0
  33. package/site.config.ts +30 -7
  34. package/src/app/authors/[author]/page.tsx +3 -1
  35. package/src/app/books/[slug]/[chapter]/page.tsx +2 -1
  36. package/src/app/books/[slug]/page.tsx +6 -5
  37. package/src/app/flows/[year]/[month]/[day]/page.tsx +35 -29
  38. package/src/app/flows/[year]/[month]/page.tsx +18 -13
  39. package/src/app/flows/[year]/page.tsx +25 -15
  40. package/src/app/flows/page/[page]/page.tsx +5 -9
  41. package/src/app/flows/page.tsx +5 -8
  42. package/src/app/globals.css +41 -0
  43. package/src/app/graph/page.tsx +21 -0
  44. package/src/app/layout.tsx +4 -2
  45. package/src/app/notes/[slug]/page.tsx +129 -0
  46. package/src/app/notes/page/[page]/page.tsx +60 -0
  47. package/src/app/notes/page.tsx +33 -0
  48. package/src/app/page/[page]/page.tsx +1 -0
  49. package/src/app/page.tsx +4 -5
  50. package/src/app/posts/[slug]/page.tsx +5 -2
  51. package/src/app/posts/page/[page]/page.tsx +4 -1
  52. package/src/app/search.json/route.ts +17 -3
  53. package/src/app/series/[slug]/page/[page]/page.tsx +1 -0
  54. package/src/app/series/[slug]/page.tsx +3 -3
  55. package/src/app/sitemap.ts +1 -1
  56. package/src/app/tags/[tag]/page.tsx +3 -3
  57. package/src/components/Backlinks.tsx +39 -0
  58. package/src/components/BookMobileNav.tsx +11 -11
  59. package/src/components/BookSidebar.tsx +17 -25
  60. package/src/components/BrowserDetectionBanner.tsx +96 -0
  61. package/src/components/FeaturedStoriesSection.tsx +1 -1
  62. package/src/components/FlowCalendarSidebar.tsx +4 -2
  63. package/src/components/FlowContent.tsx +4 -3
  64. package/src/components/FlowHubTabs.tsx +50 -0
  65. package/src/components/FlowTimelineEntry.tsx +7 -9
  66. package/src/components/KnowledgeGraph.tsx +324 -0
  67. package/src/components/LanguageProvider.tsx +14 -5
  68. package/src/components/MarkdownRenderer.tsx +13 -2
  69. package/src/components/Navbar.tsx +237 -10
  70. package/src/components/NoteContent.tsx +123 -0
  71. package/src/components/NoteSidebar.tsx +132 -0
  72. package/src/components/RecentNotesSection.tsx +6 -11
  73. package/src/components/Search.tsx +7 -3
  74. package/src/components/TagContentTabs.tsx +0 -1
  75. package/src/i18n/translations.ts +43 -17
  76. package/src/layouts/BookLayout.tsx +3 -3
  77. package/src/layouts/PostLayout.tsx +8 -3
  78. package/src/lib/i18n.ts +83 -6
  79. package/src/lib/markdown.ts +306 -19
  80. package/src/lib/remark-wikilinks.ts +59 -0
  81. package/src/lib/search-utils.ts +2 -1
  82. package/tests/unit/static-params.test.ts +238 -0
  83. package/content/series/digital-garden/01-philosophy/index.mdx +0 -23
@@ -0,0 +1,176 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+
5
+ // Usage: bun run scripts/import-book.ts <main-file-path>
6
+ // Example: bun run scripts/import-book.ts imports/books/agentic-design-patterns/chapters/Agentic\ Design\ Patterns.md
7
+
8
+ const args = process.argv.slice(2);
9
+ const mainFilePath = args[0];
10
+
11
+ if (!mainFilePath) {
12
+ console.error('Usage: bun run scripts/import-book.ts <main-file-path>');
13
+ process.exit(1);
14
+ }
15
+
16
+ const fullMainPath = path.isAbsolute(mainFilePath) ? mainFilePath : path.join(process.cwd(), mainFilePath);
17
+
18
+ if (!fs.existsSync(fullMainPath)) {
19
+ console.error(`Error: Main file not found at ${fullMainPath}`);
20
+ process.exit(1);
21
+ }
22
+
23
+ const sourceDir = path.dirname(fullMainPath);
24
+ // Parent of chapters/ is usually the book root
25
+ const bookRootDir = path.dirname(sourceDir);
26
+ const bookSlug = path.basename(bookRootDir).toLowerCase().replace(/[^a-z0-9]+/g, '-');
27
+ const contentDir = path.join(process.cwd(), 'content', 'books', bookSlug);
28
+
29
+ if (!fs.existsSync(contentDir)) {
30
+ fs.mkdirSync(contentDir, { recursive: true });
31
+ }
32
+
33
+ const mainContent = fs.readFileSync(fullMainPath, 'utf8');
34
+ const lines = mainContent.split('\n');
35
+
36
+ interface ChapterRef { title: string; id: string }
37
+ interface PartGroup { part: string; chapters: ChapterRef[] }
38
+ type TocItem = ChapterRef | PartGroup;
39
+
40
+ const toc: TocItem[] = [];
41
+ let currentPart: PartGroup | null = null;
42
+ const usedChapterIds = new Set<string>();
43
+
44
+ function slugify(text: string): string {
45
+ return text
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
48
+ .replace(/(^-|-$)+/g, '');
49
+ }
50
+
51
+ function processChapter(title: string, rawPath: string): ChapterRef | null {
52
+ // Decode URI component (e.g. %20 -> space)
53
+ const cleanPath = decodeURIComponent(rawPath.replace(/^<|>$/g, ''));
54
+ const fullSrcPath = path.resolve(sourceDir, cleanPath);
55
+ const relativeToRoot = path.relative(bookRootDir, fullSrcPath);
56
+ if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
57
+ console.warn(`Warning: Chapter path escapes book root, skipping: ${rawPath}`);
58
+ return null;
59
+ }
60
+
61
+ if (!fs.existsSync(fullSrcPath)) {
62
+ console.warn(`Warning: File not found: ${fullSrcPath}`);
63
+ return null;
64
+ }
65
+
66
+ let id = slugify(path.basename(cleanPath).replace(/\.mdx?$/, ''));
67
+ if (!id) {
68
+ id = slugify(title);
69
+ }
70
+ if (!id) {
71
+ console.warn(`Warning: Could not derive chapter id, skipping: ${rawPath}`);
72
+ return null;
73
+ }
74
+ const baseId = id;
75
+ let suffix = 2;
76
+ while (usedChapterIds.has(id) || fs.existsSync(path.join(contentDir, `${id}.mdx`))) {
77
+ id = `${baseId}-${suffix++}`;
78
+ }
79
+ usedChapterIds.add(id);
80
+ const destPath = path.join(contentDir, `${id}.mdx`);
81
+
82
+ const rawContent = fs.readFileSync(fullSrcPath, 'utf8');
83
+ const { data, content: body } = matter(rawContent);
84
+
85
+ let finalTitle = data.title;
86
+ if (!finalTitle) {
87
+ const h1Match = body.match(/^#\s+(.*)/m);
88
+ if (h1Match) {
89
+ finalTitle = h1Match[1].trim();
90
+ }
91
+ }
92
+ if (!finalTitle) finalTitle = title;
93
+
94
+ const chapterData = {
95
+ ...data,
96
+ title: finalTitle,
97
+ };
98
+
99
+ let fixedBody = body;
100
+
101
+ // Normalize image paths to ./images/ regardless of source prefix (../images/, ./images/, images/)
102
+ fixedBody = fixedBody.replace(/(?:\.\.\/|\.\/)?images\//g, './images/');
103
+
104
+ fs.writeFileSync(destPath, matter.stringify(fixedBody, chapterData));
105
+ return { title: chapterData.title, id };
106
+ }
107
+
108
+ for (const line of lines) {
109
+ // Detect Parts (H2)
110
+ const partMatch = line.match(/^##\s+(.*)/);
111
+ if (partMatch) {
112
+ currentPart = { part: partMatch[1].trim(), chapters: [] };
113
+ toc.push(currentPart);
114
+ continue;
115
+ }
116
+
117
+ // Detect Chapter links in list items
118
+ // Matches: - [Title](Path) or 1. [Title](Path)
119
+ const chapterMatch = line.match(/^[-*\d.]+\s+\[(.*?)\]\((.*)\)/);
120
+ if (chapterMatch) {
121
+ const title = chapterMatch[1];
122
+ const rawPath = chapterMatch[2];
123
+ const ch = processChapter(title, rawPath);
124
+
125
+ if (ch) {
126
+ if (currentPart) {
127
+ currentPart.chapters.push(ch);
128
+ } else {
129
+ toc.push(ch);
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ // Write index.mdx
136
+ // Use the H1 from the main file as title
137
+ const h1Match = mainContent.match(/^#\s+(.*)/);
138
+ const bookTitle = h1Match ? h1Match[1].trim() : bookSlug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
139
+
140
+ const indexData = {
141
+ title: bookTitle,
142
+ date: new Date().toISOString().split('T')[0],
143
+ excerpt: `Imported book: ${bookTitle}`,
144
+ chapters: toc,
145
+ draft: false,
146
+ };
147
+
148
+ // Also copy the main file content as the introduction/landing page content
149
+ const { content: introBody } = matter(mainContent);
150
+ let fixedIntroBody = introBody;
151
+ fixedIntroBody = fixedIntroBody.replace(/\.\.\/images\//g, './images/');
152
+
153
+ fs.writeFileSync(path.join(contentDir, 'index.mdx'), matter.stringify(fixedIntroBody, indexData));
154
+
155
+ // Copy images from bookRootDir/images to contentDir/images
156
+ const srcImages = path.join(bookRootDir, 'images');
157
+ const destImages = path.join(contentDir, 'images');
158
+ if (fs.existsSync(srcImages)) {
159
+ if (!fs.existsSync(destImages)) fs.mkdirSync(destImages, { recursive: true });
160
+ const copyDir = (src: string, dest: string) => {
161
+ const entries = fs.readdirSync(src, { withFileTypes: true });
162
+ for (const entry of entries) {
163
+ const sPath = path.join(src, entry.name);
164
+ const dPath = path.join(dest, entry.name);
165
+ if (entry.isDirectory()) {
166
+ if (!fs.existsSync(dPath)) fs.mkdirSync(dPath, { recursive: true });
167
+ copyDir(sPath, dPath);
168
+ } else {
169
+ fs.copyFileSync(sPath, dPath);
170
+ }
171
+ }
172
+ };
173
+ copyDir(srcImages, destImages);
174
+ }
175
+
176
+ console.log(`Successfully imported book to ${contentDir}`);
@@ -0,0 +1,238 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ // Usage (auto-scan — recommended):
5
+ // bun run new-flow-from-chat # import all new files in imports/chats/
6
+ // bun run new-flow-from-chat --all # re-import every file (ignore history)
7
+ // bun run new-flow-from-chat --dry-run # preview without writing
8
+ // bun run new-flow-from-chat --author "Alice" # only include Alice's messages
9
+ // bun run new-flow-from-chat --append # append to existing flow files
10
+ // bun run new-flow-from-chat --timestamp # include timestamps (default: excluded)
11
+ //
12
+ // Usage (explicit file):
13
+ // bun run new-flow-from-chat <file> # process one specific file
14
+ //
15
+ // Import history is tracked in imports/chats/.imported (one filename per line).
16
+ // That file is gitignored along with the rest of imports/.
17
+ //
18
+ // Input format (one message block per entry):
19
+ // username YYYY-MM-DD HH:mm:ss
20
+ // message line 1
21
+ // message line 2
22
+ // ...
23
+ //
24
+ // Output: content/flows/YYYY/MM/DD.md (one file per calendar day)
25
+
26
+ const CHATS_DIR = path.join(process.cwd(), 'imports', 'chats');
27
+ const IMPORTED_RECORD = path.join(CHATS_DIR, '.imported');
28
+
29
+ // ── Arg parsing ────────────────────────────────────────────────────────────
30
+
31
+ const args = process.argv.slice(2);
32
+
33
+ // Collect positional args, skipping values that follow named flags
34
+ const positional: string[] = [];
35
+ for (let i = 0; i < args.length; i++) {
36
+ if (args[i] === '--author') { i++; continue; }
37
+ if (!args[i].startsWith('--')) positional.push(args[i]);
38
+ }
39
+
40
+ const explicitFile = positional[0] ?? null;
41
+ const authorIdx = args.indexOf('--author');
42
+ const filterAuthor = authorIdx > -1 ? args[authorIdx + 1] : null;
43
+ const dryRun = args.includes('--dry-run');
44
+ const appendMode = args.includes('--append');
45
+ const reimportAll = args.includes('--all');
46
+ const includeTimestamp = args.includes('--timestamp');
47
+
48
+ // ── Types ──────────────────────────────────────────────────────────────────
49
+
50
+ interface Message {
51
+ username: string;
52
+ date: string; // YYYY-MM-DD
53
+ time: string; // HH:mm:ss
54
+ lines: string[];
55
+ }
56
+
57
+ // ── Parser ─────────────────────────────────────────────────────────────────
58
+
59
+ // Matches: "username YYYY-MM-DD HH:mm:ss" (username may contain spaces)
60
+ const HEADER_RE = /^(.+?)\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})$/;
61
+
62
+ function parseChat(raw: string): Message[] {
63
+ const messages: Message[] = [];
64
+ let current: Message | null = null;
65
+
66
+ for (const rawLine of raw.split('\n')) {
67
+ const line = rawLine.replace(/\r$/, ''); // strip Windows line endings
68
+ const match = line.match(HEADER_RE);
69
+ if (match) {
70
+ if (current) messages.push(current);
71
+ current = { username: match[1].trim(), date: match[2], time: match[3], lines: [] };
72
+ } else if (current) {
73
+ current.lines.push(line);
74
+ }
75
+ // Lines before the first header are silently ignored
76
+ }
77
+
78
+ if (current) messages.push(current);
79
+ return messages;
80
+ }
81
+
82
+ // ── Rendering ─────────────────────────────────────────────────────────────
83
+
84
+ function renderBlock(msg: Message, showUsername: boolean, showTime: boolean): string {
85
+ const content = msg.lines.join('\n').trimEnd();
86
+ if (!content.trim()) return ''; // skip empty messages
87
+
88
+ const headerParts: string[] = [];
89
+ if (showUsername) headerParts.push(`**${msg.username}**`);
90
+ if (showTime) headerParts.push(msg.time);
91
+
92
+ if (headerParts.length === 0) return content;
93
+ return `${headerParts.join(' · ')}\n\n${content}`;
94
+ }
95
+
96
+ function renderFlow(messages: Message[], showUsername: boolean, showTime: boolean): string {
97
+ const blocks = messages.map(m => renderBlock(m, showUsername, showTime)).filter(Boolean);
98
+ return `---\ntags: []\n---\n\n${blocks.join('\n\n---\n\n')}\n`;
99
+ }
100
+
101
+ // ── Import tracking ────────────────────────────────────────────────────────
102
+
103
+ function loadImported(): Set<string> {
104
+ if (!fs.existsSync(IMPORTED_RECORD)) return new Set();
105
+ return new Set(
106
+ fs.readFileSync(IMPORTED_RECORD, 'utf8')
107
+ .split('\n').map(l => l.trim()).filter(Boolean),
108
+ );
109
+ }
110
+
111
+ function markImported(filename: string): void {
112
+ fs.appendFileSync(IMPORTED_RECORD, filename + '\n');
113
+ }
114
+
115
+ // ── File discovery ─────────────────────────────────────────────────────────
116
+
117
+ function findPendingFiles(): string[] {
118
+ if (!fs.existsSync(CHATS_DIR)) {
119
+ console.error(`Directory not found: ${CHATS_DIR}`);
120
+ console.error('Create it (or run: mkdir -p imports/chats) and drop chat export files there.');
121
+ process.exit(1);
122
+ }
123
+ const imported = reimportAll ? new Set<string>() : loadImported();
124
+ return fs.readdirSync(CHATS_DIR)
125
+ .filter(name => !name.startsWith('.') && /\.(txt|log)$/i.test(name))
126
+ .filter(name => !imported.has(name))
127
+ .sort()
128
+ .map(name => path.join(CHATS_DIR, name));
129
+ }
130
+
131
+ // ── Process one file ───────────────────────────────────────────────────────
132
+
133
+ function processFile(filePath: string): boolean {
134
+ const raw = fs.readFileSync(filePath, 'utf8');
135
+ const allMessages = parseChat(raw);
136
+
137
+ if (allMessages.length === 0) {
138
+ console.log(` ⚠ no messages found — check the file format`);
139
+ return false;
140
+ }
141
+
142
+ const messages = filterAuthor
143
+ ? allMessages.filter(m => m.username.toLowerCase() === filterAuthor.toLowerCase())
144
+ : allMessages;
145
+
146
+ if (messages.length === 0) {
147
+ console.log(` ⚠ no messages from "${filterAuthor}" found`);
148
+ return false;
149
+ }
150
+
151
+ const byDate = new Map<string, Message[]>();
152
+ for (const msg of messages) {
153
+ const list = byDate.get(msg.date) ?? [];
154
+ list.push(msg);
155
+ byDate.set(msg.date, list);
156
+ }
157
+
158
+ const showUsername = filterAuthor === null;
159
+ const showTime = includeTimestamp;
160
+ const flowsDir = path.join(process.cwd(), 'content', 'flows');
161
+ let created = 0, appended = 0, skipped = 0;
162
+
163
+ for (const [date, dayMsgs] of [...byDate.entries()].sort()) {
164
+ const [year, month, day] = date.split('-');
165
+ const dirPath = path.join(flowsDir, year, month);
166
+ const mdPath = path.join(dirPath, `${day}.md`);
167
+ const mdxPath = path.join(dirPath, `${day}.mdx`);
168
+ const existing = fs.existsSync(mdPath) ? mdPath : fs.existsSync(mdxPath) ? mdxPath : null;
169
+ const flowContent = renderFlow(dayMsgs, showUsername, showTime);
170
+
171
+ if (dryRun) {
172
+ const label = `${date} (${dayMsgs.length} msg${dayMsgs.length === 1 ? '' : 's'})`;
173
+ console.log(` ── ${label} → ${path.relative(process.cwd(), mdPath)}`);
174
+ console.log(flowContent);
175
+ continue;
176
+ }
177
+
178
+ if (existing && !appendMode) {
179
+ console.log(` ⚠ ${date}: flow already exists — skipped (use --append to add)`);
180
+ skipped++;
181
+ continue;
182
+ }
183
+
184
+ if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
185
+
186
+ if (existing) {
187
+ const blocks = flowContent.replace(/^---[\s\S]*?---\n\n/, '').trimEnd();
188
+ fs.appendFileSync(existing, `\n\n---\n\n${blocks}\n`);
189
+ console.log(` + ${date}: appended ${dayMsgs.length} msg${dayMsgs.length === 1 ? '' : 's'} → ${path.basename(existing)}`);
190
+ appended++;
191
+ } else {
192
+ fs.writeFileSync(mdPath, flowContent);
193
+ console.log(` ✓ ${date}: ${path.relative(process.cwd(), mdPath)} (${dayMsgs.length} msg${dayMsgs.length === 1 ? '' : 's'})`);
194
+ created++;
195
+ }
196
+ }
197
+
198
+ const parts: string[] = [];
199
+ if (created > 0) parts.push(`${created} created`);
200
+ if (appended > 0) parts.push(`${appended} appended`);
201
+ if (skipped > 0) parts.push(`${skipped} skipped`);
202
+ const summary = dryRun ? 'dry run' : (parts.join(', ') || 'nothing to do');
203
+ console.log(` → ${summary} · ${messages.length} msg${messages.length === 1 ? '' : 's'} across ${byDate.size} day${byDate.size === 1 ? '' : 's'}`);
204
+
205
+ return true;
206
+ }
207
+
208
+ // ── Main ───────────────────────────────────────────────────────────────────
209
+
210
+ if (explicitFile) {
211
+ if (!fs.existsSync(explicitFile)) {
212
+ console.error(`Error: "${explicitFile}" not found.`);
213
+ process.exit(1);
214
+ }
215
+ console.log(`Processing: ${explicitFile}${dryRun ? ' (dry run)' : ''}`);
216
+ processFile(explicitFile);
217
+ } else {
218
+ const pending = findPendingFiles();
219
+
220
+ if (pending.length === 0) {
221
+ const hint = reimportAll ? '' : ' (run with --all to re-import everything)';
222
+ console.log(`Nothing new to import in imports/chats/${hint}.`);
223
+ process.exit(0);
224
+ }
225
+
226
+ const modeLabel = dryRun ? ' (dry run)' : reimportAll ? ' (--all)' : '';
227
+ console.log(`Found ${pending.length} file${pending.length === 1 ? '' : 's'} to import${modeLabel}:`);
228
+
229
+ for (const filePath of pending) {
230
+ console.log(`\n ${path.basename(filePath)}`);
231
+ const ok = processFile(filePath);
232
+ if (!dryRun && ok) markImported(path.basename(filePath));
233
+ }
234
+
235
+ if (!dryRun) {
236
+ console.log(`\nDone. Import history saved to ${path.relative(process.cwd(), IMPORTED_RECORD)}.`);
237
+ }
238
+ }
@@ -2,14 +2,12 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
 
4
4
  const args = process.argv.slice(2);
5
- const title = args.filter(arg => !arg.startsWith('--'))[0];
6
5
  const useMdx = args.includes('--mdx');
7
6
 
8
7
  const now = new Date();
9
8
  const year = String(now.getFullYear());
10
9
  const month = String(now.getMonth() + 1).padStart(2, '0');
11
10
  const day = String(now.getDate()).padStart(2, '0');
12
- const dateStr = `${year}-${month}-${day}`;
13
11
 
14
12
  const ext = useMdx ? '.mdx' : '.md';
15
13
  const dirPath = path.join(process.cwd(), 'content', 'flows', year, month);
@@ -34,10 +32,7 @@ if (!fs.existsSync(dirPath)) {
34
32
  fs.mkdirSync(dirPath, { recursive: true });
35
33
  }
36
34
 
37
- const flowTitle = title || dateStr;
38
-
39
35
  const content = `---
40
- title: "${flowTitle}"
41
36
  tags: []
42
37
  ---
43
38
 
@@ -0,0 +1,53 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const args = process.argv.slice(2);
5
+ const titleArg = args.filter(arg => !arg.startsWith('--'))[0];
6
+ const useMd = args.includes('--md');
7
+
8
+ if (!titleArg) {
9
+ console.error('Usage: bun run new-note "Note Title"');
10
+ process.exit(1);
11
+ }
12
+
13
+ // Slugify title
14
+ const slug = titleArg
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
17
+ .replace(/^-|-$/g, '');
18
+
19
+ const now = new Date();
20
+ const dateStr = now.toISOString().split('T')[0];
21
+ const ext = useMd ? '.md' : '.mdx';
22
+
23
+ const notesDir = path.join(process.cwd(), 'content', 'notes');
24
+ const targetPath = path.join(notesDir, `${slug}${ext}`);
25
+
26
+ const altExt = useMd ? '.mdx' : '.md';
27
+ const altPath = path.join(notesDir, `${slug}${altExt}`);
28
+
29
+ if (fs.existsSync(targetPath)) {
30
+ console.error(`Error: Note already exists at ${targetPath}`);
31
+ process.exit(1);
32
+ }
33
+
34
+ if (fs.existsSync(altPath)) {
35
+ console.error(`Error: Note already exists at ${altPath}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ if (!fs.existsSync(notesDir)) {
40
+ fs.mkdirSync(notesDir, { recursive: true });
41
+ }
42
+
43
+ const content = `---
44
+ title: "${titleArg}"
45
+ date: "${dateStr}"
46
+ tags: []
47
+ aliases: []
48
+ ---
49
+
50
+ `;
51
+
52
+ fs.writeFileSync(targetPath, content);
53
+ console.log(`Created new note: ${targetPath}`);