coding-friend-cli 1.1.1 → 1.2.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 (106) hide show
  1. package/README.md +15 -0
  2. package/dist/{chunk-KZT4AFDW.js → chunk-5HZJX47M.js} +1 -1
  3. package/dist/{chunk-AQXTNLQD.js → chunk-6OI37OZX.js} +9 -1
  4. package/dist/chunk-R6ZYK4UX.js +128 -0
  5. package/dist/dev-LZASFXZZ.js +243 -0
  6. package/dist/{host-JBTJCWM2.js → host-BK6DYFWF.js} +2 -2
  7. package/dist/index.js +26 -5
  8. package/dist/{init-E6CL3UZQ.js → init-2UKYE2KV.js} +2 -2
  9. package/dist/{mcp-MWESK6UX.js → mcp-CH4SKZSX.js} +2 -2
  10. package/dist/postinstall.js +1 -1
  11. package/dist/{statusline-7D6YU5YM.js → statusline-ARI7I5YM.js} +1 -1
  12. package/dist/{update-IH3G4SN5.js → update-5A2OP6EY.js} +58 -37
  13. package/lib/learn-host/.prettierignore +3 -0
  14. package/lib/learn-host/.prettierrc +8 -0
  15. package/lib/learn-host/CHANGELOG.md +9 -0
  16. package/lib/learn-host/eslint.config.mjs +6 -0
  17. package/lib/learn-host/next-env.d.ts +1 -1
  18. package/lib/learn-host/next.config.ts +1 -0
  19. package/lib/learn-host/package-lock.json +6039 -391
  20. package/lib/learn-host/package.json +30 -15
  21. package/lib/learn-host/public/_pagefind/fragment/en_1172b3c.pf_fragment +0 -0
  22. package/lib/learn-host/public/_pagefind/fragment/en_118ad1c.pf_fragment +0 -0
  23. package/lib/learn-host/public/_pagefind/fragment/en_32ab3d8.pf_fragment +0 -0
  24. package/lib/learn-host/public/_pagefind/fragment/en_441f1e1.pf_fragment +0 -0
  25. package/lib/learn-host/public/_pagefind/fragment/en_4452de4.pf_fragment +0 -0
  26. package/lib/learn-host/public/_pagefind/fragment/en_4ae396d.pf_fragment +0 -0
  27. package/lib/learn-host/public/_pagefind/fragment/en_58ee89d.pf_fragment +0 -0
  28. package/lib/learn-host/public/_pagefind/fragment/en_6dd2225.pf_fragment +0 -0
  29. package/lib/learn-host/public/_pagefind/fragment/en_765a297.pf_fragment +0 -0
  30. package/lib/learn-host/public/_pagefind/fragment/en_7a4cc4a.pf_fragment +0 -0
  31. package/lib/learn-host/public/_pagefind/fragment/en_8050261.pf_fragment +0 -0
  32. package/lib/learn-host/public/_pagefind/fragment/en_83eaedf.pf_fragment +0 -0
  33. package/lib/learn-host/public/_pagefind/fragment/en_925bc5f.pf_fragment +0 -0
  34. package/lib/learn-host/public/_pagefind/fragment/en_95f3dd5.pf_fragment +0 -0
  35. package/lib/learn-host/public/_pagefind/fragment/en_96d7a02.pf_fragment +0 -0
  36. package/lib/learn-host/public/_pagefind/fragment/en_971f951.pf_fragment +0 -0
  37. package/lib/learn-host/public/_pagefind/fragment/en_a446c32.pf_fragment +0 -0
  38. package/lib/learn-host/public/_pagefind/fragment/en_a5ee367.pf_fragment +0 -0
  39. package/lib/learn-host/public/_pagefind/fragment/en_b11c248.pf_fragment +0 -0
  40. package/lib/learn-host/public/_pagefind/fragment/en_b13c52e.pf_fragment +0 -0
  41. package/lib/learn-host/public/_pagefind/fragment/en_b5bd69b.pf_fragment +0 -0
  42. package/lib/learn-host/public/_pagefind/fragment/en_b625d7d.pf_fragment +0 -0
  43. package/lib/learn-host/public/_pagefind/fragment/en_bf63915.pf_fragment +0 -0
  44. package/lib/learn-host/public/_pagefind/fragment/en_c52b25b.pf_fragment +0 -0
  45. package/lib/learn-host/public/_pagefind/fragment/en_c9db556.pf_fragment +0 -0
  46. package/lib/learn-host/public/_pagefind/fragment/en_d1537ee.pf_fragment +0 -0
  47. package/lib/learn-host/public/_pagefind/fragment/en_d2e6412.pf_fragment +0 -0
  48. package/lib/learn-host/public/_pagefind/fragment/en_d2f47a4.pf_fragment +0 -0
  49. package/lib/learn-host/public/_pagefind/fragment/en_d361292.pf_fragment +0 -0
  50. package/lib/learn-host/public/_pagefind/fragment/en_d727ec8.pf_fragment +0 -0
  51. package/lib/learn-host/public/_pagefind/fragment/en_e11cd8f.pf_fragment +0 -0
  52. package/lib/learn-host/public/_pagefind/fragment/en_e481f19.pf_fragment +0 -0
  53. package/lib/learn-host/public/_pagefind/fragment/en_eee2805.pf_fragment +0 -0
  54. package/lib/learn-host/public/_pagefind/fragment/en_f4de6c4.pf_fragment +0 -0
  55. package/lib/learn-host/public/_pagefind/index/en_1ecb9d5.pf_index +0 -0
  56. package/lib/learn-host/public/_pagefind/index/en_37e362b.pf_index +0 -0
  57. package/lib/learn-host/public/_pagefind/index/en_538eee7.pf_index +0 -0
  58. package/lib/learn-host/public/_pagefind/index/en_5751dc8.pf_index +0 -0
  59. package/lib/learn-host/public/_pagefind/index/en_67f794d.pf_index +0 -0
  60. package/lib/learn-host/public/_pagefind/index/en_7458f81.pf_index +0 -0
  61. package/lib/learn-host/public/_pagefind/index/en_e21f7e1.pf_index +0 -0
  62. package/lib/learn-host/public/_pagefind/pagefind-entry.json +1 -0
  63. package/lib/learn-host/public/_pagefind/pagefind-highlight.js +1064 -0
  64. package/lib/learn-host/public/_pagefind/pagefind-modular-ui.css +214 -0
  65. package/lib/learn-host/public/_pagefind/pagefind-modular-ui.js +8 -0
  66. package/lib/learn-host/public/_pagefind/pagefind-ui.css +1 -0
  67. package/lib/learn-host/public/_pagefind/pagefind-ui.js +2 -0
  68. package/lib/learn-host/public/_pagefind/pagefind.en_104569cceb.pf_meta +0 -0
  69. package/lib/learn-host/public/_pagefind/pagefind.en_1075df6f16.pf_meta +0 -0
  70. package/lib/learn-host/public/_pagefind/pagefind.en_139f35f6e5.pf_meta +0 -0
  71. package/lib/learn-host/public/_pagefind/pagefind.en_46bfc9f7e1.pf_meta +0 -0
  72. package/lib/learn-host/public/_pagefind/pagefind.en_76b8937bbc.pf_meta +0 -0
  73. package/lib/learn-host/public/_pagefind/pagefind.en_83cbfb0fd5.pf_meta +0 -0
  74. package/lib/learn-host/public/_pagefind/pagefind.en_b1d168d536.pf_meta +0 -0
  75. package/lib/learn-host/public/_pagefind/pagefind.js +6 -0
  76. package/lib/learn-host/public/_pagefind/wasm.en.pagefind +0 -0
  77. package/lib/learn-host/public/_pagefind/wasm.unknown.pagefind +0 -0
  78. package/lib/learn-host/public/logo.svg +1 -0
  79. package/lib/learn-host/src/app/[category]/[slug]/page.tsx +36 -32
  80. package/lib/learn-host/src/app/[category]/page.tsx +2 -3
  81. package/lib/learn-host/src/app/apple-icon.svg +1 -0
  82. package/lib/learn-host/src/app/globals.css +74 -14
  83. package/lib/learn-host/src/app/icon.svg +1 -0
  84. package/lib/learn-host/src/app/layout.tsx +29 -9
  85. package/lib/learn-host/src/app/page.tsx +9 -11
  86. package/lib/learn-host/src/components/Breadcrumbs.tsx +12 -4
  87. package/lib/learn-host/src/components/DocCard.tsx +28 -10
  88. package/lib/learn-host/src/components/MarkdownRenderer.tsx +6 -2
  89. package/lib/learn-host/src/components/MobileNav.tsx +43 -35
  90. package/lib/learn-host/src/components/PagefindSearch.tsx +177 -54
  91. package/lib/learn-host/src/components/Sidebar.tsx +27 -29
  92. package/lib/learn-host/src/components/TableOfContents.tsx +62 -0
  93. package/lib/learn-host/src/components/TagBadge.tsx +1 -1
  94. package/lib/learn-host/src/components/ThemeToggle.tsx +36 -9
  95. package/lib/learn-host/src/components/layout/Footer.tsx +41 -0
  96. package/lib/learn-host/src/components/layout/Header.tsx +117 -0
  97. package/lib/learn-host/src/lib/docs.ts +98 -8
  98. package/lib/learn-host/src/lib/types.ts +7 -1
  99. package/lib/learn-host/tsconfig.json +8 -2
  100. package/lib/learn-host/tsconfig.tsbuildinfo +1 -0
  101. package/lib/learn-mcp/CHANGELOG.md +7 -0
  102. package/lib/learn-mcp/package.json +1 -1
  103. package/package.json +13 -5
  104. package/dist/chunk-VHZQ6KEU.js +0 -73
  105. package/lib/learn-host/src/app/search/page.tsx +0 -19
  106. package/lib/learn-host/src/components/SearchBar.tsx +0 -36
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import Image from "next/image";
4
+ import Link from "next/link";
5
+ import { useState } from "react";
6
+ import type { CategoryInfo } from "@/lib/types";
7
+ import PagefindSearch from "@/components/PagefindSearch";
8
+ import ThemeToggle from "@/components/ThemeToggle";
9
+
10
+ export default function Header({ categories }: { categories: CategoryInfo[] }) {
11
+ const [mobileOpen, setMobileOpen] = useState(false);
12
+
13
+ return (
14
+ <header className="dark:bg-navy-900/80 sticky top-0 z-50 border-b border-slate-200 bg-white/80 px-4 backdrop-blur-lg md:px-6 dark:border-[#a0a0a01c]">
15
+ <div className="flex h-14 items-center justify-between">
16
+ {/* Left: Logo + Title */}
17
+ <Link href="/" className="flex shrink-0 items-center gap-2.5">
18
+ <Image src="/logo.svg" alt="Coding Friend" width={32} height={32} />
19
+ <span className="text-lg font-semibold text-slate-900 dark:text-white">
20
+ Learning Notes
21
+ </span>
22
+ </Link>
23
+
24
+ {/* Right */}
25
+ <div className="flex items-center gap-1 md:gap-3">
26
+ {/* Search */}
27
+ <PagefindSearch />
28
+
29
+ {/* Theme Toggle */}
30
+ <ThemeToggle />
31
+
32
+ {/* GitHub */}
33
+ <a
34
+ href="https://github.com/dinhanhthi/coding-friend"
35
+ target="_blank"
36
+ rel="noopener noreferrer"
37
+ className="hidden cursor-pointer rounded-lg p-2 text-slate-500 transition-colors duration-200 hover:text-slate-900 md:flex dark:text-slate-400 dark:hover:text-white"
38
+ aria-label="GitHub"
39
+ >
40
+ <svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
41
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
42
+ </svg>
43
+ </a>
44
+
45
+ {/* Mobile Hamburger */}
46
+ <button
47
+ onClick={() => setMobileOpen(!mobileOpen)}
48
+ className="dark:hover:bg-navy-800 cursor-pointer rounded-lg p-2 transition-colors hover:bg-slate-100 md:hidden"
49
+ aria-label="Toggle menu"
50
+ >
51
+ {mobileOpen ? (
52
+ <svg
53
+ className="h-5 w-5"
54
+ fill="none"
55
+ viewBox="0 0 24 24"
56
+ stroke="currentColor"
57
+ >
58
+ <path
59
+ strokeLinecap="round"
60
+ strokeLinejoin="round"
61
+ strokeWidth={2}
62
+ d="M6 18L18 6M6 6l12 12"
63
+ />
64
+ </svg>
65
+ ) : (
66
+ <svg
67
+ className="h-5 w-5"
68
+ fill="none"
69
+ viewBox="0 0 24 24"
70
+ stroke="currentColor"
71
+ >
72
+ <path
73
+ strokeLinecap="round"
74
+ strokeLinejoin="round"
75
+ strokeWidth={2}
76
+ d="M4 6h16M4 12h16M4 18h16"
77
+ />
78
+ </svg>
79
+ )}
80
+ </button>
81
+ </div>
82
+ </div>
83
+
84
+ {/* Mobile Menu */}
85
+ {mobileOpen && (
86
+ <nav className="space-y-1 border-t border-slate-200 py-4 md:hidden dark:border-[#a0a0a01c]">
87
+ {categories.map((cat) => (
88
+ <Link
89
+ key={cat.name}
90
+ href={`/${cat.name}/`}
91
+ onClick={() => setMobileOpen(false)}
92
+ className="flex items-center justify-between px-3 py-2 text-sm font-medium text-slate-600 capitalize hover:text-violet-600 dark:text-slate-400 dark:hover:text-violet-400"
93
+ >
94
+ <span>{cat.name.replace(/[_-]/g, " ")}</span>
95
+ <span className="text-xs text-slate-400 dark:text-slate-500">
96
+ {cat.docCount}
97
+ </span>
98
+ </Link>
99
+ ))}
100
+ <div className="flex items-center gap-3 px-3 pt-2">
101
+ <a
102
+ href="https://github.com/dinhanhthi/coding-friend"
103
+ target="_blank"
104
+ rel="noopener noreferrer"
105
+ className="flex cursor-pointer items-center gap-2 text-sm font-medium text-slate-500 hover:text-violet-600 dark:text-slate-400 dark:hover:text-violet-400"
106
+ >
107
+ <svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
108
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
109
+ </svg>
110
+ GitHub
111
+ </a>
112
+ </div>
113
+ </nav>
114
+ )}
115
+ </header>
116
+ );
117
+ }
@@ -1,25 +1,102 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { homedir } from "node:os";
3
4
  import matter from "gray-matter";
4
- import type { CategoryInfo, Doc, DocFrontmatter, DocMeta } from "./types";
5
+ import type {
6
+ CategoryInfo,
7
+ Doc,
8
+ DocFrontmatter,
9
+ DocMeta,
10
+ TocItem,
11
+ } from "./types";
5
12
 
13
+ /**
14
+ * Resolve the docs directory using the same logic as `cf host`:
15
+ * 1. DOCS_DIR env var (set by `cf host`)
16
+ * 2. Local .coding-friend/config.json → learn.outputDir
17
+ * 3. Global ~/.coding-friend/config.json → learn.outputDir
18
+ * 4. Default: docs/learn (relative to project root)
19
+ */
6
20
  function getDocsDir(): string {
7
- const raw = process.env.DOCS_DIR || path.join(process.cwd(), "../../docs/learn");
8
- return path.resolve(raw);
21
+ if (process.env.DOCS_DIR) {
22
+ return path.resolve(process.env.DOCS_DIR);
23
+ }
24
+
25
+ // Find project root by walking up from cwd looking for .coding-friend/
26
+ const projectRoot = findProjectRoot(process.cwd());
27
+
28
+ // Try local config
29
+ const localConfig = readJsonSafe(
30
+ path.join(projectRoot, ".coding-friend", "config.json"),
31
+ );
32
+ if (localConfig?.learn?.outputDir) {
33
+ return resolvePath(localConfig.learn.outputDir, projectRoot);
34
+ }
35
+
36
+ // Try global config
37
+ const globalConfig = readJsonSafe(
38
+ path.join(homedir(), ".coding-friend", "config.json"),
39
+ );
40
+ if (globalConfig?.learn?.outputDir) {
41
+ return resolvePath(globalConfig.learn.outputDir, projectRoot);
42
+ }
43
+
44
+ // Default
45
+ return path.join(projectRoot, "docs", "learn");
46
+ }
47
+
48
+ function findProjectRoot(from: string): string {
49
+ let dir = from;
50
+ while (dir !== path.dirname(dir)) {
51
+ if (fs.existsSync(path.join(dir, ".coding-friend"))) {
52
+ return dir;
53
+ }
54
+ dir = path.dirname(dir);
55
+ }
56
+ // Fallback: assume learn-host is at <root>/lib/learn-host
57
+ return path.resolve(from, "../..");
58
+ }
59
+
60
+ function resolvePath(p: string, base: string): string {
61
+ if (p.startsWith("/")) return p;
62
+ if (p.startsWith("~/")) return path.join(homedir(), p.slice(2));
63
+ return path.resolve(base, p);
64
+ }
65
+
66
+ interface CfConfig {
67
+ learn?: { outputDir?: string };
68
+ }
69
+
70
+ function readJsonSafe(filePath: string): CfConfig | null {
71
+ try {
72
+ const content = fs.readFileSync(filePath, "utf-8");
73
+ return JSON.parse(content) as CfConfig;
74
+ } catch {
75
+ return null;
76
+ }
9
77
  }
10
78
 
11
79
  function isMarkdownDoc(fileName: string): boolean {
12
80
  return fileName.endsWith(".md") && fileName !== "README.md";
13
81
  }
14
82
 
83
+ function formatDate(value: unknown): string {
84
+ if (!value) return "";
85
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
86
+ const str = String(value);
87
+ const parsed = new Date(str);
88
+ if (!Number.isNaN(parsed.getTime())) return parsed.toISOString().slice(0, 10);
89
+ return str;
90
+ }
91
+
15
92
  function parseFrontmatter(raw: matter.GrayMatterFile<string>): DocFrontmatter {
16
93
  const d = raw.data;
17
94
  return {
18
95
  title: String(d.title ?? "Untitled"),
19
96
  category: String(d.category ?? ""),
20
97
  tags: Array.isArray(d.tags) ? d.tags.map(String) : [],
21
- created: String(d.created ?? ""),
22
- updated: String(d.updated ?? ""),
98
+ created: formatDate(d.created),
99
+ updated: formatDate(d.updated),
23
100
  };
24
101
  }
25
102
 
@@ -41,9 +118,7 @@ export function getAllCategories(): CategoryInfo[] {
41
118
  .filter((d) => d.isDirectory() && !d.name.startsWith("."))
42
119
  .map((d) => {
43
120
  const catPath = path.join(docsDir, d.name);
44
- const count = fs
45
- .readdirSync(catPath)
46
- .filter(isMarkdownDoc).length;
121
+ const count = fs.readdirSync(catPath).filter(isMarkdownDoc).length;
47
122
  return { name: d.name, docCount: count };
48
123
  })
49
124
  .filter((c) => c.docCount > 0);
@@ -100,6 +175,21 @@ export function getDocBySlug(category: string, slug: string): Doc | null {
100
175
  };
101
176
  }
102
177
 
178
+ export function extractHeadings(content: string): TocItem[] {
179
+ const headings: TocItem[] = [];
180
+ const regex = /^(#{2,3})\s+(.+)$/gm;
181
+ let match;
182
+ while ((match = regex.exec(content)) !== null) {
183
+ const text = match[2].trim();
184
+ const id = text
185
+ .toLowerCase()
186
+ .replace(/[^a-z0-9]+/g, "-")
187
+ .replace(/(^-|-$)/g, "");
188
+ headings.push({ id, text, level: match[1].length });
189
+ }
190
+ return headings;
191
+ }
192
+
103
193
  export function getAllTags(): { tag: string; count: number }[] {
104
194
  const tagMap = new Map<string, number>();
105
195
  for (const doc of getAllDocs()) {
@@ -20,4 +20,10 @@ export interface Doc extends DocMeta {
20
20
  export interface CategoryInfo {
21
21
  name: string;
22
22
  docCount: number;
23
- }
23
+ }
24
+
25
+ export interface TocItem {
26
+ id: string;
27
+ text: string;
28
+ level: number;
29
+ }
@@ -11,11 +11,17 @@
11
11
  "moduleResolution": "bundler",
12
12
  "resolveJsonModule": true,
13
13
  "isolatedModules": true,
14
- "jsx": "preserve",
14
+ "jsx": "react-jsx",
15
15
  "incremental": true,
16
16
  "plugins": [{ "name": "next" }],
17
17
  "paths": { "@/*": ["./src/*"] }
18
18
  },
19
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
19
+ "include": [
20
+ "next-env.d.ts",
21
+ "**/*.ts",
22
+ "**/*.tsx",
23
+ ".next/types/**/*.ts",
24
+ ".next/dev/types/**/*.ts"
25
+ ],
20
26
  "exclude": ["node_modules"]
21
27
  }