coding-friend-cli 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/{chunk-CSF4FAHL.js → chunk-DDISNOEK.js} +15 -1
- package/dist/{update-GGCBM7U4.js → chunk-F32SU5YM.js} +6 -2
- package/dist/{chunk-Q4DKU5IG.js → chunk-LVI3I6EN.js} +1 -1
- package/dist/{chunk-6OI37OZX.js → chunk-T5KHF3RW.js} +19 -1
- package/dist/chunk-ZA4GHAJL.js +27 -0
- package/dist/{dev-VMN2JHA6.js → dev-WMKGIPD3.js} +5 -13
- package/dist/{host-2WINWEW7.js → host-D3BR75OU.js} +2 -2
- package/dist/index.js +18 -10
- package/dist/{init-CTCDQKIQ.js → init-KX6M4XTA.js} +5 -5
- package/dist/install-3H2FZSPZ.js +89 -0
- package/dist/{mcp-43HCE2KD.js → mcp-SWXTVL3Q.js} +2 -2
- package/dist/postinstall.js +1 -1
- package/dist/{statusline-ARI7I5YM.js → statusline-7M4JUD7O.js} +1 -1
- package/dist/uninstall-ULTJA57D.js +191 -0
- package/dist/update-5C5QFLIL.js +17 -0
- package/lib/learn-host/CHANGELOG.md +6 -0
- package/lib/learn-host/package.json +1 -1
- package/lib/learn-host/src/app/globals.css +34 -4
- package/lib/learn-host/src/app/layout.tsx +2 -13
- package/lib/learn-host/src/app/page.tsx +45 -15
- package/lib/learn-host/src/components/Breadcrumbs.tsx +2 -5
- package/lib/learn-host/src/components/CodeBlock.tsx +56 -0
- package/lib/learn-host/src/components/DocCard.tsx +4 -6
- package/lib/learn-host/src/components/LayoutShell.tsx +39 -0
- package/lib/learn-host/src/components/MarkdownRenderer.tsx +2 -0
- package/lib/learn-host/src/components/MobileNav.tsx +1 -1
- package/lib/learn-host/src/components/Sidebar.tsx +1 -1
- package/lib/learn-host/src/components/TableOfContents.tsx +2 -2
- package/lib/learn-host/src/components/TagBadge.tsx +1 -1
- package/lib/learn-host/src/components/layout/Footer.tsx +8 -4
- package/lib/learn-host/src/components/layout/Header.tsx +2 -2
- package/lib/learn-host/src/lib/docs.ts +11 -2
- package/lib/learn-mcp/CHANGELOG.md +4 -0
- package/lib/learn-mcp/README.md +18 -0
- package/lib/learn-mcp/package.json +1 -1
- package/lib/learn-mcp/src/index.ts +1 -1
- package/package.json +1 -1
- package/lib/learn-host/next-env.d.ts +0 -6
package/README.md
CHANGED
|
@@ -16,6 +16,10 @@ npm i -g coding-friend-cli
|
|
|
16
16
|
## Commands
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
+
cf install # Install the Coding Friend plugin into Claude Code
|
|
20
|
+
# 💡 Safe to run multiple times (idempotent).
|
|
21
|
+
cf uninstall # Completely remove plugin, marketplace, statusline, completion
|
|
22
|
+
# 💡 Interactive — asks for confirmation before acting.
|
|
19
23
|
cf init # Initialize workspace (interactive)
|
|
20
24
|
# 💡 You can run this anywhere, anytime.
|
|
21
25
|
cf host [path] # Build and serve learning docs at localhost:3333
|
|
@@ -13,7 +13,7 @@ ${MARKER_START}
|
|
|
13
13
|
_cf_completions() {
|
|
14
14
|
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
15
15
|
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
16
|
-
local commands="init host mcp statusline update dev"
|
|
16
|
+
local commands="install uninstall init host mcp statusline update dev"
|
|
17
17
|
|
|
18
18
|
# Subcommands for 'dev'
|
|
19
19
|
if [[ "\${COMP_WORDS[1]}" == "dev" && \${COMP_CWORD} -eq 2 ]]; then
|
|
@@ -38,6 +38,8 @@ ${MARKER_START}
|
|
|
38
38
|
_cf() {
|
|
39
39
|
local -a commands
|
|
40
40
|
commands=(
|
|
41
|
+
'install:Install the Coding Friend plugin into Claude Code'
|
|
42
|
+
'uninstall:Uninstall the Coding Friend plugin from Claude Code'
|
|
41
43
|
'init:Initialize coding-friend in current project'
|
|
42
44
|
'host:Build and serve learning docs as a static website'
|
|
43
45
|
'mcp:Setup MCP server for learning docs'
|
|
@@ -94,6 +96,17 @@ function replaceBlock(content, newBlock) {
|
|
|
94
96
|
while (sliceStart > 0 && content[sliceStart - 1] === "\n") sliceStart--;
|
|
95
97
|
return content.slice(0, sliceStart) + newBlock + content.slice(endIdx + MARKER_END.length);
|
|
96
98
|
}
|
|
99
|
+
function removeShellCompletion() {
|
|
100
|
+
const rcPath = getShellRcPath();
|
|
101
|
+
if (!existsSync(rcPath)) return false;
|
|
102
|
+
const content = readFileSync(rcPath, "utf-8");
|
|
103
|
+
if (!content.includes(MARKER_START)) return false;
|
|
104
|
+
const updated = replaceBlock(content, "");
|
|
105
|
+
writeFileSync(rcPath, updated, "utf-8");
|
|
106
|
+
const rcName = getRcName(rcPath);
|
|
107
|
+
log.success(`Tab completion removed from ~/${rcName}`);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
97
110
|
function ensureShellCompletion(opts) {
|
|
98
111
|
const rcPath = getShellRcPath();
|
|
99
112
|
const rcName = getRcName(rcPath);
|
|
@@ -125,5 +138,6 @@ function ensureShellCompletion(opts) {
|
|
|
125
138
|
|
|
126
139
|
export {
|
|
127
140
|
hasShellCompletion,
|
|
141
|
+
removeShellCompletion,
|
|
128
142
|
ensureShellCompletion
|
|
129
143
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ensureShellCompletion
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-DDISNOEK.js";
|
|
4
4
|
import {
|
|
5
5
|
commandExists,
|
|
6
6
|
run,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
claudeSettingsPath,
|
|
11
11
|
installedPluginsPath,
|
|
12
12
|
pluginCachePath
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-T5KHF3RW.js";
|
|
14
14
|
import {
|
|
15
15
|
log
|
|
16
16
|
} from "./chunk-6DUFTBTO.js";
|
|
@@ -248,6 +248,10 @@ async function updateCommand(opts) {
|
|
|
248
248
|
console.log();
|
|
249
249
|
log.dim("Restart Claude Code (or start a new session) to see changes.");
|
|
250
250
|
}
|
|
251
|
+
|
|
251
252
|
export {
|
|
253
|
+
semverCompare,
|
|
254
|
+
getInstalledVersion,
|
|
255
|
+
getLatestVersion,
|
|
252
256
|
updateCommand
|
|
253
257
|
};
|
|
@@ -34,6 +34,21 @@ function devStatePath() {
|
|
|
34
34
|
function knownMarketplacesPath() {
|
|
35
35
|
return join(homedir(), ".claude", "plugins", "known_marketplaces.json");
|
|
36
36
|
}
|
|
37
|
+
function marketplaceCachePath() {
|
|
38
|
+
return join(homedir(), ".claude", "plugins", "cache", "coding-friend-marketplace");
|
|
39
|
+
}
|
|
40
|
+
function marketplaceClonePath() {
|
|
41
|
+
return join(
|
|
42
|
+
homedir(),
|
|
43
|
+
".claude",
|
|
44
|
+
"plugins",
|
|
45
|
+
"marketplaces",
|
|
46
|
+
"coding-friend-marketplace"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
function globalConfigDir() {
|
|
50
|
+
return join(homedir(), ".coding-friend");
|
|
51
|
+
}
|
|
37
52
|
|
|
38
53
|
export {
|
|
39
54
|
resolvePath,
|
|
@@ -43,5 +58,8 @@ export {
|
|
|
43
58
|
installedPluginsPath,
|
|
44
59
|
pluginCachePath,
|
|
45
60
|
devStatePath,
|
|
46
|
-
knownMarketplacesPath
|
|
61
|
+
knownMarketplacesPath,
|
|
62
|
+
marketplaceCachePath,
|
|
63
|
+
marketplaceClonePath,
|
|
64
|
+
globalConfigDir
|
|
47
65
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
installedPluginsPath,
|
|
3
|
+
knownMarketplacesPath
|
|
4
|
+
} from "./chunk-T5KHF3RW.js";
|
|
5
|
+
import {
|
|
6
|
+
readJson
|
|
7
|
+
} from "./chunk-IUTXHCP7.js";
|
|
8
|
+
|
|
9
|
+
// src/lib/plugin-state.ts
|
|
10
|
+
var MARKETPLACE_NAME = "coding-friend-marketplace";
|
|
11
|
+
var PLUGIN_NAME = "coding-friend";
|
|
12
|
+
function isPluginInstalled() {
|
|
13
|
+
const data = readJson(installedPluginsPath());
|
|
14
|
+
if (!data) return false;
|
|
15
|
+
const plugins = data.plugins ?? data;
|
|
16
|
+
return Object.keys(plugins).some((k) => k.includes(PLUGIN_NAME));
|
|
17
|
+
}
|
|
18
|
+
function isMarketplaceRegistered() {
|
|
19
|
+
const data = readJson(knownMarketplacesPath());
|
|
20
|
+
if (!data) return false;
|
|
21
|
+
return MARKETPLACE_NAME in data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
isPluginInstalled,
|
|
26
|
+
isMarketplaceRegistered
|
|
27
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isMarketplaceRegistered,
|
|
3
|
+
isPluginInstalled
|
|
4
|
+
} from "./chunk-ZA4GHAJL.js";
|
|
1
5
|
import {
|
|
2
6
|
commandExists,
|
|
3
7
|
run
|
|
@@ -5,10 +9,9 @@ import {
|
|
|
5
9
|
import {
|
|
6
10
|
claudeSettingsPath,
|
|
7
11
|
devStatePath,
|
|
8
|
-
installedPluginsPath,
|
|
9
12
|
knownMarketplacesPath,
|
|
10
13
|
pluginCachePath
|
|
11
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-T5KHF3RW.js";
|
|
12
15
|
import {
|
|
13
16
|
log
|
|
14
17
|
} from "./chunk-6DUFTBTO.js";
|
|
@@ -35,17 +38,6 @@ var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
|
35
38
|
function getDevState() {
|
|
36
39
|
return readJson(devStatePath());
|
|
37
40
|
}
|
|
38
|
-
function isPluginInstalled() {
|
|
39
|
-
const data = readJson(installedPluginsPath());
|
|
40
|
-
if (!data) return false;
|
|
41
|
-
const plugins = data.plugins ?? data;
|
|
42
|
-
return Object.keys(plugins).some((k) => k.includes(PLUGIN_NAME));
|
|
43
|
-
}
|
|
44
|
-
function isMarketplaceRegistered() {
|
|
45
|
-
const data = readJson(knownMarketplacesPath());
|
|
46
|
-
if (!data) return false;
|
|
47
|
-
return MARKETPLACE_NAME in data;
|
|
48
|
-
}
|
|
49
41
|
function ensureClaude() {
|
|
50
42
|
if (!commandExists("claude")) {
|
|
51
43
|
log.error(
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getLibPath,
|
|
3
3
|
resolveDocsDir
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LVI3I6EN.js";
|
|
5
5
|
import "./chunk-HRVSKMNA.js";
|
|
6
6
|
import {
|
|
7
7
|
run,
|
|
8
8
|
streamExec
|
|
9
9
|
} from "./chunk-UFGNO6CW.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-T5KHF3RW.js";
|
|
11
11
|
import {
|
|
12
12
|
log
|
|
13
13
|
} from "./chunk-6DUFTBTO.js";
|
package/dist/index.js
CHANGED
|
@@ -13,24 +13,32 @@ var program = new Command();
|
|
|
13
13
|
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
|
+
program.command("install").description("Install the Coding Friend plugin into Claude Code").action(async () => {
|
|
17
|
+
const { installCommand } = await import("./install-3H2FZSPZ.js");
|
|
18
|
+
await installCommand();
|
|
19
|
+
});
|
|
20
|
+
program.command("uninstall").description("Uninstall the Coding Friend plugin from Claude Code").action(async () => {
|
|
21
|
+
const { uninstallCommand } = await import("./uninstall-ULTJA57D.js");
|
|
22
|
+
await uninstallCommand();
|
|
23
|
+
});
|
|
16
24
|
program.command("init").description("Initialize coding-friend in current project").action(async () => {
|
|
17
|
-
const { initCommand } = await import("./init-
|
|
25
|
+
const { initCommand } = await import("./init-KX6M4XTA.js");
|
|
18
26
|
await initCommand();
|
|
19
27
|
});
|
|
20
28
|
program.command("host").description("Build and serve learning docs as a static website").argument("[path]", "path to docs folder").option("-p, --port <port>", "port number", "3333").action(async (path, opts) => {
|
|
21
|
-
const { hostCommand } = await import("./host-
|
|
29
|
+
const { hostCommand } = await import("./host-D3BR75OU.js");
|
|
22
30
|
await hostCommand(path, opts);
|
|
23
31
|
});
|
|
24
32
|
program.command("mcp").description("Setup MCP server for learning docs").argument("[path]", "path to docs folder").action(async (path) => {
|
|
25
|
-
const { mcpCommand } = await import("./mcp-
|
|
33
|
+
const { mcpCommand } = await import("./mcp-SWXTVL3Q.js");
|
|
26
34
|
await mcpCommand(path);
|
|
27
35
|
});
|
|
28
36
|
program.command("statusline").description("Setup coding-friend statusline in Claude Code").action(async () => {
|
|
29
|
-
const { statuslineCommand } = await import("./statusline-
|
|
37
|
+
const { statuslineCommand } = await import("./statusline-7M4JUD7O.js");
|
|
30
38
|
await statuslineCommand();
|
|
31
39
|
});
|
|
32
40
|
program.command("update").description("Update coding-friend plugin, CLI, and statusline").option("--cli", "Update only the CLI (npm package)").option("--plugin", "Update only the Claude Code plugin").option("--statusline", "Update only the statusline").action(async (opts) => {
|
|
33
|
-
const { updateCommand } = await import("./update-
|
|
41
|
+
const { updateCommand } = await import("./update-5C5QFLIL.js");
|
|
34
42
|
await updateCommand(opts);
|
|
35
43
|
});
|
|
36
44
|
var dev = program.command("dev").description("Development mode commands");
|
|
@@ -45,28 +53,28 @@ Dev subcommands:
|
|
|
45
53
|
dev restart [path] Reinstall local dev plugin (off + on)`
|
|
46
54
|
);
|
|
47
55
|
dev.command("on").description("Switch to local plugin source").argument("[path]", "path to local coding-friend repo (default: cwd)").action(async (path) => {
|
|
48
|
-
const { devOnCommand } = await import("./dev-
|
|
56
|
+
const { devOnCommand } = await import("./dev-WMKGIPD3.js");
|
|
49
57
|
await devOnCommand(path);
|
|
50
58
|
});
|
|
51
59
|
dev.command("off").description("Switch back to remote marketplace").action(async () => {
|
|
52
|
-
const { devOffCommand } = await import("./dev-
|
|
60
|
+
const { devOffCommand } = await import("./dev-WMKGIPD3.js");
|
|
53
61
|
await devOffCommand();
|
|
54
62
|
});
|
|
55
63
|
dev.command("status").description("Show current dev mode").action(async () => {
|
|
56
|
-
const { devStatusCommand } = await import("./dev-
|
|
64
|
+
const { devStatusCommand } = await import("./dev-WMKGIPD3.js");
|
|
57
65
|
await devStatusCommand();
|
|
58
66
|
});
|
|
59
67
|
dev.command("sync").description(
|
|
60
68
|
"Copy local source files to plugin cache (no version bump needed)"
|
|
61
69
|
).action(async () => {
|
|
62
|
-
const { devSyncCommand } = await import("./dev-
|
|
70
|
+
const { devSyncCommand } = await import("./dev-WMKGIPD3.js");
|
|
63
71
|
await devSyncCommand();
|
|
64
72
|
});
|
|
65
73
|
dev.command("restart").description("Reinstall local dev plugin (off + on)").argument(
|
|
66
74
|
"[path]",
|
|
67
75
|
"path to local coding-friend repo (default: saved path or cwd)"
|
|
68
76
|
).action(async (path) => {
|
|
69
|
-
const { devRestartCommand } = await import("./dev-
|
|
77
|
+
const { devRestartCommand } = await import("./dev-WMKGIPD3.js");
|
|
70
78
|
await devRestartCommand(path);
|
|
71
79
|
});
|
|
72
80
|
program.parse();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_CONFIG
|
|
3
|
-
} from "./chunk-HRVSKMNA.js";
|
|
4
1
|
import {
|
|
5
2
|
ensureShellCompletion,
|
|
6
3
|
hasShellCompletion
|
|
7
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DDISNOEK.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_CONFIG
|
|
7
|
+
} from "./chunk-HRVSKMNA.js";
|
|
8
8
|
import {
|
|
9
9
|
run
|
|
10
10
|
} from "./chunk-UFGNO6CW.js";
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
globalConfigPath,
|
|
14
14
|
localConfigPath,
|
|
15
15
|
resolvePath
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-T5KHF3RW.js";
|
|
17
17
|
import {
|
|
18
18
|
log
|
|
19
19
|
} from "./chunk-6DUFTBTO.js";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getInstalledVersion,
|
|
3
|
+
getLatestVersion,
|
|
4
|
+
semverCompare
|
|
5
|
+
} from "./chunk-F32SU5YM.js";
|
|
6
|
+
import {
|
|
7
|
+
isMarketplaceRegistered
|
|
8
|
+
} from "./chunk-ZA4GHAJL.js";
|
|
9
|
+
import "./chunk-DDISNOEK.js";
|
|
10
|
+
import {
|
|
11
|
+
commandExists,
|
|
12
|
+
run
|
|
13
|
+
} from "./chunk-UFGNO6CW.js";
|
|
14
|
+
import "./chunk-T5KHF3RW.js";
|
|
15
|
+
import {
|
|
16
|
+
log
|
|
17
|
+
} from "./chunk-6DUFTBTO.js";
|
|
18
|
+
import "./chunk-IUTXHCP7.js";
|
|
19
|
+
|
|
20
|
+
// src/commands/install.ts
|
|
21
|
+
import chalk from "chalk";
|
|
22
|
+
async function installCommand() {
|
|
23
|
+
console.log("=== \u{1F33F} Coding Friend Install \u{1F33F} ===");
|
|
24
|
+
console.log();
|
|
25
|
+
if (!commandExists("claude")) {
|
|
26
|
+
log.error(
|
|
27
|
+
"Claude CLI not found. Install it first: https://docs.anthropic.com/en/docs/claude-code/getting-started"
|
|
28
|
+
);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
if (isMarketplaceRegistered()) {
|
|
32
|
+
log.success("Marketplace already registered.");
|
|
33
|
+
} else {
|
|
34
|
+
log.step("Adding coding-friend marketplace...");
|
|
35
|
+
const result = run("claude", [
|
|
36
|
+
"plugin",
|
|
37
|
+
"marketplace",
|
|
38
|
+
"add",
|
|
39
|
+
"dinhanhthi/coding-friend"
|
|
40
|
+
]);
|
|
41
|
+
if (result === null) {
|
|
42
|
+
log.error(
|
|
43
|
+
"Failed to add marketplace. Try manually: claude plugin marketplace add dinhanhthi/coding-friend"
|
|
44
|
+
);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
log.success("Marketplace added.");
|
|
48
|
+
}
|
|
49
|
+
const installedVersion = getInstalledVersion();
|
|
50
|
+
if (!installedVersion) {
|
|
51
|
+
log.step("Installing plugin...");
|
|
52
|
+
const result = run("claude", [
|
|
53
|
+
"plugin",
|
|
54
|
+
"install",
|
|
55
|
+
"coding-friend@coding-friend-marketplace"
|
|
56
|
+
]);
|
|
57
|
+
if (result === null) {
|
|
58
|
+
log.error(
|
|
59
|
+
"Failed to install plugin. Try manually: claude plugin install coding-friend@coding-friend-marketplace"
|
|
60
|
+
);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
log.success("Plugin installed!");
|
|
64
|
+
} else {
|
|
65
|
+
log.success(`Plugin already installed (${chalk.green(`v${installedVersion}`)}).`);
|
|
66
|
+
const latestVersion = getLatestVersion();
|
|
67
|
+
if (latestVersion) {
|
|
68
|
+
const cmp = semverCompare(installedVersion, latestVersion);
|
|
69
|
+
if (cmp < 0) {
|
|
70
|
+
log.warn(
|
|
71
|
+
`Update available: ${chalk.yellow(`v${installedVersion}`)} \u2192 ${chalk.green(`v${latestVersion}`)}. Run ${chalk.cyan("cf update")} to update.`
|
|
72
|
+
);
|
|
73
|
+
} else {
|
|
74
|
+
log.success("Already on the latest version.");
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
log.dim("Could not check for updates (no network or GitHub rate limit).");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
console.log();
|
|
81
|
+
log.info("Next steps:");
|
|
82
|
+
log.dim(` ${chalk.cyan("cf init")} Initialize workspace (docs folders, config)`);
|
|
83
|
+
log.dim(` ${chalk.cyan("cf statusline")} Setup statusline in Claude Code`);
|
|
84
|
+
console.log();
|
|
85
|
+
log.dim("Restart Claude Code (or start a new session) to use the plugin.");
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
installCommand
|
|
89
|
+
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getLibPath,
|
|
3
3
|
resolveDocsDir
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LVI3I6EN.js";
|
|
5
5
|
import "./chunk-HRVSKMNA.js";
|
|
6
6
|
import {
|
|
7
7
|
run
|
|
8
8
|
} from "./chunk-UFGNO6CW.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-T5KHF3RW.js";
|
|
10
10
|
import {
|
|
11
11
|
log
|
|
12
12
|
} from "./chunk-6DUFTBTO.js";
|
package/dist/postinstall.js
CHANGED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isMarketplaceRegistered,
|
|
3
|
+
isPluginInstalled
|
|
4
|
+
} from "./chunk-ZA4GHAJL.js";
|
|
5
|
+
import {
|
|
6
|
+
hasShellCompletion,
|
|
7
|
+
removeShellCompletion
|
|
8
|
+
} from "./chunk-DDISNOEK.js";
|
|
9
|
+
import {
|
|
10
|
+
commandExists,
|
|
11
|
+
run
|
|
12
|
+
} from "./chunk-UFGNO6CW.js";
|
|
13
|
+
import {
|
|
14
|
+
claudeSettingsPath,
|
|
15
|
+
devStatePath,
|
|
16
|
+
globalConfigDir,
|
|
17
|
+
marketplaceCachePath,
|
|
18
|
+
marketplaceClonePath
|
|
19
|
+
} from "./chunk-T5KHF3RW.js";
|
|
20
|
+
import {
|
|
21
|
+
log
|
|
22
|
+
} from "./chunk-6DUFTBTO.js";
|
|
23
|
+
import {
|
|
24
|
+
readJson,
|
|
25
|
+
writeJson
|
|
26
|
+
} from "./chunk-IUTXHCP7.js";
|
|
27
|
+
|
|
28
|
+
// src/commands/uninstall.ts
|
|
29
|
+
import { existsSync, rmSync } from "fs";
|
|
30
|
+
import { confirm } from "@inquirer/prompts";
|
|
31
|
+
import chalk from "chalk";
|
|
32
|
+
var MARKETPLACE_NAME = "coding-friend-marketplace";
|
|
33
|
+
var PLUGIN_NAME = "coding-friend";
|
|
34
|
+
var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
35
|
+
function hasStatuslineReference() {
|
|
36
|
+
const settings = readJson(claudeSettingsPath());
|
|
37
|
+
if (!settings) return false;
|
|
38
|
+
const statusLine = settings.statusLine;
|
|
39
|
+
return !!statusLine?.command?.includes(PLUGIN_NAME);
|
|
40
|
+
}
|
|
41
|
+
function removeStatuslineReference() {
|
|
42
|
+
const settingsPath = claudeSettingsPath();
|
|
43
|
+
const settings = readJson(settingsPath);
|
|
44
|
+
if (!settings) return false;
|
|
45
|
+
const statusLine = settings.statusLine;
|
|
46
|
+
if (!statusLine?.command?.includes(PLUGIN_NAME)) return false;
|
|
47
|
+
delete settings.statusLine;
|
|
48
|
+
writeJson(settingsPath, settings);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
function detect() {
|
|
52
|
+
return {
|
|
53
|
+
pluginInstalled: isPluginInstalled(),
|
|
54
|
+
marketplaceRegistered: isMarketplaceRegistered(),
|
|
55
|
+
cacheExists: existsSync(marketplaceCachePath()),
|
|
56
|
+
cloneExists: existsSync(marketplaceClonePath()),
|
|
57
|
+
statuslineConfigured: hasStatuslineReference(),
|
|
58
|
+
shellCompletionExists: hasShellCompletion(),
|
|
59
|
+
globalConfigExists: existsSync(globalConfigDir()),
|
|
60
|
+
devModeActive: existsSync(devStatePath())
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function displayDetection(d) {
|
|
64
|
+
const check = (v) => v ? chalk.green("\u2714") : chalk.dim("\u2013");
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(" Detected components:");
|
|
67
|
+
console.log(` ${check(d.pluginInstalled)} Plugin registration`);
|
|
68
|
+
console.log(` ${check(d.marketplaceRegistered)} Marketplace registration`);
|
|
69
|
+
console.log(` ${check(d.cacheExists)} Plugin cache`);
|
|
70
|
+
console.log(` ${check(d.cloneExists)} Marketplace clone`);
|
|
71
|
+
console.log(` ${check(d.statuslineConfigured)} Statusline reference`);
|
|
72
|
+
console.log(` ${check(d.shellCompletionExists)} Shell tab completion`);
|
|
73
|
+
console.log(` ${check(d.globalConfigExists)} Global config (~/.coding-friend/)`);
|
|
74
|
+
console.log();
|
|
75
|
+
}
|
|
76
|
+
function nothingToRemove(d) {
|
|
77
|
+
return !d.pluginInstalled && !d.marketplaceRegistered && !d.cacheExists && !d.cloneExists && !d.statuslineConfigured && !d.shellCompletionExists && !d.globalConfigExists;
|
|
78
|
+
}
|
|
79
|
+
async function uninstallCommand() {
|
|
80
|
+
console.log(`
|
|
81
|
+
=== ${chalk.red("Coding Friend Uninstall")} ===`);
|
|
82
|
+
if (!commandExists("claude")) {
|
|
83
|
+
log.error(
|
|
84
|
+
"Claude CLI not found. Cannot uninstall plugin without it."
|
|
85
|
+
);
|
|
86
|
+
log.dim("Install Claude CLI first: https://docs.anthropic.com/en/docs/claude-code");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const detection = detect();
|
|
90
|
+
if (detection.devModeActive) {
|
|
91
|
+
log.warn("Dev mode is currently active.");
|
|
92
|
+
log.dim(`Run ${chalk.bold("cf dev off")} first, then try again.`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (nothingToRemove(detection)) {
|
|
96
|
+
log.info("Nothing to uninstall \u2014 Coding Friend is not installed.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
displayDetection(detection);
|
|
100
|
+
const proceed = await confirm({
|
|
101
|
+
message: "This will remove Coding Friend from Claude Code. Continue?",
|
|
102
|
+
default: false
|
|
103
|
+
});
|
|
104
|
+
if (!proceed) {
|
|
105
|
+
log.info("Uninstall cancelled.");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
let removeConfig = false;
|
|
109
|
+
if (detection.globalConfigExists) {
|
|
110
|
+
removeConfig = await confirm({
|
|
111
|
+
message: "Also remove ~/.coding-friend/ config directory? (global config, custom skills)",
|
|
112
|
+
default: false
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
console.log();
|
|
116
|
+
let errors = 0;
|
|
117
|
+
if (detection.pluginInstalled) {
|
|
118
|
+
log.step("Uninstalling plugin...");
|
|
119
|
+
const result = run("claude", ["plugin", "uninstall", PLUGIN_ID]);
|
|
120
|
+
if (result === null) {
|
|
121
|
+
const fallback = run("claude", ["plugin", "uninstall", PLUGIN_NAME]);
|
|
122
|
+
if (fallback === null) {
|
|
123
|
+
log.warn("Could not uninstall plugin via CLI (may already be removed).");
|
|
124
|
+
errors++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (detection.marketplaceRegistered) {
|
|
129
|
+
log.step("Removing marketplace...");
|
|
130
|
+
const result = run("claude", ["plugin", "marketplace", "remove", MARKETPLACE_NAME]);
|
|
131
|
+
if (result === null) {
|
|
132
|
+
log.warn("Could not remove marketplace via CLI (may already be removed).");
|
|
133
|
+
errors++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (detection.cacheExists) {
|
|
137
|
+
log.step("Removing plugin cache...");
|
|
138
|
+
try {
|
|
139
|
+
rmSync(marketplaceCachePath(), { recursive: true, force: true });
|
|
140
|
+
} catch {
|
|
141
|
+
log.warn("Could not remove plugin cache directory.");
|
|
142
|
+
errors++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (detection.cloneExists) {
|
|
146
|
+
log.step("Removing marketplace clone...");
|
|
147
|
+
try {
|
|
148
|
+
rmSync(marketplaceClonePath(), { recursive: true, force: true });
|
|
149
|
+
} catch {
|
|
150
|
+
log.warn("Could not remove marketplace clone directory.");
|
|
151
|
+
errors++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (detection.statuslineConfigured) {
|
|
155
|
+
log.step("Cleaning statusline...");
|
|
156
|
+
if (!removeStatuslineReference()) {
|
|
157
|
+
log.warn("Could not clean statusline from settings.");
|
|
158
|
+
errors++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (detection.shellCompletionExists) {
|
|
162
|
+
log.step("Removing shell completion...");
|
|
163
|
+
if (!removeShellCompletion()) {
|
|
164
|
+
log.warn("Could not remove shell completion.");
|
|
165
|
+
errors++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (removeConfig) {
|
|
169
|
+
log.step("Removing global config...");
|
|
170
|
+
try {
|
|
171
|
+
rmSync(globalConfigDir(), { recursive: true, force: true });
|
|
172
|
+
} catch {
|
|
173
|
+
log.warn("Could not remove ~/.coding-friend/ directory.");
|
|
174
|
+
errors++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
console.log();
|
|
178
|
+
if (errors === 0) {
|
|
179
|
+
log.success("Coding Friend has been completely uninstalled.");
|
|
180
|
+
} else {
|
|
181
|
+
log.warn(`Uninstalled with ${errors} warning(s). Check messages above.`);
|
|
182
|
+
}
|
|
183
|
+
if (!removeConfig && detection.globalConfigExists) {
|
|
184
|
+
log.dim("Global config kept at ~/.coding-friend/");
|
|
185
|
+
}
|
|
186
|
+
console.log();
|
|
187
|
+
log.dim("Restart Claude Code to complete the uninstall.");
|
|
188
|
+
}
|
|
189
|
+
export {
|
|
190
|
+
uninstallCommand
|
|
191
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getInstalledVersion,
|
|
3
|
+
getLatestVersion,
|
|
4
|
+
semverCompare,
|
|
5
|
+
updateCommand
|
|
6
|
+
} from "./chunk-F32SU5YM.js";
|
|
7
|
+
import "./chunk-DDISNOEK.js";
|
|
8
|
+
import "./chunk-UFGNO6CW.js";
|
|
9
|
+
import "./chunk-T5KHF3RW.js";
|
|
10
|
+
import "./chunk-6DUFTBTO.js";
|
|
11
|
+
import "./chunk-IUTXHCP7.js";
|
|
12
|
+
export {
|
|
13
|
+
getInstalledVersion,
|
|
14
|
+
getLatestVersion,
|
|
15
|
+
semverCompare,
|
|
16
|
+
updateCommand
|
|
17
|
+
};
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog (Learn Host)
|
|
2
2
|
|
|
3
|
+
## v0.1.0 (2026-03-03)
|
|
4
|
+
|
|
5
|
+
- Add copy button to code blocks ([#ac47c74](https://github.com/dinhanhthi/coding-friend/commit/ac47c74))
|
|
6
|
+
- Redesign home page with elegant layout and updated color scheme ([#2d53836](https://github.com/dinhanhthi/coding-friend/commit/2d53836))
|
|
7
|
+
- Improve TableOfContents heading styling with border ([#2267508](https://github.com/dinhanhthi/coding-friend/commit/2267508))
|
|
8
|
+
|
|
3
9
|
## v0.0.2 (2026-03-01)
|
|
4
10
|
|
|
5
11
|
- Update header styling for consistency with ecosystem redesign
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
--color-navy-900: #2a2d38;
|
|
12
12
|
--color-navy-950: #23262e;
|
|
13
13
|
|
|
14
|
-
/*
|
|
15
|
-
--color-accent: #
|
|
16
|
-
--color-accent-light: #
|
|
17
|
-
--color-accent-dark: #
|
|
14
|
+
/* Amber accent (matching logo gradient) */
|
|
15
|
+
--color-accent: #f59e0b;
|
|
16
|
+
--color-accent-light: #fbbf24;
|
|
17
|
+
--color-accent-dark: #d97706;
|
|
18
18
|
|
|
19
19
|
--color-surface: #2a2d38;
|
|
20
20
|
--color-surface-secondary: #31353f;
|
|
@@ -78,6 +78,36 @@ pre {
|
|
|
78
78
|
background: #0d1117;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/* Code block copy button */
|
|
82
|
+
.code-block-wrapper {
|
|
83
|
+
position: relative;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.code-copy-btn {
|
|
87
|
+
position: absolute;
|
|
88
|
+
top: 0.5rem;
|
|
89
|
+
right: 0.5rem;
|
|
90
|
+
z-index: 10;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
border-radius: 0.375rem;
|
|
93
|
+
padding: 0.375rem;
|
|
94
|
+
color: #94a3b8;
|
|
95
|
+
opacity: 0;
|
|
96
|
+
transition:
|
|
97
|
+
opacity 0.2s,
|
|
98
|
+
color 0.2s,
|
|
99
|
+
background-color 0.2s;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.code-block-wrapper:hover .code-copy-btn {
|
|
103
|
+
opacity: 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.code-copy-btn:hover {
|
|
107
|
+
background: rgba(255, 255, 255, 0.1);
|
|
108
|
+
color: #e2e8f0;
|
|
109
|
+
}
|
|
110
|
+
|
|
81
111
|
:where(code):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
|
82
112
|
font-weight: 500;
|
|
83
113
|
}
|
|
@@ -2,8 +2,7 @@ import type { Metadata } from "next";
|
|
|
2
2
|
import { ThemeProvider } from "next-themes";
|
|
3
3
|
import { getAllCategories } from "@/lib/docs";
|
|
4
4
|
import Header from "@/components/layout/Header";
|
|
5
|
-
import
|
|
6
|
-
import Sidebar from "@/components/Sidebar";
|
|
5
|
+
import LayoutShell from "@/components/LayoutShell";
|
|
7
6
|
import MobileNav from "@/components/MobileNav";
|
|
8
7
|
import "./globals.css";
|
|
9
8
|
import "highlight.js/styles/github-dark.css";
|
|
@@ -40,17 +39,7 @@ export default function RootLayout({
|
|
|
40
39
|
<div data-pagefind-ignore className="md:hidden">
|
|
41
40
|
<MobileNav categories={categories} />
|
|
42
41
|
</div>
|
|
43
|
-
<
|
|
44
|
-
<div data-pagefind-ignore>
|
|
45
|
-
<Sidebar categories={categories} />
|
|
46
|
-
</div>
|
|
47
|
-
<div className="flex min-w-0 flex-1 justify-center md:pl-64 lg:pl-[300px]">
|
|
48
|
-
<main className="min-h-screen w-full max-w-5xl p-6 pb-24 md:p-8 md:pb-24">
|
|
49
|
-
{children}
|
|
50
|
-
</main>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
<Footer />
|
|
42
|
+
<LayoutShell categories={categories}>{children}</LayoutShell>
|
|
54
43
|
</ThemeProvider>
|
|
55
44
|
</body>
|
|
56
45
|
</html>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getAllCategories, getAllDocs, getAllTags } from "@/lib/docs";
|
|
2
2
|
import DocCard from "@/components/DocCard";
|
|
3
3
|
import TagBadge from "@/components/TagBadge";
|
|
4
|
+
import Image from "next/image";
|
|
4
5
|
import Link from "next/link";
|
|
5
6
|
|
|
6
7
|
export default function HomePage() {
|
|
@@ -10,28 +11,53 @@ export default function HomePage() {
|
|
|
10
11
|
const recentDocs = docs.slice(0, 10);
|
|
11
12
|
|
|
12
13
|
return (
|
|
13
|
-
<div>
|
|
14
|
-
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
<div className="mx-auto">
|
|
15
|
+
{/* Hero */}
|
|
16
|
+
<section className="mb-12 flex flex-col items-center pt-8 text-center md:pt-14">
|
|
17
|
+
<Image
|
|
18
|
+
src="/logo.svg"
|
|
19
|
+
alt="Learning Notes"
|
|
20
|
+
width={64}
|
|
21
|
+
height={64}
|
|
22
|
+
className="mb-5"
|
|
23
|
+
/>
|
|
24
|
+
<h1 className="mb-3 text-4xl font-bold tracking-tight text-slate-900 md:text-5xl dark:text-white">
|
|
25
|
+
Learning Notes
|
|
26
|
+
</h1>
|
|
27
|
+
<p className="max-w-lg text-lg text-slate-500 dark:text-slate-400">
|
|
28
|
+
A personal knowledge base with{" "}
|
|
29
|
+
<span className="font-semibold text-amber-600 dark:text-amber-400">
|
|
30
|
+
{docs.length}
|
|
31
|
+
</span>{" "}
|
|
32
|
+
docs across{" "}
|
|
33
|
+
<span className="font-semibold text-amber-600 dark:text-amber-400">
|
|
34
|
+
{categories.length}
|
|
35
|
+
</span>{" "}
|
|
36
|
+
categories
|
|
37
|
+
</p>
|
|
38
|
+
</section>
|
|
18
39
|
|
|
19
40
|
{/* Categories */}
|
|
20
|
-
<section className="mb-
|
|
21
|
-
<h2 className="mb-
|
|
22
|
-
|
|
41
|
+
<section className="mb-12">
|
|
42
|
+
<h2 className="mb-5 text-xl font-semibold text-slate-900 dark:text-white">
|
|
43
|
+
Categories
|
|
44
|
+
</h2>
|
|
45
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
|
|
23
46
|
{categories.map((cat) => (
|
|
24
47
|
<Link
|
|
25
48
|
key={cat.name}
|
|
26
49
|
href={`/${cat.name}/`}
|
|
27
|
-
className="rounded-
|
|
50
|
+
className="group dark:bg-navy-800/50 relative cursor-pointer overflow-hidden rounded-xl border border-slate-200 bg-white p-4 transition-all duration-200 hover:-translate-y-0.5 hover:border-amber-300 hover:shadow-md hover:shadow-amber-100/50 dark:border-[#a0a0a01c] dark:hover:border-amber-500/40 dark:hover:shadow-amber-900/20"
|
|
28
51
|
>
|
|
29
|
-
<div className="font-medium text-slate-900 capitalize dark:text-slate-100">
|
|
52
|
+
<div className="mb-1 font-medium text-slate-900 capitalize dark:text-slate-100">
|
|
30
53
|
{cat.name.replace(/[_-]/g, " ")}
|
|
31
54
|
</div>
|
|
32
55
|
<div className="text-sm text-slate-500 dark:text-slate-400">
|
|
33
56
|
{cat.docCount} {cat.docCount === 1 ? "doc" : "docs"}
|
|
34
57
|
</div>
|
|
58
|
+
<div className="dark:bg-navy-950 absolute right-3 bottom-3 flex h-7 w-7 items-center justify-center rounded-full bg-slate-100 text-xs font-semibold text-slate-500 dark:text-slate-400">
|
|
59
|
+
{cat.docCount}
|
|
60
|
+
</div>
|
|
35
61
|
</Link>
|
|
36
62
|
))}
|
|
37
63
|
</div>
|
|
@@ -39,10 +65,12 @@ export default function HomePage() {
|
|
|
39
65
|
|
|
40
66
|
{/* Tags */}
|
|
41
67
|
{tags.length > 0 && (
|
|
42
|
-
<section className="mb-
|
|
43
|
-
<h2 className="mb-
|
|
68
|
+
<section className="mb-12">
|
|
69
|
+
<h2 className="mb-5 text-xl font-semibold text-slate-900 dark:text-white">
|
|
70
|
+
Tags
|
|
71
|
+
</h2>
|
|
44
72
|
<div className="flex flex-wrap gap-2">
|
|
45
|
-
{tags.slice(0,
|
|
73
|
+
{tags.slice(0, 24).map(({ tag }) => (
|
|
46
74
|
<TagBadge key={tag} tag={tag} />
|
|
47
75
|
))}
|
|
48
76
|
</div>
|
|
@@ -51,8 +79,10 @@ export default function HomePage() {
|
|
|
51
79
|
|
|
52
80
|
{/* Recent Docs */}
|
|
53
81
|
<section>
|
|
54
|
-
<h2 className="mb-
|
|
55
|
-
|
|
82
|
+
<h2 className="mb-5 text-xl font-semibold text-slate-900 dark:text-white">
|
|
83
|
+
Recently Updated
|
|
84
|
+
</h2>
|
|
85
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
56
86
|
{recentDocs.map((doc) => (
|
|
57
87
|
<DocCard key={`${doc.category}/${doc.slug}`} doc={doc} />
|
|
58
88
|
))}
|
|
@@ -8,10 +8,7 @@ interface Crumb {
|
|
|
8
8
|
export default function Breadcrumbs({ crumbs }: { crumbs: Crumb[] }) {
|
|
9
9
|
return (
|
|
10
10
|
<nav className="mb-4 flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
|
|
11
|
-
<Link
|
|
12
|
-
href="/"
|
|
13
|
-
className="hover:text-violet-600 dark:hover:text-violet-400"
|
|
14
|
-
>
|
|
11
|
+
<Link href="/" className="hover:text-amber-600 dark:hover:text-amber-400">
|
|
15
12
|
Home
|
|
16
13
|
</Link>
|
|
17
14
|
{crumbs.map((crumb, i) => (
|
|
@@ -20,7 +17,7 @@ export default function Breadcrumbs({ crumbs }: { crumbs: Crumb[] }) {
|
|
|
20
17
|
{crumb.href ? (
|
|
21
18
|
<Link
|
|
22
19
|
href={crumb.href}
|
|
23
|
-
className="capitalize hover:text-
|
|
20
|
+
className="capitalize hover:text-amber-600 dark:hover:text-amber-400"
|
|
24
21
|
>
|
|
25
22
|
{crumb.label}
|
|
26
23
|
</Link>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export default function CodeBlock(props: React.ComponentProps<"pre">) {
|
|
6
|
+
const preRef = useRef<HTMLPreElement>(null);
|
|
7
|
+
const [copied, setCopied] = useState(false);
|
|
8
|
+
|
|
9
|
+
const handleCopy = async () => {
|
|
10
|
+
const text = preRef.current?.querySelector("code")?.textContent ?? "";
|
|
11
|
+
await navigator.clipboard.writeText(text);
|
|
12
|
+
setCopied(true);
|
|
13
|
+
setTimeout(() => setCopied(false), 2000);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="code-block-wrapper">
|
|
18
|
+
<button
|
|
19
|
+
onClick={handleCopy}
|
|
20
|
+
className="code-copy-btn"
|
|
21
|
+
aria-label="Copy to clipboard"
|
|
22
|
+
>
|
|
23
|
+
{copied ? (
|
|
24
|
+
<svg
|
|
25
|
+
className="h-4 w-4 text-emerald-400"
|
|
26
|
+
fill="none"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
>
|
|
30
|
+
<path
|
|
31
|
+
strokeLinecap="round"
|
|
32
|
+
strokeLinejoin="round"
|
|
33
|
+
strokeWidth={2}
|
|
34
|
+
d="M5 13l4 4L19 7"
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
) : (
|
|
38
|
+
<svg
|
|
39
|
+
className="h-4 w-4"
|
|
40
|
+
fill="none"
|
|
41
|
+
viewBox="0 0 24 24"
|
|
42
|
+
stroke="currentColor"
|
|
43
|
+
>
|
|
44
|
+
<path
|
|
45
|
+
strokeLinecap="round"
|
|
46
|
+
strokeLinejoin="round"
|
|
47
|
+
strokeWidth={2}
|
|
48
|
+
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
49
|
+
/>
|
|
50
|
+
</svg>
|
|
51
|
+
)}
|
|
52
|
+
</button>
|
|
53
|
+
<pre ref={preRef} {...props} />
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -17,14 +17,12 @@ export default function DocCard({ doc }: { doc: DocMeta }) {
|
|
|
17
17
|
onKeyDown={(e) => {
|
|
18
18
|
if (e.key === "Enter") router.push(href);
|
|
19
19
|
}}
|
|
20
|
-
className="dark:bg-navy-800/50 block cursor-pointer rounded-lg border border-slate-200 bg-white p-4 transition-all hover:border-
|
|
20
|
+
className="dark:bg-navy-800/50 block cursor-pointer rounded-lg border border-slate-200 bg-white p-4 transition-all duration-200 hover:border-amber-300 hover:shadow-sm dark:border-[#a0a0a01c] dark:hover:border-amber-500/40"
|
|
21
21
|
>
|
|
22
|
-
<h3 className="mb-
|
|
23
|
-
<Link href={href}
|
|
24
|
-
{doc.frontmatter.title}
|
|
25
|
-
</Link>
|
|
22
|
+
<h3 className="mb-2 font-semibold text-slate-900 dark:text-slate-100">
|
|
23
|
+
<Link href={href}>{doc.frontmatter.title}</Link>
|
|
26
24
|
</h3>
|
|
27
|
-
<p className="mb-
|
|
25
|
+
<p className="mb-3 line-clamp-2 text-sm text-slate-500 dark:text-slate-400">
|
|
28
26
|
{doc.excerpt}
|
|
29
27
|
</p>
|
|
30
28
|
<div className="flex items-center justify-between">
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { usePathname } from "next/navigation";
|
|
4
|
+
import type { CategoryInfo } from "@/lib/types";
|
|
5
|
+
import Sidebar from "@/components/Sidebar";
|
|
6
|
+
import Footer from "@/components/layout/Footer";
|
|
7
|
+
|
|
8
|
+
export default function LayoutShell({
|
|
9
|
+
categories,
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
categories: CategoryInfo[];
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}) {
|
|
15
|
+
const pathname = usePathname();
|
|
16
|
+
const isHome = pathname === "/";
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<div className="flex">
|
|
21
|
+
{!isHome && (
|
|
22
|
+
<div data-pagefind-ignore>
|
|
23
|
+
<Sidebar categories={categories} />
|
|
24
|
+
</div>
|
|
25
|
+
)}
|
|
26
|
+
<div
|
|
27
|
+
className={`flex min-w-0 flex-1 justify-center ${!isHome ? "md:pl-64 lg:pl-[300px]" : ""}`}
|
|
28
|
+
>
|
|
29
|
+
<main
|
|
30
|
+
className={`min-h-screen w-full p-6 pb-24 md:p-8 md:pb-24 ${isHome ? "max-w-6xl" : "max-w-5xl"}`}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</main>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<Footer isHome={isHome} />
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -2,6 +2,7 @@ import ReactMarkdown from "react-markdown";
|
|
|
2
2
|
import remarkGfm from "remark-gfm";
|
|
3
3
|
import rehypeHighlight from "rehype-highlight";
|
|
4
4
|
import rehypeSlug from "rehype-slug";
|
|
5
|
+
import CodeBlock from "./CodeBlock";
|
|
5
6
|
|
|
6
7
|
export default function MarkdownRenderer({ content }: { content: string }) {
|
|
7
8
|
return (
|
|
@@ -9,6 +10,7 @@ export default function MarkdownRenderer({ content }: { content: string }) {
|
|
|
9
10
|
<ReactMarkdown
|
|
10
11
|
remarkPlugins={[remarkGfm]}
|
|
11
12
|
rehypePlugins={[rehypeHighlight, rehypeSlug]}
|
|
13
|
+
components={{ pre: CodeBlock }}
|
|
12
14
|
>
|
|
13
15
|
{content}
|
|
14
16
|
</ReactMarkdown>
|
|
@@ -46,7 +46,7 @@ export default function MobileNav({
|
|
|
46
46
|
onClick={() => setOpen(false)}
|
|
47
47
|
className={`flex items-center justify-between rounded-md px-3 py-1.5 text-sm capitalize ${
|
|
48
48
|
isActive
|
|
49
|
-
? "font-medium text-
|
|
49
|
+
? "font-medium text-amber-700 dark:text-amber-400"
|
|
50
50
|
: "text-slate-600 dark:text-slate-400"
|
|
51
51
|
}`}
|
|
52
52
|
>
|
|
@@ -25,7 +25,7 @@ export default function Sidebar({
|
|
|
25
25
|
href={`/${cat.name}/`}
|
|
26
26
|
className={`flex items-center justify-between rounded-full px-3 py-2 text-sm capitalize transition-colors duration-200 ${
|
|
27
27
|
isActive
|
|
28
|
-
? "font-medium text-
|
|
28
|
+
? "font-medium text-amber-700 dark:text-amber-400"
|
|
29
29
|
: "dark:hover:bg-navy-800/50 text-slate-600 hover:bg-slate-200/50 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
|
30
30
|
}`}
|
|
31
31
|
>
|
|
@@ -35,7 +35,7 @@ export default function TableOfContents({ headings }: Props) {
|
|
|
35
35
|
return (
|
|
36
36
|
<aside className="sticky top-16 hidden h-[calc(100vh-4rem)] w-56 shrink-0 overflow-y-auto lg:block">
|
|
37
37
|
<div className="p-4">
|
|
38
|
-
<h4 className="mb-3 text-xs font-semibold tracking-wider text-slate-
|
|
38
|
+
<h4 className="mb-3 border-b border-slate-600 pb-2 text-xs font-semibold tracking-wider text-slate-400 uppercase">
|
|
39
39
|
On this page
|
|
40
40
|
</h4>
|
|
41
41
|
<ul className="space-y-1.5">
|
|
@@ -47,7 +47,7 @@ export default function TableOfContents({ headings }: Props) {
|
|
|
47
47
|
h.level === 3 ? "pl-3" : ""
|
|
48
48
|
} ${
|
|
49
49
|
activeId === h.id
|
|
50
|
-
? "font-medium text-
|
|
50
|
+
? "font-medium text-amber-700 dark:text-amber-400"
|
|
51
51
|
: "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
|
52
52
|
}`}
|
|
53
53
|
>
|
|
@@ -4,7 +4,7 @@ export default function TagBadge({ tag }: { tag: string }) {
|
|
|
4
4
|
return (
|
|
5
5
|
<Link
|
|
6
6
|
href={`/search/?q=${encodeURIComponent(tag)}`}
|
|
7
|
-
className="inline-block rounded-full
|
|
7
|
+
className="dark:bg-navy-950 inline-block cursor-pointer rounded-full bg-slate-200 px-2.5 py-0.5 text-xs text-slate-600 transition-colors duration-200 hover:bg-slate-300 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200"
|
|
8
8
|
>
|
|
9
9
|
{tag}
|
|
10
10
|
</Link>
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import Image from "next/image";
|
|
2
2
|
|
|
3
|
-
export default function Footer() {
|
|
3
|
+
export default function Footer({ isHome = false }: { isHome?: boolean }) {
|
|
4
4
|
return (
|
|
5
|
-
<footer
|
|
5
|
+
<footer
|
|
6
|
+
className={`dark:bg-navy-950 fixed right-0 bottom-0 z-40 border-t border-slate-200 bg-slate-50 dark:border-[#a0a0a01c] ${
|
|
7
|
+
isHome ? "left-0" : "left-0 md:left-64 lg:left-[300px]"
|
|
8
|
+
}`}
|
|
9
|
+
>
|
|
6
10
|
<div className="flex flex-row flex-wrap items-center gap-1 px-6 py-3 text-center text-xs text-slate-500 dark:text-slate-500">
|
|
7
11
|
<div className="flex items-center gap-2">
|
|
8
12
|
<Image src="/logo.svg" alt="Coding Friend" width={20} height={20} />
|
|
@@ -12,7 +16,7 @@ export default function Footer() {
|
|
|
12
16
|
href="https://github.com/dinhanhthi/coding-friend"
|
|
13
17
|
target="_blank"
|
|
14
18
|
rel="noopener noreferrer"
|
|
15
|
-
className="text-
|
|
19
|
+
className="text-amber-600 hover:text-amber-500 dark:text-amber-400 dark:hover:text-amber-300"
|
|
16
20
|
>
|
|
17
21
|
Coding Friend
|
|
18
22
|
</a>
|
|
@@ -21,7 +25,7 @@ export default function Footer() {
|
|
|
21
25
|
href="https://dinhanhthi.com"
|
|
22
26
|
target="_blank"
|
|
23
27
|
rel="noopener noreferrer"
|
|
24
|
-
className="text-
|
|
28
|
+
className="text-amber-600 hover:text-amber-500 dark:text-amber-400 dark:hover:text-amber-300"
|
|
25
29
|
>
|
|
26
30
|
Anh-Thi Dinh
|
|
27
31
|
</a>
|
|
@@ -89,7 +89,7 @@ export default function Header({ categories }: { categories: CategoryInfo[] }) {
|
|
|
89
89
|
key={cat.name}
|
|
90
90
|
href={`/${cat.name}/`}
|
|
91
91
|
onClick={() => setMobileOpen(false)}
|
|
92
|
-
className="flex items-center justify-between px-3 py-2 text-sm font-medium text-slate-600 capitalize hover:text-
|
|
92
|
+
className="flex items-center justify-between px-3 py-2 text-sm font-medium text-slate-600 capitalize hover:text-amber-600 dark:text-slate-400 dark:hover:text-amber-400"
|
|
93
93
|
>
|
|
94
94
|
<span>{cat.name.replace(/[_-]/g, " ")}</span>
|
|
95
95
|
<span className="text-xs text-slate-400 dark:text-slate-500">
|
|
@@ -102,7 +102,7 @@ export default function Header({ categories }: { categories: CategoryInfo[] }) {
|
|
|
102
102
|
href="https://github.com/dinhanhthi/coding-friend"
|
|
103
103
|
target="_blank"
|
|
104
104
|
rel="noopener noreferrer"
|
|
105
|
-
className="flex cursor-pointer items-center gap-2 text-sm font-medium text-slate-500 hover:text-
|
|
105
|
+
className="flex cursor-pointer items-center gap-2 text-sm font-medium text-slate-500 hover:text-amber-600 dark:text-slate-400 dark:hover:text-amber-400"
|
|
106
106
|
>
|
|
107
107
|
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
|
108
108
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
|
@@ -102,8 +102,17 @@ function parseFrontmatter(raw: matter.GrayMatterFile<string>): DocFrontmatter {
|
|
|
102
102
|
|
|
103
103
|
function makeExcerpt(content: string, maxLen = 160): string {
|
|
104
104
|
const text = content
|
|
105
|
-
.replace(/^#+\s.*/gm, "")
|
|
106
|
-
.replace(/```[\s\S]*?```/g, "")
|
|
105
|
+
.replace(/^#+\s.*/gm, "") // headings
|
|
106
|
+
.replace(/```[\s\S]*?```/g, "") // fenced code blocks
|
|
107
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // [text](url) → text
|
|
108
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, "") // images
|
|
109
|
+
.replace(/(\*\*|__)(.*?)\1/g, "$2") // bold
|
|
110
|
+
.replace(/(\*|_)(.*?)\1/g, "$2") // italic
|
|
111
|
+
.replace(/~~(.*?)~~/g, "$1") // strikethrough
|
|
112
|
+
.replace(/`([^`]+)`/g, "$1") // inline code
|
|
113
|
+
.replace(/^>\s?/gm, "") // blockquotes
|
|
114
|
+
.replace(/^[-*+]\s/gm, "") // unordered list markers
|
|
115
|
+
.replace(/^\d+\.\s/gm, "") // ordered list markers
|
|
107
116
|
.replace(/\n{2,}/g, " ")
|
|
108
117
|
.replace(/\n/g, " ")
|
|
109
118
|
.trim();
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog (Learn MCP)
|
|
2
2
|
|
|
3
|
+
## v0.0.3 (2026-03-03)
|
|
4
|
+
|
|
5
|
+
- Add Claude Desktop config documentation for compiled output ([#d37a6b9](https://github.com/dinhanhthi/coding-friend/commit/d37a6b9))
|
|
6
|
+
|
|
3
7
|
## v0.0.2 (2026-03-01)
|
|
4
8
|
|
|
5
9
|
- Add support for coding-friend-cli integration
|
package/lib/learn-mcp/README.md
CHANGED
|
@@ -93,6 +93,24 @@ npm run build
|
|
|
93
93
|
node dist/index.js /path/to/docs/learn
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
For Claude Desktop with the compiled version:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"mcpServers": {
|
|
101
|
+
"coding-friend-learn": {
|
|
102
|
+
"command": "node",
|
|
103
|
+
"args": [
|
|
104
|
+
"/path/to/coding-friend/cli/lib/learn-mcp/dist/index.js",
|
|
105
|
+
"/path/to/your/docs/learn"
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> **Note:** The path must include `/cli/lib/learn-mcp/`, not `/lib/learn-mcp/`. Run `npm run build` first to generate `dist/`.
|
|
113
|
+
|
|
96
114
|
## Docs Directory Structure
|
|
97
115
|
|
|
98
116
|
```
|
package/package.json
CHANGED