coding-friend-cli 1.5.2 → 1.7.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.
package/README.md CHANGED
@@ -37,6 +37,7 @@ cf dev off # Switch back to remote marketplace
37
37
  cf dev status # Show current dev mode (local or remote)
38
38
  cf dev sync # Sync local changes to cache (no version bump needed)
39
39
  cf dev restart # Reinstall local dev plugin (off + on)
40
+ cf dev update # Update local dev plugin to latest version (off + on)
40
41
  cf help # Show all commands
41
42
  ```
42
43
 
@@ -17,12 +17,12 @@ _cf_completions() {
17
17
 
18
18
  # Subcommands for 'dev'
19
19
  if [[ "\${COMP_WORDS[1]}" == "dev" && \${COMP_CWORD} -eq 2 ]]; then
20
- COMPREPLY=($(compgen -W "on off status restart sync" -- "$cur"))
20
+ COMPREPLY=($(compgen -W "on off status restart sync update" -- "$cur"))
21
21
  return
22
22
  fi
23
23
 
24
- # Path completion for 'dev on'
25
- if [[ "\${COMP_WORDS[1]}" == "dev" && "$prev" == "on" ]]; then
24
+ # Path completion for 'dev on|restart|update'
25
+ if [[ "\${COMP_WORDS[1]}" == "dev" && ("$prev" == "on" || "$prev" == "restart" || "$prev" == "update") ]]; then
26
26
  COMPREPLY=($(compgen -d -- "$cur"))
27
27
  return
28
28
  fi
@@ -58,9 +58,10 @@ _cf() {
58
58
  'status:Show current dev mode'
59
59
  'restart:Restart dev mode (re-apply local plugin)'
60
60
  'sync:Sync local plugin files without restarting'
61
+ 'update:Update local dev plugin to latest version'
61
62
  )
62
63
  _describe 'subcommand' subcommands
63
- elif (( CURRENT == 4 )) && [[ "\${words[2]}" == "dev" && "\${words[3]}" == "on" ]]; then
64
+ elif (( CURRENT == 4 )) && [[ "\${words[2]}" == "dev" && ("\${words[3]}" == "on" || "\${words[3]}" == "restart" || "\${words[3]}" == "update") ]]; then
64
65
  _path_files -/
65
66
  fi
66
67
  }
@@ -1,26 +1,27 @@
1
+ import {
2
+ ensureStatusline,
3
+ getInstalledVersion
4
+ } from "./chunk-HSQX3PKW.js";
1
5
  import {
2
6
  ensureShellCompletion
3
- } from "./chunk-DDISNOEK.js";
7
+ } from "./chunk-4PLV2ENL.js";
4
8
  import {
5
9
  commandExists,
6
10
  run,
7
11
  sleepSync
8
12
  } from "./chunk-UFGNO6CW.js";
9
13
  import {
10
- claudeSettingsPath,
11
- installedPluginsPath,
12
- pluginCachePath
14
+ claudeSettingsPath
13
15
  } from "./chunk-WHCJT7E2.js";
14
16
  import {
15
17
  log
16
18
  } from "./chunk-6DUFTBTO.js";
17
19
  import {
18
- readJson,
19
- writeJson
20
+ readJson
20
21
  } from "./chunk-IUTXHCP7.js";
21
22
 
22
23
  // src/commands/update.ts
23
- import { existsSync, readFileSync, readdirSync } from "fs";
24
+ import { readFileSync } from "fs";
24
25
  import { dirname, join } from "path";
25
26
  import { fileURLToPath } from "url";
26
27
  import chalk from "chalk";
@@ -43,22 +44,6 @@ function getCliVersion() {
43
44
  function getLatestCliVersion() {
44
45
  return run("npm", ["view", "coding-friend-cli", "version"]);
45
46
  }
46
- function getInstalledVersion() {
47
- const data = readJson(installedPluginsPath());
48
- if (!data) return null;
49
- const plugins = data.plugins ?? data;
50
- for (const [key, value] of Object.entries(plugins)) {
51
- if (!key.includes("coding-friend")) continue;
52
- if (Array.isArray(value) && value.length > 0) {
53
- const entry = value[0];
54
- if (typeof entry.version === "string") return entry.version;
55
- }
56
- if (typeof value === "object" && value !== null && "version" in value) {
57
- return value.version;
58
- }
59
- }
60
- return null;
61
- }
62
47
  function getLatestVersion() {
63
48
  let tag = run("gh", [
64
49
  "api",
@@ -92,28 +77,6 @@ function getStatuslineVersion() {
92
77
  );
93
78
  return match?.[1] ?? null;
94
79
  }
95
- function findLatestCacheVersion() {
96
- const cachePath = pluginCachePath();
97
- if (!existsSync(cachePath)) return null;
98
- const versions = readdirSync(cachePath, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
99
- return versions[0] ?? null;
100
- }
101
- function updateStatusline(version) {
102
- const cachePath = pluginCachePath();
103
- const hookPath = `${cachePath}/${version}/hooks/statusline.sh`;
104
- if (!existsSync(hookPath)) {
105
- log.warn(`Statusline hook not found for v${version}`);
106
- return false;
107
- }
108
- const settingsPath = claudeSettingsPath();
109
- const settings = readJson(settingsPath) ?? {};
110
- settings.statusLine = {
111
- type: "command",
112
- command: `bash ${hookPath}`
113
- };
114
- writeJson(settingsPath, settings);
115
- return true;
116
- }
117
80
  async function updateCommand(opts) {
118
81
  const updateAll = !opts.cli && !opts.plugin && !opts.statusline;
119
82
  const doCli = updateAll || !!opts.cli;
@@ -232,16 +195,12 @@ async function updateCommand(opts) {
232
195
  }
233
196
  }
234
197
  if (doStatusline) {
235
- const targetVersion = findLatestCacheVersion();
236
- if (targetVersion) {
237
- log.step("Updating statusline...");
238
- if (updateStatusline(targetVersion)) {
239
- log.success(
240
- `Statusline updated to ${chalk.green(`v${targetVersion}`)}`
241
- );
242
- }
198
+ log.step("Updating statusline...");
199
+ const updatedVersion = ensureStatusline();
200
+ if (updatedVersion) {
201
+ log.success(`Statusline updated to ${chalk.green(`v${updatedVersion}`)}`);
243
202
  } else {
244
- log.warn("No cached plugin version found for statusline update.");
203
+ log.dim("Statusline already up-to-date.");
245
204
  }
246
205
  }
247
206
  ensureShellCompletion({ silent: false });
@@ -251,7 +210,6 @@ async function updateCommand(opts) {
251
210
 
252
211
  export {
253
212
  semverCompare,
254
- getInstalledVersion,
255
213
  getLatestVersion,
256
214
  updateCommand
257
215
  };
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  ALL_COMPONENT_IDS,
3
3
  STATUSLINE_COMPONENTS
4
- } from "./chunk-JWAJ4XPK.js";
4
+ } from "./chunk-WXBI2HUL.js";
5
5
  import {
6
6
  claudeSettingsPath,
7
7
  globalConfigPath,
8
+ installedPluginsPath,
8
9
  pluginCachePath
9
10
  } from "./chunk-WHCJT7E2.js";
10
11
  import {
@@ -16,6 +17,22 @@ import {
16
17
  // src/lib/statusline.ts
17
18
  import { existsSync, readdirSync } from "fs";
18
19
  import { checkbox } from "@inquirer/prompts";
20
+ function getInstalledVersion() {
21
+ const data = readJson(installedPluginsPath());
22
+ if (!data) return null;
23
+ const plugins = data.plugins ?? data;
24
+ for (const [key, value] of Object.entries(plugins)) {
25
+ if (!key.includes("coding-friend")) continue;
26
+ if (Array.isArray(value) && value.length > 0) {
27
+ const entry = value[0];
28
+ if (typeof entry.version === "string") return entry.version;
29
+ }
30
+ if (typeof value === "object" && value !== null && "version" in value) {
31
+ return value.version;
32
+ }
33
+ }
34
+ return null;
35
+ }
19
36
  function findLatestVersion() {
20
37
  const cachePath = pluginCachePath();
21
38
  if (!existsSync(cachePath)) return null;
@@ -23,11 +40,17 @@ function findLatestVersion() {
23
40
  return versions[0] ?? null;
24
41
  }
25
42
  function findStatuslineHookPath() {
26
- const version = findLatestVersion();
27
- if (!version) return null;
28
- const hookPath = `${pluginCachePath()}/${version}/hooks/statusline.sh`;
43
+ const cachePath = pluginCachePath();
44
+ const installed = getInstalledVersion();
45
+ if (installed) {
46
+ const hookPath2 = `${cachePath}/${installed}/hooks/statusline.sh`;
47
+ if (existsSync(hookPath2)) return { hookPath: hookPath2, version: installed };
48
+ }
49
+ const latest = findLatestVersion();
50
+ if (!latest) return null;
51
+ const hookPath = `${cachePath}/${latest}/hooks/statusline.sh`;
29
52
  if (!existsSync(hookPath)) return null;
30
- return { hookPath, version };
53
+ return { hookPath, version: latest };
31
54
  }
32
55
  function loadStatuslineComponents() {
33
56
  const config = readJson(globalConfigPath());
@@ -61,6 +84,18 @@ function writeStatuslineSettings(hookPath) {
61
84
  };
62
85
  writeJson(settingsPath, settings);
63
86
  }
87
+ function ensureStatusline() {
88
+ const info = findStatuslineHookPath();
89
+ if (!info) return null;
90
+ const settingsPath = claudeSettingsPath();
91
+ const settings = readJson(settingsPath) ?? {};
92
+ const current = settings.statusLine?.command;
93
+ const expected = `bash ${info.hookPath}`;
94
+ if (current === expected) return null;
95
+ settings.statusLine = { type: "command", command: expected };
96
+ writeJson(settingsPath, settings);
97
+ return info.version;
98
+ }
64
99
  function isStatuslineConfigured() {
65
100
  const settings = readJson(claudeSettingsPath());
66
101
  if (!settings) return false;
@@ -69,9 +104,11 @@ function isStatuslineConfigured() {
69
104
  }
70
105
 
71
106
  export {
107
+ getInstalledVersion,
72
108
  findStatuslineHookPath,
73
109
  selectStatuslineComponents,
74
110
  saveStatuslineConfig,
75
111
  writeStatuslineSettings,
112
+ ensureStatusline,
76
113
  isStatuslineConfigured
77
114
  };
@@ -13,6 +13,7 @@ var DEFAULT_CONFIG = {
13
13
  docsDir: "docs",
14
14
  devRulesReminder: true,
15
15
  learn: {
16
+ language: "en",
16
17
  outputDir: "docs/learn",
17
18
  categories: [
18
19
  {
@@ -2,12 +2,18 @@ import {
2
2
  isMarketplaceRegistered,
3
3
  isPluginInstalled
4
4
  } from "./chunk-MRTR7TJ4.js";
5
+ import {
6
+ ensureStatusline
7
+ } from "./chunk-HSQX3PKW.js";
8
+ import {
9
+ ensureShellCompletion
10
+ } from "./chunk-4PLV2ENL.js";
11
+ import "./chunk-WXBI2HUL.js";
5
12
  import {
6
13
  commandExists,
7
14
  run
8
15
  } from "./chunk-UFGNO6CW.js";
9
16
  import {
10
- claudeSettingsPath,
11
17
  devStatePath,
12
18
  knownMarketplacesPath,
13
19
  pluginCachePath
@@ -56,28 +62,6 @@ function runClaude(args, label) {
56
62
  }
57
63
  return true;
58
64
  }
59
- function updateSettingsCachePaths() {
60
- const cachePath = pluginCachePath();
61
- if (!existsSync(cachePath)) return;
62
- const versions = readdirSync(cachePath, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
63
- const latest = versions[0];
64
- if (!latest) return;
65
- const settingsPath = claudeSettingsPath();
66
- const settings = readJson(settingsPath);
67
- if (!settings) return;
68
- const statusLine = settings.statusLine;
69
- if (!statusLine?.command) return;
70
- const prefix = cachePath + "/";
71
- if (!statusLine.command.includes(prefix)) return;
72
- const updated = statusLine.command.replace(
73
- new RegExp(`${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^/]+/`),
74
- `${prefix}${latest}/`
75
- );
76
- if (updated === statusLine.command) return;
77
- settings.statusLine = { ...statusLine, command: updated };
78
- writeJson(settingsPath, settings);
79
- log.info(`Updated statusline path to ${chalk.green(`v${latest}`)}`);
80
- }
81
65
  async function devOnCommand(path) {
82
66
  const state = getDevState();
83
67
  if (state) {
@@ -131,7 +115,8 @@ async function devOnCommand(path) {
131
115
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
132
116
  };
133
117
  writeJson(devStatePath(), devState);
134
- updateSettingsCachePaths();
118
+ ensureStatusline();
119
+ ensureShellCompletion({ silent: true });
135
120
  console.log();
136
121
  log.success(
137
122
  `Dev mode ${chalk.green("ON")} \u2014 using local plugin from ${chalk.cyan(localPath)}`
@@ -260,12 +245,12 @@ async function devSyncCommand() {
260
245
  `Synced ${chalk.green(fileCount.n)} files. Restart Claude Code to apply changes.`
261
246
  );
262
247
  }
263
- async function devRestartCommand(path) {
248
+ async function devReinstall(path, label) {
264
249
  const state = getDevState();
265
250
  if (!ensureClaude()) return;
266
251
  const localPath = path ?? state?.localPath;
267
252
  console.log(`
268
- === ${chalk.cyan("Restarting dev mode")} ===
253
+ === ${chalk.cyan(label)} ===
269
254
  `);
270
255
  if (state) {
271
256
  await devOffCommand();
@@ -274,7 +259,10 @@ async function devRestartCommand(path) {
274
259
  log.info("Dev mode was OFF \u2014 skipping off step.");
275
260
  }
276
261
  await devOnCommand(localPath);
262
+ ensureShellCompletion({ silent: true });
277
263
  }
264
+ var devRestartCommand = (path) => devReinstall(path, "Restarting dev mode");
265
+ var devUpdateCommand = (path) => devReinstall(path, "Updating dev plugin");
278
266
  async function devStatusCommand() {
279
267
  const state = getDevState();
280
268
  const source = getMarketplaceSource();
@@ -302,5 +290,6 @@ export {
302
290
  devOnCommand,
303
291
  devRestartCommand,
304
292
  devStatusCommand,
305
- devSyncCommand
293
+ devSyncCommand,
294
+ devUpdateCommand
306
295
  };
@@ -2,7 +2,7 @@ import {
2
2
  getLibPath,
3
3
  resolveDocsDir
4
4
  } from "./chunk-WK5YYHXM.js";
5
- import "./chunk-JWAJ4XPK.js";
5
+ import "./chunk-WXBI2HUL.js";
6
6
  import {
7
7
  run,
8
8
  streamExec
package/dist/index.js CHANGED
@@ -14,31 +14,31 @@ program.name("cf").description(
14
14
  "coding-friend CLI \u2014 host learning docs, setup MCP, init projects"
15
15
  ).version(pkg.version, "-v, --version");
16
16
  program.command("install").description("Install the Coding Friend plugin into Claude Code").action(async () => {
17
- const { installCommand } = await import("./install-BEFMUMKE.js");
17
+ const { installCommand } = await import("./install-ORIDNWRW.js");
18
18
  await installCommand();
19
19
  });
20
20
  program.command("uninstall").description("Uninstall the Coding Friend plugin from Claude Code").action(async () => {
21
- const { uninstallCommand } = await import("./uninstall-BHYS52L3.js");
21
+ const { uninstallCommand } = await import("./uninstall-KOAJFPD6.js");
22
22
  await uninstallCommand();
23
23
  });
24
24
  program.command("init").description("Initialize coding-friend in current project").action(async () => {
25
- const { initCommand } = await import("./init-K4EVPAHK.js");
25
+ const { initCommand } = await import("./init-5BJVESH7.js");
26
26
  await initCommand();
27
27
  });
28
28
  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) => {
29
- const { hostCommand } = await import("./host-SQEDE3NN.js");
29
+ const { hostCommand } = await import("./host-EERZVOHY.js");
30
30
  await hostCommand(path, opts);
31
31
  });
32
32
  program.command("mcp").description("Setup MCP server for learning docs").argument("[path]", "path to docs folder").action(async (path) => {
33
- const { mcpCommand } = await import("./mcp-QRPBL4ML.js");
33
+ const { mcpCommand } = await import("./mcp-3MUUQZQD.js");
34
34
  await mcpCommand(path);
35
35
  });
36
36
  program.command("statusline").description("Setup coding-friend statusline in Claude Code").action(async () => {
37
- const { statuslineCommand } = await import("./statusline-3MQQDRCI.js");
37
+ const { statuslineCommand } = await import("./statusline-BWGI5PQ5.js");
38
38
  await statuslineCommand();
39
39
  });
40
40
  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) => {
41
- const { updateCommand } = await import("./update-TALQ7TAO.js");
41
+ const { updateCommand } = await import("./update-GW37S23M.js");
42
42
  await updateCommand(opts);
43
43
  });
44
44
  var dev = program.command("dev").description("Development mode commands");
@@ -50,31 +50,39 @@ Dev subcommands:
50
50
  dev off Switch back to remote marketplace
51
51
  dev status Show current dev mode
52
52
  dev sync Copy local source to plugin cache
53
- dev restart [path] Reinstall local dev plugin (off + on)`
53
+ dev restart [path] Reinstall local dev plugin (off + on)
54
+ dev update [path] Update local dev plugin to latest version`
54
55
  );
55
56
  dev.command("on").description("Switch to local plugin source").argument("[path]", "path to local coding-friend repo (default: cwd)").action(async (path) => {
56
- const { devOnCommand } = await import("./dev-WINMWRZK.js");
57
+ const { devOnCommand } = await import("./dev-QW6VPG4G.js");
57
58
  await devOnCommand(path);
58
59
  });
59
60
  dev.command("off").description("Switch back to remote marketplace").action(async () => {
60
- const { devOffCommand } = await import("./dev-WINMWRZK.js");
61
+ const { devOffCommand } = await import("./dev-QW6VPG4G.js");
61
62
  await devOffCommand();
62
63
  });
63
64
  dev.command("status").description("Show current dev mode").action(async () => {
64
- const { devStatusCommand } = await import("./dev-WINMWRZK.js");
65
+ const { devStatusCommand } = await import("./dev-QW6VPG4G.js");
65
66
  await devStatusCommand();
66
67
  });
67
68
  dev.command("sync").description(
68
69
  "Copy local source files to plugin cache (no version bump needed)"
69
70
  ).action(async () => {
70
- const { devSyncCommand } = await import("./dev-WINMWRZK.js");
71
+ const { devSyncCommand } = await import("./dev-QW6VPG4G.js");
71
72
  await devSyncCommand();
72
73
  });
73
74
  dev.command("restart").description("Reinstall local dev plugin (off + on)").argument(
74
75
  "[path]",
75
76
  "path to local coding-friend repo (default: saved path or cwd)"
76
77
  ).action(async (path) => {
77
- const { devRestartCommand } = await import("./dev-WINMWRZK.js");
78
+ const { devRestartCommand } = await import("./dev-QW6VPG4G.js");
78
79
  await devRestartCommand(path);
79
80
  });
81
+ dev.command("update").description("Update local dev plugin to latest version (off + on)").argument(
82
+ "[path]",
83
+ "path to local coding-friend repo (default: saved path or cwd)"
84
+ ).action(async (path) => {
85
+ const { devUpdateCommand } = await import("./dev-QW6VPG4G.js");
86
+ await devUpdateCommand(path);
87
+ });
80
88
  program.parse();
@@ -4,14 +4,14 @@ import {
4
4
  saveStatuslineConfig,
5
5
  selectStatuslineComponents,
6
6
  writeStatuslineSettings
7
- } from "./chunk-5OALHHKR.js";
7
+ } from "./chunk-HSQX3PKW.js";
8
8
  import {
9
9
  ensureShellCompletion,
10
10
  hasShellCompletion
11
- } from "./chunk-DDISNOEK.js";
11
+ } from "./chunk-4PLV2ENL.js";
12
12
  import {
13
13
  DEFAULT_CONFIG
14
- } from "./chunk-JWAJ4XPK.js";
14
+ } from "./chunk-WXBI2HUL.js";
15
15
  import {
16
16
  run
17
17
  } from "./chunk-UFGNO6CW.js";
@@ -31,8 +31,10 @@ import {
31
31
 
32
32
  // src/commands/init.ts
33
33
  import { checkbox, confirm, input, select } from "@inquirer/prompts";
34
- import { appendFileSync, existsSync, readFileSync } from "fs";
34
+ import { existsSync, readFileSync, writeFileSync } from "fs";
35
35
  import { homedir } from "os";
36
+ var GITIGNORE_START = "# >>> coding-friend managed";
37
+ var GITIGNORE_END = "# <<< coding-friend managed";
36
38
  function isGitRepo() {
37
39
  return run("git", ["rev-parse", "--is-inside-work-tree"]) === "true";
38
40
  }
@@ -42,13 +44,29 @@ function checkDocsFolders() {
42
44
  }
43
45
  function checkGitignore() {
44
46
  if (!existsSync(".gitignore")) return false;
45
- return readFileSync(".gitignore", "utf-8").includes("# coding-friend");
47
+ const content = readFileSync(".gitignore", "utf-8");
48
+ return content.includes(GITIGNORE_START) || content.includes("# coding-friend");
46
49
  }
47
- function checkLanguage() {
50
+ function checkDocsLanguage() {
48
51
  const local = readJson(localConfigPath());
49
52
  const global = readJson(globalConfigPath());
50
53
  return !!(local?.language || global?.language);
51
54
  }
55
+ async function selectLanguage(message) {
56
+ const choice = await select({
57
+ message,
58
+ choices: [
59
+ { name: "English", value: "en" },
60
+ { name: "Vietnamese", value: "vi" },
61
+ { name: "Other", value: "_other" }
62
+ ]
63
+ });
64
+ if (choice === "_other") {
65
+ const lang = await input({ message: "Enter language name:" });
66
+ return lang || "en";
67
+ }
68
+ return choice;
69
+ }
52
70
  function checkLearnConfig() {
53
71
  const local = readJson(localConfigPath());
54
72
  const global = readJson(globalConfigPath());
@@ -123,34 +141,38 @@ async function setupGitignore() {
123
141
  }
124
142
  }
125
143
  const existing = existsSync(".gitignore") ? readFileSync(".gitignore", "utf-8") : "";
126
- const newEntries = entries.filter((e) => !existing.includes(e));
127
- if (newEntries.length === 0) {
128
- log.dim("All entries already in .gitignore.");
129
- return;
144
+ const block = `${GITIGNORE_START}
145
+ ${entries.join("\n")}
146
+ ${GITIGNORE_END}`;
147
+ const managedBlockRe = new RegExp(
148
+ `${escapeRegExp(GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(GITIGNORE_END)}`
149
+ );
150
+ const legacyBlockRe = /# coding-friend\n([\w/.]+\n)*/;
151
+ let updated;
152
+ if (managedBlockRe.test(existing)) {
153
+ updated = existing.replace(managedBlockRe, block);
154
+ log.success(`Updated .gitignore: ${entries.join(", ")}`);
155
+ } else if (legacyBlockRe.test(existing)) {
156
+ updated = existing.replace(legacyBlockRe, block);
157
+ log.success(`Migrated .gitignore block: ${entries.join(", ")}`);
158
+ } else {
159
+ updated = existing.trimEnd() + "\n\n" + block + "\n";
160
+ log.success(`Added to .gitignore: ${entries.join(", ")}`);
130
161
  }
131
- const block = `
132
- # coding-friend
133
- ${newEntries.join("\n")}
134
- `;
135
- appendFileSync(".gitignore", block);
136
- log.success(`Added to .gitignore: ${newEntries.join(", ")}`);
162
+ writeFileSync(".gitignore", updated);
137
163
  }
138
- async function setupLanguage() {
139
- const choice = await select({
140
- message: "What language should generated docs be written in?",
141
- choices: [
142
- { name: "English", value: "en" },
143
- { name: "Vietnamese", value: "vi" },
144
- { name: "Other", value: "_other" }
145
- ]
146
- });
147
- if (choice === "_other") {
148
- const lang = await input({ message: "Enter language name:" });
149
- return lang || "en";
150
- }
151
- return choice;
164
+ function escapeRegExp(str) {
165
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
166
+ }
167
+ async function setupDocsLanguage() {
168
+ return selectLanguage(
169
+ "What language should generated docs be written in? (plans, memory, research, ask)"
170
+ );
152
171
  }
153
172
  async function setupLearnConfig(gitAvailable = true) {
173
+ const language = await selectLanguage(
174
+ "What language should /cf-learn notes be written in?"
175
+ );
154
176
  const locationChoice = await select({
155
177
  message: "Where to store learning docs?",
156
178
  choices: [
@@ -178,21 +200,46 @@ async function setupLearnConfig(gitAvailable = true) {
178
200
  }
179
201
  }
180
202
  }
203
+ const existingConfig = readJson(localConfigPath()) ?? readJson(globalConfigPath());
204
+ const existingCats = existingConfig?.learn?.categories;
205
+ const defaultNames = DEFAULT_CONFIG.learn.categories.map((c) => c.name).join(", ");
206
+ const choices = [
207
+ {
208
+ name: `Use defaults (${defaultNames})`,
209
+ value: "defaults"
210
+ }
211
+ ];
212
+ if (existingCats && existingCats.length > 0) {
213
+ const existingNames = existingCats.map((c) => c.name).join(", ");
214
+ choices.push({
215
+ name: `Keep current (${existingNames})`,
216
+ value: "existing"
217
+ });
218
+ }
219
+ choices.push({ name: "Customize", value: "custom" });
181
220
  const catChoice = await select({
182
221
  message: "Categories for organizing learning docs?",
183
- choices: [
184
- {
185
- name: "Use defaults (concepts, patterns, languages, tools, debugging)",
186
- value: "defaults"
187
- },
188
- { name: "Customize", value: "custom" }
189
- ]
222
+ choices
190
223
  });
191
224
  let categories = DEFAULT_CONFIG.learn.categories;
192
- if (catChoice === "custom") {
225
+ if (catChoice === "existing" && existingCats) {
226
+ categories = existingCats;
227
+ } else if (catChoice === "custom") {
228
+ console.log();
229
+ if (existingCats && existingCats.length > 0) {
230
+ console.log("Current categories in config.json:");
231
+ for (const c of existingCats) {
232
+ log.dim(` ${c.name}: ${c.description}`);
233
+ }
234
+ console.log();
235
+ }
193
236
  console.log(
194
237
  'Enter categories (format: "name: description"). Empty line to finish.'
195
238
  );
239
+ log.dim(
240
+ "Tip: you can also edit config.json later \u2014 see https://cf.dinhanhthi.com/docs/cli/cf-init/"
241
+ );
242
+ console.log();
196
243
  const customCats = [];
197
244
  let keepGoing = true;
198
245
  while (keepGoing) {
@@ -229,7 +276,14 @@ async function setupLearnConfig(gitAvailable = true) {
229
276
  let readmeIndex = false;
230
277
  if (indexChoice === "single") readmeIndex = true;
231
278
  else if (indexChoice === "per-category") readmeIndex = "per-category";
232
- return { outputDir, categories, autoCommit, readmeIndex, isExternal };
279
+ return {
280
+ language,
281
+ outputDir,
282
+ categories,
283
+ autoCommit,
284
+ readmeIndex,
285
+ isExternal
286
+ };
233
287
  }
234
288
  async function setupClaudePermissions(outputDir, autoCommit) {
235
289
  const resolved = resolvePath(outputDir);
@@ -286,6 +340,7 @@ function isDefaultConfig(config) {
286
340
  if (config.language && config.language !== "en") return false;
287
341
  if (config.learn) {
288
342
  const l = config.learn;
343
+ if (l.language && l.language !== "en") return false;
289
344
  if (l.outputDir && l.outputDir !== "docs/learn") return false;
290
345
  if (l.autoCommit) return false;
291
346
  if (l.readmeIndex) return false;
@@ -339,7 +394,11 @@ async function initCommand() {
339
394
  done: checkGitignore()
340
395
  }
341
396
  ] : [],
342
- { name: "language", label: "Set docs language", done: checkLanguage() },
397
+ {
398
+ name: "docsLanguage",
399
+ label: "Set docs language (plans, memory, research, ask)",
400
+ done: checkDocsLanguage()
401
+ },
343
402
  { name: "learn", label: "Configure /cf-learn", done: checkLearnConfig() },
344
403
  {
345
404
  name: "completion",
@@ -408,14 +467,16 @@ async function initCommand() {
408
467
  case "gitignore":
409
468
  await setupGitignore();
410
469
  break;
411
- case "language": {
412
- const lang = await setupLanguage();
470
+ case "docsLanguage": {
471
+ const lang = await setupDocsLanguage();
413
472
  config.language = lang;
414
473
  break;
415
474
  }
416
475
  case "learn": {
417
476
  const learn = await setupLearnConfig(gitAvailable);
418
477
  config.learn = {
478
+ ...config.learn,
479
+ language: learn.language,
419
480
  outputDir: learn.outputDir,
420
481
  categories: learn.categories,
421
482
  autoCommit: learn.autoCommit,
@@ -1,12 +1,15 @@
1
1
  import {
2
- getInstalledVersion,
3
2
  getLatestVersion,
4
3
  semverCompare
5
- } from "./chunk-GTTSBOHM.js";
4
+ } from "./chunk-DHPWBSF5.js";
6
5
  import {
7
6
  isMarketplaceRegistered
8
7
  } from "./chunk-MRTR7TJ4.js";
9
- import "./chunk-DDISNOEK.js";
8
+ import {
9
+ getInstalledVersion
10
+ } from "./chunk-HSQX3PKW.js";
11
+ import "./chunk-4PLV2ENL.js";
12
+ import "./chunk-WXBI2HUL.js";
10
13
  import {
11
14
  commandExists,
12
15
  run
@@ -2,7 +2,7 @@ import {
2
2
  getLibPath,
3
3
  resolveDocsDir
4
4
  } from "./chunk-WK5YYHXM.js";
5
- import "./chunk-JWAJ4XPK.js";
5
+ import "./chunk-WXBI2HUL.js";
6
6
  import {
7
7
  run
8
8
  } from "./chunk-UFGNO6CW.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ensureShellCompletion
4
- } from "./chunk-DDISNOEK.js";
4
+ } from "./chunk-4PLV2ENL.js";
5
5
  import "./chunk-6DUFTBTO.js";
6
6
 
7
7
  // src/postinstall.ts
@@ -4,10 +4,10 @@ import {
4
4
  saveStatuslineConfig,
5
5
  selectStatuslineComponents,
6
6
  writeStatuslineSettings
7
- } from "./chunk-5OALHHKR.js";
7
+ } from "./chunk-HSQX3PKW.js";
8
8
  import {
9
9
  ALL_COMPONENT_IDS
10
- } from "./chunk-JWAJ4XPK.js";
10
+ } from "./chunk-WXBI2HUL.js";
11
11
  import "./chunk-WHCJT7E2.js";
12
12
  import {
13
13
  log
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  hasShellCompletion,
7
7
  removeShellCompletion
8
- } from "./chunk-DDISNOEK.js";
8
+ } from "./chunk-4PLV2ENL.js";
9
9
  import {
10
10
  commandExists,
11
11
  run
@@ -1,16 +1,16 @@
1
1
  import {
2
- getInstalledVersion,
3
2
  getLatestVersion,
4
3
  semverCompare,
5
4
  updateCommand
6
- } from "./chunk-GTTSBOHM.js";
7
- import "./chunk-DDISNOEK.js";
5
+ } from "./chunk-DHPWBSF5.js";
6
+ import "./chunk-HSQX3PKW.js";
7
+ import "./chunk-4PLV2ENL.js";
8
+ import "./chunk-WXBI2HUL.js";
8
9
  import "./chunk-UFGNO6CW.js";
9
10
  import "./chunk-WHCJT7E2.js";
10
11
  import "./chunk-6DUFTBTO.js";
11
12
  import "./chunk-IUTXHCP7.js";
12
13
  export {
13
- getInstalledVersion,
14
14
  getLatestVersion,
15
15
  semverCompare,
16
16
  updateCommand
@@ -1,5 +1,11 @@
1
1
  # Changelog (Learn Host)
2
2
 
3
+ ## v0.2.1 (2026-03-05)
4
+
5
+ - Add package manager tabs (npm, yarn, pnpm) to website ([#72e9e05](https://github.com/dinhanhthi/coding-friend/commit/72e9e05))
6
+ - Fix TOC heading text stripping markdown links from slug generation ([#9a8fb5c](https://github.com/dinhanhthi/coding-friend/commit/9a8fb5c))
7
+ - Decorate inline codes for TOC ([#573d7b0](https://github.com/dinhanhthi/coding-friend/commit/573d7b0))
8
+
3
9
  ## v0.2.0 (2026-03-03)
4
10
 
5
11
  - Add dedicated tag pages for filtering docs by tag ([#06f5847](https://github.com/dinhanhthi/coding-friend/commit/06f5847))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-learn-host",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev -p 3333",
@@ -37,7 +37,9 @@ export default async function DocPage({
37
37
  />
38
38
 
39
39
  <header className="mb-8">
40
- <h1 className="mb-2 text-3xl font-bold">{doc.frontmatter.title}</h1>
40
+ <h1 className="text-accent mb-2 text-3xl font-bold">
41
+ {doc.frontmatter.title}
42
+ </h1>
41
43
  {(doc.frontmatter.created || doc.frontmatter.updated) && (
42
44
  <div className="flex flex-wrap items-center gap-3 text-sm text-slate-500 dark:text-slate-400">
43
45
  {doc.frontmatter.created && (
@@ -119,3 +119,23 @@ pre {
119
119
  padding: 1px 5px;
120
120
  border-radius: 4px;
121
121
  }
122
+
123
+ /* Tighter prose list spacing */
124
+ .prose
125
+ :where(ul, ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
126
+ margin-top: 0.5em;
127
+ margin-bottom: 0.5em;
128
+ }
129
+
130
+ .prose :where(li):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
131
+ margin-top: 0.25em;
132
+ margin-bottom: 0.25em;
133
+ }
134
+
135
+ .prose
136
+ :where(li > p, li > ul, li > ol, li > pre, li > blockquote, li > div):not(
137
+ :where([class~="not-prose"], [class~="not-prose"] *)
138
+ ) {
139
+ margin-top: 0.35em;
140
+ margin-bottom: 0.35em;
141
+ }
@@ -3,6 +3,20 @@
3
3
  import { useEffect, useState } from "react";
4
4
  import type { TocItem } from "@/lib/types";
5
5
 
6
+ function renderText(text: string) {
7
+ const stripped = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
8
+ const parts = stripped.split(/(`[^`]+`)/g);
9
+ return parts.map((part, i) =>
10
+ part.startsWith("`") && part.endsWith("`") ? (
11
+ <code key={i} className="rounded bg-slate-700/60 px-1 py-0.5 text-xs">
12
+ {part.slice(1, -1)}
13
+ </code>
14
+ ) : (
15
+ part
16
+ ),
17
+ );
18
+ }
19
+
6
20
  interface Props {
7
21
  headings: TocItem[];
8
22
  }
@@ -51,7 +65,7 @@ export default function TableOfContents({ headings }: Props) {
51
65
  : "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
52
66
  }`}
53
67
  >
54
- {h.text}
68
+ {renderText(h.text)}
55
69
  </a>
56
70
  </li>
57
71
  ))}
@@ -189,7 +189,8 @@ export function extractHeadings(content: string): TocItem[] {
189
189
  const regex = /^(#{2,3})\s+(.+)$/gm;
190
190
  let match;
191
191
  while ((match = regex.exec(content)) !== null) {
192
- const text = match[2].trim();
192
+ const raw = match[2].trim();
193
+ const text = raw.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
193
194
  const id = text
194
195
  .toLowerCase()
195
196
  .replace(/[^a-z0-9]+/g, "-")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.5.2",
3
+ "version": "1.7.0",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {