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 +2 -0
- package/dist/chunk-7N64TDZ6.js +277 -0
- package/dist/{chunk-JS75SVQA.js → chunk-VYMXERKM.js} +13 -7
- package/dist/{config-JZEFZIPY.js → config-VAML7F7K.js} +163 -2
- package/dist/{dev-U7LPXAHR.js → dev-2GBY3GKC.js} +1 -1
- package/dist/index.js +13 -13
- package/dist/{init-JJATBCHC.js → init-CIEDOFNC.js} +11 -11
- package/dist/{install-7MSZ7B5O.js → install-D4NW3OAA.js} +2 -2
- package/dist/postinstall.js +1 -1
- package/dist/{session-3MWYAKKY.js → session-74F7L5LV.js} +15 -25
- package/dist/{uninstall-HDLTWPXG.js → uninstall-SOHU5WGK.js} +1 -1
- package/dist/{update-E4MQDRFC.js → update-LA4B3LN4.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-FYGACWU6.js +0 -145
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-
|
|
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 =
|
|
46
|
+
let tag = null;
|
|
47
|
+
tag = run("gh", [
|
|
47
48
|
"api",
|
|
48
|
-
"repos/dinhanhthi/coding-friend/releases
|
|
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
|
|
56
|
+
"https://api.github.com/repos/dinhanhthi/coding-friend/releases?per_page=100"
|
|
56
57
|
]);
|
|
57
58
|
if (json) {
|
|
58
59
|
try {
|
|
59
|
-
const
|
|
60
|
-
|
|
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
|
}
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
11
|
+
import "./chunk-7N64TDZ6.js";
|
|
12
12
|
import "./chunk-PGLUEN7D.js";
|
|
13
13
|
import {
|
|
14
14
|
commandExists,
|
package/dist/postinstall.js
CHANGED
|
@@ -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
|
-
|
|
114
|
+
function resolveDocsDir() {
|
|
114
115
|
const config = loadConfig();
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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(
|
|
183
|
+
log.dim(` \u2192 ${join2(docsDir, "sessions", sessionId)}`);
|
|
194
184
|
}
|
|
195
185
|
async function sessionLoadCommand() {
|
|
196
|
-
const
|
|
197
|
-
const sessions = listSyncedSessions(
|
|
186
|
+
const docsDir = resolveDocsDir();
|
|
187
|
+
const sessions = listSyncedSessions(docsDir);
|
|
198
188
|
if (sessions.length === 0) {
|
|
199
|
-
log.warn("No saved sessions found
|
|
200
|
-
log.dim(`
|
|
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,
|
|
215
|
+
loadSession(chosen, localProjectPath, docsDir);
|
|
226
216
|
log.success(`Session "${chosen.label}" loaded.`);
|
|
227
217
|
log.info(`To resume, run:`);
|
|
228
218
|
console.log(`
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
getLatestVersion,
|
|
3
3
|
semverCompare,
|
|
4
4
|
updateCommand
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-VYMXERKM.js";
|
|
6
6
|
import "./chunk-BPLN4LDL.js";
|
|
7
|
-
import "./chunk-
|
|
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
package/dist/chunk-FYGACWU6.js
DELETED
|
@@ -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
|
-
};
|