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 === "
|
|
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-
|
|
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-
|
|
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-
|
|
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 === "
|
|
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
package/dist/clean-67ACB4OS.js
DELETED
|
@@ -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
|
-
};
|