coding-friend-cli 1.7.0 → 1.9.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-7N64TDZ6.js +277 -0
- package/dist/{chunk-HSQX3PKW.js → chunk-BPLN4LDL.js} +3 -5
- package/dist/chunk-FYHHNX7K.js +35 -0
- package/dist/{chunk-MRTR7TJ4.js → chunk-HFLBFX6J.js} +2 -4
- package/dist/{chunk-WXBI2HUL.js → chunk-PGLUEN7D.js} +0 -1
- package/dist/chunk-QQ5SVZET.js +135 -0
- package/dist/chunk-RZRT7NGT.js +18 -0
- package/dist/{chunk-WHCJT7E2.js → chunk-TPRZHSFS.js} +38 -1
- package/dist/{chunk-DHPWBSF5.js → chunk-VYMXERKM.js} +18 -14
- package/dist/{chunk-6DUFTBTO.js → chunk-W5CD7WTX.js} +1 -0
- package/dist/config-VAML7F7K.js +567 -0
- package/dist/{dev-QW6VPG4G.js → dev-2GBY3GKC.js} +9 -11
- package/dist/{host-EERZVOHY.js → host-LOG5RPZ7.js} +7 -6
- package/dist/index.js +36 -13
- package/dist/init-CIEDOFNC.js +512 -0
- package/dist/{install-ORIDNWRW.js → install-D4NW3OAA.js} +7 -8
- package/dist/{mcp-3MUUQZQD.js → mcp-ORMYETXQ.js} +7 -6
- package/dist/postinstall.js +2 -2
- package/dist/session-NCQQJRSH.js +222 -0
- package/dist/{statusline-BWGI5PQ5.js → statusline-5HWRTSVL.js} +4 -5
- package/dist/{uninstall-KOAJFPD6.js → uninstall-SOHU5WGK.js} +8 -10
- package/dist/update-LA4B3LN4.js +16 -0
- package/package.json +1 -1
- package/dist/chunk-4PLV2ENL.js +0 -144
- package/dist/chunk-IUTXHCP7.js +0 -28
- package/dist/chunk-WK5YYHXM.js +0 -44
- package/dist/init-5BJVESH7.js +0 -529
- package/dist/json-2XS56OJY.js +0 -10
- package/dist/update-GW37S23M.js +0 -17
package/dist/index.js
CHANGED
|
@@ -14,33 +14,56 @@ program.name("cf").description(
|
|
|
14
14
|
"coding-friend CLI \u2014 host learning docs, setup MCP, init projects"
|
|
15
15
|
).version(pkg.version, "-v, --version");
|
|
16
16
|
program.command("install").description("Install the Coding Friend plugin into Claude Code").action(async () => {
|
|
17
|
-
const { installCommand } = await import("./install-
|
|
17
|
+
const { installCommand } = await import("./install-D4NW3OAA.js");
|
|
18
18
|
await installCommand();
|
|
19
19
|
});
|
|
20
20
|
program.command("uninstall").description("Uninstall the Coding Friend plugin from Claude Code").action(async () => {
|
|
21
|
-
const { uninstallCommand } = await import("./uninstall-
|
|
21
|
+
const { uninstallCommand } = await import("./uninstall-SOHU5WGK.js");
|
|
22
22
|
await uninstallCommand();
|
|
23
23
|
});
|
|
24
24
|
program.command("init").description("Initialize coding-friend in current project").action(async () => {
|
|
25
|
-
const { initCommand } = await import("./init-
|
|
25
|
+
const { initCommand } = await import("./init-CIEDOFNC.js");
|
|
26
26
|
await initCommand();
|
|
27
27
|
});
|
|
28
|
+
program.command("config").description("Manage Coding Friend configuration").action(async () => {
|
|
29
|
+
const { configCommand } = await import("./config-VAML7F7K.js");
|
|
30
|
+
await configCommand();
|
|
31
|
+
});
|
|
28
32
|
program.command("host").description("Build and serve learning docs as a static website").argument("[path]", "path to docs folder").option("-p, --port <port>", "port number", "3333").action(async (path, opts) => {
|
|
29
|
-
const { hostCommand } = await import("./host-
|
|
33
|
+
const { hostCommand } = await import("./host-LOG5RPZ7.js");
|
|
30
34
|
await hostCommand(path, opts);
|
|
31
35
|
});
|
|
32
36
|
program.command("mcp").description("Setup MCP server for learning docs").argument("[path]", "path to docs folder").action(async (path) => {
|
|
33
|
-
const { mcpCommand } = await import("./mcp-
|
|
37
|
+
const { mcpCommand } = await import("./mcp-ORMYETXQ.js");
|
|
34
38
|
await mcpCommand(path);
|
|
35
39
|
});
|
|
36
40
|
program.command("statusline").description("Setup coding-friend statusline in Claude Code").action(async () => {
|
|
37
|
-
const { statuslineCommand } = await import("./statusline-
|
|
41
|
+
const { statuslineCommand } = await import("./statusline-5HWRTSVL.js");
|
|
38
42
|
await statuslineCommand();
|
|
39
43
|
});
|
|
40
44
|
program.command("update").description("Update coding-friend plugin, CLI, and statusline").option("--cli", "Update only the CLI (npm package)").option("--plugin", "Update only the Claude Code plugin").option("--statusline", "Update only the statusline").action(async (opts) => {
|
|
41
|
-
const { updateCommand } = await import("./update-
|
|
45
|
+
const { updateCommand } = await import("./update-LA4B3LN4.js");
|
|
42
46
|
await updateCommand(opts);
|
|
43
47
|
});
|
|
48
|
+
var session = program.command("session").description("Save and load Claude Code sessions across machines");
|
|
49
|
+
program.addHelpText(
|
|
50
|
+
"after",
|
|
51
|
+
`
|
|
52
|
+
Session subcommands:
|
|
53
|
+
session save Save current session to sync folder (use /cf-session inside a conversation)
|
|
54
|
+
session load Load a saved session from sync folder and print resume command`
|
|
55
|
+
);
|
|
56
|
+
session.command("save").description("Save current Claude Code session to sync folder").option(
|
|
57
|
+
"-s, --session-id <id>",
|
|
58
|
+
"session UUID to save (default: auto-detect newest)"
|
|
59
|
+
).option("-l, --label <label>", "label for this session").action(async (opts) => {
|
|
60
|
+
const { sessionSaveCommand } = await import("./session-NCQQJRSH.js");
|
|
61
|
+
await sessionSaveCommand(opts);
|
|
62
|
+
});
|
|
63
|
+
session.command("load").description("Load a saved session from sync folder").action(async () => {
|
|
64
|
+
const { sessionLoadCommand } = await import("./session-NCQQJRSH.js");
|
|
65
|
+
await sessionLoadCommand();
|
|
66
|
+
});
|
|
44
67
|
var dev = program.command("dev").description("Development mode commands");
|
|
45
68
|
program.addHelpText(
|
|
46
69
|
"after",
|
|
@@ -54,35 +77,35 @@ Dev subcommands:
|
|
|
54
77
|
dev update [path] Update local dev plugin to latest version`
|
|
55
78
|
);
|
|
56
79
|
dev.command("on").description("Switch to local plugin source").argument("[path]", "path to local coding-friend repo (default: cwd)").action(async (path) => {
|
|
57
|
-
const { devOnCommand } = await import("./dev-
|
|
80
|
+
const { devOnCommand } = await import("./dev-2GBY3GKC.js");
|
|
58
81
|
await devOnCommand(path);
|
|
59
82
|
});
|
|
60
83
|
dev.command("off").description("Switch back to remote marketplace").action(async () => {
|
|
61
|
-
const { devOffCommand } = await import("./dev-
|
|
84
|
+
const { devOffCommand } = await import("./dev-2GBY3GKC.js");
|
|
62
85
|
await devOffCommand();
|
|
63
86
|
});
|
|
64
87
|
dev.command("status").description("Show current dev mode").action(async () => {
|
|
65
|
-
const { devStatusCommand } = await import("./dev-
|
|
88
|
+
const { devStatusCommand } = await import("./dev-2GBY3GKC.js");
|
|
66
89
|
await devStatusCommand();
|
|
67
90
|
});
|
|
68
91
|
dev.command("sync").description(
|
|
69
92
|
"Copy local source files to plugin cache (no version bump needed)"
|
|
70
93
|
).action(async () => {
|
|
71
|
-
const { devSyncCommand } = await import("./dev-
|
|
94
|
+
const { devSyncCommand } = await import("./dev-2GBY3GKC.js");
|
|
72
95
|
await devSyncCommand();
|
|
73
96
|
});
|
|
74
97
|
dev.command("restart").description("Reinstall local dev plugin (off + on)").argument(
|
|
75
98
|
"[path]",
|
|
76
99
|
"path to local coding-friend repo (default: saved path or cwd)"
|
|
77
100
|
).action(async (path) => {
|
|
78
|
-
const { devRestartCommand } = await import("./dev-
|
|
101
|
+
const { devRestartCommand } = await import("./dev-2GBY3GKC.js");
|
|
79
102
|
await devRestartCommand(path);
|
|
80
103
|
});
|
|
81
104
|
dev.command("update").description("Update local dev plugin to latest version (off + on)").argument(
|
|
82
105
|
"[path]",
|
|
83
106
|
"path to local coding-friend repo (default: saved path or cwd)"
|
|
84
107
|
).action(async (path) => {
|
|
85
|
-
const { devUpdateCommand } = await import("./dev-
|
|
108
|
+
const { devUpdateCommand } = await import("./dev-2GBY3GKC.js");
|
|
86
109
|
await devUpdateCommand(path);
|
|
87
110
|
});
|
|
88
111
|
program.parse();
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACK,
|
|
3
|
+
applyDocsDirChange,
|
|
4
|
+
askScope,
|
|
5
|
+
formatScopeLabel,
|
|
6
|
+
getMergedValue,
|
|
7
|
+
getScopeLabel,
|
|
8
|
+
injectBackChoice,
|
|
9
|
+
showConfigHint
|
|
10
|
+
} from "./chunk-QQ5SVZET.js";
|
|
11
|
+
import {
|
|
12
|
+
findStatuslineHookPath,
|
|
13
|
+
isStatuslineConfigured,
|
|
14
|
+
saveStatuslineConfig,
|
|
15
|
+
selectStatuslineComponents,
|
|
16
|
+
writeStatuslineSettings
|
|
17
|
+
} from "./chunk-BPLN4LDL.js";
|
|
18
|
+
import {
|
|
19
|
+
ensureShellCompletion,
|
|
20
|
+
hasShellCompletion
|
|
21
|
+
} from "./chunk-7N64TDZ6.js";
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_CONFIG
|
|
24
|
+
} from "./chunk-PGLUEN7D.js";
|
|
25
|
+
import {
|
|
26
|
+
run
|
|
27
|
+
} from "./chunk-UFGNO6CW.js";
|
|
28
|
+
import {
|
|
29
|
+
claudeSettingsPath,
|
|
30
|
+
globalConfigPath,
|
|
31
|
+
localConfigPath,
|
|
32
|
+
mergeJson,
|
|
33
|
+
readJson,
|
|
34
|
+
resolvePath,
|
|
35
|
+
writeJson
|
|
36
|
+
} from "./chunk-TPRZHSFS.js";
|
|
37
|
+
import {
|
|
38
|
+
log
|
|
39
|
+
} from "./chunk-W5CD7WTX.js";
|
|
40
|
+
|
|
41
|
+
// src/commands/init.ts
|
|
42
|
+
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
43
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
44
|
+
import { homedir } from "os";
|
|
45
|
+
import chalk from "chalk";
|
|
46
|
+
var GITIGNORE_START = "# >>> coding-friend managed";
|
|
47
|
+
var GITIGNORE_END = "# <<< coding-friend managed";
|
|
48
|
+
var em = chalk.hex("#10b981");
|
|
49
|
+
var dk = chalk.hex("#064e3b");
|
|
50
|
+
function printBanner() {
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(em(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
53
|
+
console.log(
|
|
54
|
+
em(" \u2502 ") + "\u2726" + em(" ") + chalk.bold.white("Coding Friend") + em(" \u2726 \u2502")
|
|
55
|
+
);
|
|
56
|
+
console.log(em(" \u2502 ") + chalk.dim("Setup Wizard") + em(" \u2502"));
|
|
57
|
+
console.log(em(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
58
|
+
console.log(em(" \u2570\u2500\u25B8"));
|
|
59
|
+
console.log();
|
|
60
|
+
}
|
|
61
|
+
var _stepIndex = 0;
|
|
62
|
+
function printStepHeader(label, description) {
|
|
63
|
+
_stepIndex++;
|
|
64
|
+
const line = chalk.hex("#10b981")("\u2500".repeat(44));
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(`${line}`);
|
|
67
|
+
console.log(
|
|
68
|
+
`${em("\u{1F536}")} ${chalk.bold.hex("#f59e0b")(`Step ${_stepIndex}`)} ${chalk.dim(label)}`
|
|
69
|
+
);
|
|
70
|
+
if (description) {
|
|
71
|
+
console.log(` ${chalk.dim(description)}`);
|
|
72
|
+
}
|
|
73
|
+
console.log(`${line}`);
|
|
74
|
+
}
|
|
75
|
+
function isGitRepo() {
|
|
76
|
+
return run("git", ["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
77
|
+
}
|
|
78
|
+
function escapeRegExp(str) {
|
|
79
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
80
|
+
}
|
|
81
|
+
function writeToScope(scope, data) {
|
|
82
|
+
const targetPath = scope === "global" ? globalConfigPath() : localConfigPath();
|
|
83
|
+
mergeJson(targetPath, data);
|
|
84
|
+
log.success(`Saved to ${targetPath}`);
|
|
85
|
+
}
|
|
86
|
+
function handleBack(value) {
|
|
87
|
+
if (value === BACK) {
|
|
88
|
+
console.log();
|
|
89
|
+
log.dim("Init cancelled. Remaining steps skipped \u2014 nothing further saved.");
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function getDocsDir(globalCfg, localCfg) {
|
|
94
|
+
return localCfg?.docsDir ?? globalCfg?.docsDir ?? DEFAULT_CONFIG.docsDir;
|
|
95
|
+
}
|
|
96
|
+
async function stepDocsDir(globalCfg, localCfg) {
|
|
97
|
+
const currentValue = getMergedValue("docsDir", globalCfg, localCfg);
|
|
98
|
+
const scopeLabel = getScopeLabel("docsDir", globalCfg, localCfg);
|
|
99
|
+
printStepHeader(
|
|
100
|
+
`Docs folder name ${formatScopeLabel(scopeLabel)}${currentValue ? ` (${chalk.dim(currentValue)})` : ""}`,
|
|
101
|
+
"Where plans, memory, research, and session docs are stored in your project."
|
|
102
|
+
);
|
|
103
|
+
const value = await input({
|
|
104
|
+
message: "Docs folder name:",
|
|
105
|
+
default: currentValue ?? DEFAULT_CONFIG.docsDir,
|
|
106
|
+
validate: (val) => {
|
|
107
|
+
if (!val) return "Folder name cannot be empty";
|
|
108
|
+
if (val.includes("/") || val.includes("\\"))
|
|
109
|
+
return "Must be a folder name, not a path (no slashes)";
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const scope = await askScope();
|
|
114
|
+
if (scope === "back") {
|
|
115
|
+
log.dim("Skipped docsDir.");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const DOCS_SUBFOLDERS = ["plans", "memory", "research", "learn", "sessions"];
|
|
119
|
+
applyDocsDirChange(value, currentValue, scope, DOCS_SUBFOLDERS);
|
|
120
|
+
writeToScope(scope, { docsDir: value });
|
|
121
|
+
}
|
|
122
|
+
async function stepGitignore(docsDir) {
|
|
123
|
+
const hasBlock = (() => {
|
|
124
|
+
if (!existsSync(".gitignore")) return false;
|
|
125
|
+
const content = readFileSync(".gitignore", "utf-8");
|
|
126
|
+
return content.includes(GITIGNORE_START) || content.includes("# coding-friend");
|
|
127
|
+
})();
|
|
128
|
+
if (hasBlock) {
|
|
129
|
+
printStepHeader(
|
|
130
|
+
`Configure .gitignore ${chalk.green("[done]")}`,
|
|
131
|
+
"Keeps AI-generated docs and config out of your git history."
|
|
132
|
+
);
|
|
133
|
+
log.dim(".gitignore already configured.");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
printStepHeader(
|
|
137
|
+
"Configure .gitignore",
|
|
138
|
+
"Keeps Coding Friend's AI-generated docs and config out of your git history. You can edit it later in .gitignore."
|
|
139
|
+
);
|
|
140
|
+
const choice = await select({
|
|
141
|
+
message: "Add coding-friend artifacts to .gitignore?",
|
|
142
|
+
choices: injectBackChoice(
|
|
143
|
+
[
|
|
144
|
+
{ name: "Yes, ignore all", value: "all" },
|
|
145
|
+
{ name: "Partial -- pick which to ignore", value: "partial" },
|
|
146
|
+
{ name: "No -- keep everything tracked", value: "none" }
|
|
147
|
+
],
|
|
148
|
+
"Cancel init"
|
|
149
|
+
)
|
|
150
|
+
});
|
|
151
|
+
handleBack(choice);
|
|
152
|
+
if (choice === "none") {
|
|
153
|
+
log.dim("Skipped .gitignore config.");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const allEntries = [
|
|
157
|
+
`${docsDir}/plans/`,
|
|
158
|
+
`${docsDir}/memory/`,
|
|
159
|
+
`${docsDir}/research/`,
|
|
160
|
+
`${docsDir}/learn/`,
|
|
161
|
+
`${docsDir}/sessions/`,
|
|
162
|
+
".coding-friend/"
|
|
163
|
+
];
|
|
164
|
+
let entries = allEntries;
|
|
165
|
+
if (choice === "partial") {
|
|
166
|
+
entries = await checkbox({
|
|
167
|
+
message: "Which folders to ignore?",
|
|
168
|
+
choices: allEntries.map((e) => ({ name: e, value: e }))
|
|
169
|
+
});
|
|
170
|
+
if (entries.length === 0) {
|
|
171
|
+
log.dim("Nothing selected.");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const existing = existsSync(".gitignore") ? readFileSync(".gitignore", "utf-8") : "";
|
|
176
|
+
const block = `${GITIGNORE_START}
|
|
177
|
+
${entries.join("\n")}
|
|
178
|
+
${GITIGNORE_END}`;
|
|
179
|
+
const managedBlockRe = new RegExp(
|
|
180
|
+
`${escapeRegExp(GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(GITIGNORE_END)}`
|
|
181
|
+
);
|
|
182
|
+
const legacyBlockRe = /# coding-friend\n([\w/.]+\n)*/;
|
|
183
|
+
let updated;
|
|
184
|
+
if (managedBlockRe.test(existing)) {
|
|
185
|
+
updated = existing.replace(managedBlockRe, block);
|
|
186
|
+
log.success(`Updated .gitignore: ${entries.join(", ")}`);
|
|
187
|
+
} else if (legacyBlockRe.test(existing)) {
|
|
188
|
+
updated = existing.replace(legacyBlockRe, block);
|
|
189
|
+
log.success(`Migrated .gitignore block: ${entries.join(", ")}`);
|
|
190
|
+
} else {
|
|
191
|
+
updated = existing.trimEnd() + "\n\n" + block + "\n";
|
|
192
|
+
log.success(`Added to .gitignore: ${entries.join(", ")}`);
|
|
193
|
+
}
|
|
194
|
+
writeFileSync(".gitignore", updated);
|
|
195
|
+
}
|
|
196
|
+
async function stepDocsLanguage(globalCfg, localCfg) {
|
|
197
|
+
const currentValue = getMergedValue("language", globalCfg, localCfg);
|
|
198
|
+
const scopeLabel = getScopeLabel("language", globalCfg, localCfg);
|
|
199
|
+
printStepHeader(
|
|
200
|
+
`Docs language ${formatScopeLabel(scopeLabel)}${currentValue ? ` (${chalk.dim(currentValue)})` : ""}`,
|
|
201
|
+
"Skills like /cf-plan, /cf-ask, /cf-remember will write docs in this language."
|
|
202
|
+
);
|
|
203
|
+
const lang = await selectLanguage(
|
|
204
|
+
"What language should generated docs be written in?"
|
|
205
|
+
);
|
|
206
|
+
const scope = await askScope();
|
|
207
|
+
if (scope === "back") {
|
|
208
|
+
log.dim("Skipped docs language.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
writeToScope(scope, { language: lang });
|
|
212
|
+
}
|
|
213
|
+
async function selectLanguage(message) {
|
|
214
|
+
const choice = await select({
|
|
215
|
+
message,
|
|
216
|
+
choices: injectBackChoice(
|
|
217
|
+
[
|
|
218
|
+
{ name: "English", value: "en" },
|
|
219
|
+
{ name: "Vietnamese", value: "vi" },
|
|
220
|
+
{ name: "Other", value: "_other" }
|
|
221
|
+
],
|
|
222
|
+
"Cancel init"
|
|
223
|
+
)
|
|
224
|
+
});
|
|
225
|
+
handleBack(choice);
|
|
226
|
+
if (choice === "_other") {
|
|
227
|
+
const lang = await input({ message: "Enter language name:" });
|
|
228
|
+
return lang || "en";
|
|
229
|
+
}
|
|
230
|
+
return choice;
|
|
231
|
+
}
|
|
232
|
+
async function stepLearnConfig(globalCfg, localCfg, gitAvailable) {
|
|
233
|
+
const currentLearn = localCfg?.learn ?? globalCfg?.learn;
|
|
234
|
+
const scopeLabel = getScopeLabel("learn", globalCfg, localCfg);
|
|
235
|
+
const docsDir = getDocsDir(globalCfg, localCfg);
|
|
236
|
+
printStepHeader(
|
|
237
|
+
`/cf-learn config ${formatScopeLabel(scopeLabel)}`,
|
|
238
|
+
"Controls where and how /cf-learn saves your learning notes."
|
|
239
|
+
);
|
|
240
|
+
const language = await selectLanguage(
|
|
241
|
+
"What language should /cf-learn notes be written in?"
|
|
242
|
+
);
|
|
243
|
+
const locationChoice = await select({
|
|
244
|
+
message: "Where to store learning docs?",
|
|
245
|
+
choices: injectBackChoice(
|
|
246
|
+
[
|
|
247
|
+
{ name: `In this project (${docsDir}/learn/)`, value: "local" },
|
|
248
|
+
{ name: "A separate folder", value: "external" }
|
|
249
|
+
],
|
|
250
|
+
"Cancel init"
|
|
251
|
+
)
|
|
252
|
+
});
|
|
253
|
+
handleBack(locationChoice);
|
|
254
|
+
let outputDir = `${docsDir}/learn`;
|
|
255
|
+
let isExternal = false;
|
|
256
|
+
if (locationChoice === "external") {
|
|
257
|
+
outputDir = await input({
|
|
258
|
+
message: "Enter path (absolute or ~/...):",
|
|
259
|
+
default: currentLearn?.outputDir ?? void 0,
|
|
260
|
+
validate: (val) => val.length > 0 ? true : "Path cannot be empty"
|
|
261
|
+
});
|
|
262
|
+
isExternal = true;
|
|
263
|
+
const resolved = resolvePath(outputDir);
|
|
264
|
+
if (!existsSync(resolved)) {
|
|
265
|
+
const create = await confirm({
|
|
266
|
+
message: `Folder ${resolved} doesn't exist. Create it?`,
|
|
267
|
+
default: true
|
|
268
|
+
});
|
|
269
|
+
if (create) {
|
|
270
|
+
run("mkdir", ["-p", resolved]);
|
|
271
|
+
log.success(`Created ${resolved}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const existingCats = currentLearn?.categories;
|
|
276
|
+
const defaultNames = DEFAULT_CONFIG.learn.categories.map((c) => c.name).join(", ");
|
|
277
|
+
const catChoices = [
|
|
278
|
+
{ name: `Use defaults (${defaultNames})`, value: "defaults" }
|
|
279
|
+
];
|
|
280
|
+
if (existingCats && existingCats.length > 0) {
|
|
281
|
+
const existingNames = existingCats.map((c) => c.name).join(", ");
|
|
282
|
+
catChoices.push({
|
|
283
|
+
name: `Keep current (${existingNames})`,
|
|
284
|
+
value: "existing"
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
catChoices.push({ name: "Customize", value: "custom" });
|
|
288
|
+
const catChoice = await select({
|
|
289
|
+
message: "Categories for organizing learning docs?",
|
|
290
|
+
choices: injectBackChoice(catChoices, "Cancel init")
|
|
291
|
+
});
|
|
292
|
+
handleBack(catChoice);
|
|
293
|
+
let categories = DEFAULT_CONFIG.learn.categories;
|
|
294
|
+
if (catChoice === "existing" && existingCats) {
|
|
295
|
+
categories = existingCats;
|
|
296
|
+
} else if (catChoice === "custom") {
|
|
297
|
+
console.log();
|
|
298
|
+
if (existingCats && existingCats.length > 0) {
|
|
299
|
+
console.log("Current categories in config.json:");
|
|
300
|
+
for (const c of existingCats) {
|
|
301
|
+
log.dim(` ${c.name}: ${c.description}`);
|
|
302
|
+
}
|
|
303
|
+
console.log();
|
|
304
|
+
}
|
|
305
|
+
console.log(
|
|
306
|
+
'Enter categories (format: "name: description"). Empty line to finish.'
|
|
307
|
+
);
|
|
308
|
+
log.dim(
|
|
309
|
+
"Tip: you can also edit config.json later -- see https://cf.dinhanhthi.com/docs/configuration/config-json/#learning-extraction"
|
|
310
|
+
);
|
|
311
|
+
console.log();
|
|
312
|
+
const customCats = [];
|
|
313
|
+
let keepGoing = true;
|
|
314
|
+
while (keepGoing) {
|
|
315
|
+
const line = await input({
|
|
316
|
+
message: `Category ${customCats.length + 1}:`
|
|
317
|
+
});
|
|
318
|
+
if (!line) {
|
|
319
|
+
keepGoing = false;
|
|
320
|
+
} else {
|
|
321
|
+
const [name, ...descParts] = line.split(":");
|
|
322
|
+
customCats.push({
|
|
323
|
+
name: name.trim(),
|
|
324
|
+
description: descParts.join(":").trim() || name.trim()
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (customCats.length > 0) categories = customCats;
|
|
329
|
+
}
|
|
330
|
+
let autoCommit = false;
|
|
331
|
+
if (isExternal && gitAvailable) {
|
|
332
|
+
autoCommit = await confirm({
|
|
333
|
+
message: "Auto-commit learning docs to git after each /cf-learn?",
|
|
334
|
+
default: currentLearn?.autoCommit ?? false
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const indexChoice = await select({
|
|
338
|
+
message: "How should learning docs be indexed?",
|
|
339
|
+
choices: injectBackChoice(
|
|
340
|
+
[
|
|
341
|
+
{ name: "No index", value: "none" },
|
|
342
|
+
{ name: "Single README at root", value: "single" },
|
|
343
|
+
{ name: "Per-category READMEs", value: "per-category" }
|
|
344
|
+
],
|
|
345
|
+
"Cancel init"
|
|
346
|
+
)
|
|
347
|
+
});
|
|
348
|
+
handleBack(indexChoice);
|
|
349
|
+
let readmeIndex = false;
|
|
350
|
+
if (indexChoice === "single") readmeIndex = true;
|
|
351
|
+
else if (indexChoice === "per-category") readmeIndex = "per-category";
|
|
352
|
+
const learnObj = {
|
|
353
|
+
language,
|
|
354
|
+
outputDir,
|
|
355
|
+
categories,
|
|
356
|
+
autoCommit,
|
|
357
|
+
readmeIndex
|
|
358
|
+
};
|
|
359
|
+
const scope = await askScope();
|
|
360
|
+
if (scope === "back") {
|
|
361
|
+
log.dim("Skipped /cf-learn config.");
|
|
362
|
+
return { outputDir, autoCommit, isExternal };
|
|
363
|
+
}
|
|
364
|
+
const targetPath = scope === "global" ? globalConfigPath() : localConfigPath();
|
|
365
|
+
const existingConfig = readJson(targetPath);
|
|
366
|
+
const existingLearn = existingConfig?.learn ?? {};
|
|
367
|
+
mergeJson(targetPath, { learn: { ...existingLearn, ...learnObj } });
|
|
368
|
+
log.success(`Saved to ${targetPath}`);
|
|
369
|
+
return { outputDir, autoCommit, isExternal };
|
|
370
|
+
}
|
|
371
|
+
async function stepShellCompletion() {
|
|
372
|
+
if (hasShellCompletion()) {
|
|
373
|
+
printStepHeader(
|
|
374
|
+
`Shell tab completion ${chalk.green("[done]")}`,
|
|
375
|
+
"Enables tab-complete for cf commands in your shell."
|
|
376
|
+
);
|
|
377
|
+
ensureShellCompletion({ silent: false });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
printStepHeader(
|
|
381
|
+
"Shell tab completion",
|
|
382
|
+
"Enables tab-complete for cf commands in your shell."
|
|
383
|
+
);
|
|
384
|
+
ensureShellCompletion();
|
|
385
|
+
}
|
|
386
|
+
async function stepStatusline() {
|
|
387
|
+
if (isStatuslineConfigured()) {
|
|
388
|
+
printStepHeader(
|
|
389
|
+
`Configure statusline ${chalk.green("[done]")}`,
|
|
390
|
+
"Shows token count, model, and session info in your terminal prompt."
|
|
391
|
+
);
|
|
392
|
+
log.dim("Statusline already configured.");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
printStepHeader(
|
|
396
|
+
"Configure statusline",
|
|
397
|
+
"Shows token count, model, and session info in your terminal prompt."
|
|
398
|
+
);
|
|
399
|
+
const hookResult = findStatuslineHookPath();
|
|
400
|
+
if (!hookResult) {
|
|
401
|
+
log.warn(
|
|
402
|
+
"coding-friend plugin not found. Install it via Claude Code first, then re-run."
|
|
403
|
+
);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const components = await selectStatuslineComponents();
|
|
407
|
+
saveStatuslineConfig(components);
|
|
408
|
+
writeStatuslineSettings(hookResult.hookPath);
|
|
409
|
+
log.success("Statusline configured!");
|
|
410
|
+
}
|
|
411
|
+
async function stepClaudePermissions(outputDir, autoCommit) {
|
|
412
|
+
const resolved = resolvePath(outputDir);
|
|
413
|
+
const homePath = resolved.startsWith(homedir()) ? resolved.replace(homedir(), "~") : resolved;
|
|
414
|
+
const rules = [
|
|
415
|
+
`Read(${homePath}/**)`,
|
|
416
|
+
`Edit(${homePath}/**)`,
|
|
417
|
+
`Write(${homePath}/**)`
|
|
418
|
+
];
|
|
419
|
+
if (autoCommit) {
|
|
420
|
+
rules.push(`Bash(cd ${homePath} && git add:*)`);
|
|
421
|
+
rules.push(`Bash(cd ${homePath} && git commit:*)`);
|
|
422
|
+
}
|
|
423
|
+
const settingsPath = claudeSettingsPath();
|
|
424
|
+
const settings = readJson(settingsPath);
|
|
425
|
+
if (!settings) {
|
|
426
|
+
log.warn(
|
|
427
|
+
"~/.claude/settings.json not found. Create it via Claude Code settings first."
|
|
428
|
+
);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const permissions = settings.permissions ?? {};
|
|
432
|
+
const existing = permissions.allow ?? [];
|
|
433
|
+
const missing = rules.filter(
|
|
434
|
+
(r) => !existing.some((e) => e === r || e.includes(homePath))
|
|
435
|
+
);
|
|
436
|
+
if (missing.length === 0) {
|
|
437
|
+
log.dim("All permission rules already configured.");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
console.log("\nTo avoid repeated permission prompts, add these rules:");
|
|
441
|
+
for (const r of missing) {
|
|
442
|
+
console.log(` ${r}`);
|
|
443
|
+
}
|
|
444
|
+
const ok = await confirm({
|
|
445
|
+
message: "Add these to ~/.claude/settings.json?",
|
|
446
|
+
default: true
|
|
447
|
+
});
|
|
448
|
+
if (!ok) {
|
|
449
|
+
log.dim("Skipped. You'll get prompted each time.");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
permissions.allow = [...existing, ...missing];
|
|
453
|
+
settings.permissions = permissions;
|
|
454
|
+
writeJson(settingsPath, settings);
|
|
455
|
+
log.success(`Added ${missing.length} permission rules.`);
|
|
456
|
+
}
|
|
457
|
+
async function initCommand() {
|
|
458
|
+
_stepIndex = 0;
|
|
459
|
+
printBanner();
|
|
460
|
+
showConfigHint();
|
|
461
|
+
const gitAvailable = isGitRepo();
|
|
462
|
+
if (!gitAvailable) {
|
|
463
|
+
log.warn("Not inside a git repo -- git-related steps will be skipped.");
|
|
464
|
+
console.log();
|
|
465
|
+
}
|
|
466
|
+
const globalCfg = readJson(globalConfigPath());
|
|
467
|
+
const localCfg = readJson(localConfigPath());
|
|
468
|
+
console.log("Current configuration:");
|
|
469
|
+
const docsDirScope = getScopeLabel("docsDir", globalCfg, localCfg);
|
|
470
|
+
const docsDirVal = getMergedValue("docsDir", globalCfg, localCfg);
|
|
471
|
+
console.log(
|
|
472
|
+
` ${formatScopeLabel(docsDirScope)} docsDir${docsDirVal ? ` (${chalk.dim(docsDirVal)})` : ""}`
|
|
473
|
+
);
|
|
474
|
+
const langScope = getScopeLabel("language", globalCfg, localCfg);
|
|
475
|
+
const langVal = getMergedValue("language", globalCfg, localCfg);
|
|
476
|
+
console.log(
|
|
477
|
+
` ${formatScopeLabel(langScope)} Docs language${langVal ? ` (${chalk.dim(langVal)})` : ""}`
|
|
478
|
+
);
|
|
479
|
+
const learnScope = getScopeLabel("learn", globalCfg, localCfg);
|
|
480
|
+
console.log(` ${formatScopeLabel(learnScope)} /cf-learn config`);
|
|
481
|
+
console.log();
|
|
482
|
+
await stepDocsDir(globalCfg, localCfg);
|
|
483
|
+
const updatedGlobal = readJson(globalConfigPath());
|
|
484
|
+
const updatedLocal = readJson(localConfigPath());
|
|
485
|
+
const docsDir = getDocsDir(updatedGlobal, updatedLocal);
|
|
486
|
+
if (gitAvailable) {
|
|
487
|
+
await stepGitignore(docsDir);
|
|
488
|
+
}
|
|
489
|
+
await stepDocsLanguage(globalCfg, localCfg);
|
|
490
|
+
const { outputDir, autoCommit, isExternal } = await stepLearnConfig(
|
|
491
|
+
updatedGlobal,
|
|
492
|
+
updatedLocal,
|
|
493
|
+
gitAvailable
|
|
494
|
+
);
|
|
495
|
+
await stepShellCompletion();
|
|
496
|
+
await stepStatusline();
|
|
497
|
+
if (isExternal) {
|
|
498
|
+
printStepHeader(
|
|
499
|
+
"Configure Claude permissions",
|
|
500
|
+
"Grants Claude read/write access to your external learn folder without repeated prompts."
|
|
501
|
+
);
|
|
502
|
+
await stepClaudePermissions(outputDir, autoCommit);
|
|
503
|
+
}
|
|
504
|
+
console.log();
|
|
505
|
+
log.congrats("Setup complete!");
|
|
506
|
+
log.dim(
|
|
507
|
+
"Available commands: /cf-ask, /cf-plan, /cf-fix, /cf-commit, /cf-review, /cf-ship, /cf-optimize, /cf-remember, /cf-learn, /cf-research, /cf-session, /cf-help"
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
export {
|
|
511
|
+
initCommand
|
|
512
|
+
};
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getLatestVersion,
|
|
3
3
|
semverCompare
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-VYMXERKM.js";
|
|
5
5
|
import {
|
|
6
6
|
isMarketplaceRegistered
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-HFLBFX6J.js";
|
|
8
8
|
import {
|
|
9
9
|
getInstalledVersion
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
10
|
+
} from "./chunk-BPLN4LDL.js";
|
|
11
|
+
import "./chunk-7N64TDZ6.js";
|
|
12
|
+
import "./chunk-PGLUEN7D.js";
|
|
13
13
|
import {
|
|
14
14
|
commandExists,
|
|
15
15
|
run
|
|
16
16
|
} from "./chunk-UFGNO6CW.js";
|
|
17
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-TPRZHSFS.js";
|
|
18
18
|
import {
|
|
19
19
|
log
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
import "./chunk-IUTXHCP7.js";
|
|
20
|
+
} from "./chunk-W5CD7WTX.js";
|
|
22
21
|
|
|
23
22
|
// src/commands/install.ts
|
|
24
23
|
import chalk from "chalk";
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getLibPath
|
|
2
|
+
getLibPath
|
|
3
|
+
} from "./chunk-RZRT7NGT.js";
|
|
4
|
+
import {
|
|
3
5
|
resolveDocsDir
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-FYHHNX7K.js";
|
|
7
|
+
import "./chunk-PGLUEN7D.js";
|
|
6
8
|
import {
|
|
7
9
|
run
|
|
8
10
|
} from "./chunk-UFGNO6CW.js";
|
|
9
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-TPRZHSFS.js";
|
|
10
12
|
import {
|
|
11
13
|
log
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-IUTXHCP7.js";
|
|
14
|
+
} from "./chunk-W5CD7WTX.js";
|
|
14
15
|
|
|
15
16
|
// src/commands/mcp.ts
|
|
16
17
|
import { existsSync, readdirSync } from "fs";
|
package/dist/postinstall.js
CHANGED