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.
@@ -1,529 +0,0 @@
1
- import {
2
- findStatuslineHookPath,
3
- isStatuslineConfigured,
4
- saveStatuslineConfig,
5
- selectStatuslineComponents,
6
- writeStatuslineSettings
7
- } from "./chunk-HSQX3PKW.js";
8
- import {
9
- ensureShellCompletion,
10
- hasShellCompletion
11
- } from "./chunk-4PLV2ENL.js";
12
- import {
13
- DEFAULT_CONFIG
14
- } from "./chunk-WXBI2HUL.js";
15
- import {
16
- run
17
- } from "./chunk-UFGNO6CW.js";
18
- import {
19
- claudeSettingsPath,
20
- globalConfigPath,
21
- localConfigPath,
22
- resolvePath
23
- } from "./chunk-WHCJT7E2.js";
24
- import {
25
- log
26
- } from "./chunk-6DUFTBTO.js";
27
- import {
28
- mergeJson,
29
- readJson
30
- } from "./chunk-IUTXHCP7.js";
31
-
32
- // src/commands/init.ts
33
- import { checkbox, confirm, input, select } from "@inquirer/prompts";
34
- import { existsSync, readFileSync, writeFileSync } from "fs";
35
- import { homedir } from "os";
36
- var GITIGNORE_START = "# >>> coding-friend managed";
37
- var GITIGNORE_END = "# <<< coding-friend managed";
38
- function isGitRepo() {
39
- return run("git", ["rev-parse", "--is-inside-work-tree"]) === "true";
40
- }
41
- function checkDocsFolders() {
42
- const folders = ["docs/plans", "docs/memory", "docs/research", "docs/learn"];
43
- return folders.every((f) => existsSync(f));
44
- }
45
- function checkGitignore() {
46
- if (!existsSync(".gitignore")) return false;
47
- const content = readFileSync(".gitignore", "utf-8");
48
- return content.includes(GITIGNORE_START) || content.includes("# coding-friend");
49
- }
50
- function checkDocsLanguage() {
51
- const local = readJson(localConfigPath());
52
- const global = readJson(globalConfigPath());
53
- return !!(local?.language || global?.language);
54
- }
55
- async function selectLanguage(message) {
56
- const choice = await select({
57
- message,
58
- choices: [
59
- { name: "English", value: "en" },
60
- { name: "Vietnamese", value: "vi" },
61
- { name: "Other", value: "_other" }
62
- ]
63
- });
64
- if (choice === "_other") {
65
- const lang = await input({ message: "Enter language name:" });
66
- return lang || "en";
67
- }
68
- return choice;
69
- }
70
- function checkLearnConfig() {
71
- const local = readJson(localConfigPath());
72
- const global = readJson(globalConfigPath());
73
- return !!(local?.learn || global?.learn);
74
- }
75
- function getResolvedOutputDir() {
76
- const local = readJson(localConfigPath());
77
- if (local?.learn?.outputDir) return resolvePath(local.learn.outputDir);
78
- const global = readJson(globalConfigPath());
79
- if (global?.learn?.outputDir) return resolvePath(global.learn.outputDir);
80
- return null;
81
- }
82
- function isExternalOutputDir(outputDir) {
83
- if (!outputDir) return false;
84
- const cwd = process.cwd();
85
- return !outputDir.startsWith(cwd);
86
- }
87
- function checkClaudePermissions(outputDir) {
88
- const settings = readJson(claudeSettingsPath());
89
- if (!settings) return false;
90
- const permissions = settings.permissions;
91
- if (!permissions?.allow) return false;
92
- const homePath = outputDir.replace(homedir(), "~");
93
- return permissions.allow.some(
94
- (rule) => rule.includes(outputDir) || rule.includes(homePath)
95
- );
96
- }
97
- async function setupDocsFolders() {
98
- const folders = ["docs/plans", "docs/memory", "docs/research", "docs/learn"];
99
- const created = [];
100
- for (const f of folders) {
101
- if (!existsSync(f)) {
102
- run("mkdir", ["-p", f]);
103
- created.push(f);
104
- }
105
- }
106
- if (created.length > 0) {
107
- log.success(`Created: ${created.join(", ")}`);
108
- } else {
109
- log.dim("All docs folders already exist.");
110
- }
111
- }
112
- async function setupGitignore() {
113
- const choice = await select({
114
- message: "Add coding-friend artifacts to .gitignore?",
115
- choices: [
116
- { name: "Yes, ignore all", value: "all" },
117
- { name: "Partial \u2014 pick which to ignore", value: "partial" },
118
- { name: "No \u2014 keep everything tracked", value: "none" }
119
- ]
120
- });
121
- if (choice === "none") {
122
- log.dim("Skipped .gitignore config.");
123
- return;
124
- }
125
- const allEntries = [
126
- "docs/plans/",
127
- "docs/memory/",
128
- "docs/research/",
129
- "docs/learn/",
130
- ".coding-friend/"
131
- ];
132
- let entries = allEntries;
133
- if (choice === "partial") {
134
- entries = await checkbox({
135
- message: "Which folders to ignore?",
136
- choices: allEntries.map((e) => ({ name: e, value: e }))
137
- });
138
- if (entries.length === 0) {
139
- log.dim("Nothing selected.");
140
- return;
141
- }
142
- }
143
- const existing = existsSync(".gitignore") ? readFileSync(".gitignore", "utf-8") : "";
144
- const block = `${GITIGNORE_START}
145
- ${entries.join("\n")}
146
- ${GITIGNORE_END}`;
147
- const managedBlockRe = new RegExp(
148
- `${escapeRegExp(GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(GITIGNORE_END)}`
149
- );
150
- const legacyBlockRe = /# coding-friend\n([\w/.]+\n)*/;
151
- let updated;
152
- if (managedBlockRe.test(existing)) {
153
- updated = existing.replace(managedBlockRe, block);
154
- log.success(`Updated .gitignore: ${entries.join(", ")}`);
155
- } else if (legacyBlockRe.test(existing)) {
156
- updated = existing.replace(legacyBlockRe, block);
157
- log.success(`Migrated .gitignore block: ${entries.join(", ")}`);
158
- } else {
159
- updated = existing.trimEnd() + "\n\n" + block + "\n";
160
- log.success(`Added to .gitignore: ${entries.join(", ")}`);
161
- }
162
- writeFileSync(".gitignore", updated);
163
- }
164
- function escapeRegExp(str) {
165
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
166
- }
167
- async function setupDocsLanguage() {
168
- return selectLanguage(
169
- "What language should generated docs be written in? (plans, memory, research, ask)"
170
- );
171
- }
172
- async function setupLearnConfig(gitAvailable = true) {
173
- const language = await selectLanguage(
174
- "What language should /cf-learn notes be written in?"
175
- );
176
- const locationChoice = await select({
177
- message: "Where to store learning docs?",
178
- choices: [
179
- { name: "In this project (docs/learn/)", value: "local" },
180
- { name: "A separate folder", value: "external" }
181
- ]
182
- });
183
- let outputDir = "docs/learn";
184
- let isExternal = false;
185
- if (locationChoice === "external") {
186
- outputDir = await input({
187
- message: "Enter path (absolute or ~/...):",
188
- validate: (val) => val.length > 0 ? true : "Path cannot be empty"
189
- });
190
- isExternal = true;
191
- const resolved = resolvePath(outputDir);
192
- if (!existsSync(resolved)) {
193
- const create = await confirm({
194
- message: `Folder ${resolved} doesn't exist. Create it?`,
195
- default: true
196
- });
197
- if (create) {
198
- run("mkdir", ["-p", resolved]);
199
- log.success(`Created ${resolved}`);
200
- }
201
- }
202
- }
203
- const existingConfig = readJson(localConfigPath()) ?? readJson(globalConfigPath());
204
- const existingCats = existingConfig?.learn?.categories;
205
- const defaultNames = DEFAULT_CONFIG.learn.categories.map((c) => c.name).join(", ");
206
- const choices = [
207
- {
208
- name: `Use defaults (${defaultNames})`,
209
- value: "defaults"
210
- }
211
- ];
212
- if (existingCats && existingCats.length > 0) {
213
- const existingNames = existingCats.map((c) => c.name).join(", ");
214
- choices.push({
215
- name: `Keep current (${existingNames})`,
216
- value: "existing"
217
- });
218
- }
219
- choices.push({ name: "Customize", value: "custom" });
220
- const catChoice = await select({
221
- message: "Categories for organizing learning docs?",
222
- choices
223
- });
224
- let categories = DEFAULT_CONFIG.learn.categories;
225
- if (catChoice === "existing" && existingCats) {
226
- categories = existingCats;
227
- } else if (catChoice === "custom") {
228
- console.log();
229
- if (existingCats && existingCats.length > 0) {
230
- console.log("Current categories in config.json:");
231
- for (const c of existingCats) {
232
- log.dim(` ${c.name}: ${c.description}`);
233
- }
234
- console.log();
235
- }
236
- console.log(
237
- 'Enter categories (format: "name: description"). Empty line to finish.'
238
- );
239
- log.dim(
240
- "Tip: you can also edit config.json later \u2014 see https://cf.dinhanhthi.com/docs/cli/cf-init/"
241
- );
242
- console.log();
243
- const customCats = [];
244
- let keepGoing = true;
245
- while (keepGoing) {
246
- const line = await input({
247
- message: `Category ${customCats.length + 1}:`
248
- });
249
- if (!line) {
250
- keepGoing = false;
251
- } else {
252
- const [name, ...descParts] = line.split(":");
253
- customCats.push({
254
- name: name.trim(),
255
- description: descParts.join(":").trim() || name.trim()
256
- });
257
- }
258
- }
259
- if (customCats.length > 0) categories = customCats;
260
- }
261
- let autoCommit = false;
262
- if (isExternal && gitAvailable) {
263
- autoCommit = await confirm({
264
- message: "Auto-commit learning docs to git after each /cf-learn?",
265
- default: false
266
- });
267
- }
268
- const indexChoice = await select({
269
- message: "How should learning docs be indexed?",
270
- choices: [
271
- { name: "No index", value: "none" },
272
- { name: "Single README at root", value: "single" },
273
- { name: "Per-category READMEs", value: "per-category" }
274
- ]
275
- });
276
- let readmeIndex = false;
277
- if (indexChoice === "single") readmeIndex = true;
278
- else if (indexChoice === "per-category") readmeIndex = "per-category";
279
- return {
280
- language,
281
- outputDir,
282
- categories,
283
- autoCommit,
284
- readmeIndex,
285
- isExternal
286
- };
287
- }
288
- async function setupClaudePermissions(outputDir, autoCommit) {
289
- const resolved = resolvePath(outputDir);
290
- const homePath = resolved.startsWith(homedir()) ? resolved.replace(homedir(), "~") : resolved;
291
- const rules = [
292
- `Read(${homePath}/**)`,
293
- `Edit(${homePath}/**)`,
294
- `Write(${homePath}/**)`
295
- ];
296
- if (autoCommit) {
297
- rules.push(`Bash(cd ${homePath} && git add:*)`);
298
- rules.push(`Bash(cd ${homePath} && git commit:*)`);
299
- }
300
- const settingsPath = claudeSettingsPath();
301
- const settings = readJson(settingsPath);
302
- if (!settings) {
303
- log.warn(
304
- "~/.claude/settings.json not found. Create it via Claude Code settings first."
305
- );
306
- return;
307
- }
308
- const permissions = settings.permissions ?? {};
309
- const existing = permissions.allow ?? [];
310
- const missing = rules.filter(
311
- (r) => !existing.some((e) => e === r || e.includes(homePath))
312
- );
313
- if (missing.length === 0) {
314
- log.dim("All permission rules already configured.");
315
- return;
316
- }
317
- console.log("\nTo avoid repeated permission prompts, add these rules:");
318
- for (const r of missing) {
319
- console.log(` ${r}`);
320
- }
321
- const ok = await confirm({
322
- message: "Add these to ~/.claude/settings.json?",
323
- default: true
324
- });
325
- if (!ok) {
326
- log.dim("Skipped. You'll get prompted each time.");
327
- return;
328
- }
329
- permissions.allow = [...existing, ...missing];
330
- settings.permissions = permissions;
331
- const {
332
- readJson: _r,
333
- writeJson: _w,
334
- ...restImports
335
- } = await import("./json-2XS56OJY.js");
336
- _w(settingsPath, settings);
337
- log.success(`Added ${missing.length} permission rules.`);
338
- }
339
- function isDefaultConfig(config) {
340
- if (config.language && config.language !== "en") return false;
341
- if (config.learn) {
342
- const l = config.learn;
343
- if (l.language && l.language !== "en") return false;
344
- if (l.outputDir && l.outputDir !== "docs/learn") return false;
345
- if (l.autoCommit) return false;
346
- if (l.readmeIndex) return false;
347
- if (l.categories) {
348
- const defaultNames = DEFAULT_CONFIG.learn.categories.map((c) => c.name);
349
- const configNames = l.categories.map((c) => c.name);
350
- if (JSON.stringify(defaultNames) !== JSON.stringify(configNames))
351
- return false;
352
- }
353
- }
354
- return true;
355
- }
356
- async function saveConfig(config) {
357
- if (isDefaultConfig(config)) {
358
- log.dim("All settings match defaults \u2014 no config file needed.");
359
- return;
360
- }
361
- const target = await select({
362
- message: "Save settings as global or project-only?",
363
- choices: [
364
- { name: "Global (all projects)", value: "global" },
365
- { name: "This project only", value: "local" },
366
- { name: "Both", value: "both" }
367
- ]
368
- });
369
- if (target === "global" || target === "both") {
370
- mergeJson(globalConfigPath(), config);
371
- log.success(`Saved to ${globalConfigPath()}`);
372
- }
373
- if (target === "local" || target === "both") {
374
- mergeJson(localConfigPath(), config);
375
- log.success(`Saved to ${localConfigPath()}`);
376
- }
377
- }
378
- async function initCommand() {
379
- console.log("=== \u{1F33F} Coding Friend Init \u{1F33F} ===");
380
- console.log();
381
- const gitAvailable = isGitRepo();
382
- if (!gitAvailable) {
383
- log.warn("Not inside a git repo \u2014 git-related steps will be skipped.");
384
- console.log();
385
- }
386
- const resolvedOutputDir = getResolvedOutputDir();
387
- const hasExternalDir = checkLearnConfig() && isExternalOutputDir(resolvedOutputDir);
388
- const steps = [
389
- { name: "docs", label: "Create docs folders", done: checkDocsFolders() },
390
- ...gitAvailable ? [
391
- {
392
- name: "gitignore",
393
- label: "Configure .gitignore",
394
- done: checkGitignore()
395
- }
396
- ] : [],
397
- {
398
- name: "docsLanguage",
399
- label: "Set docs language (plans, memory, research, ask)",
400
- done: checkDocsLanguage()
401
- },
402
- { name: "learn", label: "Configure /cf-learn", done: checkLearnConfig() },
403
- {
404
- name: "completion",
405
- label: "Setup shell tab completion",
406
- done: hasShellCompletion()
407
- },
408
- {
409
- name: "statusline",
410
- label: "Configure statusline",
411
- done: isStatuslineConfigured()
412
- }
413
- ];
414
- if (hasExternalDir && resolvedOutputDir) {
415
- steps.push({
416
- name: "permissions",
417
- label: "Configure Claude permissions",
418
- done: checkClaudePermissions(resolvedOutputDir)
419
- });
420
- }
421
- console.log("coding-friend setup status:");
422
- for (const step of steps) {
423
- const status = step.done ? "\x1B[32m[done]\x1B[0m" : "\x1B[33m[pending]\x1B[0m";
424
- console.log(` ${status} ${step.label}`);
425
- }
426
- console.log();
427
- const pending = steps.filter((s) => !s.done);
428
- if (pending.length === 0) {
429
- log.success("Everything is already configured!");
430
- return;
431
- }
432
- const action = await select({
433
- message: `${pending.length} pending step(s). What do you want to do?`,
434
- choices: [
435
- { name: "Apply all pending", value: "all" },
436
- { name: "Pick which to apply", value: "pick" },
437
- { name: "Cancel", value: "cancel" }
438
- ]
439
- });
440
- if (action === "cancel") {
441
- log.dim("Cancelled.");
442
- return;
443
- }
444
- let selected = pending;
445
- if (action === "pick") {
446
- const picked = await checkbox({
447
- message: "Which steps to apply?",
448
- choices: pending.map((s) => ({ name: s.label, value: s.name }))
449
- });
450
- selected = pending.filter((s) => picked.includes(s.name));
451
- if (selected.length === 0) {
452
- log.dim("Nothing selected.");
453
- return;
454
- }
455
- }
456
- const config = {};
457
- let learnOutputDir = "docs/learn";
458
- let learnAutoCommit = false;
459
- let isExternal = false;
460
- for (const step of selected) {
461
- console.log();
462
- log.step(step.label);
463
- switch (step.name) {
464
- case "docs":
465
- await setupDocsFolders();
466
- break;
467
- case "gitignore":
468
- await setupGitignore();
469
- break;
470
- case "docsLanguage": {
471
- const lang = await setupDocsLanguage();
472
- config.language = lang;
473
- break;
474
- }
475
- case "learn": {
476
- const learn = await setupLearnConfig(gitAvailable);
477
- config.learn = {
478
- ...config.learn,
479
- language: learn.language,
480
- outputDir: learn.outputDir,
481
- categories: learn.categories,
482
- autoCommit: learn.autoCommit,
483
- readmeIndex: learn.readmeIndex
484
- };
485
- learnOutputDir = learn.outputDir;
486
- learnAutoCommit = learn.autoCommit;
487
- isExternal = learn.isExternal;
488
- break;
489
- }
490
- case "completion":
491
- ensureShellCompletion();
492
- break;
493
- case "statusline": {
494
- const hookResult = findStatuslineHookPath();
495
- if (!hookResult) {
496
- log.warn(
497
- "coding-friend plugin not found. Install it via Claude Code first, then re-run."
498
- );
499
- break;
500
- }
501
- const components = await selectStatuslineComponents();
502
- saveStatuslineConfig(components);
503
- writeStatuslineSettings(hookResult.hookPath);
504
- log.success("Statusline configured!");
505
- break;
506
- }
507
- case "permissions":
508
- if (resolvedOutputDir) {
509
- await setupClaudePermissions(resolvedOutputDir, learnAutoCommit);
510
- }
511
- break;
512
- }
513
- }
514
- if (isExternal && !selected.some((s) => s.name === "permissions") && !checkClaudePermissions(resolvePath(learnOutputDir))) {
515
- console.log();
516
- log.step("Configure Claude permissions");
517
- await setupClaudePermissions(learnOutputDir, learnAutoCommit);
518
- }
519
- if (Object.keys(config).length > 0) {
520
- console.log();
521
- await saveConfig(config);
522
- }
523
- console.log();
524
- log.success("Setup complete!");
525
- log.dim("Available commands: /cf-plan, /cf-commit, /cf-review, /cf-learn");
526
- }
527
- export {
528
- initCommand
529
- };
@@ -1,10 +0,0 @@
1
- import {
2
- mergeJson,
3
- readJson,
4
- writeJson
5
- } from "./chunk-IUTXHCP7.js";
6
- export {
7
- mergeJson,
8
- readJson,
9
- writeJson
10
- };
@@ -1,17 +0,0 @@
1
- import {
2
- getLatestVersion,
3
- semverCompare,
4
- updateCommand
5
- } from "./chunk-DHPWBSF5.js";
6
- import "./chunk-HSQX3PKW.js";
7
- import "./chunk-4PLV2ENL.js";
8
- import "./chunk-WXBI2HUL.js";
9
- import "./chunk-UFGNO6CW.js";
10
- import "./chunk-WHCJT7E2.js";
11
- import "./chunk-6DUFTBTO.js";
12
- import "./chunk-IUTXHCP7.js";
13
- export {
14
- getLatestVersion,
15
- semverCompare,
16
- updateCommand
17
- };