coding-friend-cli 1.27.1 → 1.28.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 +9 -3
- package/dist/{chunk-PRIH34UB.js → chunk-DHH6SRXV.js} +5 -1
- package/dist/{chunk-75IEE2OE.js → chunk-E7OY4UWW.js} +2 -2
- package/dist/chunk-J4N2ODQ5.js +191 -0
- package/dist/{chunk-YCWRBOB3.js → chunk-RGQGKOBH.js} +1 -1
- package/dist/{chunk-GDZTU2Z4.js → chunk-XQ5KAXKL.js} +1 -1
- package/dist/{chunk-BX3DRKTU.js → chunk-XROT7L2F.js} +6 -5
- package/dist/{config-N55LTJ76.js → config-U2WEZXF6.js} +4 -4
- package/dist/{dev-BSXR55PZ.js → dev-4OAJCQRQ.js} +29 -5
- package/dist/guide-F2FLCDYO.js +113 -0
- package/dist/{host-JVLWYY5A.js → host-2KBOPER6.js} +2 -2
- package/dist/index.js +35 -26
- package/dist/{init-KX6Y3ELD.js → init-R5L2GAFV.js} +6 -6
- package/dist/{install-G4XYNVL3.js → install-XZC6UO54.js} +3 -3
- package/dist/{mcp-DABSO5PE.js → mcp-CNZLJ57B.js} +4 -4
- package/dist/{memory-7Q5P7LIU.js → memory-KU2MOCPQ.js} +4 -4
- package/dist/{session-DMQZXZ7F.js → session-ZGNQKV4M.js} +26 -7
- package/dist/{status-K7TBUAXT.js → status-JXUXVHRH.js} +4 -4
- package/dist/{statusline-CDGADB3C.js → statusline-RMTO4MQA.js} +2 -2
- package/dist/{update-6M3YFHS6.js → update-HS6A2C5V.js} +3 -3
- package/lib/cf-memory/README.md +4 -4
- package/lib/cf-memory/src/__tests__/claude-md.test.ts +105 -0
- package/lib/cf-memory/src/__tests__/tier.test.ts +59 -0
- package/lib/cf-memory/src/daemon/process.ts +1 -1
- package/lib/cf-memory/src/lib/claude-md.ts +12 -3
- package/lib/cf-memory/src/lib/tier.ts +19 -7
- package/lib/cf-memory/src/tools/delete.ts +3 -3
- package/lib/cf-memory/src/tools/store.ts +14 -2
- package/lib/cf-memory/src/tools/update.ts +21 -3
- package/package.json +7 -4
- package/dist/chunk-2HQVNQB7.js +0 -71
package/README.md
CHANGED
|
@@ -147,19 +147,25 @@ npm ls -g coding-friend-cli
|
|
|
147
147
|
|
|
148
148
|
### Running tests
|
|
149
149
|
|
|
150
|
-
Tests are written with [Vitest](https://vitest.dev/)
|
|
150
|
+
Tests are written with [Vitest](https://vitest.dev/).
|
|
151
151
|
|
|
152
152
|
```bash
|
|
153
153
|
cd cli
|
|
154
154
|
|
|
155
|
-
# Run all tests once
|
|
155
|
+
# Run all unit tests once
|
|
156
156
|
npm test
|
|
157
157
|
|
|
158
158
|
# Watch mode (re-runs on file changes)
|
|
159
159
|
npm run test:watch
|
|
160
|
+
|
|
161
|
+
# Run E2E tests (real CLI commands against temp directories)
|
|
162
|
+
npm run test:e2e
|
|
163
|
+
|
|
164
|
+
# Run E2E tests in Docker (isolated, no host pollution)
|
|
165
|
+
cd e2e && docker compose up --build --abort-on-container-exit
|
|
160
166
|
```
|
|
161
167
|
|
|
162
|
-
|
|
168
|
+
Unit tests live in `src/**/__tests__/` and use mocks. E2E tests live in `e2e/` and run actual CLI commands — see [`e2e/README.md`](e2e/README.md) for details.
|
|
163
169
|
|
|
164
170
|
## Publish CLI to npm
|
|
165
171
|
|
|
@@ -6,12 +6,16 @@ var STATUSLINE_COMPONENTS = [
|
|
|
6
6
|
{ id: "branch", label: "Git branch (\u2387 main)" },
|
|
7
7
|
{
|
|
8
8
|
id: "account",
|
|
9
|
-
label: "Account info \u2014 name, email
|
|
9
|
+
label: "Account info \u2014 name, email (from ~/.claude.json)"
|
|
10
10
|
},
|
|
11
11
|
{ id: "context", label: "Context window usage (ctx 42%)" },
|
|
12
12
|
{
|
|
13
13
|
id: "rate_limit",
|
|
14
14
|
label: "Rate limit \u2014 current & weekly usage (requires curl & jq)"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "task_agent",
|
|
18
|
+
label: "Task progress & active agent (Tasks: 2/5 | Agent: cf-reviewer)"
|
|
15
19
|
}
|
|
16
20
|
];
|
|
17
21
|
var ALL_COMPONENT_IDS = STATUSLINE_COMPONENTS.map((c) => c.id);
|
|
@@ -7,14 +7,14 @@ import {
|
|
|
7
7
|
getMemoryMcpStatus,
|
|
8
8
|
memoryConfigMenu,
|
|
9
9
|
writeMemoryMcpEntry
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-XROT7L2F.js";
|
|
11
11
|
import {
|
|
12
12
|
getLibPath
|
|
13
13
|
} from "./chunk-RZRT7NGT.js";
|
|
14
14
|
import {
|
|
15
15
|
loadConfig,
|
|
16
16
|
resolveMemoryDir
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-J4N2ODQ5.js";
|
|
18
18
|
import {
|
|
19
19
|
showConfigHint
|
|
20
20
|
} from "./chunk-HPNRQYLM.js";
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CONFIG
|
|
3
|
+
} from "./chunk-DHH6SRXV.js";
|
|
4
|
+
import {
|
|
5
|
+
globalConfigPath,
|
|
6
|
+
localConfigPath,
|
|
7
|
+
resolvePath
|
|
8
|
+
} from "./chunk-TWKNGPBO.js";
|
|
9
|
+
import {
|
|
10
|
+
log
|
|
11
|
+
} from "./chunk-NREZK463.js";
|
|
12
|
+
import {
|
|
13
|
+
readJson
|
|
14
|
+
} from "./chunk-5UVDWG5L.js";
|
|
15
|
+
|
|
16
|
+
// src/lib/config.ts
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
function deepMerge(base, override) {
|
|
19
|
+
const result = { ...base };
|
|
20
|
+
for (const key of Object.keys(override)) {
|
|
21
|
+
const baseVal = result[key];
|
|
22
|
+
const overVal = override[key];
|
|
23
|
+
if (baseVal && overVal && typeof baseVal === "object" && typeof overVal === "object" && !Array.isArray(baseVal) && !Array.isArray(overVal)) {
|
|
24
|
+
result[key] = deepMerge(
|
|
25
|
+
baseVal,
|
|
26
|
+
overVal
|
|
27
|
+
);
|
|
28
|
+
} else {
|
|
29
|
+
result[key] = overVal;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
var LearnCategorySchema = z.object({
|
|
35
|
+
name: z.string(),
|
|
36
|
+
description: z.string()
|
|
37
|
+
});
|
|
38
|
+
var LearnConfigSchema = z.object({
|
|
39
|
+
language: z.string().optional(),
|
|
40
|
+
outputDir: z.string().optional(),
|
|
41
|
+
categories: z.array(LearnCategorySchema).optional(),
|
|
42
|
+
autoCommit: z.boolean().optional(),
|
|
43
|
+
readmeIndex: z.union([z.boolean(), z.literal("per-category")]).optional()
|
|
44
|
+
});
|
|
45
|
+
var StatuslineComponentSchema = z.enum([
|
|
46
|
+
"version",
|
|
47
|
+
"folder",
|
|
48
|
+
"model",
|
|
49
|
+
"branch",
|
|
50
|
+
"account",
|
|
51
|
+
"context",
|
|
52
|
+
"rate_limit",
|
|
53
|
+
"task_agent"
|
|
54
|
+
]);
|
|
55
|
+
var StatuslineConfigSchema = z.object({
|
|
56
|
+
components: z.array(StatuslineComponentSchema).optional()
|
|
57
|
+
});
|
|
58
|
+
var MemoryConfigSchema = z.object({
|
|
59
|
+
tier: z.enum(["auto", "full", "lite", "markdown"]).optional(),
|
|
60
|
+
daemon: z.object({
|
|
61
|
+
idleTimeout: z.number().optional()
|
|
62
|
+
}).optional(),
|
|
63
|
+
embedding: z.object({
|
|
64
|
+
provider: z.enum(["transformers", "ollama"]).optional(),
|
|
65
|
+
model: z.string().optional(),
|
|
66
|
+
ollamaUrl: z.string().optional()
|
|
67
|
+
}).optional(),
|
|
68
|
+
autoCapture: z.boolean().optional(),
|
|
69
|
+
autoStart: z.boolean().optional()
|
|
70
|
+
});
|
|
71
|
+
var ConfigSchema = z.strictObject({
|
|
72
|
+
language: z.string().optional(),
|
|
73
|
+
docsDir: z.string().optional(),
|
|
74
|
+
learn: LearnConfigSchema.optional(),
|
|
75
|
+
statusline: StatuslineConfigSchema.optional(),
|
|
76
|
+
memory: MemoryConfigSchema.optional(),
|
|
77
|
+
autoApprove: z.boolean().optional()
|
|
78
|
+
});
|
|
79
|
+
var KNOWN_KEYS = [
|
|
80
|
+
"language",
|
|
81
|
+
"docsDir",
|
|
82
|
+
"learn",
|
|
83
|
+
"statusline",
|
|
84
|
+
"memory",
|
|
85
|
+
"autoApprove"
|
|
86
|
+
];
|
|
87
|
+
function suggestKey(unknown) {
|
|
88
|
+
let best = null;
|
|
89
|
+
let bestDist = Infinity;
|
|
90
|
+
for (const known of KNOWN_KEYS) {
|
|
91
|
+
const dist = levenshtein(unknown.toLowerCase(), known.toLowerCase());
|
|
92
|
+
if (dist < bestDist && dist <= 3) {
|
|
93
|
+
bestDist = dist;
|
|
94
|
+
best = known;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return best;
|
|
98
|
+
}
|
|
99
|
+
function levenshtein(a, b) {
|
|
100
|
+
const m = a.length;
|
|
101
|
+
const n = b.length;
|
|
102
|
+
const dp = Array.from(
|
|
103
|
+
{ length: m + 1 },
|
|
104
|
+
() => Array(n + 1).fill(0)
|
|
105
|
+
);
|
|
106
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
107
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
108
|
+
for (let i = 1; i <= m; i++) {
|
|
109
|
+
for (let j = 1; j <= n; j++) {
|
|
110
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return dp[m][n];
|
|
114
|
+
}
|
|
115
|
+
function validateConfig(merged) {
|
|
116
|
+
const result = ConfigSchema.safeParse(merged);
|
|
117
|
+
if (result.success) return result.data;
|
|
118
|
+
const sanitized = { ...merged };
|
|
119
|
+
for (const issue of result.error.issues) {
|
|
120
|
+
const pathStr = issue.path.join(".");
|
|
121
|
+
if (issue.code === "unrecognized_keys") {
|
|
122
|
+
const keys = issue.keys ?? [];
|
|
123
|
+
for (const key of keys) {
|
|
124
|
+
const suggestion = suggestKey(key);
|
|
125
|
+
const hint = suggestion ? ` Did you mean '${suggestion}'?` : "";
|
|
126
|
+
log.warn(`Unknown config key '${key}'.${hint}`);
|
|
127
|
+
delete sanitized[key];
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
log.warn(`Config: ${pathStr} \u2014 ${issue.message}. Using default.`);
|
|
131
|
+
stripPath(sanitized, issue.path);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return deepMerge(
|
|
135
|
+
DEFAULT_CONFIG,
|
|
136
|
+
sanitized
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
function stripPath(obj, path) {
|
|
140
|
+
if (path.length === 0) return;
|
|
141
|
+
let current = obj;
|
|
142
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
143
|
+
const key = String(path[i]);
|
|
144
|
+
if (current[key] && typeof current[key] === "object") {
|
|
145
|
+
current = current[key];
|
|
146
|
+
} else {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
delete current[String(path[path.length - 1])];
|
|
151
|
+
}
|
|
152
|
+
function loadConfig() {
|
|
153
|
+
const global = readJson(globalConfigPath());
|
|
154
|
+
const local = readJson(localConfigPath());
|
|
155
|
+
const base = deepMerge(
|
|
156
|
+
DEFAULT_CONFIG,
|
|
157
|
+
global ?? {}
|
|
158
|
+
);
|
|
159
|
+
const merged = deepMerge(base, local ?? {});
|
|
160
|
+
return validateConfig(merged);
|
|
161
|
+
}
|
|
162
|
+
function resolveDocsDir(explicitPath) {
|
|
163
|
+
if (explicitPath) {
|
|
164
|
+
return resolvePath(explicitPath);
|
|
165
|
+
}
|
|
166
|
+
const local = readJson(localConfigPath());
|
|
167
|
+
if (local?.learn?.outputDir) {
|
|
168
|
+
return resolvePath(local.learn.outputDir);
|
|
169
|
+
}
|
|
170
|
+
const global = readJson(globalConfigPath());
|
|
171
|
+
if (global?.learn?.outputDir) {
|
|
172
|
+
return resolvePath(global.learn.outputDir);
|
|
173
|
+
}
|
|
174
|
+
return resolvePath("docs/learn");
|
|
175
|
+
}
|
|
176
|
+
function resolveMemoryDir(explicitPath) {
|
|
177
|
+
if (explicitPath) {
|
|
178
|
+
return resolvePath(explicitPath);
|
|
179
|
+
}
|
|
180
|
+
const config = loadConfig();
|
|
181
|
+
if (config.docsDir) {
|
|
182
|
+
return resolvePath(`${config.docsDir}/memory`);
|
|
183
|
+
}
|
|
184
|
+
return resolvePath("docs/memory");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export {
|
|
188
|
+
loadConfig,
|
|
189
|
+
resolveDocsDir,
|
|
190
|
+
resolveMemoryDir
|
|
191
|
+
};
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-RZRT7NGT.js";
|
|
4
4
|
import {
|
|
5
5
|
resolveMemoryDir
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-J4N2ODQ5.js";
|
|
7
7
|
import {
|
|
8
8
|
BACK,
|
|
9
9
|
askScope,
|
|
@@ -222,16 +222,17 @@ async function editMemoryEmbedding(globalCfg, localCfg) {
|
|
|
222
222
|
async function editMemoryDaemonTimeout(globalCfg, localCfg) {
|
|
223
223
|
const currentDaemon = getMergedMemoryValue("daemon", globalCfg, localCfg);
|
|
224
224
|
const currentMs = currentDaemon?.idleTimeout;
|
|
225
|
-
const currentMin = currentMs ? currentMs / 6e4 : void 0;
|
|
225
|
+
const currentMin = currentMs !== void 0 ? currentMs / 6e4 : void 0;
|
|
226
226
|
if (currentMin !== void 0) {
|
|
227
227
|
log.dim(`Current: ${currentMin} minutes`);
|
|
228
228
|
}
|
|
229
229
|
const value = await input({
|
|
230
|
-
message: "Daemon idle timeout (minutes):",
|
|
231
|
-
default: String(currentMin ??
|
|
230
|
+
message: "Daemon idle timeout (minutes, 0 = no timeout):",
|
|
231
|
+
default: String(currentMin ?? 0),
|
|
232
232
|
validate: (val) => {
|
|
233
233
|
const n = Number(val);
|
|
234
|
-
if (isNaN(n) || n <
|
|
234
|
+
if (isNaN(n) || n < 0)
|
|
235
|
+
return "Must be 0 (no timeout) or a positive number";
|
|
235
236
|
return true;
|
|
236
237
|
}
|
|
237
238
|
});
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
memoryConfigMenu
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-XROT7L2F.js";
|
|
4
4
|
import "./chunk-RZRT7NGT.js";
|
|
5
|
-
import "./chunk-
|
|
5
|
+
import "./chunk-J4N2ODQ5.js";
|
|
6
6
|
import {
|
|
7
7
|
findStatuslineHookPath,
|
|
8
8
|
isStatuslineConfigured,
|
|
9
9
|
saveStatuslineConfig,
|
|
10
10
|
selectStatuslineComponents,
|
|
11
11
|
writeStatuslineSettings
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-XQ5KAXKL.js";
|
|
13
13
|
import {
|
|
14
14
|
ALL_COMPONENT_IDS,
|
|
15
15
|
DEFAULT_CONFIG
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-DHH6SRXV.js";
|
|
17
17
|
import {
|
|
18
18
|
ensureShellCompletion,
|
|
19
19
|
hasShellCompletion,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ensureStatusline
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-XQ5KAXKL.js";
|
|
4
|
+
import "./chunk-DHH6SRXV.js";
|
|
5
5
|
import {
|
|
6
6
|
ensureShellCompletion
|
|
7
7
|
} from "./chunk-DMJCO6LJ.js";
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
existsSync,
|
|
33
33
|
unlinkSync,
|
|
34
34
|
readdirSync,
|
|
35
|
+
readFileSync,
|
|
35
36
|
statSync,
|
|
36
37
|
mkdirSync,
|
|
37
38
|
copyFileSync
|
|
@@ -255,11 +256,29 @@ async function devSyncCommand() {
|
|
|
255
256
|
}
|
|
256
257
|
const shortDest = cacheVersionDir.replace(homedir(), "~");
|
|
257
258
|
log.step(`Syncing ${chalk.cyan(pluginSrcDir)} \u2192 ${chalk.dim(shortDest)}`);
|
|
259
|
+
const cachedHooksPath = join(cacheVersionDir, "hooks", "hooks.json");
|
|
260
|
+
const sourceHooksPath = join(pluginSrcDir, "hooks", "hooks.json");
|
|
261
|
+
let hooksEventsChanged = false;
|
|
262
|
+
try {
|
|
263
|
+
const cachedHooks = JSON.parse(readFileSync(cachedHooksPath, "utf-8"));
|
|
264
|
+
const sourceHooks = JSON.parse(readFileSync(sourceHooksPath, "utf-8"));
|
|
265
|
+
const cachedEvents = Object.keys(cachedHooks.hooks || {}).sort();
|
|
266
|
+
const sourceEvents = Object.keys(sourceHooks.hooks || {}).sort();
|
|
267
|
+
hooksEventsChanged = JSON.stringify(cachedEvents) !== JSON.stringify(sourceEvents);
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
258
270
|
const fileCount = { n: 0 };
|
|
259
271
|
copyDirRecursive(pluginSrcDir, cacheVersionDir, fileCount);
|
|
260
|
-
|
|
261
|
-
`Synced ${chalk.green(fileCount.n)} files
|
|
262
|
-
|
|
272
|
+
if (hooksEventsChanged) {
|
|
273
|
+
log.success(`Synced ${chalk.green(fileCount.n)} files.`);
|
|
274
|
+
log.warn(
|
|
275
|
+
`hooks.json event types changed. Run ${chalk.bold("cf dev restart")} for Claude Code to register the new hook events.`
|
|
276
|
+
);
|
|
277
|
+
} else {
|
|
278
|
+
log.success(
|
|
279
|
+
`Synced ${chalk.green(fileCount.n)} files. Restart Claude Code to apply changes.`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
263
282
|
}
|
|
264
283
|
async function devReinstall(path, label) {
|
|
265
284
|
const state = getDevState();
|
|
@@ -288,6 +307,11 @@ async function devStatusCommand() {
|
|
|
288
307
|
if (state) {
|
|
289
308
|
log.info(`Dev mode: ${chalk.green("ON")}`);
|
|
290
309
|
log.info(`Local path: ${chalk.cyan(state.localPath)}`);
|
|
310
|
+
if (!existsSync(state.localPath)) {
|
|
311
|
+
log.warn(
|
|
312
|
+
`Dev mode path no longer exists: ${state.localPath}. Run 'cf host <new-path>' to update.`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
291
315
|
log.dim(`Since: ${state.savedAt}`);
|
|
292
316
|
} else {
|
|
293
317
|
log.info(`Dev mode: ${chalk.yellow("OFF")}`);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
devStatePath,
|
|
3
|
+
pluginCachePath
|
|
4
|
+
} from "./chunk-TWKNGPBO.js";
|
|
5
|
+
import {
|
|
6
|
+
log
|
|
7
|
+
} from "./chunk-NREZK463.js";
|
|
8
|
+
import {
|
|
9
|
+
readJson
|
|
10
|
+
} from "./chunk-5UVDWG5L.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/guide.ts
|
|
13
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
|
|
14
|
+
import { join, resolve } from "path";
|
|
15
|
+
var GUIDE_TEMPLATE = `# Custom Guide for {{SKILL_NAME}}
|
|
16
|
+
|
|
17
|
+
<!-- Sections are optional \u2014 include only the ones you need. -->
|
|
18
|
+
<!-- This file is loaded automatically when the skill runs. -->
|
|
19
|
+
|
|
20
|
+
## Before
|
|
21
|
+
|
|
22
|
+
<!-- Steps to execute BEFORE the skill's first step. -->
|
|
23
|
+
<!-- Example: run a linter, check prerequisites, set up context. -->
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
|
|
27
|
+
<!-- Additional rules to apply THROUGHOUT all steps of the skill. -->
|
|
28
|
+
<!-- Example: "always use TypeScript strict mode", "never modify tests in src/__tests__". -->
|
|
29
|
+
|
|
30
|
+
## After
|
|
31
|
+
|
|
32
|
+
<!-- Steps to execute AFTER the skill's final step. -->
|
|
33
|
+
<!-- Example: run a formatter, update a changelog, notify a channel. -->
|
|
34
|
+
`;
|
|
35
|
+
function findPluginSkillsDir() {
|
|
36
|
+
const devState = readJson(devStatePath());
|
|
37
|
+
if (devState?.localPath) {
|
|
38
|
+
const dir = join(devState.localPath, "plugin", "skills");
|
|
39
|
+
if (existsSync(dir)) return dir;
|
|
40
|
+
}
|
|
41
|
+
const cachePath = pluginCachePath();
|
|
42
|
+
if (existsSync(cachePath)) {
|
|
43
|
+
try {
|
|
44
|
+
const versions = readdirSync(cachePath, { withFileTypes: true }).filter((e) => e.isDirectory() && /^\d/.test(e.name)).map((e) => e.name).sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
|
|
45
|
+
for (const ver of versions) {
|
|
46
|
+
const dir = join(cachePath, ver, "plugin", "skills");
|
|
47
|
+
if (existsSync(dir)) return dir;
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function skillExists(skillsDir, skillName) {
|
|
55
|
+
return existsSync(join(skillsDir, skillName, "SKILL.md"));
|
|
56
|
+
}
|
|
57
|
+
function guideCreateCommand(skillName) {
|
|
58
|
+
if (!/^[a-z0-9-]+$/.test(skillName)) {
|
|
59
|
+
log.error(
|
|
60
|
+
`Invalid skill name "${skillName}". Use lowercase letters, numbers, and hyphens only.`
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const skillsDir = findPluginSkillsDir();
|
|
65
|
+
if (!skillsDir || !skillExists(skillsDir, skillName)) {
|
|
66
|
+
log.error(
|
|
67
|
+
`Skill "${skillName}" not found. Run "cf guide list" to see available skills.`
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const guideDir = resolve(".coding-friend", "skills", `${skillName}-custom`);
|
|
72
|
+
const guidePath = join(guideDir, "SKILL.md");
|
|
73
|
+
if (existsSync(guidePath)) {
|
|
74
|
+
log.warn(`Custom guide already exists: ${guidePath}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
mkdirSync(guideDir, { recursive: true });
|
|
78
|
+
writeFileSync(
|
|
79
|
+
guidePath,
|
|
80
|
+
GUIDE_TEMPLATE.replace(/\{\{SKILL_NAME\}\}/g, skillName)
|
|
81
|
+
);
|
|
82
|
+
log.success(`Created custom guide: ${guidePath}`);
|
|
83
|
+
log.dim("Edit the file to add your Before/Rules/After sections.");
|
|
84
|
+
}
|
|
85
|
+
function guideListCommand() {
|
|
86
|
+
const customDir = resolve(".coding-friend", "skills");
|
|
87
|
+
if (!existsSync(customDir)) {
|
|
88
|
+
log.dim(
|
|
89
|
+
"No custom guides found. Run `cf guide create <skill-name>` to create one."
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const entries = readdirSync(customDir, { withFileTypes: true }).filter(
|
|
94
|
+
(e) => e.isDirectory() && e.name.endsWith("-custom") && existsSync(join(customDir, e.name, "SKILL.md"))
|
|
95
|
+
).map((e) => e.name.replace(/-custom$/, ""));
|
|
96
|
+
if (entries.length === 0) {
|
|
97
|
+
log.dim(
|
|
98
|
+
"No custom guides found. Run `cf guide create <skill-name>` to create one."
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const skillsDir = findPluginSkillsDir();
|
|
103
|
+
log.info(`Custom guides (${entries.length}):`);
|
|
104
|
+
for (const name of entries) {
|
|
105
|
+
const path = join(customDir, `${name}-custom`, "SKILL.md");
|
|
106
|
+
const exists = skillsDir != null && skillExists(skillsDir, name) ? "\u2714" : "\u26A0 skill not found";
|
|
107
|
+
log.info(` ${name} \u2192 ${path} (${exists})`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
guideCreateCommand,
|
|
112
|
+
guideListCommand
|
|
113
|
+
};
|