coding-friend-cli 1.35.4 → 1.35.6

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.
@@ -0,0 +1,187 @@
1
+ import {
2
+ loadConfig,
3
+ resolveMemoryDir
4
+ } from "./chunk-B2NFXRFP.js";
5
+ import "./chunk-57EGAXYI.js";
6
+ import {
7
+ resolvePath
8
+ } from "./chunk-TWKNGPBO.js";
9
+ import {
10
+ log,
11
+ printBanner
12
+ } from "./chunk-NREZK463.js";
13
+ import "./chunk-5UVDWG5L.js";
14
+
15
+ // src/commands/clean.ts
16
+ import { existsSync, readdirSync, rmSync, statSync } from "fs";
17
+ import { join, relative } from "path";
18
+ import { checkbox, confirm, input, select } from "@inquirer/prompts";
19
+ import chalk from "chalk";
20
+ var DATE_PREFIX_RE = /^(\d{4}-\d{2}-\d{2})/;
21
+ function parseDateFromName(name) {
22
+ const m = DATE_PREFIX_RE.exec(name);
23
+ if (!m) return null;
24
+ const d = /* @__PURE__ */ new Date(`${m[1]}T00:00:00.000Z`);
25
+ return isNaN(d.getTime()) ? null : d;
26
+ }
27
+ function getEffectiveDate(entryPath, name) {
28
+ const fromName = parseDateFromName(name);
29
+ if (fromName) return fromName;
30
+ try {
31
+ return new Date(statSync(entryPath).mtime);
32
+ } catch {
33
+ return /* @__PURE__ */ new Date(0);
34
+ }
35
+ }
36
+ function olderThanDays(date, days, now) {
37
+ return now.getTime() - date.getTime() > days * 24 * 60 * 60 * 1e3;
38
+ }
39
+ function matchesRange(entryPath, name, range, cutoff, now) {
40
+ if (range === "all") return true;
41
+ const date = getEffectiveDate(entryPath, name);
42
+ switch (range) {
43
+ case "more_than_1_day":
44
+ return olderThanDays(date, 1, now);
45
+ case "more_than_3_days":
46
+ return olderThanDays(date, 3, now);
47
+ case "more_than_1_week":
48
+ return olderThanDays(date, 7, now);
49
+ case "more_than_1_month":
50
+ return olderThanDays(date, 30, now);
51
+ case "more_than_1_year":
52
+ return olderThanDays(date, 365, now);
53
+ case "before_date":
54
+ return cutoff !== null && date.getTime() < cutoff.getTime();
55
+ }
56
+ }
57
+ function countFiles(dir) {
58
+ if (!existsSync(dir)) return 0;
59
+ let count = 0;
60
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
61
+ const entryPath = join(dir, entry.name);
62
+ if (entry.isDirectory()) {
63
+ count += countFiles(entryPath);
64
+ } else {
65
+ count++;
66
+ }
67
+ }
68
+ return count;
69
+ }
70
+ function deleteMatchingEntries(dir, range, cutoff, now) {
71
+ if (!existsSync(dir)) return { deleted: 0, failed: 0 };
72
+ let deleted = 0;
73
+ let failed = 0;
74
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
75
+ const entryPath = join(dir, entry.name);
76
+ if (matchesRange(entryPath, entry.name, range, cutoff, now)) {
77
+ const filesBefore = entry.isDirectory() ? countFiles(entryPath) : 1;
78
+ try {
79
+ rmSync(entryPath, { recursive: true });
80
+ deleted += filesBefore;
81
+ } catch {
82
+ log.warn(` Could not delete: ${entry.name}`);
83
+ failed++;
84
+ }
85
+ }
86
+ }
87
+ return { deleted, failed };
88
+ }
89
+ async function promptDateRange() {
90
+ const range = await select({
91
+ message: "Delete files older than:",
92
+ choices: [
93
+ { name: "More than 1 day", value: "more_than_1_day" },
94
+ { name: "More than 3 days", value: "more_than_3_days" },
95
+ { name: "More than 1 week", value: "more_than_1_week" },
96
+ { name: "More than 1 month", value: "more_than_1_month" },
97
+ { name: "More than 1 year", value: "more_than_1_year" },
98
+ { name: "Before a specific date (YYYY-MM-DD)", value: "before_date" },
99
+ { name: "Clean all (no date filter)", value: "all" }
100
+ ]
101
+ });
102
+ if (range !== "before_date") return { range, cutoff: null };
103
+ while (true) {
104
+ const raw = await input({ message: "Enter date (YYYY-MM-DD):" });
105
+ const d = /* @__PURE__ */ new Date(`${raw.trim()}T00:00:00.000Z`);
106
+ if (!isNaN(d.getTime())) return { range, cutoff: d };
107
+ log.warn(`Invalid date "${raw}". Use YYYY-MM-DD format.`);
108
+ }
109
+ }
110
+ async function cleanCommand() {
111
+ console.log();
112
+ printBanner("\u{1F9F9} Coding Friend Clean");
113
+ console.log();
114
+ const config = loadConfig();
115
+ const docsDir = resolvePath(config.docsDir ?? "docs");
116
+ const candidates = [
117
+ { key: "context", path: join(docsDir, "context") },
118
+ { key: "memory", path: resolveMemoryDir() },
119
+ { key: "research", path: join(docsDir, "research") },
120
+ { key: "reviews", path: join(docsDir, "reviews") },
121
+ { key: "plans", path: join(docsDir, "plans") },
122
+ { key: "sessions", path: join(docsDir, "sessions") },
123
+ { key: "learn", path: join(docsDir, "learn") }
124
+ ];
125
+ const cleanable = candidates.map((c) => ({ ...c, fileCount: countFiles(c.path) })).filter((c) => existsSync(c.path) && c.fileCount > 0);
126
+ if (cleanable.length === 0) {
127
+ log.info("Nothing to clean.");
128
+ console.log();
129
+ return;
130
+ }
131
+ const selected = await checkbox({
132
+ message: "Select directories to clean:",
133
+ choices: cleanable.map((c) => ({
134
+ name: `${c.key} \u2014 ${c.fileCount} ${c.fileCount === 1 ? "file" : "files"}`,
135
+ value: c.key
136
+ }))
137
+ });
138
+ if (selected.length === 0) {
139
+ log.dim(" Nothing selected.");
140
+ console.log();
141
+ return;
142
+ }
143
+ const now = /* @__PURE__ */ new Date();
144
+ const summary = [];
145
+ for (const entry of cleanable.filter((c) => selected.includes(c.key))) {
146
+ const relPath = relative(process.cwd(), entry.path);
147
+ console.log();
148
+ console.log(`${chalk.yellow("\u2192")} ${chalk.bold(entry.key)} (${relPath})`);
149
+ const { range, cutoff } = await promptDateRange();
150
+ const isMemory = entry.key === "memory";
151
+ const message = isMemory ? `Delete matching contents of ${relPath}/? (includes SQLite databases \u2014 stop memory daemon first if running)` : `Delete matching contents of ${relPath}/?`;
152
+ const ok = await confirm({ message, default: false });
153
+ if (ok) {
154
+ const { deleted, failed } = deleteMatchingEntries(
155
+ entry.path,
156
+ range,
157
+ cutoff,
158
+ now
159
+ );
160
+ summary.push({ key: entry.key, deleted });
161
+ const failNote = failed > 0 ? `, ${failed} failed` : "";
162
+ log.success(
163
+ `Cleared: ${relPath} (${deleted} ${deleted === 1 ? "file" : "files"} removed${failNote})`
164
+ );
165
+ } else {
166
+ log.dim(" Skipped.");
167
+ }
168
+ console.log();
169
+ }
170
+ if (summary.length === 0) {
171
+ log.info("Done. Nothing was deleted.");
172
+ } else {
173
+ const total = summary.reduce((sum, s) => sum + s.deleted, 0);
174
+ const breakdown = summary.map((s) => `${chalk.bold(s.key)}: ${s.deleted}`).join(", ");
175
+ log.info(
176
+ `Done. ${chalk.bold(total)} ${total === 1 ? "file" : "files"} removed \u2014 ${breakdown}`
177
+ );
178
+ }
179
+ console.log();
180
+ }
181
+ export {
182
+ cleanCommand,
183
+ getEffectiveDate,
184
+ matchesRange,
185
+ olderThanDays,
186
+ parseDateFromName
187
+ };
@@ -141,18 +141,27 @@ async function editLearnOutputDir(globalCfg) {
141
141
  const defaultLearnDir = DEFAULT_CONFIG.learn.outputDir;
142
142
  const oldOutputDir = globalCfg?.learn?.outputDir ?? defaultLearnDir;
143
143
  log.dim(`Current: ${oldOutputDir}`);
144
+ const hasDistinctCurrent = oldOutputDir !== defaultLearnDir;
145
+ const locationChoices = [];
146
+ if (hasDistinctCurrent) {
147
+ locationChoices.push({
148
+ name: `Keep current (${oldOutputDir})`,
149
+ value: "current"
150
+ });
151
+ }
152
+ locationChoices.push({
153
+ name: `Use default (${defaultLearnDir})`,
154
+ value: "default"
155
+ });
156
+ locationChoices.push({ name: "Custom path\u2026", value: "custom" });
144
157
  const locationChoice = await select({
145
158
  message: "Where to store learning docs?",
146
- choices: [
147
- {
148
- name: `Use default (${defaultLearnDir})`,
149
- value: "default"
150
- },
151
- { name: "Custom path\u2026", value: "custom" }
152
- ]
159
+ choices: locationChoices
153
160
  });
154
161
  let newOutputDir = defaultLearnDir;
155
- if (locationChoice === "custom") {
162
+ if (locationChoice === "current") {
163
+ newOutputDir = oldOutputDir;
164
+ } else if (locationChoice === "custom") {
156
165
  newOutputDir = await input({
157
166
  message: "Enter path (absolute or ~/...):",
158
167
  default: oldOutputDir !== defaultLearnDir ? oldOutputDir : void 0,
package/dist/index.js CHANGED
@@ -30,11 +30,11 @@ program.command("enable").description("Re-enable the Coding Friend plugin").opti
30
30
  await enableCommand(opts);
31
31
  });
32
32
  program.command("init").description("Initialize coding-friend in current project").action(async () => {
33
- const { initCommand } = await import("./init-XXSQDMEB.js");
33
+ const { initCommand } = await import("./init-MFUX5INO.js");
34
34
  await initCommand();
35
35
  });
36
36
  program.command("config").description("Manage Coding Friend configuration").action(async () => {
37
- const { configCommand } = await import("./config-YPK4ADYG.js");
37
+ const { configCommand } = await import("./config-Z3GE6HAP.js");
38
38
  await configCommand();
39
39
  });
40
40
  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) => {
@@ -73,7 +73,7 @@ program.command("status").description("Show comprehensive Coding Friend status")
73
73
  await statusCommand();
74
74
  });
75
75
  program.command("clean").description("Clean files generated by Coding Friend in docs/").action(async () => {
76
- const { cleanCommand } = await import("./clean-67ACB4OS.js");
76
+ const { cleanCommand } = await import("./clean-VFWQ4AJP.js");
77
77
  await cleanCommand();
78
78
  });
79
79
  var session = program.command("session").description("[beta] Save and load Claude Code sessions across machines");
@@ -404,22 +404,28 @@ async function stepLearnConfig(globalCfg) {
404
404
  const language = await selectLanguage(
405
405
  "What language should /cf-learn notes be written in?"
406
406
  );
407
+ const hasDistinctCurrent = currentOutputDir !== defaultLearnDir;
408
+ const locationChoices = [];
409
+ if (hasDistinctCurrent) {
410
+ locationChoices.push({
411
+ name: `Keep current (${currentOutputDir})`,
412
+ value: "current"
413
+ });
414
+ }
415
+ locationChoices.push({
416
+ name: `Use default (${defaultLearnDir})`,
417
+ value: "default"
418
+ });
419
+ locationChoices.push({ name: "Custom path\u2026", value: "custom" });
407
420
  const locationChoice = await select({
408
421
  message: `Current learn folder: ${currentOutputDir}`,
409
- choices: injectBackChoice(
410
- [
411
- {
412
- name: `Use default (${defaultLearnDir})`,
413
- value: "default"
414
- },
415
- { name: "Custom path\u2026", value: "custom" }
416
- ],
417
- "Cancel init"
418
- )
422
+ choices: injectBackChoice(locationChoices, "Cancel init")
419
423
  });
420
424
  handleBack(locationChoice);
421
425
  let outputDir = defaultLearnDir;
422
- if (locationChoice === "custom") {
426
+ if (locationChoice === "current") {
427
+ outputDir = currentOutputDir;
428
+ } else if (locationChoice === "custom") {
423
429
  outputDir = await input({
424
430
  message: "Enter path (absolute or ~/...):",
425
431
  default: currentOutputDir !== defaultLearnDir ? currentOutputDir : void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.35.4",
3
+ "version": "1.35.6",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,95 +0,0 @@
1
- import {
2
- loadConfig,
3
- resolveMemoryDir
4
- } from "./chunk-B2NFXRFP.js";
5
- import "./chunk-57EGAXYI.js";
6
- import {
7
- resolvePath
8
- } from "./chunk-TWKNGPBO.js";
9
- import {
10
- log,
11
- printBanner
12
- } from "./chunk-NREZK463.js";
13
- import "./chunk-5UVDWG5L.js";
14
-
15
- // src/commands/clean.ts
16
- import { existsSync, readdirSync, rmSync } from "fs";
17
- import { join, relative } from "path";
18
- import { checkbox, confirm } from "@inquirer/prompts";
19
- import chalk from "chalk";
20
- function countFiles(dir) {
21
- if (!existsSync(dir)) return 0;
22
- let count = 0;
23
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
24
- const entryPath = join(dir, entry.name);
25
- if (entry.isDirectory()) {
26
- count += countFiles(entryPath);
27
- } else {
28
- count++;
29
- }
30
- }
31
- return count;
32
- }
33
- function clearDirContents(dir) {
34
- for (const entry of readdirSync(dir)) {
35
- rmSync(join(dir, entry), { recursive: true });
36
- }
37
- }
38
- async function cleanCommand() {
39
- console.log();
40
- printBanner("\u{1F9F9} Coding Friend Clean");
41
- console.log();
42
- const config = loadConfig();
43
- const docsDir = resolvePath(config.docsDir ?? "docs");
44
- const candidates = [
45
- { key: "context", path: join(docsDir, "context") },
46
- { key: "memory", path: resolveMemoryDir() },
47
- { key: "research", path: join(docsDir, "research") },
48
- { key: "reviews", path: join(docsDir, "reviews") },
49
- { key: "plans", path: join(docsDir, "plans") },
50
- { key: "sessions", path: join(docsDir, "sessions") },
51
- { key: "learn", path: join(docsDir, "learn") }
52
- ];
53
- const cleanable = candidates.map((c) => ({ ...c, fileCount: countFiles(c.path) })).filter((c) => existsSync(c.path) && c.fileCount > 0);
54
- if (cleanable.length === 0) {
55
- log.info("Nothing to clean.");
56
- console.log();
57
- return;
58
- }
59
- const selected = await checkbox({
60
- message: "Select directories to clean:",
61
- choices: cleanable.map((c) => ({
62
- name: `${c.key} \u2014 ${c.fileCount} ${c.fileCount === 1 ? "file" : "files"}`,
63
- value: c.key
64
- }))
65
- });
66
- if (selected.length === 0) {
67
- log.dim(" Nothing selected.");
68
- console.log();
69
- return;
70
- }
71
- console.log();
72
- let cleared = 0;
73
- for (const entry of cleanable.filter((c) => selected.includes(c.key))) {
74
- const relPath = relative(process.cwd(), entry.path);
75
- console.log(
76
- `${chalk.yellow("\u2192")} Clean ${chalk.bold(entry.key)} (${relPath})`
77
- );
78
- const isMemory = entry.key === "memory";
79
- const message = isMemory ? `Delete all contents of ${relPath}/? (includes SQLite databases \u2014 stop memory daemon first if running)` : `Delete all contents of ${relPath}/?`;
80
- const ok = await confirm({ message, default: false });
81
- if (ok) {
82
- clearDirContents(entry.path);
83
- log.success(`Cleared: ${relPath}`);
84
- cleared++;
85
- } else {
86
- log.dim(" Skipped.");
87
- }
88
- console.log();
89
- }
90
- log.info(`Done. ${cleared} of ${selected.length} directories cleared.`);
91
- console.log();
92
- }
93
- export {
94
- cleanCommand
95
- };