coding-friend-cli 1.5.1 → 1.6.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 +1 -0
- package/dist/{chunk-DDISNOEK.js → chunk-4PLV2ENL.js} +5 -4
- package/dist/{chunk-GTTSBOHM.js → chunk-ESIIWKPD.js} +13 -55
- package/dist/{chunk-5OALHHKR.js → chunk-FWHEMJS3.js} +41 -4
- package/dist/{dev-WINMWRZK.js → dev-EWSTIVM7.js} +16 -27
- package/dist/index.js +19 -11
- package/dist/{init-K4EVPAHK.js → init-HX5T5DBV.js} +2 -2
- package/dist/{install-BEFMUMKE.js → install-RZFSIPFD.js} +6 -3
- package/dist/postinstall.js +1 -1
- package/dist/{statusline-3MQQDRCI.js → statusline-WGPSURDC.js} +1 -1
- package/dist/{uninstall-BHYS52L3.js → uninstall-KOAJFPD6.js} +1 -1
- package/dist/{update-TALQ7TAO.js → update-VAFEWOLA.js} +4 -4
- package/lib/learn-host/CHANGELOG.md +5 -0
- package/lib/learn-host/package.json +1 -1
- package/lib/learn-host/src/app/[category]/[slug]/page.tsx +1 -1
- package/lib/learn-host/src/app/[category]/page.tsx +1 -1
- package/lib/learn-host/src/app/layout.tsx +1 -1
- package/lib/learn-host/src/app/page.tsx +2 -2
- package/lib/learn-host/src/app/tag/[tag]/page.tsx +39 -0
- package/lib/learn-host/src/components/DocCard.tsx +12 -9
- package/lib/learn-host/src/components/PagefindSearch.tsx +4 -7
- package/lib/learn-host/src/components/Sidebar.tsx +5 -5
- package/lib/learn-host/src/components/TableOfContents.tsx +2 -2
- package/lib/learn-host/src/components/TagBadge.tsx +11 -3
- package/lib/learn-host/src/components/ThemeToggle.tsx +1 -1
- package/lib/learn-host/src/components/layout/Footer.tsx +3 -3
- package/lib/learn-host/src/components/layout/Header.tsx +1 -1
- package/lib/learn-host/src/lib/docs.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ cf dev off # Switch back to remote marketplace
|
|
|
37
37
|
cf dev status # Show current dev mode (local or remote)
|
|
38
38
|
cf dev sync # Sync local changes to cache (no version bump needed)
|
|
39
39
|
cf dev restart # Reinstall local dev plugin (off + on)
|
|
40
|
+
cf dev update # Update local dev plugin to latest version (off + on)
|
|
40
41
|
cf help # Show all commands
|
|
41
42
|
```
|
|
42
43
|
|
|
@@ -17,12 +17,12 @@ _cf_completions() {
|
|
|
17
17
|
|
|
18
18
|
# Subcommands for 'dev'
|
|
19
19
|
if [[ "\${COMP_WORDS[1]}" == "dev" && \${COMP_CWORD} -eq 2 ]]; then
|
|
20
|
-
COMPREPLY=($(compgen -W "on off status restart sync" -- "$cur"))
|
|
20
|
+
COMPREPLY=($(compgen -W "on off status restart sync update" -- "$cur"))
|
|
21
21
|
return
|
|
22
22
|
fi
|
|
23
23
|
|
|
24
|
-
# Path completion for 'dev on'
|
|
25
|
-
if [[ "\${COMP_WORDS[1]}" == "dev" && "$prev" == "on" ]]; then
|
|
24
|
+
# Path completion for 'dev on|restart|update'
|
|
25
|
+
if [[ "\${COMP_WORDS[1]}" == "dev" && ("$prev" == "on" || "$prev" == "restart" || "$prev" == "update") ]]; then
|
|
26
26
|
COMPREPLY=($(compgen -d -- "$cur"))
|
|
27
27
|
return
|
|
28
28
|
fi
|
|
@@ -58,9 +58,10 @@ _cf() {
|
|
|
58
58
|
'status:Show current dev mode'
|
|
59
59
|
'restart:Restart dev mode (re-apply local plugin)'
|
|
60
60
|
'sync:Sync local plugin files without restarting'
|
|
61
|
+
'update:Update local dev plugin to latest version'
|
|
61
62
|
)
|
|
62
63
|
_describe 'subcommand' subcommands
|
|
63
|
-
elif (( CURRENT == 4 )) && [[ "\${words[2]}" == "dev" && "\${words[3]}" == "on" ]]; then
|
|
64
|
+
elif (( CURRENT == 4 )) && [[ "\${words[2]}" == "dev" && ("\${words[3]}" == "on" || "\${words[3]}" == "restart" || "\${words[3]}" == "update") ]]; then
|
|
64
65
|
_path_files -/
|
|
65
66
|
fi
|
|
66
67
|
}
|
|
@@ -1,26 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureStatusline,
|
|
3
|
+
getInstalledVersion
|
|
4
|
+
} from "./chunk-FWHEMJS3.js";
|
|
1
5
|
import {
|
|
2
6
|
ensureShellCompletion
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-4PLV2ENL.js";
|
|
4
8
|
import {
|
|
5
9
|
commandExists,
|
|
6
10
|
run,
|
|
7
11
|
sleepSync
|
|
8
12
|
} from "./chunk-UFGNO6CW.js";
|
|
9
13
|
import {
|
|
10
|
-
claudeSettingsPath
|
|
11
|
-
installedPluginsPath,
|
|
12
|
-
pluginCachePath
|
|
14
|
+
claudeSettingsPath
|
|
13
15
|
} from "./chunk-WHCJT7E2.js";
|
|
14
16
|
import {
|
|
15
17
|
log
|
|
16
18
|
} from "./chunk-6DUFTBTO.js";
|
|
17
19
|
import {
|
|
18
|
-
readJson
|
|
19
|
-
writeJson
|
|
20
|
+
readJson
|
|
20
21
|
} from "./chunk-IUTXHCP7.js";
|
|
21
22
|
|
|
22
23
|
// src/commands/update.ts
|
|
23
|
-
import {
|
|
24
|
+
import { readFileSync } from "fs";
|
|
24
25
|
import { dirname, join } from "path";
|
|
25
26
|
import { fileURLToPath } from "url";
|
|
26
27
|
import chalk from "chalk";
|
|
@@ -43,22 +44,6 @@ function getCliVersion() {
|
|
|
43
44
|
function getLatestCliVersion() {
|
|
44
45
|
return run("npm", ["view", "coding-friend-cli", "version"]);
|
|
45
46
|
}
|
|
46
|
-
function getInstalledVersion() {
|
|
47
|
-
const data = readJson(installedPluginsPath());
|
|
48
|
-
if (!data) return null;
|
|
49
|
-
const plugins = data.plugins ?? data;
|
|
50
|
-
for (const [key, value] of Object.entries(plugins)) {
|
|
51
|
-
if (!key.includes("coding-friend")) continue;
|
|
52
|
-
if (Array.isArray(value) && value.length > 0) {
|
|
53
|
-
const entry = value[0];
|
|
54
|
-
if (typeof entry.version === "string") return entry.version;
|
|
55
|
-
}
|
|
56
|
-
if (typeof value === "object" && value !== null && "version" in value) {
|
|
57
|
-
return value.version;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
47
|
function getLatestVersion() {
|
|
63
48
|
let tag = run("gh", [
|
|
64
49
|
"api",
|
|
@@ -92,28 +77,6 @@ function getStatuslineVersion() {
|
|
|
92
77
|
);
|
|
93
78
|
return match?.[1] ?? null;
|
|
94
79
|
}
|
|
95
|
-
function findLatestCacheVersion() {
|
|
96
|
-
const cachePath = pluginCachePath();
|
|
97
|
-
if (!existsSync(cachePath)) return null;
|
|
98
|
-
const versions = readdirSync(cachePath, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
99
|
-
return versions[0] ?? null;
|
|
100
|
-
}
|
|
101
|
-
function updateStatusline(version) {
|
|
102
|
-
const cachePath = pluginCachePath();
|
|
103
|
-
const hookPath = `${cachePath}/${version}/hooks/statusline.sh`;
|
|
104
|
-
if (!existsSync(hookPath)) {
|
|
105
|
-
log.warn(`Statusline hook not found for v${version}`);
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
const settingsPath = claudeSettingsPath();
|
|
109
|
-
const settings = readJson(settingsPath) ?? {};
|
|
110
|
-
settings.statusLine = {
|
|
111
|
-
type: "command",
|
|
112
|
-
command: `bash ${hookPath}`
|
|
113
|
-
};
|
|
114
|
-
writeJson(settingsPath, settings);
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
80
|
async function updateCommand(opts) {
|
|
118
81
|
const updateAll = !opts.cli && !opts.plugin && !opts.statusline;
|
|
119
82
|
const doCli = updateAll || !!opts.cli;
|
|
@@ -232,16 +195,12 @@ async function updateCommand(opts) {
|
|
|
232
195
|
}
|
|
233
196
|
}
|
|
234
197
|
if (doStatusline) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
log.success(
|
|
240
|
-
`Statusline updated to ${chalk.green(`v${targetVersion}`)}`
|
|
241
|
-
);
|
|
242
|
-
}
|
|
198
|
+
log.step("Updating statusline...");
|
|
199
|
+
const updatedVersion = ensureStatusline();
|
|
200
|
+
if (updatedVersion) {
|
|
201
|
+
log.success(`Statusline updated to ${chalk.green(`v${updatedVersion}`)}`);
|
|
243
202
|
} else {
|
|
244
|
-
log.
|
|
203
|
+
log.dim("Statusline already up-to-date.");
|
|
245
204
|
}
|
|
246
205
|
}
|
|
247
206
|
ensureShellCompletion({ silent: false });
|
|
@@ -251,7 +210,6 @@ async function updateCommand(opts) {
|
|
|
251
210
|
|
|
252
211
|
export {
|
|
253
212
|
semverCompare,
|
|
254
|
-
getInstalledVersion,
|
|
255
213
|
getLatestVersion,
|
|
256
214
|
updateCommand
|
|
257
215
|
};
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
claudeSettingsPath,
|
|
7
7
|
globalConfigPath,
|
|
8
|
+
installedPluginsPath,
|
|
8
9
|
pluginCachePath
|
|
9
10
|
} from "./chunk-WHCJT7E2.js";
|
|
10
11
|
import {
|
|
@@ -16,6 +17,22 @@ import {
|
|
|
16
17
|
// src/lib/statusline.ts
|
|
17
18
|
import { existsSync, readdirSync } from "fs";
|
|
18
19
|
import { checkbox } from "@inquirer/prompts";
|
|
20
|
+
function getInstalledVersion() {
|
|
21
|
+
const data = readJson(installedPluginsPath());
|
|
22
|
+
if (!data) return null;
|
|
23
|
+
const plugins = data.plugins ?? data;
|
|
24
|
+
for (const [key, value] of Object.entries(plugins)) {
|
|
25
|
+
if (!key.includes("coding-friend")) continue;
|
|
26
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
27
|
+
const entry = value[0];
|
|
28
|
+
if (typeof entry.version === "string") return entry.version;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "object" && value !== null && "version" in value) {
|
|
31
|
+
return value.version;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
19
36
|
function findLatestVersion() {
|
|
20
37
|
const cachePath = pluginCachePath();
|
|
21
38
|
if (!existsSync(cachePath)) return null;
|
|
@@ -23,11 +40,17 @@ function findLatestVersion() {
|
|
|
23
40
|
return versions[0] ?? null;
|
|
24
41
|
}
|
|
25
42
|
function findStatuslineHookPath() {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
const cachePath = pluginCachePath();
|
|
44
|
+
const installed = getInstalledVersion();
|
|
45
|
+
if (installed) {
|
|
46
|
+
const hookPath2 = `${cachePath}/${installed}/hooks/statusline.sh`;
|
|
47
|
+
if (existsSync(hookPath2)) return { hookPath: hookPath2, version: installed };
|
|
48
|
+
}
|
|
49
|
+
const latest = findLatestVersion();
|
|
50
|
+
if (!latest) return null;
|
|
51
|
+
const hookPath = `${cachePath}/${latest}/hooks/statusline.sh`;
|
|
29
52
|
if (!existsSync(hookPath)) return null;
|
|
30
|
-
return { hookPath, version };
|
|
53
|
+
return { hookPath, version: latest };
|
|
31
54
|
}
|
|
32
55
|
function loadStatuslineComponents() {
|
|
33
56
|
const config = readJson(globalConfigPath());
|
|
@@ -61,6 +84,18 @@ function writeStatuslineSettings(hookPath) {
|
|
|
61
84
|
};
|
|
62
85
|
writeJson(settingsPath, settings);
|
|
63
86
|
}
|
|
87
|
+
function ensureStatusline() {
|
|
88
|
+
const info = findStatuslineHookPath();
|
|
89
|
+
if (!info) return null;
|
|
90
|
+
const settingsPath = claudeSettingsPath();
|
|
91
|
+
const settings = readJson(settingsPath) ?? {};
|
|
92
|
+
const current = settings.statusLine?.command;
|
|
93
|
+
const expected = `bash ${info.hookPath}`;
|
|
94
|
+
if (current === expected) return null;
|
|
95
|
+
settings.statusLine = { type: "command", command: expected };
|
|
96
|
+
writeJson(settingsPath, settings);
|
|
97
|
+
return info.version;
|
|
98
|
+
}
|
|
64
99
|
function isStatuslineConfigured() {
|
|
65
100
|
const settings = readJson(claudeSettingsPath());
|
|
66
101
|
if (!settings) return false;
|
|
@@ -69,9 +104,11 @@ function isStatuslineConfigured() {
|
|
|
69
104
|
}
|
|
70
105
|
|
|
71
106
|
export {
|
|
107
|
+
getInstalledVersion,
|
|
72
108
|
findStatuslineHookPath,
|
|
73
109
|
selectStatuslineComponents,
|
|
74
110
|
saveStatuslineConfig,
|
|
75
111
|
writeStatuslineSettings,
|
|
112
|
+
ensureStatusline,
|
|
76
113
|
isStatuslineConfigured
|
|
77
114
|
};
|
|
@@ -2,12 +2,18 @@ import {
|
|
|
2
2
|
isMarketplaceRegistered,
|
|
3
3
|
isPluginInstalled
|
|
4
4
|
} from "./chunk-MRTR7TJ4.js";
|
|
5
|
+
import {
|
|
6
|
+
ensureStatusline
|
|
7
|
+
} from "./chunk-FWHEMJS3.js";
|
|
8
|
+
import {
|
|
9
|
+
ensureShellCompletion
|
|
10
|
+
} from "./chunk-4PLV2ENL.js";
|
|
11
|
+
import "./chunk-JWAJ4XPK.js";
|
|
5
12
|
import {
|
|
6
13
|
commandExists,
|
|
7
14
|
run
|
|
8
15
|
} from "./chunk-UFGNO6CW.js";
|
|
9
16
|
import {
|
|
10
|
-
claudeSettingsPath,
|
|
11
17
|
devStatePath,
|
|
12
18
|
knownMarketplacesPath,
|
|
13
19
|
pluginCachePath
|
|
@@ -56,28 +62,6 @@ function runClaude(args, label) {
|
|
|
56
62
|
}
|
|
57
63
|
return true;
|
|
58
64
|
}
|
|
59
|
-
function updateSettingsCachePaths() {
|
|
60
|
-
const cachePath = pluginCachePath();
|
|
61
|
-
if (!existsSync(cachePath)) return;
|
|
62
|
-
const versions = readdirSync(cachePath, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
63
|
-
const latest = versions[0];
|
|
64
|
-
if (!latest) return;
|
|
65
|
-
const settingsPath = claudeSettingsPath();
|
|
66
|
-
const settings = readJson(settingsPath);
|
|
67
|
-
if (!settings) return;
|
|
68
|
-
const statusLine = settings.statusLine;
|
|
69
|
-
if (!statusLine?.command) return;
|
|
70
|
-
const prefix = cachePath + "/";
|
|
71
|
-
if (!statusLine.command.includes(prefix)) return;
|
|
72
|
-
const updated = statusLine.command.replace(
|
|
73
|
-
new RegExp(`${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^/]+/`),
|
|
74
|
-
`${prefix}${latest}/`
|
|
75
|
-
);
|
|
76
|
-
if (updated === statusLine.command) return;
|
|
77
|
-
settings.statusLine = { ...statusLine, command: updated };
|
|
78
|
-
writeJson(settingsPath, settings);
|
|
79
|
-
log.info(`Updated statusline path to ${chalk.green(`v${latest}`)}`);
|
|
80
|
-
}
|
|
81
65
|
async function devOnCommand(path) {
|
|
82
66
|
const state = getDevState();
|
|
83
67
|
if (state) {
|
|
@@ -131,7 +115,8 @@ async function devOnCommand(path) {
|
|
|
131
115
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
132
116
|
};
|
|
133
117
|
writeJson(devStatePath(), devState);
|
|
134
|
-
|
|
118
|
+
ensureStatusline();
|
|
119
|
+
ensureShellCompletion({ silent: true });
|
|
135
120
|
console.log();
|
|
136
121
|
log.success(
|
|
137
122
|
`Dev mode ${chalk.green("ON")} \u2014 using local plugin from ${chalk.cyan(localPath)}`
|
|
@@ -260,12 +245,12 @@ async function devSyncCommand() {
|
|
|
260
245
|
`Synced ${chalk.green(fileCount.n)} files. Restart Claude Code to apply changes.`
|
|
261
246
|
);
|
|
262
247
|
}
|
|
263
|
-
async function
|
|
248
|
+
async function devReinstall(path, label) {
|
|
264
249
|
const state = getDevState();
|
|
265
250
|
if (!ensureClaude()) return;
|
|
266
251
|
const localPath = path ?? state?.localPath;
|
|
267
252
|
console.log(`
|
|
268
|
-
=== ${chalk.cyan(
|
|
253
|
+
=== ${chalk.cyan(label)} ===
|
|
269
254
|
`);
|
|
270
255
|
if (state) {
|
|
271
256
|
await devOffCommand();
|
|
@@ -274,7 +259,10 @@ async function devRestartCommand(path) {
|
|
|
274
259
|
log.info("Dev mode was OFF \u2014 skipping off step.");
|
|
275
260
|
}
|
|
276
261
|
await devOnCommand(localPath);
|
|
262
|
+
ensureShellCompletion({ silent: true });
|
|
277
263
|
}
|
|
264
|
+
var devRestartCommand = (path) => devReinstall(path, "Restarting dev mode");
|
|
265
|
+
var devUpdateCommand = (path) => devReinstall(path, "Updating dev plugin");
|
|
278
266
|
async function devStatusCommand() {
|
|
279
267
|
const state = getDevState();
|
|
280
268
|
const source = getMarketplaceSource();
|
|
@@ -302,5 +290,6 @@ export {
|
|
|
302
290
|
devOnCommand,
|
|
303
291
|
devRestartCommand,
|
|
304
292
|
devStatusCommand,
|
|
305
|
-
devSyncCommand
|
|
293
|
+
devSyncCommand,
|
|
294
|
+
devUpdateCommand
|
|
306
295
|
};
|
package/dist/index.js
CHANGED
|
@@ -14,15 +14,15 @@ 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-RZFSIPFD.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-KOAJFPD6.js");
|
|
22
22
|
await uninstallCommand();
|
|
23
23
|
});
|
|
24
24
|
program.command("init").description("Initialize coding-friend in current project").action(async () => {
|
|
25
|
-
const { initCommand } = await import("./init-
|
|
25
|
+
const { initCommand } = await import("./init-HX5T5DBV.js");
|
|
26
26
|
await initCommand();
|
|
27
27
|
});
|
|
28
28
|
program.command("host").description("Build and serve learning docs as a static website").argument("[path]", "path to docs folder").option("-p, --port <port>", "port number", "3333").action(async (path, opts) => {
|
|
@@ -34,11 +34,11 @@ program.command("mcp").description("Setup MCP server for learning docs").argumen
|
|
|
34
34
|
await mcpCommand(path);
|
|
35
35
|
});
|
|
36
36
|
program.command("statusline").description("Setup coding-friend statusline in Claude Code").action(async () => {
|
|
37
|
-
const { statuslineCommand } = await import("./statusline-
|
|
37
|
+
const { statuslineCommand } = await import("./statusline-WGPSURDC.js");
|
|
38
38
|
await statuslineCommand();
|
|
39
39
|
});
|
|
40
40
|
program.command("update").description("Update coding-friend plugin, CLI, and statusline").option("--cli", "Update only the CLI (npm package)").option("--plugin", "Update only the Claude Code plugin").option("--statusline", "Update only the statusline").action(async (opts) => {
|
|
41
|
-
const { updateCommand } = await import("./update-
|
|
41
|
+
const { updateCommand } = await import("./update-VAFEWOLA.js");
|
|
42
42
|
await updateCommand(opts);
|
|
43
43
|
});
|
|
44
44
|
var dev = program.command("dev").description("Development mode commands");
|
|
@@ -50,31 +50,39 @@ Dev subcommands:
|
|
|
50
50
|
dev off Switch back to remote marketplace
|
|
51
51
|
dev status Show current dev mode
|
|
52
52
|
dev sync Copy local source to plugin cache
|
|
53
|
-
dev restart [path] Reinstall local dev plugin (off + on)
|
|
53
|
+
dev restart [path] Reinstall local dev plugin (off + on)
|
|
54
|
+
dev update [path] Update local dev plugin to latest version`
|
|
54
55
|
);
|
|
55
56
|
dev.command("on").description("Switch to local plugin source").argument("[path]", "path to local coding-friend repo (default: cwd)").action(async (path) => {
|
|
56
|
-
const { devOnCommand } = await import("./dev-
|
|
57
|
+
const { devOnCommand } = await import("./dev-EWSTIVM7.js");
|
|
57
58
|
await devOnCommand(path);
|
|
58
59
|
});
|
|
59
60
|
dev.command("off").description("Switch back to remote marketplace").action(async () => {
|
|
60
|
-
const { devOffCommand } = await import("./dev-
|
|
61
|
+
const { devOffCommand } = await import("./dev-EWSTIVM7.js");
|
|
61
62
|
await devOffCommand();
|
|
62
63
|
});
|
|
63
64
|
dev.command("status").description("Show current dev mode").action(async () => {
|
|
64
|
-
const { devStatusCommand } = await import("./dev-
|
|
65
|
+
const { devStatusCommand } = await import("./dev-EWSTIVM7.js");
|
|
65
66
|
await devStatusCommand();
|
|
66
67
|
});
|
|
67
68
|
dev.command("sync").description(
|
|
68
69
|
"Copy local source files to plugin cache (no version bump needed)"
|
|
69
70
|
).action(async () => {
|
|
70
|
-
const { devSyncCommand } = await import("./dev-
|
|
71
|
+
const { devSyncCommand } = await import("./dev-EWSTIVM7.js");
|
|
71
72
|
await devSyncCommand();
|
|
72
73
|
});
|
|
73
74
|
dev.command("restart").description("Reinstall local dev plugin (off + on)").argument(
|
|
74
75
|
"[path]",
|
|
75
76
|
"path to local coding-friend repo (default: saved path or cwd)"
|
|
76
77
|
).action(async (path) => {
|
|
77
|
-
const { devRestartCommand } = await import("./dev-
|
|
78
|
+
const { devRestartCommand } = await import("./dev-EWSTIVM7.js");
|
|
78
79
|
await devRestartCommand(path);
|
|
79
80
|
});
|
|
81
|
+
dev.command("update").description("Update local dev plugin to latest version (off + on)").argument(
|
|
82
|
+
"[path]",
|
|
83
|
+
"path to local coding-friend repo (default: saved path or cwd)"
|
|
84
|
+
).action(async (path) => {
|
|
85
|
+
const { devUpdateCommand } = await import("./dev-EWSTIVM7.js");
|
|
86
|
+
await devUpdateCommand(path);
|
|
87
|
+
});
|
|
80
88
|
program.parse();
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
saveStatuslineConfig,
|
|
5
5
|
selectStatuslineComponents,
|
|
6
6
|
writeStatuslineSettings
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-FWHEMJS3.js";
|
|
8
8
|
import {
|
|
9
9
|
ensureShellCompletion,
|
|
10
10
|
hasShellCompletion
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-4PLV2ENL.js";
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_CONFIG
|
|
14
14
|
} from "./chunk-JWAJ4XPK.js";
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getInstalledVersion,
|
|
3
2
|
getLatestVersion,
|
|
4
3
|
semverCompare
|
|
5
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ESIIWKPD.js";
|
|
6
5
|
import {
|
|
7
6
|
isMarketplaceRegistered
|
|
8
7
|
} from "./chunk-MRTR7TJ4.js";
|
|
9
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
getInstalledVersion
|
|
10
|
+
} from "./chunk-FWHEMJS3.js";
|
|
11
|
+
import "./chunk-4PLV2ENL.js";
|
|
12
|
+
import "./chunk-JWAJ4XPK.js";
|
|
10
13
|
import {
|
|
11
14
|
commandExists,
|
|
12
15
|
run
|
package/dist/postinstall.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getInstalledVersion,
|
|
3
2
|
getLatestVersion,
|
|
4
3
|
semverCompare,
|
|
5
4
|
updateCommand
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-ESIIWKPD.js";
|
|
6
|
+
import "./chunk-FWHEMJS3.js";
|
|
7
|
+
import "./chunk-4PLV2ENL.js";
|
|
8
|
+
import "./chunk-JWAJ4XPK.js";
|
|
8
9
|
import "./chunk-UFGNO6CW.js";
|
|
9
10
|
import "./chunk-WHCJT7E2.js";
|
|
10
11
|
import "./chunk-6DUFTBTO.js";
|
|
11
12
|
import "./chunk-IUTXHCP7.js";
|
|
12
13
|
export {
|
|
13
|
-
getInstalledVersion,
|
|
14
14
|
getLatestVersion,
|
|
15
15
|
semverCompare,
|
|
16
16
|
updateCommand
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog (Learn Host)
|
|
2
2
|
|
|
3
|
+
## v0.2.0 (2026-03-03)
|
|
4
|
+
|
|
5
|
+
- Add dedicated tag pages for filtering docs by tag ([#06f5847](https://github.com/dinhanhthi/coding-friend/commit/06f5847))
|
|
6
|
+
- Style improvements across website and learn-host ([#0029522](https://github.com/dinhanhthi/coding-friend/commit/0029522))
|
|
7
|
+
|
|
3
8
|
## v0.1.0 (2026-03-03)
|
|
4
9
|
|
|
5
10
|
- Add copy button to code blocks ([#ac47c74](https://github.com/dinhanhthi/coding-friend/commit/ac47c74))
|
|
@@ -51,7 +51,7 @@ export default async function DocPage({
|
|
|
51
51
|
{doc.frontmatter.tags.length > 0 && (
|
|
52
52
|
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
53
53
|
{doc.frontmatter.tags.map((tag) => (
|
|
54
|
-
<TagBadge key={tag} tag={tag} />
|
|
54
|
+
<TagBadge key={tag} tag={tag} size="sm" />
|
|
55
55
|
))}
|
|
56
56
|
</div>
|
|
57
57
|
)}
|
|
@@ -21,7 +21,7 @@ export default async function CategoryPage({
|
|
|
21
21
|
<div>
|
|
22
22
|
<Breadcrumbs crumbs={[{ label: displayName }]} />
|
|
23
23
|
<h1 className="mb-1 text-2xl font-bold capitalize">{displayName}</h1>
|
|
24
|
-
<p className="mb-6 text-slate-500 dark:text-slate-400">
|
|
24
|
+
<p className="mb-6 pl-0.5 text-slate-500 dark:text-slate-400">
|
|
25
25
|
{docs.length} {docs.length === 1 ? "doc" : "docs"}
|
|
26
26
|
</p>
|
|
27
27
|
|
|
@@ -33,7 +33,7 @@ export default function RootLayout({
|
|
|
33
33
|
rel="stylesheet"
|
|
34
34
|
/>
|
|
35
35
|
</head>
|
|
36
|
-
<body className="dark:bg-navy-
|
|
36
|
+
<body className="dark:bg-navy-950 bg-white text-slate-900 antialiased dark:text-slate-50">
|
|
37
37
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
|
38
38
|
<Header categories={categories} />
|
|
39
39
|
<div data-pagefind-ignore className="md:hidden">
|
|
@@ -47,7 +47,7 @@ export default function HomePage() {
|
|
|
47
47
|
<Link
|
|
48
48
|
key={cat.name}
|
|
49
49
|
href={`/${cat.name}/`}
|
|
50
|
-
className="group dark:bg-navy-800/
|
|
50
|
+
className="group dark:bg-navy-900/80 dark:hover:bg-navy-800/60 hover:border-navy-400 relative cursor-pointer overflow-hidden rounded-xl border border-slate-200 bg-slate-50 p-4 transition-all duration-200 hover:-translate-y-0.5 dark:border-[#a0a0a01c]"
|
|
51
51
|
>
|
|
52
52
|
<div className="mb-1 font-medium text-slate-900 capitalize dark:text-slate-100">
|
|
53
53
|
{cat.name.replace(/[_-]/g, " ")}
|
|
@@ -55,7 +55,7 @@ export default function HomePage() {
|
|
|
55
55
|
<div className="text-sm text-slate-500 dark:text-slate-400">
|
|
56
56
|
{cat.docCount} {cat.docCount === 1 ? "doc" : "docs"}
|
|
57
57
|
</div>
|
|
58
|
-
<div className="
|
|
58
|
+
<div className="absolute right-3 bottom-3 flex h-7 w-7 items-center justify-center rounded-full bg-slate-200 text-xs font-semibold text-slate-500 dark:bg-slate-600/50 dark:text-slate-400">
|
|
59
59
|
{cat.docCount}
|
|
60
60
|
</div>
|
|
61
61
|
</Link>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getAllTags, getDocsByTag } from "@/lib/docs";
|
|
2
|
+
import DocCard from "@/components/DocCard";
|
|
3
|
+
import Breadcrumbs from "@/components/Breadcrumbs";
|
|
4
|
+
import { notFound } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
export async function generateStaticParams() {
|
|
7
|
+
return getAllTags().map((t) => ({ tag: t.tag }));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const dynamicParams = true;
|
|
11
|
+
|
|
12
|
+
export default async function TagPage({
|
|
13
|
+
params,
|
|
14
|
+
}: {
|
|
15
|
+
params: Promise<{ tag: string }>;
|
|
16
|
+
}) {
|
|
17
|
+
const { tag } = await params;
|
|
18
|
+
const docs = getDocsByTag(tag);
|
|
19
|
+
if (docs.length === 0) notFound();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div>
|
|
23
|
+
<Breadcrumbs crumbs={[{ label: `# ${tag}` }]} />
|
|
24
|
+
<h1 className="mb-1 flex items-center gap-2 text-2xl font-bold">
|
|
25
|
+
<span className="text-slate-400 dark:text-slate-500">#</span>
|
|
26
|
+
{tag}
|
|
27
|
+
</h1>
|
|
28
|
+
<p className="mb-6 pl-0.5 text-slate-500 dark:text-slate-400">
|
|
29
|
+
{docs.length} {docs.length === 1 ? "doc" : "docs"}
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<div className="grid gap-3">
|
|
33
|
+
{docs.map((doc) => (
|
|
34
|
+
<DocCard key={`${doc.category}/${doc.slug}`} doc={doc} />
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
|
-
import TagBadge from "./TagBadge";
|
|
6
5
|
import type { DocMeta } from "@/lib/types";
|
|
7
6
|
|
|
8
7
|
export default function DocCard({ doc }: { doc: DocMeta }) {
|
|
@@ -17,21 +16,25 @@ export default function DocCard({ doc }: { doc: DocMeta }) {
|
|
|
17
16
|
onKeyDown={(e) => {
|
|
18
17
|
if (e.key === "Enter") router.push(href);
|
|
19
18
|
}}
|
|
20
|
-
className="dark:bg-navy-800/
|
|
19
|
+
className="hover:border-navy-400 dark:bg-navy-900/80 dark:hover:bg-navy-800/60 block cursor-pointer rounded-xl border border-slate-300 p-4 transition-all duration-200 hover:-translate-y-0.5 dark:border-[#a0a0a01c]"
|
|
21
20
|
>
|
|
22
|
-
<h3 className="mb-2 font-
|
|
21
|
+
<h3 className="mb-2 font-medium text-slate-900 dark:text-slate-100">
|
|
23
22
|
<Link href={href}>{doc.frontmatter.title}</Link>
|
|
24
23
|
</h3>
|
|
25
|
-
<p className="mb-3 line-clamp-2 text-sm text-slate-
|
|
24
|
+
<p className="mb-3 line-clamp-2 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
|
|
26
25
|
{doc.excerpt}
|
|
27
26
|
</p>
|
|
28
27
|
<div className="flex items-center justify-between">
|
|
29
|
-
<div
|
|
30
|
-
className="flex flex-wrap items-center gap-2"
|
|
31
|
-
onClick={(e) => e.stopPropagation()}
|
|
32
|
-
>
|
|
28
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
33
29
|
{doc.frontmatter.tags.slice(0, 3).map((tag) => (
|
|
34
|
-
<
|
|
30
|
+
<Link
|
|
31
|
+
key={tag}
|
|
32
|
+
href={`/tag/${encodeURIComponent(tag)}/`}
|
|
33
|
+
onClick={(e) => e.stopPropagation()}
|
|
34
|
+
className="inline-flex items-center justify-center rounded-full border border-slate-200 bg-slate-100 px-2.5 py-0.5 text-xs text-slate-600 dark:border-slate-600 dark:bg-slate-600/50 dark:text-slate-300"
|
|
35
|
+
>
|
|
36
|
+
{tag}
|
|
37
|
+
</Link>
|
|
35
38
|
))}
|
|
36
39
|
{doc.frontmatter.tags.length > 3 && (
|
|
37
40
|
<span className="text-xs text-slate-400">
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { DialogDescription, DialogTitle } from "@radix-ui/react-dialog";
|
|
4
|
+
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
|
4
5
|
import { Command } from "cmdk";
|
|
5
6
|
import { useRouter } from "next/navigation";
|
|
6
|
-
import {
|
|
7
|
-
import { DialogTitle, DialogDescription } from "@radix-ui/react-dialog";
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
8
|
|
|
9
9
|
interface PagefindResult {
|
|
10
10
|
id: string;
|
|
@@ -41,7 +41,7 @@ function normalizePagefindUrl(url: string): string {
|
|
|
41
41
|
function ExcerptMarkup({ html }: { html: string }) {
|
|
42
42
|
return (
|
|
43
43
|
<p
|
|
44
|
-
className="mt-0.5 line-clamp-2 text-sm text-slate-500 dark:text-slate-400 [&_mark]:bg-transparent [&_mark]
|
|
44
|
+
className="mt-0.5 line-clamp-2 text-sm text-slate-500 dark:text-slate-400 [&_mark]:bg-transparent [&_mark]:text-yellow-600!"
|
|
45
45
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
46
46
|
/>
|
|
47
47
|
);
|
|
@@ -162,9 +162,6 @@ export default function PagefindSearch() {
|
|
|
162
162
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
163
163
|
/>
|
|
164
164
|
</svg>
|
|
165
|
-
<kbd className="dark:bg-navy-800/80 hidden items-center gap-0.5 rounded border border-slate-300 px-1.5 py-0.5 text-[10px] font-medium text-slate-400 sm:inline-flex dark:border-[#a0a0a01c]">
|
|
166
|
-
<span className="text-xs">⌘</span>K
|
|
167
|
-
</kbd>
|
|
168
165
|
</button>
|
|
169
166
|
|
|
170
167
|
{/* cmdk dialog */}
|
|
@@ -12,9 +12,9 @@ export default function Sidebar({
|
|
|
12
12
|
const pathname = usePathname();
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
|
-
<aside className="dark:bg-navy-
|
|
15
|
+
<aside className="dark:bg-navy-900/80 fixed top-14 left-0 z-10 hidden h-[calc(100vh-3.5rem)] w-64 shrink-0 border-r border-slate-200 bg-slate-50 md:flex md:flex-col lg:w-[300px] dark:border-[#a0a0a01c]">
|
|
16
16
|
<nav
|
|
17
|
-
className="scrollbar-none flex-1 space-y-1 overflow-y-auto
|
|
17
|
+
className="scrollbar-none flex-1 space-y-1 overflow-y-auto px-2 py-4"
|
|
18
18
|
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
|
|
19
19
|
>
|
|
20
20
|
{categories.map((cat) => {
|
|
@@ -23,14 +23,14 @@ export default function Sidebar({
|
|
|
23
23
|
<Link
|
|
24
24
|
key={cat.name}
|
|
25
25
|
href={`/${cat.name}/`}
|
|
26
|
-
className={`flex items-center justify-between rounded-full
|
|
26
|
+
className={`flex items-center justify-between rounded-full py-1.5 pr-2 pl-4 text-sm capitalize transition-colors duration-200 ${
|
|
27
27
|
isActive
|
|
28
28
|
? "font-medium text-amber-700 dark:text-amber-400"
|
|
29
|
-
: "dark:hover:bg-navy-800/
|
|
29
|
+
: "dark:hover:bg-navy-800/70 text-slate-600 hover:bg-slate-200/50 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
|
30
30
|
}`}
|
|
31
31
|
>
|
|
32
32
|
<span>{cat.name.replace(/[_-]/g, " ")}</span>
|
|
33
|
-
<span className="
|
|
33
|
+
<span className="flex h-7 w-7 scale-85 items-center justify-center rounded-full border border-slate-200 bg-slate-200/60 text-xs font-semibold text-slate-400 dark:border-slate-600/50 dark:bg-slate-600/40 dark:text-slate-400/80">
|
|
34
34
|
{cat.docCount}
|
|
35
35
|
</span>
|
|
36
36
|
</Link>
|
|
@@ -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 border-b border-slate-
|
|
38
|
+
<h4 className="mb-3 border-b border-slate-300 pb-2 text-xs font-medium tracking-wider text-slate-500 uppercase dark:border-slate-600">
|
|
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
|
-
? "
|
|
50
|
+
? "text-slate-900 dark:text-white"
|
|
51
51
|
: "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
|
52
52
|
}`}
|
|
53
53
|
>
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
|
|
3
|
-
export default function TagBadge({
|
|
3
|
+
export default function TagBadge({
|
|
4
|
+
tag,
|
|
5
|
+
size = "md",
|
|
6
|
+
}: {
|
|
7
|
+
tag: string;
|
|
8
|
+
size?: "sm" | "md";
|
|
9
|
+
}) {
|
|
10
|
+
const sizeClass = size === "sm" ? "text-xs" : "text-sm";
|
|
11
|
+
const paddingClass = size === "sm" ? "px-2 py-0.5" : "px-2.5 py-0.5";
|
|
4
12
|
return (
|
|
5
13
|
<Link
|
|
6
|
-
href={`/
|
|
7
|
-
className=
|
|
14
|
+
href={`/tag/${encodeURIComponent(tag)}/`}
|
|
15
|
+
className={`hover:border-navy-400 dark:bg-navy-900/80 dark:hover:bg-navy-800/80 inline-block cursor-pointer rounded-full border border-slate-200 bg-slate-50 ${paddingClass} ${sizeClass} text-slate-700 duration-200 hover:-translate-y-0.5 hover:text-slate-800 dark:border-[#a0a0a01c] dark:text-slate-400 dark:hover:text-slate-200`}
|
|
8
16
|
>
|
|
9
17
|
{tag}
|
|
10
18
|
</Link>
|
|
@@ -20,7 +20,7 @@ export default function ThemeToggle() {
|
|
|
20
20
|
return (
|
|
21
21
|
<button
|
|
22
22
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
23
|
-
className="
|
|
23
|
+
className="cursor-pointer rounded-lg p-2 text-slate-500 transition-colors duration-200 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
|
24
24
|
aria-label="Toggle theme"
|
|
25
25
|
>
|
|
26
26
|
{theme === "dark" ? (
|
|
@@ -7,7 +7,7 @@ export default function Footer({ isHome = false }: { isHome?: boolean }) {
|
|
|
7
7
|
isHome ? "left-0" : "left-0 md:left-64 lg:left-[300px]"
|
|
8
8
|
}`}
|
|
9
9
|
>
|
|
10
|
-
<div className="flex flex-row flex-wrap items-center gap-1 px-6 py-3 text-center text-xs text-slate-
|
|
10
|
+
<div className="flex flex-row flex-wrap items-center gap-1 px-6 py-3 text-center text-xs text-slate-600 dark:text-slate-400">
|
|
11
11
|
<div className="flex items-center gap-2">
|
|
12
12
|
<Image src="/logo.svg" alt="Coding Friend" width={20} height={20} />
|
|
13
13
|
<span>
|
|
@@ -16,7 +16,7 @@ export default function Footer({ isHome = false }: { isHome?: boolean }) {
|
|
|
16
16
|
href="https://github.com/dinhanhthi/coding-friend"
|
|
17
17
|
target="_blank"
|
|
18
18
|
rel="noopener noreferrer"
|
|
19
|
-
className="text-
|
|
19
|
+
className="text-sky-600 hover:text-orange-500 dark:text-sky-300 dark:hover:text-orange-400"
|
|
20
20
|
>
|
|
21
21
|
Coding Friend
|
|
22
22
|
</a>
|
|
@@ -25,7 +25,7 @@ export default function Footer({ isHome = false }: { isHome?: boolean }) {
|
|
|
25
25
|
href="https://dinhanhthi.com"
|
|
26
26
|
target="_blank"
|
|
27
27
|
rel="noopener noreferrer"
|
|
28
|
-
className="text-
|
|
28
|
+
className="text-sky-600 hover:text-orange-500 dark:text-sky-300 dark:hover:text-orange-400"
|
|
29
29
|
>
|
|
30
30
|
Anh-Thi Dinh
|
|
31
31
|
</a>
|
|
@@ -199,6 +199,10 @@ export function extractHeadings(content: string): TocItem[] {
|
|
|
199
199
|
return headings;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
export function getDocsByTag(tag: string): DocMeta[] {
|
|
203
|
+
return getAllDocs().filter((d) => d.frontmatter.tags.includes(tag));
|
|
204
|
+
}
|
|
205
|
+
|
|
202
206
|
export function getAllTags(): { tag: string; count: number }[] {
|
|
203
207
|
const tagMap = new Map<string, number>();
|
|
204
208
|
for (const doc of getAllDocs()) {
|