coding-friend-cli 1.15.0 → 1.16.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.
@@ -116,6 +116,13 @@ function getMergedValue(key, globalCfg, localCfg) {
116
116
  if (localVal !== void 0) return localVal;
117
117
  return globalCfg ? globalCfg[key] : void 0;
118
118
  }
119
+ function ensureDocsFolders(docsDir, subfolders) {
120
+ if (!existsSync(docsDir)) {
121
+ run("mkdir", ["-p", docsDir]);
122
+ log.success(`Created "${docsDir}"`);
123
+ }
124
+ createSubfolders(docsDir, subfolders);
125
+ }
119
126
  function createSubfolders(docsDir, subfolders) {
120
127
  const created = [];
121
128
  for (const sub of subfolders) {
@@ -177,5 +184,6 @@ export {
177
184
  getScopeLabel,
178
185
  formatScopeLabel,
179
186
  getMergedValue,
187
+ ensureDocsFolders,
180
188
  applyDocsDirChange
181
189
  };
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-KJUGTLPQ.js";
8
8
  import {
9
9
  resolveScope
10
- } from "./chunk-PYRGNY5P.js";
10
+ } from "./chunk-D4EWPGBL.js";
11
11
  import {
12
12
  commandExists,
13
13
  run,
@@ -23,7 +23,7 @@ import {
23
23
  getScopeLabel,
24
24
  injectBackChoice,
25
25
  showConfigHint
26
- } from "./chunk-PYRGNY5P.js";
26
+ } from "./chunk-D4EWPGBL.js";
27
27
  import {
28
28
  run
29
29
  } from "./chunk-X5WEODUD.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-YC6MBHCT.js";
5
5
  import {
6
6
  resolveScope
7
- } from "./chunk-PYRGNY5P.js";
7
+ } from "./chunk-D4EWPGBL.js";
8
8
  import "./chunk-X5WEODUD.js";
9
9
  import "./chunk-RWUTFVRB.js";
10
10
  import {
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-YC6MBHCT.js";
5
5
  import {
6
6
  resolveScope
7
- } from "./chunk-PYRGNY5P.js";
7
+ } from "./chunk-D4EWPGBL.js";
8
8
  import "./chunk-X5WEODUD.js";
9
9
  import "./chunk-RWUTFVRB.js";
10
10
  import {
package/dist/index.js CHANGED
@@ -14,27 +14,27 @@ 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").option("--user", "Install at user scope (all projects)").option("--global", "Install at user scope (all projects)").option("--project", "Install at project scope (shared via git)").option("--local", "Install at local scope (this machine only)").action(async (opts) => {
17
- const { installCommand } = await import("./install-EIN7Z5V3.js");
17
+ const { installCommand } = await import("./install-I3GOS56Q.js");
18
18
  await installCommand(opts);
19
19
  });
20
20
  program.command("uninstall").description("Uninstall the Coding Friend plugin from Claude Code").option("--user", "Uninstall from user scope (all projects)").option("--global", "Uninstall from user scope (all projects)").option("--project", "Uninstall from project scope").option("--local", "Uninstall from local scope").action(async (opts) => {
21
- const { uninstallCommand } = await import("./uninstall-2IOZZERP.js");
21
+ const { uninstallCommand } = await import("./uninstall-JN5YIKKM.js");
22
22
  await uninstallCommand(opts);
23
23
  });
24
24
  program.command("disable").description("Disable the Coding Friend plugin without uninstalling").option("--user", "Disable at user scope (all projects)").option("--global", "Disable at user scope (all projects)").option("--project", "Disable at project scope").option("--local", "Disable at local scope").action(async (opts) => {
25
- const { disableCommand } = await import("./disable-AOZ7FLZD.js");
25
+ const { disableCommand } = await import("./disable-JDVOQNZG.js");
26
26
  await disableCommand(opts);
27
27
  });
28
28
  program.command("enable").description("Re-enable the Coding Friend plugin").option("--user", "Enable at user scope (all projects)").option("--global", "Enable at user scope (all projects)").option("--project", "Enable at project scope").option("--local", "Enable at local scope").action(async (opts) => {
29
- const { enableCommand } = await import("./enable-MJVTT3RU.js");
29
+ const { enableCommand } = await import("./enable-JBJ4Q2S7.js");
30
30
  await enableCommand(opts);
31
31
  });
32
32
  program.command("init").description("Initialize coding-friend in current project").action(async () => {
33
- const { initCommand } = await import("./init-AHIEQ27W.js");
33
+ const { initCommand } = await import("./init-FZ3GG53E.js");
34
34
  await initCommand();
35
35
  });
36
36
  program.command("config").description("Manage Coding Friend configuration").action(async () => {
37
- const { configCommand } = await import("./config-UQ742WPQ.js");
37
+ const { configCommand } = await import("./config-AIZJJ5D2.js");
38
38
  await configCommand();
39
39
  });
40
40
  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) => {
@@ -54,7 +54,7 @@ program.command("statusline").description("Setup coding-friend statusline in Cla
54
54
  await statuslineCommand();
55
55
  });
56
56
  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").option("--user", "Update plugin at user scope (all projects)").option("--global", "Update plugin at user scope (all projects)").option("--project", "Update plugin at project scope").option("--local", "Update plugin at local scope").action(async (opts) => {
57
- const { updateCommand } = await import("./update-IZ5UEKZN.js");
57
+ const { updateCommand } = await import("./update-OWS4IJTG.js");
58
58
  await updateCommand(opts);
59
59
  });
60
60
  var session = program.command("session").description("Save and load Claude Code sessions across machines");
@@ -22,12 +22,13 @@ import {
22
22
  BACK,
23
23
  applyDocsDirChange,
24
24
  askScope,
25
+ ensureDocsFolders,
25
26
  formatScopeLabel,
26
27
  getMergedValue,
27
28
  getScopeLabel,
28
29
  injectBackChoice,
29
30
  showConfigHint
30
- } from "./chunk-PYRGNY5P.js";
31
+ } from "./chunk-D4EWPGBL.js";
31
32
  import {
32
33
  commandExists,
33
34
  run
@@ -84,6 +85,75 @@ function isGitRepo() {
84
85
  function escapeRegExp(str) {
85
86
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
86
87
  }
88
+ function hasGitignoreBlock() {
89
+ if (!existsSync(".gitignore")) return false;
90
+ const content = readFileSync(".gitignore", "utf-8");
91
+ return content.includes(GITIGNORE_START) || content.includes("# coding-friend");
92
+ }
93
+ function paddedScopeLabel(scope) {
94
+ const label = formatScopeLabel(scope);
95
+ const visibleLen = scope.length + 2;
96
+ return label + " ".repeat(Math.max(1, 9 - visibleLen));
97
+ }
98
+ function printSetupStatus(globalCfg, localCfg, gitAvailable) {
99
+ const docsDir = getDocsDir(globalCfg, localCfg);
100
+ const subfolders = ["plans", "memory", "research", "learn", "sessions"];
101
+ const folderStatus = subfolders.map((sub) => ({
102
+ name: `${docsDir}/${sub}`,
103
+ exists: existsSync(`${docsDir}/${sub}`)
104
+ }));
105
+ const foldersReady = folderStatus.filter((f) => f.exists).length;
106
+ const missingFolders = subfolders.length - foldersReady;
107
+ const allFoldersDone = missingFolders === 0;
108
+ const countColor = allFoldersDone ? chalk.green : chalk.yellow;
109
+ console.log(
110
+ ` ${chalk.bold("Folders")} ${countColor(`(${foldersReady}/${subfolders.length})`)}:`
111
+ );
112
+ for (const f of folderStatus) {
113
+ const icon = f.exists ? chalk.green("\u2713") : chalk.red("\u2717");
114
+ const name = f.exists ? chalk.dim(f.name) : f.name;
115
+ console.log(` ${icon} ${name}`);
116
+ }
117
+ console.log();
118
+ const configKeys = [
119
+ { key: "docsDir", label: "Docs folder" },
120
+ { key: "language", label: "Docs language" },
121
+ { key: "learn", label: "/cf-learn config" }
122
+ ];
123
+ let notLocalCount = 0;
124
+ let notConfiguredCount = 0;
125
+ console.log(` ${chalk.bold("Settings")}:`);
126
+ for (const s of configKeys) {
127
+ const scope = getScopeLabel(s.key, globalCfg, localCfg);
128
+ const value = getMergedValue(s.key, globalCfg, localCfg);
129
+ const valueStr = value && typeof value === "string" ? ` (${chalk.dim(value)})` : "";
130
+ console.log(` ${paddedScopeLabel(scope)}${s.label}${valueStr}`);
131
+ if (scope === "-") notConfiguredCount++;
132
+ if (scope === "-" || scope === "global") notLocalCount++;
133
+ }
134
+ const setupItems = [
135
+ {
136
+ label: ".gitignore",
137
+ done: !gitAvailable || hasGitignoreBlock(),
138
+ skipped: !gitAvailable
139
+ },
140
+ { label: "Shell completion", done: hasShellCompletion(), skipped: false },
141
+ { label: "Statusline", done: isStatuslineConfigured(), skipped: false }
142
+ ];
143
+ for (const item of setupItems) {
144
+ if (item.skipped) {
145
+ console.log(` ${paddedScopeLabel("skip")}${item.label}`);
146
+ } else if (item.done) {
147
+ console.log(` ${paddedScopeLabel("done")}${item.label}`);
148
+ } else {
149
+ console.log(` ${paddedScopeLabel("-")}${item.label}`);
150
+ notConfiguredCount++;
151
+ }
152
+ }
153
+ console.log();
154
+ const allDone = allFoldersDone && notConfiguredCount === 0;
155
+ return { allDone, notLocalCount, notConfiguredCount, missingFolders };
156
+ }
87
157
  function writeToScope(scope, data) {
88
158
  const targetPath = scope === "global" ? globalConfigPath() : localConfigPath();
89
159
  mergeJson(targetPath, data);
@@ -99,6 +169,27 @@ function handleBack(value) {
99
169
  function getDocsDir(globalCfg, localCfg) {
100
170
  return localCfg?.docsDir ?? globalCfg?.docsDir ?? DEFAULT_CONFIG.docsDir;
101
171
  }
172
+ async function offerGlobalShortcut(globalDisplay) {
173
+ const choice = await select({
174
+ message: "How to configure?",
175
+ choices: injectBackChoice(
176
+ [
177
+ {
178
+ name: `Use global setting (${globalDisplay})`,
179
+ value: "use_global"
180
+ },
181
+ { name: "Configure manually", value: "configure" }
182
+ ],
183
+ "Cancel init"
184
+ )
185
+ });
186
+ handleBack(choice);
187
+ if (choice === "use_global") {
188
+ log.dim("Using global setting.");
189
+ return true;
190
+ }
191
+ return false;
192
+ }
102
193
  async function stepDocsDir(globalCfg, localCfg) {
103
194
  const currentValue = getMergedValue("docsDir", globalCfg, localCfg);
104
195
  const scopeLabel = getScopeLabel("docsDir", globalCfg, localCfg);
@@ -106,6 +197,18 @@ async function stepDocsDir(globalCfg, localCfg) {
106
197
  `Docs folder name ${formatScopeLabel(scopeLabel)}${currentValue ? ` (${chalk.dim(currentValue)})` : ""}`,
107
198
  "Where plans, memory, research, and session docs are stored in your project."
108
199
  );
200
+ const globalValue = globalCfg?.docsDir;
201
+ if (globalValue && await offerGlobalShortcut(globalValue)) {
202
+ const DOCS_SUBFOLDERS2 = [
203
+ "plans",
204
+ "memory",
205
+ "research",
206
+ "learn",
207
+ "sessions"
208
+ ];
209
+ ensureDocsFolders(globalValue, DOCS_SUBFOLDERS2);
210
+ return;
211
+ }
109
212
  const value = await input({
110
213
  message: "Docs folder name:",
111
214
  default: currentValue ?? DEFAULT_CONFIG.docsDir,
@@ -126,11 +229,7 @@ async function stepDocsDir(globalCfg, localCfg) {
126
229
  writeToScope(scope, { docsDir: value });
127
230
  }
128
231
  async function stepGitignore(docsDir) {
129
- const hasBlock = (() => {
130
- if (!existsSync(".gitignore")) return false;
131
- const content = readFileSync(".gitignore", "utf-8");
132
- return content.includes(GITIGNORE_START) || content.includes("# coding-friend");
133
- })();
232
+ const hasBlock = hasGitignoreBlock();
134
233
  if (hasBlock) {
135
234
  printStepHeader(
136
235
  `Configure .gitignore ${chalk.green("[done]")}`,
@@ -206,6 +305,8 @@ async function stepDocsLanguage(globalCfg, localCfg) {
206
305
  `Docs language ${formatScopeLabel(scopeLabel)}${currentValue ? ` (${chalk.dim(currentValue)})` : ""}`,
207
306
  "Skills like /cf-plan, /cf-ask, /cf-remember will write docs in this language."
208
307
  );
308
+ const globalValue = globalCfg?.language;
309
+ if (globalValue && await offerGlobalShortcut(globalValue)) return;
209
310
  const lang = await selectLanguage(
210
311
  "What language should generated docs be written in?"
211
312
  );
@@ -243,6 +344,25 @@ async function stepLearnConfig(globalCfg, localCfg, gitAvailable) {
243
344
  `/cf-learn config ${formatScopeLabel(scopeLabel)}`,
244
345
  "Controls where and how /cf-learn saves your learning notes."
245
346
  );
347
+ const globalLearn = globalCfg?.learn;
348
+ if (globalLearn) {
349
+ const parts = [
350
+ globalLearn.language || "en",
351
+ globalLearn.outputDir || `${docsDir}/learn`
352
+ ];
353
+ if (globalLearn.categories) {
354
+ parts.push(`${globalLearn.categories.length} categories`);
355
+ }
356
+ if (await offerGlobalShortcut(parts.join(", "))) {
357
+ const gOutputDir = globalLearn.outputDir || `${docsDir}/learn`;
358
+ const gIsExternal = !gOutputDir.startsWith(`${docsDir}/`);
359
+ return {
360
+ outputDir: gOutputDir,
361
+ autoCommit: globalLearn.autoCommit || false,
362
+ isExternal: gIsExternal
363
+ };
364
+ }
365
+ }
246
366
  const language = await selectLanguage(
247
367
  "What language should /cf-learn notes be written in?"
248
368
  );
@@ -472,20 +592,52 @@ async function initCommand() {
472
592
  }
473
593
  const globalCfg = readJson(globalConfigPath());
474
594
  const localCfg = readJson(localConfigPath());
475
- console.log("Current configuration:");
476
- const docsDirScope = getScopeLabel("docsDir", globalCfg, localCfg);
477
- const docsDirVal = getMergedValue("docsDir", globalCfg, localCfg);
478
- console.log(
479
- ` ${formatScopeLabel(docsDirScope)} docsDir${docsDirVal ? ` (${chalk.dim(docsDirVal)})` : ""}`
480
- );
481
- const langScope = getScopeLabel("language", globalCfg, localCfg);
482
- const langVal = getMergedValue("language", globalCfg, localCfg);
483
- console.log(
484
- ` ${formatScopeLabel(langScope)} Docs language${langVal ? ` (${chalk.dim(langVal)})` : ""}`
485
- );
486
- const learnScope = getScopeLabel("learn", globalCfg, localCfg);
487
- console.log(` ${formatScopeLabel(learnScope)} /cf-learn config`);
595
+ console.log("Project status:");
488
596
  console.log();
597
+ const { allDone, notLocalCount, notConfiguredCount, missingFolders } = printSetupStatus(globalCfg, localCfg, gitAvailable);
598
+ if (allDone) {
599
+ if (notLocalCount > 0) {
600
+ console.log(
601
+ chalk.dim(
602
+ ` ${notLocalCount} setting(s) inherited from global config only.`
603
+ )
604
+ );
605
+ console.log();
606
+ }
607
+ log.success("All settings configured!");
608
+ console.log();
609
+ const proceed = await confirm({
610
+ message: "Modify settings?",
611
+ default: false
612
+ });
613
+ if (!proceed) {
614
+ log.dim("No changes. Run `cf init` anytime to reconfigure.");
615
+ return;
616
+ }
617
+ } else {
618
+ const parts = [];
619
+ if (missingFolders > 0) {
620
+ parts.push(`${missingFolders} folder(s) missing`);
621
+ }
622
+ if (notConfiguredCount > 0) {
623
+ parts.push(`${notConfiguredCount} setting(s) not configured`);
624
+ }
625
+ if (notLocalCount > 0) {
626
+ parts.push(`${notLocalCount} not set locally`);
627
+ }
628
+ if (parts.length > 0) {
629
+ console.log(` ${chalk.yellow("\u26A0")} ${parts.join(" \xB7 ")}`);
630
+ console.log();
631
+ }
632
+ const proceed = await confirm({
633
+ message: "Run setup wizard?",
634
+ default: true
635
+ });
636
+ if (!proceed) {
637
+ log.dim("Init cancelled. Run `cf init` anytime to resume.");
638
+ return;
639
+ }
640
+ }
489
641
  await stepDocsDir(globalCfg, localCfg);
490
642
  const updatedGlobal = readJson(globalConfigPath());
491
643
  const updatedLocal = readJson(localConfigPath());
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getLatestVersion,
3
3
  semverCompare
4
- } from "./chunk-ITL5TY3B.js";
4
+ } from "./chunk-QNLL3ZDF.js";
5
5
  import {
6
6
  enableMarketplaceAutoUpdate,
7
7
  isMarketplaceRegistered,
@@ -16,7 +16,7 @@ import {
16
16
  } from "./chunk-KJUGTLPQ.js";
17
17
  import {
18
18
  resolveScope
19
- } from "./chunk-PYRGNY5P.js";
19
+ } from "./chunk-D4EWPGBL.js";
20
20
  import {
21
21
  commandExists,
22
22
  run
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-KJUGTLPQ.js";
9
9
  import {
10
10
  resolveScope
11
- } from "./chunk-PYRGNY5P.js";
11
+ } from "./chunk-D4EWPGBL.js";
12
12
  import {
13
13
  commandExists,
14
14
  run
@@ -2,11 +2,11 @@ import {
2
2
  getLatestVersion,
3
3
  semverCompare,
4
4
  updateCommand
5
- } from "./chunk-ITL5TY3B.js";
5
+ } from "./chunk-QNLL3ZDF.js";
6
6
  import "./chunk-ORACWEDN.js";
7
7
  import "./chunk-POC2WHU2.js";
8
8
  import "./chunk-KJUGTLPQ.js";
9
- import "./chunk-PYRGNY5P.js";
9
+ import "./chunk-D4EWPGBL.js";
10
10
  import "./chunk-X5WEODUD.js";
11
11
  import "./chunk-RWUTFVRB.js";
12
12
  import "./chunk-W5CD7WTX.js";
@@ -2,7 +2,6 @@
2
2
 
3
3
  ## v0.2.1 (2026-03-05)
4
4
 
5
- - Add package manager tabs (npm, yarn, pnpm) to website ([#72e9e05](https://github.com/dinhanhthi/coding-friend/commit/72e9e05))
6
5
  - Fix TOC heading text stripping markdown links from slug generation ([#9a8fb5c](https://github.com/dinhanhthi/coding-friend/commit/9a8fb5c))
7
6
  - Decorate inline codes for TOC ([#573d7b0](https://github.com/dinhanhthi/coding-friend/commit/573d7b0))
8
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {