coding-friend-cli 1.8.0 → 1.9.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.
package/README.md CHANGED
@@ -40,6 +40,8 @@ cf dev status # Show current dev mode (local or remote)
40
40
  cf dev sync # Sync local changes to cache (no version bump needed)
41
41
  cf dev restart # Reinstall local dev plugin (off + on)
42
42
  cf dev update # Update local dev plugin to latest version (off + on)
43
+ cf session save # Save current Claude Code session to docs/sessions/
44
+ cf session load # Load a saved session from docs/sessions/
43
45
  cf help # Show all commands
44
46
  ```
45
47
 
@@ -0,0 +1,277 @@
1
+ import {
2
+ log
3
+ } from "./chunk-W5CD7WTX.js";
4
+
5
+ // src/lib/shell-completion.ts
6
+ import {
7
+ appendFileSync,
8
+ existsSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ rmSync,
12
+ writeFileSync
13
+ } from "fs";
14
+ import { homedir } from "os";
15
+ import { basename, join } from "path";
16
+ var MARKER_START = "# >>> coding-friend CLI completion >>>";
17
+ var MARKER_END = "# <<< coding-friend CLI completion <<<";
18
+ var BASH_BLOCK = `
19
+
20
+ ${MARKER_START}
21
+ _cf_completions() {
22
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
23
+ local prev="\${COMP_WORDS[COMP_CWORD-1]}"
24
+ local commands="install uninstall init config host mcp statusline update dev session"
25
+
26
+ # Subcommands for 'dev'
27
+ if [[ "\${COMP_WORDS[1]}" == "dev" && \${COMP_CWORD} -eq 2 ]]; then
28
+ COMPREPLY=($(compgen -W "on off status restart sync update" -- "$cur"))
29
+ return
30
+ fi
31
+
32
+ # Subcommands for 'session'
33
+ if [[ "\${COMP_WORDS[1]}" == "session" && \${COMP_CWORD} -eq 2 ]]; then
34
+ COMPREPLY=($(compgen -W "save load" -- "$cur"))
35
+ return
36
+ fi
37
+
38
+ # Path completion for 'dev on|restart|update'
39
+ if [[ "\${COMP_WORDS[1]}" == "dev" && ("$prev" == "on" || "$prev" == "restart" || "$prev" == "update") ]]; then
40
+ COMPREPLY=($(compgen -d -- "$cur"))
41
+ return
42
+ fi
43
+
44
+ COMPREPLY=($(compgen -W "$commands" -- "$cur"))
45
+ }
46
+ complete -o default -F _cf_completions cf
47
+ ${MARKER_END}
48
+ `;
49
+ var ZSH_FUNCTION_BODY = `_cf() {
50
+ local -a commands
51
+ commands=(
52
+ 'install:Install the Coding Friend plugin into Claude Code'
53
+ 'uninstall:Uninstall the Coding Friend plugin from Claude Code'
54
+ 'init:Initialize coding-friend in current project'
55
+ 'config:Manage Coding Friend configuration'
56
+ 'host:Build and serve learning docs as a static website'
57
+ 'mcp:Setup MCP server for learning docs'
58
+ 'statusline:Setup coding-friend statusline in Claude Code'
59
+ 'update:Update coding-friend plugin and refresh statusline'
60
+ 'dev:Switch between local and remote plugin for development'
61
+ 'session:Save and load Claude Code sessions across machines'
62
+ )
63
+
64
+ if (( CURRENT == 2 )); then
65
+ _describe 'command' commands
66
+ elif (( CURRENT == 3 )) && [[ "\${words[2]}" == "dev" ]]; then
67
+ local -a subcommands
68
+ subcommands=(
69
+ 'on:Switch to local plugin source'
70
+ 'off:Switch back to remote marketplace'
71
+ 'status:Show current dev mode'
72
+ 'restart:Restart dev mode (re-apply local plugin)'
73
+ 'sync:Sync local plugin files without restarting'
74
+ 'update:Update local dev plugin to latest version'
75
+ )
76
+ _describe 'subcommand' subcommands
77
+ elif (( CURRENT == 3 )) && [[ "\${words[2]}" == "session" ]]; then
78
+ local -a subcommands
79
+ subcommands=(
80
+ 'save:Save current session to docs/sessions/'
81
+ 'load:Load a saved session from docs/sessions/'
82
+ )
83
+ _describe 'subcommand' subcommands
84
+ elif (( CURRENT == 4 )) && [[ "\${words[2]}" == "dev" && ("\${words[3]}" == "on" || "\${words[3]}" == "restart" || "\${words[3]}" == "update") ]]; then
85
+ _path_files -/
86
+ fi
87
+ }
88
+ compdef _cf cf`;
89
+ function buildZshBlock(needsCompinit) {
90
+ const compinit = needsCompinit ? "autoload -Uz compinit && compinit\n" : "";
91
+ return `
92
+
93
+ ${MARKER_START}
94
+ ${compinit}${ZSH_FUNCTION_BODY}
95
+ ${MARKER_END}
96
+ `;
97
+ }
98
+ var FISH_CONTENT = `# coding-friend CLI completions
99
+ complete -c cf -f
100
+ complete -c cf -n "__fish_use_subcommand" -a install -d "Install the Coding Friend plugin into Claude Code"
101
+ complete -c cf -n "__fish_use_subcommand" -a uninstall -d "Uninstall the Coding Friend plugin from Claude Code"
102
+ complete -c cf -n "__fish_use_subcommand" -a init -d "Initialize coding-friend in current project"
103
+ complete -c cf -n "__fish_use_subcommand" -a config -d "Manage Coding Friend configuration"
104
+ complete -c cf -n "__fish_use_subcommand" -a host -d "Build and serve learning docs as a static website"
105
+ complete -c cf -n "__fish_use_subcommand" -a mcp -d "Setup MCP server for learning docs"
106
+ complete -c cf -n "__fish_use_subcommand" -a statusline -d "Setup coding-friend statusline in Claude Code"
107
+ complete -c cf -n "__fish_use_subcommand" -a update -d "Update coding-friend plugin and refresh statusline"
108
+ complete -c cf -n "__fish_use_subcommand" -a dev -d "Switch between local and remote plugin for development"
109
+ complete -c cf -n "__fish_use_subcommand" -a session -d "Save and load Claude Code sessions across machines"
110
+ complete -c cf -n "__fish_seen_subcommand_from dev" -a on -d "Switch to local plugin source"
111
+ complete -c cf -n "__fish_seen_subcommand_from dev" -a off -d "Switch back to remote marketplace"
112
+ complete -c cf -n "__fish_seen_subcommand_from dev" -a status -d "Show current dev mode"
113
+ complete -c cf -n "__fish_seen_subcommand_from dev" -a restart -d "Restart dev mode"
114
+ complete -c cf -n "__fish_seen_subcommand_from dev" -a sync -d "Sync local plugin files"
115
+ complete -c cf -n "__fish_seen_subcommand_from dev" -a update -d "Update local dev plugin"
116
+ complete -c cf -n "__fish_seen_subcommand_from session" -a save -d "Save current session to docs/sessions/"
117
+ complete -c cf -n "__fish_seen_subcommand_from session" -a load -d "Load a saved session from docs/sessions/"
118
+ `;
119
+ var POWERSHELL_BLOCK = `
120
+
121
+ ${MARKER_START}
122
+ Register-ArgumentCompleter -Native -CommandName cf -ScriptBlock {
123
+ param($wordToComplete, $commandAst, $cursorPosition)
124
+ $commands = @('install','uninstall','init','config','host','mcp','statusline','update','dev','session')
125
+ $devSubcommands = @('on','off','status','restart','sync','update')
126
+ $sessionSubcommands = @('save','load')
127
+ $words = $commandAst.CommandElements
128
+ if ($words.Count -ge 2 -and $words[1].ToString() -eq 'dev') {
129
+ $devSubcommands | Where-Object { $_ -like "$wordToComplete*" } |
130
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
131
+ } elseif ($words.Count -ge 2 -and $words[1].ToString() -eq 'session') {
132
+ $sessionSubcommands | Where-Object { $_ -like "$wordToComplete*" } |
133
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
134
+ } else {
135
+ $commands | Where-Object { $_ -like "$wordToComplete*" } |
136
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
137
+ }
138
+ }
139
+ ${MARKER_END}
140
+ `;
141
+ function detectShell() {
142
+ if (process.platform === "win32") return "powershell";
143
+ const shell = process.env.SHELL ?? "";
144
+ if (shell.includes("zsh")) return "zsh";
145
+ if (shell.includes("bash")) return "bash";
146
+ if (shell.includes("fish")) return "fish";
147
+ return "unsupported";
148
+ }
149
+ function getRcPath(shell) {
150
+ const home = homedir();
151
+ switch (shell) {
152
+ case "zsh":
153
+ return join(home, ".zshrc");
154
+ case "bash":
155
+ return process.platform === "darwin" ? join(home, ".bash_profile") : join(home, ".bashrc");
156
+ case "fish":
157
+ return join(home, ".config", "fish", "completions", "cf.fish");
158
+ case "powershell":
159
+ return join(
160
+ process.env.USERPROFILE ?? home,
161
+ "Documents",
162
+ "PowerShell",
163
+ "Microsoft.PowerShell_profile.ps1"
164
+ );
165
+ default:
166
+ return null;
167
+ }
168
+ }
169
+ function extractExistingBlock(content) {
170
+ const startIdx = content.indexOf(MARKER_START);
171
+ const endIdx = content.indexOf(MARKER_END);
172
+ if (startIdx === -1 || endIdx === -1) return null;
173
+ return content.slice(startIdx, endIdx + MARKER_END.length);
174
+ }
175
+ function replaceBlock(content, newBlock) {
176
+ const startIdx = content.indexOf(MARKER_START);
177
+ const endIdx = content.indexOf(MARKER_END);
178
+ let sliceStart = startIdx;
179
+ while (sliceStart > 0 && content[sliceStart - 1] === "\n") sliceStart--;
180
+ return content.slice(0, sliceStart) + newBlock + content.slice(endIdx + MARKER_END.length);
181
+ }
182
+ function hasShellCompletion() {
183
+ const shell = detectShell();
184
+ const rcPath = getRcPath(shell);
185
+ if (!rcPath) return false;
186
+ if (shell === "fish") return existsSync(rcPath);
187
+ if (!existsSync(rcPath)) return false;
188
+ return readFileSync(rcPath, "utf-8").includes(MARKER_START);
189
+ }
190
+ function removeShellCompletion() {
191
+ const shell = detectShell();
192
+ const rcPath = getRcPath(shell);
193
+ if (!rcPath) return false;
194
+ if (shell === "fish") {
195
+ if (!existsSync(rcPath)) return false;
196
+ rmSync(rcPath);
197
+ log.success(`Tab completion removed (${basename(rcPath)})`);
198
+ return true;
199
+ }
200
+ if (!existsSync(rcPath)) return false;
201
+ const content = readFileSync(rcPath, "utf-8");
202
+ if (!content.includes(MARKER_START)) return false;
203
+ writeFileSync(rcPath, replaceBlock(content, ""), "utf-8");
204
+ log.success(`Tab completion removed from ~/${basename(rcPath)}`);
205
+ return true;
206
+ }
207
+ function ensureShellCompletion(opts) {
208
+ const shell = detectShell();
209
+ if (shell === "unsupported") {
210
+ if (!opts?.silent)
211
+ log.warn(
212
+ `Shell not supported for tab completion (SHELL=${process.env.SHELL ?? ""}). Skipping.`
213
+ );
214
+ return false;
215
+ }
216
+ const rcPath = getRcPath(shell);
217
+ const rcName = basename(rcPath);
218
+ if (shell === "fish") {
219
+ const dir = join(homedir(), ".config", "fish", "completions");
220
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
221
+ const existing = existsSync(rcPath) ? readFileSync(rcPath, "utf-8") : null;
222
+ if (existing === FISH_CONTENT) {
223
+ if (!opts?.silent)
224
+ log.dim(`Tab completion already up-to-date (${rcName})`);
225
+ return false;
226
+ }
227
+ writeFileSync(rcPath, FISH_CONTENT, "utf-8");
228
+ if (!opts?.silent) {
229
+ log.success(`Tab completion written to ${rcPath}`);
230
+ log.dim("Open a new terminal to activate.");
231
+ }
232
+ return true;
233
+ }
234
+ let newBlock;
235
+ if (shell === "zsh") {
236
+ const existingContent = existsSync(rcPath) ? readFileSync(rcPath, "utf-8") : "";
237
+ const needsCompinit = !existingContent.includes("autoload -Uz compinit");
238
+ newBlock = buildZshBlock(needsCompinit);
239
+ } else if (shell === "powershell") {
240
+ newBlock = POWERSHELL_BLOCK;
241
+ } else {
242
+ newBlock = BASH_BLOCK;
243
+ }
244
+ if (existsSync(rcPath)) {
245
+ const content = readFileSync(rcPath, "utf-8");
246
+ if (content.includes(MARKER_START)) {
247
+ const existing = extractExistingBlock(content);
248
+ if (existing && existing.trim() === newBlock.trim()) {
249
+ if (!opts?.silent)
250
+ log.dim(`Tab completion already up-to-date in ~/${rcName}`);
251
+ return false;
252
+ }
253
+ writeFileSync(rcPath, replaceBlock(content, newBlock), "utf-8");
254
+ if (!opts?.silent) {
255
+ log.success(`Tab completion updated in ~/${rcName}`);
256
+ log.dim(
257
+ `Run \`source ~/${rcName}\` or open a new terminal to activate.`
258
+ );
259
+ }
260
+ return true;
261
+ }
262
+ }
263
+ appendFileSync(rcPath, newBlock);
264
+ if (!opts?.silent) {
265
+ log.success(`Tab completion added to ~/${rcName}`);
266
+ if (shell !== "powershell")
267
+ log.dim(`Run \`source ~/${rcName}\` or open a new terminal to activate.`);
268
+ else log.dim("Open a new PowerShell terminal to activate.");
269
+ }
270
+ return true;
271
+ }
272
+
273
+ export {
274
+ hasShellCompletion,
275
+ removeShellCompletion,
276
+ ensureShellCompletion
277
+ };
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-BPLN4LDL.js";
5
5
  import {
6
6
  ensureShellCompletion
7
- } from "./chunk-FYGACWU6.js";
7
+ } from "./chunk-7N64TDZ6.js";
8
8
  import {
9
9
  commandExists,
10
10
  run,
@@ -43,21 +43,27 @@ function getLatestCliVersion() {
43
43
  return run("npm", ["view", "coding-friend-cli", "version"]);
44
44
  }
45
45
  function getLatestVersion() {
46
- let tag = run("gh", [
46
+ let tag = null;
47
+ tag = run("gh", [
47
48
  "api",
48
- "repos/dinhanhthi/coding-friend/releases/latest",
49
+ "repos/dinhanhthi/coding-friend/releases?per_page=100",
49
50
  "--jq",
50
- ".tag_name"
51
+ '[.[] | select(.tag_name | test("^v[0-9]"))][0].tag_name'
51
52
  ]);
52
53
  if (!tag) {
53
54
  const json = run("curl", [
54
55
  "-s",
55
- "https://api.github.com/repos/dinhanhthi/coding-friend/releases/latest"
56
+ "https://api.github.com/repos/dinhanhthi/coding-friend/releases?per_page=100"
56
57
  ]);
57
58
  if (json) {
58
59
  try {
59
- const data = JSON.parse(json);
60
- tag = data.tag_name;
60
+ const releases = JSON.parse(json);
61
+ if (Array.isArray(releases)) {
62
+ const pluginRelease = releases.find(
63
+ (r) => /^v[0-9]/.test(r.tag_name ?? "")
64
+ );
65
+ if (pluginRelease) tag = pluginRelease.tag_name;
66
+ }
61
67
  } catch {
62
68
  }
63
69
  }
@@ -9,6 +9,19 @@ import {
9
9
  showConfigHint
10
10
  } from "./chunk-QQ5SVZET.js";
11
11
  import {
12
+ findStatuslineHookPath,
13
+ isStatuslineConfigured,
14
+ saveStatuslineConfig,
15
+ selectStatuslineComponents,
16
+ writeStatuslineSettings
17
+ } from "./chunk-BPLN4LDL.js";
18
+ import {
19
+ ensureShellCompletion,
20
+ hasShellCompletion,
21
+ removeShellCompletion
22
+ } from "./chunk-7N64TDZ6.js";
23
+ import {
24
+ ALL_COMPONENT_IDS,
12
25
  DEFAULT_CONFIG
13
26
  } from "./chunk-PGLUEN7D.js";
14
27
  import {
@@ -26,9 +39,9 @@ import {
26
39
  } from "./chunk-W5CD7WTX.js";
27
40
 
28
41
  // src/commands/config.ts
29
- import { confirm, input, select } from "@inquirer/prompts";
42
+ import { checkbox, confirm, input, select } from "@inquirer/prompts";
30
43
  import chalk from "chalk";
31
- import { existsSync } from "fs";
44
+ import { existsSync, readFileSync, writeFileSync } from "fs";
32
45
  function getLearnFieldScope(field, globalCfg, localCfg) {
33
46
  const inGlobal = globalCfg?.learn ? globalCfg.learn[field] !== void 0 : false;
34
47
  const inLocal = localCfg?.learn ? localCfg.learn[field] !== void 0 : false;
@@ -341,6 +354,128 @@ async function learnSubMenu() {
341
354
  }
342
355
  }
343
356
  }
357
+ async function editStatusline() {
358
+ const hookResult = findStatuslineHookPath();
359
+ if (!hookResult) {
360
+ log.error(
361
+ "coding-friend plugin not found in cache. Install it first via Claude Code."
362
+ );
363
+ return;
364
+ }
365
+ log.info(`Found plugin ${chalk.green(`v${hookResult.version}`)}`);
366
+ if (isStatuslineConfigured()) {
367
+ log.dim("Statusline already configured.");
368
+ const overwrite = await confirm({
369
+ message: "Reconfigure statusline?",
370
+ default: true
371
+ });
372
+ if (!overwrite) return;
373
+ }
374
+ const components = await selectStatuslineComponents();
375
+ saveStatuslineConfig(components);
376
+ writeStatuslineSettings(hookResult.hookPath);
377
+ log.success("Statusline configured!");
378
+ if (components.length < ALL_COMPONENT_IDS.length) {
379
+ log.dim(`Showing: ${components.join(", ")}`);
380
+ } else {
381
+ log.dim("Showing all components.");
382
+ }
383
+ log.dim("Restart Claude Code (or start a new session) to see it.");
384
+ }
385
+ var GITIGNORE_START = "# >>> coding-friend managed";
386
+ var GITIGNORE_END = "# <<< coding-friend managed";
387
+ function escapeRegExp(str) {
388
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
389
+ }
390
+ async function editGitignore(globalCfg, localCfg) {
391
+ const docsDir = localCfg?.docsDir ?? globalCfg?.docsDir ?? DEFAULT_CONFIG.docsDir;
392
+ const allEntries = [
393
+ `${docsDir}/plans/`,
394
+ `${docsDir}/memory/`,
395
+ `${docsDir}/research/`,
396
+ `${docsDir}/learn/`,
397
+ `${docsDir}/sessions/`,
398
+ ".coding-friend/"
399
+ ];
400
+ const existing = existsSync(".gitignore") ? readFileSync(".gitignore", "utf-8") : "";
401
+ const hasBlock = existing.includes(GITIGNORE_START) || existing.includes("# coding-friend");
402
+ if (hasBlock) {
403
+ log.dim(".gitignore already has a coding-friend block.");
404
+ }
405
+ const choice = await select({
406
+ message: "Add coding-friend artifacts to .gitignore?",
407
+ choices: injectBackChoice(
408
+ [
409
+ { name: "Yes, ignore all", value: "all" },
410
+ { name: "Partial \u2014 pick which to ignore", value: "partial" },
411
+ { name: "No \u2014 keep everything tracked", value: "none" }
412
+ ],
413
+ "Back"
414
+ )
415
+ });
416
+ if (choice === BACK) return;
417
+ if (choice === "none") {
418
+ log.dim("Skipped .gitignore config.");
419
+ return;
420
+ }
421
+ let entries = allEntries;
422
+ if (choice === "partial") {
423
+ entries = await checkbox({
424
+ message: "Which folders to ignore?",
425
+ choices: allEntries.map((e) => ({ name: e, value: e }))
426
+ });
427
+ if (entries.length === 0) {
428
+ log.dim("Nothing selected.");
429
+ return;
430
+ }
431
+ }
432
+ const block = `${GITIGNORE_START}
433
+ ${entries.join("\n")}
434
+ ${GITIGNORE_END}`;
435
+ const managedBlockRe = new RegExp(
436
+ `${escapeRegExp(GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(GITIGNORE_END)}`
437
+ );
438
+ const legacyBlockRe = /# coding-friend\n([\w/.]+\n)*/;
439
+ let updated;
440
+ if (managedBlockRe.test(existing)) {
441
+ updated = existing.replace(managedBlockRe, block);
442
+ log.success(`Updated .gitignore: ${entries.join(", ")}`);
443
+ } else if (legacyBlockRe.test(existing)) {
444
+ updated = existing.replace(legacyBlockRe, block);
445
+ log.success(`Migrated .gitignore block: ${entries.join(", ")}`);
446
+ } else {
447
+ updated = existing.trimEnd() + "\n\n" + block + "\n";
448
+ log.success(`Added to .gitignore: ${entries.join(", ")}`);
449
+ }
450
+ writeFileSync(".gitignore", updated);
451
+ }
452
+ async function editShellCompletion() {
453
+ const installed = hasShellCompletion();
454
+ if (installed) {
455
+ const choice = await select({
456
+ message: "Shell tab completion is already installed.",
457
+ choices: injectBackChoice(
458
+ [
459
+ { name: "Update to latest", value: "update" },
460
+ { name: "Remove", value: "remove" }
461
+ ],
462
+ "Back"
463
+ )
464
+ });
465
+ if (choice === BACK) return;
466
+ if (choice === "remove") {
467
+ if (removeShellCompletion()) {
468
+ log.success("Shell completion removed.");
469
+ } else {
470
+ log.warn("Could not remove shell completion.");
471
+ }
472
+ return;
473
+ }
474
+ ensureShellCompletion({ silent: false });
475
+ } else {
476
+ ensureShellCompletion({ silent: false });
477
+ }
478
+ }
344
479
  var em = chalk.hex("#10b981");
345
480
  async function configCommand() {
346
481
  console.log();
@@ -361,6 +496,8 @@ async function configCommand() {
361
496
  const langScope = getScopeLabel("language", globalCfg, localCfg);
362
497
  const langVal = getMergedValue("language", globalCfg, localCfg);
363
498
  const learnScope = getScopeLabel("learn", globalCfg, localCfg);
499
+ const statuslineStatus = isStatuslineConfigured() ? chalk.green("configured") : chalk.yellow("not configured");
500
+ const completionStatus = hasShellCompletion() ? chalk.green("installed") : chalk.yellow("not installed");
364
501
  const choice = await select({
365
502
  message: "What to configure?",
366
503
  choices: injectBackChoice(
@@ -379,6 +516,21 @@ async function configCommand() {
379
516
  name: `Learn settings ${formatScopeLabel(learnScope)}`,
380
517
  value: "learn",
381
518
  description: " Output dir, language, categories, auto-commit, README index"
519
+ },
520
+ {
521
+ name: `Statusline (${statuslineStatus})`,
522
+ value: "statusline",
523
+ description: " Choose which components to show in the Claude Code statusline"
524
+ },
525
+ {
526
+ name: `.gitignore`,
527
+ value: "gitignore",
528
+ description: " Add or update coding-friend artifacts in .gitignore"
529
+ },
530
+ {
531
+ name: `Shell completion (${completionStatus})`,
532
+ value: "completion",
533
+ description: " Install, update, or remove tab completion for the cf command"
382
534
  }
383
535
  ],
384
536
  "Exit"
@@ -397,6 +549,15 @@ async function configCommand() {
397
549
  case "learn":
398
550
  await learnSubMenu();
399
551
  break;
552
+ case "statusline":
553
+ await editStatusline();
554
+ break;
555
+ case "gitignore":
556
+ await editGitignore(globalCfg, localCfg);
557
+ break;
558
+ case "completion":
559
+ await editShellCompletion();
560
+ break;
400
561
  }
401
562
  console.log();
402
563
  }
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-BPLN4LDL.js";
8
8
  import {
9
9
  ensureShellCompletion
10
- } from "./chunk-FYGACWU6.js";
10
+ } from "./chunk-7N64TDZ6.js";
11
11
  import "./chunk-PGLUEN7D.js";
12
12
  import {
13
13
  commandExists,
package/dist/index.js CHANGED
@@ -14,19 +14,19 @@ 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-7MSZ7B5O.js");
17
+ const { installCommand } = await import("./install-D4NW3OAA.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-HDLTWPXG.js");
21
+ const { uninstallCommand } = await import("./uninstall-SOHU5WGK.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-JJATBCHC.js");
25
+ const { initCommand } = await import("./init-CIEDOFNC.js");
26
26
  await initCommand();
27
27
  });
28
28
  program.command("config").description("Manage Coding Friend configuration").action(async () => {
29
- const { configCommand } = await import("./config-JZEFZIPY.js");
29
+ const { configCommand } = await import("./config-VAML7F7K.js");
30
30
  await configCommand();
31
31
  });
32
32
  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) => {
@@ -42,7 +42,7 @@ program.command("statusline").description("Setup coding-friend statusline in Cla
42
42
  await statuslineCommand();
43
43
  });
44
44
  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) => {
45
- const { updateCommand } = await import("./update-E4MQDRFC.js");
45
+ const { updateCommand } = await import("./update-LA4B3LN4.js");
46
46
  await updateCommand(opts);
47
47
  });
48
48
  var session = program.command("session").description("Save and load Claude Code sessions across machines");
@@ -57,11 +57,11 @@ session.command("save").description("Save current Claude Code session to sync fo
57
57
  "-s, --session-id <id>",
58
58
  "session UUID to save (default: auto-detect newest)"
59
59
  ).option("-l, --label <label>", "label for this session").action(async (opts) => {
60
- const { sessionSaveCommand } = await import("./session-3MWYAKKY.js");
60
+ const { sessionSaveCommand } = await import("./session-74F7L5LV.js");
61
61
  await sessionSaveCommand(opts);
62
62
  });
63
63
  session.command("load").description("Load a saved session from sync folder").action(async () => {
64
- const { sessionLoadCommand } = await import("./session-3MWYAKKY.js");
64
+ const { sessionLoadCommand } = await import("./session-74F7L5LV.js");
65
65
  await sessionLoadCommand();
66
66
  });
67
67
  var dev = program.command("dev").description("Development mode commands");
@@ -77,35 +77,35 @@ Dev subcommands:
77
77
  dev update [path] Update local dev plugin to latest version`
78
78
  );
79
79
  dev.command("on").description("Switch to local plugin source").argument("[path]", "path to local coding-friend repo (default: cwd)").action(async (path) => {
80
- const { devOnCommand } = await import("./dev-U7LPXAHR.js");
80
+ const { devOnCommand } = await import("./dev-2GBY3GKC.js");
81
81
  await devOnCommand(path);
82
82
  });
83
83
  dev.command("off").description("Switch back to remote marketplace").action(async () => {
84
- const { devOffCommand } = await import("./dev-U7LPXAHR.js");
84
+ const { devOffCommand } = await import("./dev-2GBY3GKC.js");
85
85
  await devOffCommand();
86
86
  });
87
87
  dev.command("status").description("Show current dev mode").action(async () => {
88
- const { devStatusCommand } = await import("./dev-U7LPXAHR.js");
88
+ const { devStatusCommand } = await import("./dev-2GBY3GKC.js");
89
89
  await devStatusCommand();
90
90
  });
91
91
  dev.command("sync").description(
92
92
  "Copy local source files to plugin cache (no version bump needed)"
93
93
  ).action(async () => {
94
- const { devSyncCommand } = await import("./dev-U7LPXAHR.js");
94
+ const { devSyncCommand } = await import("./dev-2GBY3GKC.js");
95
95
  await devSyncCommand();
96
96
  });
97
97
  dev.command("restart").description("Reinstall local dev plugin (off + on)").argument(
98
98
  "[path]",
99
99
  "path to local coding-friend repo (default: saved path or cwd)"
100
100
  ).action(async (path) => {
101
- const { devRestartCommand } = await import("./dev-U7LPXAHR.js");
101
+ const { devRestartCommand } = await import("./dev-2GBY3GKC.js");
102
102
  await devRestartCommand(path);
103
103
  });
104
104
  dev.command("update").description("Update local dev plugin to latest version (off + on)").argument(
105
105
  "[path]",
106
106
  "path to local coding-friend repo (default: saved path or cwd)"
107
107
  ).action(async (path) => {
108
- const { devUpdateCommand } = await import("./dev-U7LPXAHR.js");
108
+ const { devUpdateCommand } = await import("./dev-2GBY3GKC.js");
109
109
  await devUpdateCommand(path);
110
110
  });
111
111
  program.parse();
@@ -1,14 +1,3 @@
1
- import {
2
- findStatuslineHookPath,
3
- isStatuslineConfigured,
4
- saveStatuslineConfig,
5
- selectStatuslineComponents,
6
- writeStatuslineSettings
7
- } from "./chunk-BPLN4LDL.js";
8
- import {
9
- ensureShellCompletion,
10
- hasShellCompletion
11
- } from "./chunk-FYGACWU6.js";
12
1
  import {
13
2
  BACK,
14
3
  applyDocsDirChange,
@@ -19,6 +8,17 @@ import {
19
8
  injectBackChoice,
20
9
  showConfigHint
21
10
  } from "./chunk-QQ5SVZET.js";
11
+ import {
12
+ findStatuslineHookPath,
13
+ isStatuslineConfigured,
14
+ saveStatuslineConfig,
15
+ selectStatuslineComponents,
16
+ writeStatuslineSettings
17
+ } from "./chunk-BPLN4LDL.js";
18
+ import {
19
+ ensureShellCompletion,
20
+ hasShellCompletion
21
+ } from "./chunk-7N64TDZ6.js";
22
22
  import {
23
23
  DEFAULT_CONFIG
24
24
  } from "./chunk-PGLUEN7D.js";
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  getLatestVersion,
3
3
  semverCompare
4
- } from "./chunk-JS75SVQA.js";
4
+ } from "./chunk-VYMXERKM.js";
5
5
  import {
6
6
  isMarketplaceRegistered
7
7
  } from "./chunk-HFLBFX6J.js";
8
8
  import {
9
9
  getInstalledVersion
10
10
  } from "./chunk-BPLN4LDL.js";
11
- import "./chunk-FYGACWU6.js";
11
+ import "./chunk-7N64TDZ6.js";
12
12
  import "./chunk-PGLUEN7D.js";
13
13
  import {
14
14
  commandExists,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ensureShellCompletion
4
- } from "./chunk-FYGACWU6.js";
4
+ } from "./chunk-7N64TDZ6.js";
5
5
  import "./chunk-W5CD7WTX.js";
6
6
 
7
7
  // src/postinstall.ts
@@ -5,8 +5,6 @@ import "./chunk-PGLUEN7D.js";
5
5
  import {
6
6
  claudeSessionDir,
7
7
  encodeProjectPath,
8
- globalConfigPath,
9
- mergeJson,
10
8
  readJson,
11
9
  writeJson
12
10
  } from "./chunk-TPRZHSFS.js";
@@ -26,7 +24,8 @@ import {
26
24
  statSync,
27
25
  copyFileSync,
28
26
  existsSync,
29
- readFileSync
27
+ readFileSync,
28
+ mkdirSync
30
29
  } from "fs";
31
30
  import { join } from "path";
32
31
  import { hostname as osHostname } from "os";
@@ -83,6 +82,7 @@ function saveSession(opts) {
83
82
  const destDir = join(syncDir, "sessions", sessionId);
84
83
  const destJsonl = join(destDir, "session.jsonl");
85
84
  const destMeta = join(destDir, "meta.json");
85
+ mkdirSync(destDir, { recursive: true });
86
86
  copyFileSync(jsonlPath, destJsonl);
87
87
  const meta = {
88
88
  sessionId,
@@ -99,6 +99,7 @@ function loadSession(meta, localProjectPath, syncDir) {
99
99
  const destDir = claudeSessionDir(encodedPath);
100
100
  const destPath = join(destDir, `${meta.sessionId}.jsonl`);
101
101
  const srcPath = join(syncDir, "sessions", meta.sessionId, "session.jsonl");
102
+ mkdirSync(destDir, { recursive: true });
102
103
  copyFileSync(srcPath, destPath);
103
104
  }
104
105
  function hostname() {
@@ -110,21 +111,10 @@ function hostname() {
110
111
  }
111
112
 
112
113
  // src/commands/session.ts
113
- async function resolveSyncDir() {
114
+ function resolveDocsDir() {
114
115
  const config = loadConfig();
115
- if (config.sessionSyncDir) return config.sessionSyncDir;
116
- log.warn("No session sync folder configured.");
117
- const syncDir = await input({
118
- message: "Enter path to your sync folder (e.g. ~/Dropbox/cf-sessions or a git repo path):",
119
- validate: (v) => v.trim().length > 0 || "Path cannot be empty"
120
- });
121
- const resolved = syncDir.startsWith("~/") ? join2(homedir2(), syncDir.slice(2)) : syncDir;
122
- mergeJson(globalConfigPath(), { sessionSyncDir: resolved });
123
- log.success(`Sync folder saved to global config: ${resolved}`);
124
- log.warn(
125
- "Session files contain your full conversation history. Make sure this folder is private."
126
- );
127
- return resolved;
116
+ const docsDir = config.docsDir ?? "docs";
117
+ return join2(process.cwd(), docsDir);
128
118
  }
129
119
  function formatSessionChoice(meta) {
130
120
  const date = new Date(meta.savedAt).toLocaleString();
@@ -132,7 +122,7 @@ function formatSessionChoice(meta) {
132
122
  return `[${meta.label}] ${date} @${meta.machine} \u2014 ${preview}`;
133
123
  }
134
124
  async function sessionSaveCommand(opts = {}) {
135
- const syncDir = await resolveSyncDir();
125
+ const docsDir = resolveDocsDir();
136
126
  const cwd = process.cwd();
137
127
  let jsonlPath = null;
138
128
  if (opts.sessionId) {
@@ -186,18 +176,18 @@ async function sessionSaveCommand(opts = {}) {
186
176
  sessionId,
187
177
  label,
188
178
  projectPath: cwd,
189
- syncDir,
179
+ syncDir: docsDir,
190
180
  previewText
191
181
  });
192
182
  log.success(`Session saved: "${label}"`);
193
- log.dim(` \u2192 ${join2(syncDir, "sessions", sessionId)}`);
183
+ log.dim(` \u2192 ${join2(docsDir, "sessions", sessionId)}`);
194
184
  }
195
185
  async function sessionLoadCommand() {
196
- const syncDir = await resolveSyncDir();
197
- const sessions = listSyncedSessions(syncDir);
186
+ const docsDir = resolveDocsDir();
187
+ const sessions = listSyncedSessions(docsDir);
198
188
  if (sessions.length === 0) {
199
- log.warn("No saved sessions found in sync folder.");
200
- log.dim(` Sync folder: ${syncDir}`);
189
+ log.warn("No saved sessions found.");
190
+ log.dim(` Sessions dir: ${join2(docsDir, "sessions")}`);
201
191
  log.dim(" Run /cf-session inside a Claude Code conversation to save one.");
202
192
  return;
203
193
  }
@@ -222,7 +212,7 @@ Remapped to: ${remapped}`
222
212
  });
223
213
  localProjectPath = confirmed.trim() || remapped;
224
214
  }
225
- loadSession(chosen, localProjectPath, syncDir);
215
+ loadSession(chosen, localProjectPath, docsDir);
226
216
  log.success(`Session "${chosen.label}" loaded.`);
227
217
  log.info(`To resume, run:`);
228
218
  console.log(`
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  hasShellCompletion,
7
7
  removeShellCompletion
8
- } from "./chunk-FYGACWU6.js";
8
+ } from "./chunk-7N64TDZ6.js";
9
9
  import {
10
10
  commandExists,
11
11
  run
@@ -2,9 +2,9 @@ import {
2
2
  getLatestVersion,
3
3
  semverCompare,
4
4
  updateCommand
5
- } from "./chunk-JS75SVQA.js";
5
+ } from "./chunk-VYMXERKM.js";
6
6
  import "./chunk-BPLN4LDL.js";
7
- import "./chunk-FYGACWU6.js";
7
+ import "./chunk-7N64TDZ6.js";
8
8
  import "./chunk-PGLUEN7D.js";
9
9
  import "./chunk-UFGNO6CW.js";
10
10
  import "./chunk-TPRZHSFS.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,145 +0,0 @@
1
- import {
2
- log
3
- } from "./chunk-W5CD7WTX.js";
4
-
5
- // src/lib/shell-completion.ts
6
- import { appendFileSync, existsSync, readFileSync, writeFileSync } from "fs";
7
- import { homedir } from "os";
8
- var MARKER_START = "# >>> coding-friend CLI completion >>>";
9
- var MARKER_END = "# <<< coding-friend CLI completion <<<";
10
- var BASH_BLOCK = `
11
-
12
- ${MARKER_START}
13
- _cf_completions() {
14
- local cur="\${COMP_WORDS[COMP_CWORD]}"
15
- local prev="\${COMP_WORDS[COMP_CWORD-1]}"
16
- local commands="install uninstall init config host mcp statusline update dev"
17
-
18
- # Subcommands for 'dev'
19
- if [[ "\${COMP_WORDS[1]}" == "dev" && \${COMP_CWORD} -eq 2 ]]; then
20
- COMPREPLY=($(compgen -W "on off status restart sync update" -- "$cur"))
21
- return
22
- fi
23
-
24
- # Path completion for 'dev on|restart|update'
25
- if [[ "\${COMP_WORDS[1]}" == "dev" && ("$prev" == "on" || "$prev" == "restart" || "$prev" == "update") ]]; then
26
- COMPREPLY=($(compgen -d -- "$cur"))
27
- return
28
- fi
29
-
30
- COMPREPLY=($(compgen -W "$commands" -- "$cur"))
31
- }
32
- complete -o default -F _cf_completions cf
33
- ${MARKER_END}
34
- `;
35
- var ZSH_BLOCK = `
36
-
37
- ${MARKER_START}
38
- _cf() {
39
- local -a commands
40
- commands=(
41
- 'install:Install the Coding Friend plugin into Claude Code'
42
- 'uninstall:Uninstall the Coding Friend plugin from Claude Code'
43
- 'init:Initialize coding-friend in current project'
44
- 'config:Manage Coding Friend configuration'
45
- 'host:Build and serve learning docs as a static website'
46
- 'mcp:Setup MCP server for learning docs'
47
- 'statusline:Setup coding-friend statusline in Claude Code'
48
- 'update:Update coding-friend plugin and refresh statusline'
49
- 'dev:Switch between local and remote plugin for development'
50
- )
51
-
52
- if (( CURRENT == 2 )); then
53
- _describe 'command' commands
54
- elif (( CURRENT == 3 )) && [[ "\${words[2]}" == "dev" ]]; then
55
- local -a subcommands
56
- subcommands=(
57
- 'on:Switch to local plugin source'
58
- 'off:Switch back to remote marketplace'
59
- 'status:Show current dev mode'
60
- 'restart:Restart dev mode (re-apply local plugin)'
61
- 'sync:Sync local plugin files without restarting'
62
- 'update:Update local dev plugin to latest version'
63
- )
64
- _describe 'subcommand' subcommands
65
- elif (( CURRENT == 4 )) && [[ "\${words[2]}" == "dev" && ("\${words[3]}" == "on" || "\${words[3]}" == "restart" || "\${words[3]}" == "update") ]]; then
66
- _path_files -/
67
- fi
68
- }
69
- compdef _cf cf
70
- ${MARKER_END}
71
- `;
72
- function getShellRcPath() {
73
- const shell = process.env.SHELL ?? "";
74
- if (shell.includes("zsh")) return `${homedir()}/.zshrc`;
75
- return `${homedir()}/.bashrc`;
76
- }
77
- function getRcName(rcPath) {
78
- return rcPath.endsWith(".zshrc") ? ".zshrc" : ".bashrc";
79
- }
80
- function isZsh(rcPath) {
81
- return rcPath.endsWith(".zshrc");
82
- }
83
- function hasShellCompletion() {
84
- const rcPath = getShellRcPath();
85
- if (!existsSync(rcPath)) return false;
86
- return readFileSync(rcPath, "utf-8").includes(MARKER_START);
87
- }
88
- function extractExistingBlock(content) {
89
- const startIdx = content.indexOf(MARKER_START);
90
- const endIdx = content.indexOf(MARKER_END);
91
- if (startIdx === -1 || endIdx === -1) return null;
92
- return content.slice(startIdx, endIdx + MARKER_END.length);
93
- }
94
- function replaceBlock(content, newBlock) {
95
- const startIdx = content.indexOf(MARKER_START);
96
- const endIdx = content.indexOf(MARKER_END);
97
- let sliceStart = startIdx;
98
- while (sliceStart > 0 && content[sliceStart - 1] === "\n") sliceStart--;
99
- return content.slice(0, sliceStart) + newBlock + content.slice(endIdx + MARKER_END.length);
100
- }
101
- function removeShellCompletion() {
102
- const rcPath = getShellRcPath();
103
- if (!existsSync(rcPath)) return false;
104
- const content = readFileSync(rcPath, "utf-8");
105
- if (!content.includes(MARKER_START)) return false;
106
- const updated = replaceBlock(content, "");
107
- writeFileSync(rcPath, updated, "utf-8");
108
- const rcName = getRcName(rcPath);
109
- log.success(`Tab completion removed from ~/${rcName}`);
110
- return true;
111
- }
112
- function ensureShellCompletion(opts) {
113
- const rcPath = getShellRcPath();
114
- const rcName = getRcName(rcPath);
115
- const newBlock = isZsh(rcPath) ? ZSH_BLOCK : BASH_BLOCK;
116
- if (hasShellCompletion()) {
117
- const content = readFileSync(rcPath, "utf-8");
118
- const existing = extractExistingBlock(content);
119
- const expectedBlock = newBlock.trim();
120
- if (existing && existing.trim() === expectedBlock) {
121
- if (!opts?.silent)
122
- log.dim(`Tab completion already up-to-date in ~/${rcName}`);
123
- return false;
124
- }
125
- const updated = replaceBlock(content, newBlock);
126
- writeFileSync(rcPath, updated, "utf-8");
127
- if (!opts?.silent) {
128
- log.success(`Tab completion updated in ~/${rcName}`);
129
- log.dim(`Run \`source ~/${rcName}\` or open a new terminal to activate.`);
130
- }
131
- return true;
132
- }
133
- appendFileSync(rcPath, newBlock);
134
- if (!opts?.silent) {
135
- log.success(`Tab completion added to ~/${rcName}`);
136
- log.dim(`Run \`source ~/${rcName}\` or open a new terminal to activate.`);
137
- }
138
- return true;
139
- }
140
-
141
- export {
142
- hasShellCompletion,
143
- removeShellCompletion,
144
- ensureShellCompletion
145
- };