coding-friend-cli 1.2.0 → 1.2.1

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 (87) hide show
  1. package/README.md +29 -6
  2. package/dist/{chunk-R6ZYK4UX.js → chunk-CSF4FAHL.js} +2 -1
  3. package/dist/{chunk-5HZJX47M.js → chunk-Q4DKU5IG.js} +3 -5
  4. package/dist/{dev-LZASFXZZ.js → dev-MAAWPWML.js} +65 -18
  5. package/dist/{host-BK6DYFWF.js → host-2WINWEW7.js} +1 -1
  6. package/dist/index.js +30 -13
  7. package/dist/{init-2UKYE2KV.js → init-CTCDQKIQ.js} +23 -12
  8. package/dist/{mcp-CH4SKZSX.js → mcp-43HCE2KD.js} +1 -1
  9. package/dist/postinstall.js +1 -1
  10. package/dist/{update-5A2OP6EY.js → update-GGCBM7U4.js} +45 -15
  11. package/lib/learn-host/.prettierignore +1 -0
  12. package/lib/learn-host/CHANGELOG.md +5 -0
  13. package/lib/learn-host/README.md +114 -0
  14. package/lib/learn-host/next-env.d.ts +1 -1
  15. package/lib/learn-host/next.config.ts +3 -0
  16. package/lib/learn-host/package.json +1 -1
  17. package/lib/learn-host/public/logo.svg +1 -1
  18. package/lib/learn-host/src/app/apple-icon.svg +1 -1
  19. package/lib/learn-host/src/app/icon.svg +1 -1
  20. package/lib/learn-host/src/components/Sidebar.tsx +1 -1
  21. package/lib/learn-host/src/lib/docs.ts +2 -2
  22. package/lib/learn-mcp/CHANGELOG.md +5 -0
  23. package/lib/learn-mcp/README.md +169 -0
  24. package/lib/learn-mcp/package.json +2 -1
  25. package/lib/learn-mcp/src/index.ts +1 -1
  26. package/lib/learn-mcp/src/lib/docs.ts +1 -3
  27. package/lib/learn-mcp/src/lib/knowledge.ts +2 -1
  28. package/lib/learn-mcp/src/tools/get-review-list.ts +1 -4
  29. package/lib/learn-mcp/src/tools/search-docs.ts +1 -4
  30. package/package.json +2 -2
  31. package/lib/learn-host/public/_pagefind/fragment/en_1172b3c.pf_fragment +0 -0
  32. package/lib/learn-host/public/_pagefind/fragment/en_118ad1c.pf_fragment +0 -0
  33. package/lib/learn-host/public/_pagefind/fragment/en_32ab3d8.pf_fragment +0 -0
  34. package/lib/learn-host/public/_pagefind/fragment/en_441f1e1.pf_fragment +0 -0
  35. package/lib/learn-host/public/_pagefind/fragment/en_4452de4.pf_fragment +0 -0
  36. package/lib/learn-host/public/_pagefind/fragment/en_4ae396d.pf_fragment +0 -0
  37. package/lib/learn-host/public/_pagefind/fragment/en_58ee89d.pf_fragment +0 -0
  38. package/lib/learn-host/public/_pagefind/fragment/en_6dd2225.pf_fragment +0 -0
  39. package/lib/learn-host/public/_pagefind/fragment/en_765a297.pf_fragment +0 -0
  40. package/lib/learn-host/public/_pagefind/fragment/en_7a4cc4a.pf_fragment +0 -0
  41. package/lib/learn-host/public/_pagefind/fragment/en_8050261.pf_fragment +0 -0
  42. package/lib/learn-host/public/_pagefind/fragment/en_83eaedf.pf_fragment +0 -0
  43. package/lib/learn-host/public/_pagefind/fragment/en_925bc5f.pf_fragment +0 -0
  44. package/lib/learn-host/public/_pagefind/fragment/en_95f3dd5.pf_fragment +0 -0
  45. package/lib/learn-host/public/_pagefind/fragment/en_96d7a02.pf_fragment +0 -0
  46. package/lib/learn-host/public/_pagefind/fragment/en_971f951.pf_fragment +0 -0
  47. package/lib/learn-host/public/_pagefind/fragment/en_a446c32.pf_fragment +0 -0
  48. package/lib/learn-host/public/_pagefind/fragment/en_a5ee367.pf_fragment +0 -0
  49. package/lib/learn-host/public/_pagefind/fragment/en_b11c248.pf_fragment +0 -0
  50. package/lib/learn-host/public/_pagefind/fragment/en_b13c52e.pf_fragment +0 -0
  51. package/lib/learn-host/public/_pagefind/fragment/en_b5bd69b.pf_fragment +0 -0
  52. package/lib/learn-host/public/_pagefind/fragment/en_b625d7d.pf_fragment +0 -0
  53. package/lib/learn-host/public/_pagefind/fragment/en_bf63915.pf_fragment +0 -0
  54. package/lib/learn-host/public/_pagefind/fragment/en_c52b25b.pf_fragment +0 -0
  55. package/lib/learn-host/public/_pagefind/fragment/en_c9db556.pf_fragment +0 -0
  56. package/lib/learn-host/public/_pagefind/fragment/en_d1537ee.pf_fragment +0 -0
  57. package/lib/learn-host/public/_pagefind/fragment/en_d2e6412.pf_fragment +0 -0
  58. package/lib/learn-host/public/_pagefind/fragment/en_d2f47a4.pf_fragment +0 -0
  59. package/lib/learn-host/public/_pagefind/fragment/en_d361292.pf_fragment +0 -0
  60. package/lib/learn-host/public/_pagefind/fragment/en_d727ec8.pf_fragment +0 -0
  61. package/lib/learn-host/public/_pagefind/fragment/en_e11cd8f.pf_fragment +0 -0
  62. package/lib/learn-host/public/_pagefind/fragment/en_e481f19.pf_fragment +0 -0
  63. package/lib/learn-host/public/_pagefind/fragment/en_eee2805.pf_fragment +0 -0
  64. package/lib/learn-host/public/_pagefind/fragment/en_f4de6c4.pf_fragment +0 -0
  65. package/lib/learn-host/public/_pagefind/index/en_1ecb9d5.pf_index +0 -0
  66. package/lib/learn-host/public/_pagefind/index/en_37e362b.pf_index +0 -0
  67. package/lib/learn-host/public/_pagefind/index/en_538eee7.pf_index +0 -0
  68. package/lib/learn-host/public/_pagefind/index/en_5751dc8.pf_index +0 -0
  69. package/lib/learn-host/public/_pagefind/index/en_67f794d.pf_index +0 -0
  70. package/lib/learn-host/public/_pagefind/index/en_7458f81.pf_index +0 -0
  71. package/lib/learn-host/public/_pagefind/index/en_e21f7e1.pf_index +0 -0
  72. package/lib/learn-host/public/_pagefind/pagefind-entry.json +0 -1
  73. package/lib/learn-host/public/_pagefind/pagefind-highlight.js +0 -1064
  74. package/lib/learn-host/public/_pagefind/pagefind-modular-ui.css +0 -214
  75. package/lib/learn-host/public/_pagefind/pagefind-modular-ui.js +0 -8
  76. package/lib/learn-host/public/_pagefind/pagefind-ui.css +0 -1
  77. package/lib/learn-host/public/_pagefind/pagefind-ui.js +0 -2
  78. package/lib/learn-host/public/_pagefind/pagefind.en_104569cceb.pf_meta +0 -0
  79. package/lib/learn-host/public/_pagefind/pagefind.en_1075df6f16.pf_meta +0 -0
  80. package/lib/learn-host/public/_pagefind/pagefind.en_139f35f6e5.pf_meta +0 -0
  81. package/lib/learn-host/public/_pagefind/pagefind.en_46bfc9f7e1.pf_meta +0 -0
  82. package/lib/learn-host/public/_pagefind/pagefind.en_76b8937bbc.pf_meta +0 -0
  83. package/lib/learn-host/public/_pagefind/pagefind.en_83cbfb0fd5.pf_meta +0 -0
  84. package/lib/learn-host/public/_pagefind/pagefind.en_b1d168d536.pf_meta +0 -0
  85. package/lib/learn-host/public/_pagefind/pagefind.js +0 -6
  86. package/lib/learn-host/public/_pagefind/wasm.en.pagefind +0 -0
  87. package/lib/learn-host/public/_pagefind/wasm.unknown.pagefind +0 -0
package/README.md CHANGED
@@ -75,17 +75,40 @@ npm ls -g coding-friend-cli
75
75
  #└── coding-friend-cli@1.1.1 -> ./../../../../../git/coding-friend/cli
76
76
  ```
77
77
 
78
+ ### Running tests
79
+
80
+ Tests are written with [Vitest](https://vitest.dev/) and live in `src/lib/__tests__/`.
81
+
82
+ ```bash
83
+ cd cli
84
+
85
+ # Run all tests once
86
+ npm test
87
+
88
+ # Watch mode (re-runs on file changes)
89
+ npm run test:watch
90
+ ```
91
+
92
+ Current coverage: `lib/json.ts`, `lib/paths.ts`, `lib/exec.ts`.
93
+
78
94
  ## Publish CLI to npm
79
95
 
96
+ Publishing is automated via GitHub Actions (`.github/workflows/publish-cli.yml`). Push a tag with the `cli-v*` prefix to trigger it:
97
+
98
+ ```bash
99
+ # Bump version in cli/package.json first, then tag and push
100
+ git tag cli-v1.2.3
101
+ git push origin cli-v1.2.3
102
+ ```
103
+
104
+ The workflow will build, bundle, and publish to npm automatically (with provenance), then create a GitHub Release with the changelog for that version.
105
+
106
+ **Manual publish (if needed):**
107
+
80
108
  ```bash
81
- # From the root of coding-friend project
82
109
  cd cli
83
110
  npm login # Login if not already
84
111
  npm publish # Build + bundle + publish
85
-
86
- # To bump a version
87
- npm version patch # 1.0.1 -> 1.0.2
88
- npm version minor # 1.0.1 -> 1.1.0
89
112
  ```
90
113
 
91
114
  `prepublishOnly` runs automatically: builds TypeScript → `dist/` and bundles libs from `lib/`.
@@ -96,4 +119,4 @@ npm version minor # 1.0.1 -> 1.1.0
96
119
 
97
120
  ## License
98
121
 
99
- MIT
122
+ MIT
@@ -103,7 +103,8 @@ function ensureShellCompletion(opts) {
103
103
  const existing = extractExistingBlock(content);
104
104
  const expectedBlock = newBlock.trim();
105
105
  if (existing && existing.trim() === expectedBlock) {
106
- if (!opts?.silent) log.dim(`Tab completion already up-to-date in ~/${rcName}`);
106
+ if (!opts?.silent)
107
+ log.dim(`Tab completion already up-to-date in ~/${rcName}`);
107
108
  return false;
108
109
  }
109
110
  const updated = replaceBlock(content, newBlock);
@@ -29,12 +29,10 @@ import { dirname, join } from "path";
29
29
  import { fileURLToPath } from "url";
30
30
  var __dirname = dirname(fileURLToPath(import.meta.url));
31
31
  function getLibPath(name) {
32
- const bundled = join(__dirname, "..", "..", "lib", name);
33
- if (existsSync(bundled)) return bundled;
34
- const sibling = join(__dirname, "..", "..", "..", "lib", name);
35
- if (existsSync(sibling)) return sibling;
32
+ const libDir = join(__dirname, "..", "..", "lib", name);
33
+ if (existsSync(libDir)) return libDir;
36
34
  throw new Error(
37
- `Could not find lib/${name}. Ensure it exists in the CLI package or repo.`
35
+ `Could not find lib/${name}. Ensure it exists in the CLI package.`
38
36
  );
39
37
  }
40
38
 
@@ -17,7 +17,14 @@ import {
17
17
  } from "./chunk-IUTXHCP7.js";
18
18
 
19
19
  // src/commands/dev.ts
20
- import { existsSync, unlinkSync, readdirSync, statSync, mkdirSync, copyFileSync } from "fs";
20
+ import {
21
+ existsSync,
22
+ unlinkSync,
23
+ readdirSync,
24
+ statSync,
25
+ mkdirSync,
26
+ copyFileSync
27
+ } from "fs";
21
28
  import { resolve, join } from "path";
22
29
  import chalk from "chalk";
23
30
  var REMOTE_URL = "https://github.com/dinhanhthi/coding-friend.git";
@@ -40,7 +47,9 @@ function isMarketplaceRegistered() {
40
47
  }
41
48
  function ensureClaude() {
42
49
  if (!commandExists("claude")) {
43
- log.error("Claude CLI not found. Install it first: https://docs.anthropic.com/en/docs/claude-code");
50
+ log.error(
51
+ "Claude CLI not found. Install it first: https://docs.anthropic.com/en/docs/claude-code"
52
+ );
44
53
  return false;
45
54
  }
46
55
  return true;
@@ -73,18 +82,30 @@ async function devOnCommand(path) {
73
82
  `);
74
83
  log.info(`Local path: ${chalk.cyan(localPath)}`);
75
84
  if (isPluginInstalled()) {
76
- if (!runClaude(["plugin", "uninstall", PLUGIN_ID], "Uninstalling remote plugin...")) {
85
+ if (!runClaude(
86
+ ["plugin", "uninstall", PLUGIN_ID],
87
+ "Uninstalling remote plugin..."
88
+ )) {
77
89
  run("claude", ["plugin", "uninstall", PLUGIN_NAME]);
78
90
  }
79
91
  }
80
92
  if (isMarketplaceRegistered()) {
81
- runClaude(["plugin", "marketplace", "remove", MARKETPLACE_NAME], "Removing remote marketplace...");
93
+ runClaude(
94
+ ["plugin", "marketplace", "remove", MARKETPLACE_NAME],
95
+ "Removing remote marketplace..."
96
+ );
82
97
  }
83
- if (!runClaude(["plugin", "marketplace", "add", localPath], "Adding local marketplace...")) {
98
+ if (!runClaude(
99
+ ["plugin", "marketplace", "add", localPath],
100
+ "Adding local marketplace..."
101
+ )) {
84
102
  log.error("Failed to add local marketplace. Aborting.");
85
103
  return;
86
104
  }
87
- if (!runClaude(["plugin", "install", PLUGIN_ID], "Installing plugin from local source...")) {
105
+ if (!runClaude(
106
+ ["plugin", "install", PLUGIN_ID],
107
+ "Installing plugin from local source..."
108
+ )) {
88
109
  if (!runClaude(["plugin", "install", PLUGIN_NAME], "Retrying install...")) {
89
110
  log.error("Failed to install local plugin.");
90
111
  return;
@@ -96,7 +117,9 @@ async function devOnCommand(path) {
96
117
  };
97
118
  writeJson(devStatePath(), devState);
98
119
  console.log();
99
- log.success(`Dev mode ${chalk.green("ON")} \u2014 using local plugin from ${chalk.cyan(localPath)}`);
120
+ log.success(
121
+ `Dev mode ${chalk.green("ON")} \u2014 using local plugin from ${chalk.cyan(localPath)}`
122
+ );
100
123
  log.dim("Restart Claude Code to see changes.");
101
124
  }
102
125
  async function devOffCommand() {
@@ -106,22 +129,36 @@ async function devOffCommand() {
106
129
  return;
107
130
  }
108
131
  if (!ensureClaude()) return;
109
- console.log(`
132
+ console.log(
133
+ `
110
134
  === ${chalk.yellow("Switching back to remote marketplace")} ===
111
- `);
135
+ `
136
+ );
112
137
  if (isPluginInstalled()) {
113
- if (!runClaude(["plugin", "uninstall", PLUGIN_ID], "Uninstalling local plugin...")) {
138
+ if (!runClaude(
139
+ ["plugin", "uninstall", PLUGIN_ID],
140
+ "Uninstalling local plugin..."
141
+ )) {
114
142
  run("claude", ["plugin", "uninstall", PLUGIN_NAME]);
115
143
  }
116
144
  }
117
145
  if (isMarketplaceRegistered()) {
118
- runClaude(["plugin", "marketplace", "remove", MARKETPLACE_NAME], "Removing local marketplace...");
146
+ runClaude(
147
+ ["plugin", "marketplace", "remove", MARKETPLACE_NAME],
148
+ "Removing local marketplace..."
149
+ );
119
150
  }
120
- if (!runClaude(["plugin", "marketplace", "add", REMOTE_URL], "Adding remote marketplace...")) {
151
+ if (!runClaude(
152
+ ["plugin", "marketplace", "add", REMOTE_URL],
153
+ "Adding remote marketplace..."
154
+ )) {
121
155
  log.error("Failed to add remote marketplace.");
122
156
  return;
123
157
  }
124
- if (!runClaude(["plugin", "install", PLUGIN_ID], "Installing plugin from remote...")) {
158
+ if (!runClaude(
159
+ ["plugin", "install", PLUGIN_ID],
160
+ "Installing plugin from remote..."
161
+ )) {
125
162
  if (!runClaude(["plugin", "install", PLUGIN_NAME], "Retrying install...")) {
126
163
  log.error("Failed to install remote plugin.");
127
164
  return;
@@ -136,7 +173,9 @@ async function devOffCommand() {
136
173
  log.dim("Restart Claude Code to see changes.");
137
174
  }
138
175
  function getMarketplaceSource() {
139
- const data = readJson(knownMarketplacesPath());
176
+ const data = readJson(
177
+ knownMarketplacesPath()
178
+ );
140
179
  if (!data || !(MARKETPLACE_NAME in data)) return null;
141
180
  const entry = data[MARKETPLACE_NAME];
142
181
  const src = entry.source;
@@ -171,7 +210,9 @@ async function devSyncCommand() {
171
210
  const localPath = state.localPath;
172
211
  const pluginSrcDir = join(localPath, "plugin");
173
212
  if (!existsSync(pluginSrcDir)) {
174
- log.error(`No plugin/ directory found at ${localPath}. Make sure you point to the coding-friend repo root.`);
213
+ log.error(
214
+ `No plugin/ directory found at ${localPath}. Make sure you point to the coding-friend repo root.`
215
+ );
175
216
  return;
176
217
  }
177
218
  const cacheBase = pluginCachePath();
@@ -190,14 +231,18 @@ async function devSyncCommand() {
190
231
  }
191
232
  }
192
233
  if (!cacheVersionDir) {
193
- log.error("No cached plugin version found. Run `cf dev off && cf dev on` first.");
234
+ log.error(
235
+ "No cached plugin version found. Run `cf dev off && cf dev on` first."
236
+ );
194
237
  return;
195
238
  }
196
239
  const shortDest = cacheVersionDir.replace(process.env.HOME ?? "", "~");
197
240
  log.step(`Syncing ${chalk.cyan(pluginSrcDir)} \u2192 ${chalk.dim(shortDest)}`);
198
241
  const fileCount = { n: 0 };
199
242
  copyDirRecursive(pluginSrcDir, cacheVersionDir, fileCount);
200
- log.success(`Synced ${chalk.green(fileCount.n)} files. Restart Claude Code to apply changes.`);
243
+ log.success(
244
+ `Synced ${chalk.green(fileCount.n)} files. Restart Claude Code to apply changes.`
245
+ );
201
246
  }
202
247
  async function devRestartCommand(path) {
203
248
  const state = getDevState();
@@ -232,7 +277,9 @@ async function devStatusCommand() {
232
277
  } else {
233
278
  log.warn(`Marketplace "${MARKETPLACE_NAME}" not registered.`);
234
279
  }
235
- log.info(`Plugin installed: ${installed ? chalk.green("yes") : chalk.yellow("no")}`);
280
+ log.info(
281
+ `Plugin installed: ${installed ? chalk.green("yes") : chalk.yellow("no")}`
282
+ );
236
283
  }
237
284
  export {
238
285
  devOffCommand,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getLibPath,
3
3
  resolveDocsDir
4
- } from "./chunk-5HZJX47M.js";
4
+ } from "./chunk-Q4DKU5IG.js";
5
5
  import "./chunk-HRVSKMNA.js";
6
6
  import {
7
7
  run,
package/dist/index.js CHANGED
@@ -10,17 +10,19 @@ var pkg = JSON.parse(
10
10
  readFileSync(join(__dirname, "..", "package.json"), "utf-8")
11
11
  );
12
12
  var program = new Command();
13
- program.name("cf").description("coding-friend CLI \u2014 host learning docs, setup MCP, init projects").version(pkg.version, "-v, --version");
13
+ program.name("cf").description(
14
+ "coding-friend CLI \u2014 host learning docs, setup MCP, init projects"
15
+ ).version(pkg.version, "-v, --version");
14
16
  program.command("init").description("Initialize coding-friend in current project").action(async () => {
15
- const { initCommand } = await import("./init-2UKYE2KV.js");
17
+ const { initCommand } = await import("./init-CTCDQKIQ.js");
16
18
  await initCommand();
17
19
  });
18
20
  program.command("host").description("Build and serve learning docs as a static website").argument("[path]", "path to docs folder").option("-p, --port <port>", "port number", "3333").action(async (path, opts) => {
19
- const { hostCommand } = await import("./host-BK6DYFWF.js");
21
+ const { hostCommand } = await import("./host-2WINWEW7.js");
20
22
  await hostCommand(path, opts);
21
23
  });
22
24
  program.command("mcp").description("Setup MCP server for learning docs").argument("[path]", "path to docs folder").action(async (path) => {
23
- const { mcpCommand } = await import("./mcp-CH4SKZSX.js");
25
+ const { mcpCommand } = await import("./mcp-43HCE2KD.js");
24
26
  await mcpCommand(path);
25
27
  });
26
28
  program.command("statusline").description("Setup coding-friend statusline in Claude Code").action(async () => {
@@ -28,28 +30,43 @@ program.command("statusline").description("Setup coding-friend statusline in Cla
28
30
  await statuslineCommand();
29
31
  });
30
32
  program.command("update").description("Update coding-friend plugin, CLI, and statusline").option("--cli", "Update only the CLI (npm package)").option("--plugin", "Update only the Claude Code plugin").option("--statusline", "Update only the statusline").action(async (opts) => {
31
- const { updateCommand } = await import("./update-5A2OP6EY.js");
33
+ const { updateCommand } = await import("./update-GGCBM7U4.js");
32
34
  await updateCommand(opts);
33
35
  });
34
- var dev = program.command("dev").description("Switch between local and remote plugin for development");
36
+ var dev = program.command("dev").description("Development mode commands");
37
+ program.addHelpText(
38
+ "after",
39
+ `
40
+ Dev subcommands:
41
+ dev on [path] Switch to local plugin source
42
+ dev off Switch back to remote marketplace
43
+ dev status Show current dev mode
44
+ dev sync Copy local source to plugin cache
45
+ dev restart [path] Reinstall local dev plugin (off + on)`
46
+ );
35
47
  dev.command("on").description("Switch to local plugin source").argument("[path]", "path to local coding-friend repo (default: cwd)").action(async (path) => {
36
- const { devOnCommand } = await import("./dev-LZASFXZZ.js");
48
+ const { devOnCommand } = await import("./dev-MAAWPWML.js");
37
49
  await devOnCommand(path);
38
50
  });
39
51
  dev.command("off").description("Switch back to remote marketplace").action(async () => {
40
- const { devOffCommand } = await import("./dev-LZASFXZZ.js");
52
+ const { devOffCommand } = await import("./dev-MAAWPWML.js");
41
53
  await devOffCommand();
42
54
  });
43
55
  dev.command("status").description("Show current dev mode").action(async () => {
44
- const { devStatusCommand } = await import("./dev-LZASFXZZ.js");
56
+ const { devStatusCommand } = await import("./dev-MAAWPWML.js");
45
57
  await devStatusCommand();
46
58
  });
47
- dev.command("sync").description("Copy local source files to plugin cache (no version bump needed)").action(async () => {
48
- const { devSyncCommand } = await import("./dev-LZASFXZZ.js");
59
+ dev.command("sync").description(
60
+ "Copy local source files to plugin cache (no version bump needed)"
61
+ ).action(async () => {
62
+ const { devSyncCommand } = await import("./dev-MAAWPWML.js");
49
63
  await devSyncCommand();
50
64
  });
51
- dev.command("restart").description("Reinstall local dev plugin (off + on)").argument("[path]", "path to local coding-friend repo (default: saved path or cwd)").action(async (path) => {
52
- const { devRestartCommand } = await import("./dev-LZASFXZZ.js");
65
+ dev.command("restart").description("Reinstall local dev plugin (off + on)").argument(
66
+ "[path]",
67
+ "path to local coding-friend repo (default: saved path or cwd)"
68
+ ).action(async (path) => {
69
+ const { devRestartCommand } = await import("./dev-MAAWPWML.js");
53
70
  await devRestartCommand(path);
54
71
  });
55
72
  program.parse();
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  ensureShellCompletion,
6
6
  hasShellCompletion
7
- } from "./chunk-R6ZYK4UX.js";
7
+ } from "./chunk-CSF4FAHL.js";
8
8
  import {
9
9
  run
10
10
  } from "./chunk-UFGNO6CW.js";
@@ -183,7 +183,9 @@ async function setupLearnConfig(gitAvailable = true) {
183
183
  });
184
184
  let categories = DEFAULT_CONFIG.learn.categories;
185
185
  if (catChoice === "custom") {
186
- console.log('Enter categories (format: "name: description"). Empty line to finish.');
186
+ console.log(
187
+ 'Enter categories (format: "name: description"). Empty line to finish.'
188
+ );
187
189
  const customCats = [];
188
190
  let keepGoing = true;
189
191
  while (keepGoing) {
@@ -265,7 +267,11 @@ async function setupClaudePermissions(outputDir, autoCommit) {
265
267
  }
266
268
  permissions.allow = [...existing, ...missing];
267
269
  settings.permissions = permissions;
268
- const { readJson: _r, writeJson: _w, ...restImports } = await import("./json-2XS56OJY.js");
270
+ const {
271
+ readJson: _r,
272
+ writeJson: _w,
273
+ ...restImports
274
+ } = await import("./json-2XS56OJY.js");
269
275
  _w(settingsPath, settings);
270
276
  log.success(`Added ${missing.length} permission rules.`);
271
277
  }
@@ -287,9 +293,7 @@ function isDefaultConfig(config) {
287
293
  }
288
294
  async function saveConfig(config) {
289
295
  if (isDefaultConfig(config)) {
290
- log.dim(
291
- "All settings match defaults \u2014 no config file needed."
292
- );
296
+ log.dim("All settings match defaults \u2014 no config file needed.");
293
297
  return;
294
298
  }
295
299
  const target = await select({
@@ -321,10 +325,20 @@ async function initCommand() {
321
325
  const hasExternalDir = checkLearnConfig() && isExternalOutputDir(resolvedOutputDir);
322
326
  const steps = [
323
327
  { name: "docs", label: "Create docs folders", done: checkDocsFolders() },
324
- ...gitAvailable ? [{ name: "gitignore", label: "Configure .gitignore", done: checkGitignore() }] : [],
328
+ ...gitAvailable ? [
329
+ {
330
+ name: "gitignore",
331
+ label: "Configure .gitignore",
332
+ done: checkGitignore()
333
+ }
334
+ ] : [],
325
335
  { name: "language", label: "Set docs language", done: checkLanguage() },
326
336
  { name: "learn", label: "Configure /cf-learn", done: checkLearnConfig() },
327
- { name: "completion", label: "Setup shell tab completion", done: hasShellCompletion() }
337
+ {
338
+ name: "completion",
339
+ label: "Setup shell tab completion",
340
+ done: hasShellCompletion()
341
+ }
328
342
  ];
329
343
  if (hasExternalDir && resolvedOutputDir) {
330
344
  steps.push({
@@ -405,10 +419,7 @@ async function initCommand() {
405
419
  break;
406
420
  case "permissions":
407
421
  if (resolvedOutputDir) {
408
- await setupClaudePermissions(
409
- resolvedOutputDir,
410
- learnAutoCommit
411
- );
422
+ await setupClaudePermissions(resolvedOutputDir, learnAutoCommit);
412
423
  }
413
424
  break;
414
425
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getLibPath,
3
3
  resolveDocsDir
4
- } from "./chunk-5HZJX47M.js";
4
+ } from "./chunk-Q4DKU5IG.js";
5
5
  import "./chunk-HRVSKMNA.js";
6
6
  import {
7
7
  run
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ensureShellCompletion
4
- } from "./chunk-R6ZYK4UX.js";
4
+ } from "./chunk-CSF4FAHL.js";
5
5
  import "./chunk-6DUFTBTO.js";
6
6
 
7
7
  // src/postinstall.ts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ensureShellCompletion
3
- } from "./chunk-R6ZYK4UX.js";
3
+ } from "./chunk-CSF4FAHL.js";
4
4
  import {
5
5
  commandExists,
6
6
  run,
@@ -126,10 +126,16 @@ async function updateCommand(opts) {
126
126
  const statuslineVersion = getStatuslineVersion();
127
127
  const cliVersion = getCliVersion();
128
128
  const latestCliVersion = getLatestCliVersion();
129
- log.info(`Plugin version: ${currentVersion ? `v${currentVersion}` : chalk.yellow("not found")}`);
130
- log.info(`Latest plugin version: ${latestVersion ? chalk.green(`v${latestVersion}`) : chalk.yellow("unknown (cannot reach GitHub)")}`);
129
+ log.info(
130
+ `Plugin version: ${currentVersion ? `v${currentVersion}` : chalk.yellow("not found")}`
131
+ );
132
+ log.info(
133
+ `Latest plugin version: ${latestVersion ? chalk.green(`v${latestVersion}`) : chalk.yellow("unknown (cannot reach GitHub)")}`
134
+ );
131
135
  log.info(`CLI version: v${cliVersion}`);
132
- log.info(`Latest CLI version: ${latestCliVersion ? chalk.green(`v${latestCliVersion}`) : chalk.yellow("unknown (cannot reach npm)")}`);
136
+ log.info(
137
+ `Latest CLI version: ${latestCliVersion ? chalk.green(`v${latestCliVersion}`) : chalk.yellow("unknown (cannot reach npm)")}`
138
+ );
133
139
  log.info(
134
140
  `Statusline version: ${statuslineVersion ? chalk.green(`v${statuslineVersion}`) : chalk.yellow("not configured")}`
135
141
  );
@@ -140,15 +146,23 @@ async function updateCommand(opts) {
140
146
  "Cannot check latest plugin version. Verify manually at https://github.com/dinhanhthi/coding-friend/releases"
141
147
  );
142
148
  } else if (!currentVersion) {
143
- log.warn("Plugin not installed. Run: claude plugin install coding-friend@coding-friend-marketplace");
149
+ log.warn(
150
+ "Plugin not installed. Run: claude plugin install coding-friend@coding-friend-marketplace"
151
+ );
144
152
  } else {
145
153
  const cmp = semverCompare(currentVersion, latestVersion);
146
154
  if (cmp === 0) {
147
- log.success(`Plugin already on the latest version (${chalk.green(`v${latestVersion}`)}).`);
155
+ log.success(
156
+ `Plugin already on the latest version (${chalk.green(`v${latestVersion}`)}).`
157
+ );
148
158
  } else if (cmp > 0) {
149
- log.info(`Plugin is ahead of latest release (local: ${chalk.cyan(`v${currentVersion}`)}, latest: v${latestVersion}). Skipping.`);
159
+ log.info(
160
+ `Plugin is ahead of latest release (local: ${chalk.cyan(`v${currentVersion}`)}, latest: v${latestVersion}). Skipping.`
161
+ );
150
162
  } else {
151
- log.step(`Plugin update available: ${chalk.yellow(`v${currentVersion}`)} \u2192 ${chalk.green(`v${latestVersion}`)}`);
163
+ log.step(
164
+ `Plugin update available: ${chalk.yellow(`v${currentVersion}`)} \u2192 ${chalk.green(`v${latestVersion}`)}`
165
+ );
152
166
  if (!commandExists("claude")) {
153
167
  log.error(
154
168
  "Claude CLI not found. Install it first, or run: claude plugin update coding-friend@coding-friend-marketplace"
@@ -161,7 +175,9 @@ async function updateCommand(opts) {
161
175
  "coding-friend@coding-friend-marketplace"
162
176
  ]);
163
177
  if (result === null) {
164
- log.error("Plugin update failed. Try manually: claude plugin update coding-friend@coding-friend-marketplace");
178
+ log.error(
179
+ "Plugin update failed. Try manually: claude plugin update coding-friend@coding-friend-marketplace"
180
+ );
165
181
  } else {
166
182
  log.success("Plugin updated!");
167
183
  let newVersion = currentVersion;
@@ -188,15 +204,27 @@ async function updateCommand(opts) {
188
204
  } else {
189
205
  const cmp = semverCompare(cliVersion, latestCliVersion);
190
206
  if (cmp === 0) {
191
- log.success(`CLI already on the latest version (${chalk.green(`v${latestCliVersion}`)}).`);
207
+ log.success(
208
+ `CLI already on the latest version (${chalk.green(`v${latestCliVersion}`)}).`
209
+ );
192
210
  } else if (cmp > 0) {
193
- log.info(`CLI is ahead of latest release (local: ${chalk.cyan(`v${cliVersion}`)}, latest: v${latestCliVersion}). Skipping.`);
211
+ log.info(
212
+ `CLI is ahead of latest release (local: ${chalk.cyan(`v${cliVersion}`)}, latest: v${latestCliVersion}). Skipping.`
213
+ );
194
214
  } else {
195
- log.step(`CLI update available: ${chalk.yellow(`v${cliVersion}`)} \u2192 ${chalk.green(`v${latestCliVersion}`)}`);
215
+ log.step(
216
+ `CLI update available: ${chalk.yellow(`v${cliVersion}`)} \u2192 ${chalk.green(`v${latestCliVersion}`)}`
217
+ );
196
218
  log.step("Updating CLI...");
197
- const result = run("npm", ["install", "-g", "coding-friend-cli@latest"]);
219
+ const result = run("npm", [
220
+ "install",
221
+ "-g",
222
+ "coding-friend-cli@latest"
223
+ ]);
198
224
  if (result === null) {
199
- log.error("CLI update failed. Try manually: npm install -g coding-friend-cli@latest");
225
+ log.error(
226
+ "CLI update failed. Try manually: npm install -g coding-friend-cli@latest"
227
+ );
200
228
  } else {
201
229
  log.success(`CLI updated to ${chalk.green(`v${latestCliVersion}`)}`);
202
230
  }
@@ -208,7 +236,9 @@ async function updateCommand(opts) {
208
236
  if (targetVersion) {
209
237
  log.step("Updating statusline...");
210
238
  if (updateStatusline(targetVersion)) {
211
- log.success(`Statusline updated to ${chalk.green(`v${targetVersion}`)}`);
239
+ log.success(
240
+ `Statusline updated to ${chalk.green(`v${targetVersion}`)}`
241
+ );
212
242
  }
213
243
  } else {
214
244
  log.warn("No cached plugin version found for statusline update.");
@@ -1,3 +1,4 @@
1
1
  node_modules
2
2
  .next
3
+ out
3
4
  public/_pagefind
@@ -1,5 +1,10 @@
1
1
  # Changelog (Learn Host)
2
2
 
3
+ ## v0.0.2 (2026-03-01)
4
+
5
+ - Update header styling for consistency with ecosystem redesign
6
+ - Code formatting and style improvements
7
+
3
8
  ## v0.0.1
4
9
 
5
10
  - Next.js app for hosting learning docs at `localhost:3333`
@@ -0,0 +1,114 @@
1
+ # coding-friend-learn-host
2
+
3
+ Next.js app that renders your `/cf-learn` docs as a browsable website with search, categories, and dark mode.
4
+
5
+ ## Usage (via CLI)
6
+
7
+ ```bash
8
+ cf host # serves docs/learn/ on port 3333
9
+ cf host ./my-docs # serves a custom directory
10
+ cf host -p 4000 # custom port
11
+ ```
12
+
13
+ The CLI handles deps install, build, and serving automatically.
14
+
15
+ ## Local Development
16
+
17
+ Run the app directly without the CLI — useful when working on the UI itself.
18
+
19
+ ### 1. Install dependencies
20
+
21
+ ```bash
22
+ cd cli/lib/learn-host
23
+ npm install
24
+ ```
25
+
26
+ ### 2. Point to a docs directory
27
+
28
+ The app resolves docs via (in order):
29
+
30
+ 1. `DOCS_DIR` env var
31
+ 2. Local `.coding-friend/config.json` → `learn.outputDir`
32
+ 3. Global `~/.coding-friend/config.json` → `learn.outputDir`
33
+ 4. Default: `docs/learn/` relative to project root
34
+
35
+ For local dev, set `DOCS_DIR` to any directory with the expected structure:
36
+
37
+ ```
38
+ docs/
39
+ └── learn/
40
+ ├── category-one/
41
+ │ ├── my-doc.md
42
+ │ └── another-doc.md
43
+ └── category-two/
44
+ └── some-doc.md
45
+ ```
46
+
47
+ Each `.md` file should have frontmatter:
48
+
49
+ ```md
50
+ ---
51
+ title: My Doc Title
52
+ category: category-one
53
+ tags: [typescript, patterns]
54
+ created: 2025-01-01
55
+ updated: 2025-01-15
56
+ ---
57
+
58
+ Content here...
59
+ ```
60
+
61
+ ### 3. Run dev server
62
+
63
+ ```bash
64
+ # Point to this repo's own learn docs (if they exist)
65
+ DOCS_DIR=../../../docs/learn npm run dev
66
+
67
+ # Or point to any other project's docs
68
+ DOCS_DIR=/path/to/your/project/docs/learn npm run dev
69
+ ```
70
+
71
+ App runs at `http://localhost:3333`.
72
+
73
+ > **Note:** `npm run dev` does not rebuild the Pagefind search index. If you've previously run `npm run build`, search will still work but uses the old index — new or edited docs won't appear in search results until you build again.
74
+
75
+ ### 4. Full build (with search)
76
+
77
+ ```bash
78
+ DOCS_DIR=/path/to/docs npm run build
79
+ npx next start -p 3333
80
+ ```
81
+
82
+ `postbuild` runs `pagefind` to index docs for full-text search.
83
+
84
+ ## Structure
85
+
86
+ ```
87
+ src/
88
+ ├── app/
89
+ │ ├── page.tsx # Homepage: recent docs, categories, tags
90
+ │ ├── [category]/page.tsx # Category listing
91
+ │ ├── [category]/[slug]/page.tsx # Individual doc
92
+ │ └── layout.tsx
93
+ ├── components/
94
+ │ ├── MarkdownRenderer.tsx # Renders .md with syntax highlighting
95
+ │ ├── TableOfContents.tsx # Auto-generated from headings
96
+ │ ├── Sidebar.tsx # Category navigation
97
+ │ ├── PagefindSearch.tsx # Full-text search (build-time index)
98
+ │ └── ThemeToggle.tsx
99
+ └── lib/
100
+ ├── docs.ts # getAllDocs, getDocBySlug, etc.
101
+ └── types.ts
102
+ ```
103
+
104
+ ## How It Fits Together
105
+
106
+ ```
107
+ cf host [path]
108
+ └─ resolves docs dir
109
+ └─ npm install (one-time)
110
+ └─ npm run build (with DOCS_DIR env)
111
+ └─ npx next start -p 3333 (with DOCS_DIR env)
112
+ ```
113
+
114
+ ISR (Incremental Static Regeneration) is enabled, so new or edited docs appear on the next page refresh without a rebuild.