@sooink/ai-session-tidy 0.1.1 → 0.1.3
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.ko.md +56 -14
- package/README.md +56 -14
- package/assets/demo-interactive.gif +0 -0
- package/assets/demo.gif +0 -0
- package/dist/index.js +333 -109
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/clean.ts +161 -78
- package/src/commands/config.ts +66 -6
- package/src/commands/scan.ts +51 -2
- package/src/commands/watch.ts +8 -6
- package/src/core/constants.ts +22 -0
- package/src/core/service.ts +11 -0
- package/src/core/watcher.ts +22 -0
- package/src/utils/config.ts +34 -4
- package/src/utils/paths.ts +36 -3
package/dist/index.js
CHANGED
|
@@ -67,19 +67,29 @@ async function getDirectorySize(dirPath) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// src/scanners/claude-code.ts
|
|
71
|
-
import { readdir as readdir2, readFile, stat as stat2, access } from "fs/promises";
|
|
72
|
-
import { join as join3 } from "path";
|
|
73
|
-
import { createReadStream } from "fs";
|
|
74
|
-
import { createInterface } from "readline";
|
|
75
|
-
|
|
76
70
|
// src/utils/paths.ts
|
|
71
|
+
import { existsSync } from "fs";
|
|
77
72
|
import { homedir } from "os";
|
|
78
73
|
import { join as join2 } from "path";
|
|
79
74
|
function decodePath(encoded) {
|
|
80
75
|
if (encoded === "") return "";
|
|
81
76
|
if (!encoded.startsWith("-")) return encoded;
|
|
82
|
-
|
|
77
|
+
const parts = encoded.slice(1).split("-");
|
|
78
|
+
return reconstructPath(parts, "");
|
|
79
|
+
}
|
|
80
|
+
function reconstructPath(parts, currentPath) {
|
|
81
|
+
if (parts.length === 0) return currentPath;
|
|
82
|
+
for (let len = parts.length; len >= 1; len--) {
|
|
83
|
+
const segment = parts.slice(0, len).join("-");
|
|
84
|
+
const testPath = currentPath + "/" + segment;
|
|
85
|
+
if (existsSync(testPath)) {
|
|
86
|
+
const result = reconstructPath(parts.slice(len), testPath);
|
|
87
|
+
if (existsSync(result) || parts.slice(len).length === 0) {
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return currentPath + "/" + parts.join("/");
|
|
83
93
|
}
|
|
84
94
|
function getConfigDir(appName) {
|
|
85
95
|
switch (process.platform) {
|
|
@@ -118,6 +128,10 @@ function tildify(path) {
|
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
// src/scanners/claude-code.ts
|
|
131
|
+
import { readdir as readdir2, readFile, stat as stat2, access } from "fs/promises";
|
|
132
|
+
import { join as join3 } from "path";
|
|
133
|
+
import { createReadStream } from "fs";
|
|
134
|
+
import { createInterface } from "readline";
|
|
121
135
|
var ClaudeCodeScanner = class {
|
|
122
136
|
name = "claude-code";
|
|
123
137
|
projectsDir;
|
|
@@ -375,7 +389,7 @@ var ClaudeCodeScanner = class {
|
|
|
375
389
|
}
|
|
376
390
|
}
|
|
377
391
|
async findCwdInJsonl(jsonlPath) {
|
|
378
|
-
return new Promise((
|
|
392
|
+
return new Promise((resolve4) => {
|
|
379
393
|
const stream = createReadStream(jsonlPath, { encoding: "utf-8" });
|
|
380
394
|
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
381
395
|
let found = false;
|
|
@@ -393,16 +407,16 @@ var ClaudeCodeScanner = class {
|
|
|
393
407
|
found = true;
|
|
394
408
|
rl.close();
|
|
395
409
|
stream.destroy();
|
|
396
|
-
|
|
410
|
+
resolve4(entry.cwd);
|
|
397
411
|
}
|
|
398
412
|
} catch {
|
|
399
413
|
}
|
|
400
414
|
});
|
|
401
415
|
rl.on("close", () => {
|
|
402
|
-
if (!found)
|
|
416
|
+
if (!found) resolve4(null);
|
|
403
417
|
});
|
|
404
418
|
rl.on("error", () => {
|
|
405
|
-
|
|
419
|
+
resolve4(null);
|
|
406
420
|
});
|
|
407
421
|
});
|
|
408
422
|
}
|
|
@@ -669,7 +683,7 @@ function outputTable(results, verbose) {
|
|
|
669
683
|
console.log(
|
|
670
684
|
` ${chalk2.cyan(`[${session.toolName}]`)} ${chalk2.white(projectName)} ${chalk2.dim(`(${formatSize(session.size)})`)}`
|
|
671
685
|
);
|
|
672
|
-
console.log(` ${chalk2.dim("\u2192")} ${session.projectPath}`);
|
|
686
|
+
console.log(` ${chalk2.dim("\u2192")} ${tildify(session.projectPath)}`);
|
|
673
687
|
console.log(` ${chalk2.dim("Modified:")} ${session.lastModified.toLocaleDateString()}`);
|
|
674
688
|
console.log();
|
|
675
689
|
}
|
|
@@ -683,7 +697,7 @@ function outputTable(results, verbose) {
|
|
|
683
697
|
console.log(
|
|
684
698
|
` ${chalk2.yellow("[config]")} ${chalk2.white(projectName)}`
|
|
685
699
|
);
|
|
686
|
-
console.log(` ${chalk2.dim("\u2192")} ${entry.projectPath}`);
|
|
700
|
+
console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.projectPath)}`);
|
|
687
701
|
if (entry.configStats?.lastCost) {
|
|
688
702
|
const cost = `$${entry.configStats.lastCost.toFixed(2)}`;
|
|
689
703
|
const inTokens = formatTokens(entry.configStats.lastTotalInputTokens);
|
|
@@ -693,6 +707,45 @@ function outputTable(results, verbose) {
|
|
|
693
707
|
console.log();
|
|
694
708
|
}
|
|
695
709
|
}
|
|
710
|
+
if (sessionEnvEntries.length > 0) {
|
|
711
|
+
console.log();
|
|
712
|
+
console.log(chalk2.bold("Empty Session Env Folders:"));
|
|
713
|
+
console.log();
|
|
714
|
+
for (const entry of sessionEnvEntries) {
|
|
715
|
+
const folderName = entry.sessionPath.split("/").pop() || entry.sessionPath;
|
|
716
|
+
console.log(
|
|
717
|
+
` ${chalk2.green("[session-env]")} ${chalk2.white(folderName)} ${chalk2.dim("(empty)")}`
|
|
718
|
+
);
|
|
719
|
+
console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.sessionPath)}`);
|
|
720
|
+
console.log();
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (todosEntries.length > 0) {
|
|
724
|
+
console.log();
|
|
725
|
+
console.log(chalk2.bold("Orphaned Todos:"));
|
|
726
|
+
console.log();
|
|
727
|
+
for (const entry of todosEntries) {
|
|
728
|
+
const fileName = entry.sessionPath.split("/").pop() || entry.sessionPath;
|
|
729
|
+
console.log(
|
|
730
|
+
` ${chalk2.magenta("[todos]")} ${chalk2.white(fileName)} ${chalk2.dim(`(${formatSize(entry.size)})`)}`
|
|
731
|
+
);
|
|
732
|
+
console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.sessionPath)}`);
|
|
733
|
+
console.log();
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (fileHistoryEntries.length > 0) {
|
|
737
|
+
console.log();
|
|
738
|
+
console.log(chalk2.bold("Orphaned File History:"));
|
|
739
|
+
console.log();
|
|
740
|
+
for (const entry of fileHistoryEntries) {
|
|
741
|
+
const folderName = entry.sessionPath.split("/").pop() || entry.sessionPath;
|
|
742
|
+
console.log(
|
|
743
|
+
` ${chalk2.blue("[file-history]")} ${chalk2.white(folderName)} ${chalk2.dim(`(${formatSize(entry.size)})`)}`
|
|
744
|
+
);
|
|
745
|
+
console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.sessionPath)}`);
|
|
746
|
+
console.log();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
696
749
|
}
|
|
697
750
|
console.log();
|
|
698
751
|
console.log(
|
|
@@ -709,7 +762,7 @@ import inquirer from "inquirer";
|
|
|
709
762
|
|
|
710
763
|
// src/core/cleaner.ts
|
|
711
764
|
import { readFile as readFile3, writeFile, mkdir, copyFile } from "fs/promises";
|
|
712
|
-
import { existsSync } from "fs";
|
|
765
|
+
import { existsSync as existsSync2 } from "fs";
|
|
713
766
|
import { join as join5 } from "path";
|
|
714
767
|
import { homedir as homedir2 } from "os";
|
|
715
768
|
|
|
@@ -821,7 +874,7 @@ var Cleaner = class {
|
|
|
821
874
|
const configPath = entries[0].sessionPath;
|
|
822
875
|
const projectPathsToRemove = new Set(entries.map((e) => e.projectPath));
|
|
823
876
|
const backupDir = getBackupDir();
|
|
824
|
-
if (!
|
|
877
|
+
if (!existsSync2(backupDir)) {
|
|
825
878
|
await mkdir(backupDir, { recursive: true });
|
|
826
879
|
}
|
|
827
880
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -852,10 +905,27 @@ function formatSessionChoice(session) {
|
|
|
852
905
|
const isConfig = session.type === "config";
|
|
853
906
|
const toolTag = isConfig ? chalk3.yellow("[config]") : chalk3.cyan(`[${session.toolName}]`);
|
|
854
907
|
const name = chalk3.white(projectName);
|
|
855
|
-
const size = isConfig ? "" : chalk3.dim(`(${formatSize(session.size)})`)
|
|
856
|
-
const path = chalk3.dim(`\u2192 ${session.projectPath}`);
|
|
857
|
-
return `${toolTag} ${name}
|
|
858
|
-
|
|
908
|
+
const size = isConfig ? "" : ` ${chalk3.dim(`(${formatSize(session.size)})`)}`;
|
|
909
|
+
const path = chalk3.dim(`\u2192 ${tildify(session.projectPath)}`);
|
|
910
|
+
return `${toolTag} ${name}${size}
|
|
911
|
+
${path}`;
|
|
912
|
+
}
|
|
913
|
+
function formatGroupChoice(group) {
|
|
914
|
+
const labels = {
|
|
915
|
+
"session-env": "empty session-env folder",
|
|
916
|
+
"todos": "orphaned todos file",
|
|
917
|
+
"file-history": "orphaned file-history folder"
|
|
918
|
+
};
|
|
919
|
+
const colors = {
|
|
920
|
+
"session-env": chalk3.green,
|
|
921
|
+
"todos": chalk3.magenta,
|
|
922
|
+
"file-history": chalk3.blue
|
|
923
|
+
};
|
|
924
|
+
const label = labels[group.type] || group.type;
|
|
925
|
+
const count = group.sessions.length;
|
|
926
|
+
const plural = count > 1 ? "s" : "";
|
|
927
|
+
const color = colors[group.type] || chalk3.white;
|
|
928
|
+
return `${color(`[${group.type}]`)} ${chalk3.white(`${count} ${label}${plural}`)} ${chalk3.dim(`(${formatSize(group.totalSize)})`)}`;
|
|
859
929
|
}
|
|
860
930
|
var cleanCommand = new Command2("clean").description("Remove orphaned session data").option("-f, --force", "Skip confirmation prompt").option("-n, --dry-run", "Show what would be deleted without deleting").option("-i, --interactive", "Select sessions to delete interactively").option("--no-trash", "Permanently delete instead of moving to trash").option("-v, --verbose", "Enable verbose output").action(async (options) => {
|
|
861
931
|
const spinner = ora2("Scanning for orphaned sessions...").start();
|
|
@@ -885,14 +955,31 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
|
|
|
885
955
|
const fileHistoryEntries = allSessions.filter(
|
|
886
956
|
(s) => s.type === "file-history"
|
|
887
957
|
);
|
|
888
|
-
const
|
|
889
|
-
...sessionEnvEntries,
|
|
890
|
-
...todosEntries,
|
|
891
|
-
...fileHistoryEntries
|
|
892
|
-
];
|
|
893
|
-
const selectableSessions = allSessions.filter(
|
|
958
|
+
const individualSessions = allSessions.filter(
|
|
894
959
|
(s) => s.type !== "session-env" && s.type !== "todos" && s.type !== "file-history"
|
|
895
960
|
);
|
|
961
|
+
const groupChoices = [];
|
|
962
|
+
if (sessionEnvEntries.length > 0) {
|
|
963
|
+
groupChoices.push({
|
|
964
|
+
type: "session-env",
|
|
965
|
+
sessions: sessionEnvEntries,
|
|
966
|
+
totalSize: sessionEnvEntries.reduce((sum, s) => sum + s.size, 0)
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
if (todosEntries.length > 0) {
|
|
970
|
+
groupChoices.push({
|
|
971
|
+
type: "todos",
|
|
972
|
+
sessions: todosEntries,
|
|
973
|
+
totalSize: todosEntries.reduce((sum, s) => sum + s.size, 0)
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
if (fileHistoryEntries.length > 0) {
|
|
977
|
+
groupChoices.push({
|
|
978
|
+
type: "file-history",
|
|
979
|
+
sessions: fileHistoryEntries,
|
|
980
|
+
totalSize: fileHistoryEntries.reduce((sum, s) => sum + s.size, 0)
|
|
981
|
+
});
|
|
982
|
+
}
|
|
896
983
|
console.log();
|
|
897
984
|
const parts = [];
|
|
898
985
|
if (folderSessions.length > 0) {
|
|
@@ -913,59 +1000,83 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
|
|
|
913
1000
|
logger.warn(`Found ${parts.join(" + ")} (${formatSize(totalSize)})`);
|
|
914
1001
|
if (options.verbose && !options.interactive) {
|
|
915
1002
|
console.log();
|
|
916
|
-
for (const session of
|
|
1003
|
+
for (const session of individualSessions) {
|
|
917
1004
|
console.log(
|
|
918
|
-
chalk3.dim(` ${session.toolName}: ${session.projectPath}`)
|
|
1005
|
+
chalk3.dim(` ${session.toolName}: ${tildify(session.projectPath)}`)
|
|
919
1006
|
);
|
|
920
1007
|
}
|
|
921
1008
|
}
|
|
922
1009
|
let sessionsToClean = allSessions;
|
|
923
1010
|
if (options.interactive) {
|
|
924
|
-
if (
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1011
|
+
if (!process.stdout.isTTY) {
|
|
1012
|
+
logger.error("Interactive mode requires a TTY. Omit -i or use -f to skip confirmation.");
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const choices = [];
|
|
1016
|
+
for (const session of individualSessions) {
|
|
1017
|
+
choices.push({
|
|
1018
|
+
name: formatSessionChoice(session),
|
|
1019
|
+
value: { type: "individual", session },
|
|
1020
|
+
checked: false
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
for (const group of groupChoices) {
|
|
1024
|
+
choices.push({
|
|
1025
|
+
name: formatGroupChoice(group),
|
|
1026
|
+
value: { type: "group", group },
|
|
1027
|
+
checked: false
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
if (choices.length === 0) {
|
|
1031
|
+
logger.info("No selectable sessions found.");
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
console.log();
|
|
1035
|
+
const { selected } = await inquirer.prompt([
|
|
1036
|
+
{
|
|
1037
|
+
type: "checkbox",
|
|
1038
|
+
name: "selected",
|
|
1039
|
+
message: "Select items to delete:",
|
|
1040
|
+
choices,
|
|
1041
|
+
pageSize: 15,
|
|
1042
|
+
loop: false
|
|
933
1043
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
value: session,
|
|
944
|
-
checked: false
|
|
945
|
-
})),
|
|
946
|
-
pageSize: 15,
|
|
947
|
-
loop: false
|
|
948
|
-
}
|
|
949
|
-
]);
|
|
950
|
-
if (selected.length === 0) {
|
|
951
|
-
logger.info("No sessions selected. Cancelled.");
|
|
952
|
-
return;
|
|
1044
|
+
]);
|
|
1045
|
+
if (selected.length === 0) {
|
|
1046
|
+
logger.info("No items selected. Cancelled.");
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (process.stdout.isTTY) {
|
|
1050
|
+
const linesToClear = selected.length + 1;
|
|
1051
|
+
for (let i = 0; i < linesToClear; i++) {
|
|
1052
|
+
process.stdout.write("\x1B[1A\x1B[2K");
|
|
953
1053
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
)
|
|
1054
|
+
}
|
|
1055
|
+
console.log(chalk3.green("\u2714") + " " + chalk3.bold("Selected items:"));
|
|
1056
|
+
for (const item of selected) {
|
|
1057
|
+
if (item.type === "individual") {
|
|
1058
|
+
const s = item.session;
|
|
1059
|
+
const tag = s.type === "config" ? chalk3.yellow("[config]") : chalk3.cyan(`[${s.toolName}]`);
|
|
1060
|
+
const size = s.type === "config" ? "" : ` ${chalk3.dim(`(${formatSize(s.size)})`)}`;
|
|
1061
|
+
console.log(` ${tag} ${basename(s.projectPath)}${size}`);
|
|
1062
|
+
console.log(` ${chalk3.dim(`\u2192 ${tildify(s.projectPath)}`)}`);
|
|
1063
|
+
} else {
|
|
1064
|
+
console.log(` ${formatGroupChoice(item.group)}`);
|
|
961
1065
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
);
|
|
1066
|
+
}
|
|
1067
|
+
sessionsToClean = [];
|
|
1068
|
+
for (const item of selected) {
|
|
1069
|
+
if (item.type === "individual") {
|
|
1070
|
+
sessionsToClean.push(item.session);
|
|
1071
|
+
} else {
|
|
1072
|
+
sessionsToClean.push(...item.group.sessions);
|
|
967
1073
|
}
|
|
968
1074
|
}
|
|
1075
|
+
const selectedSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
|
|
1076
|
+
console.log();
|
|
1077
|
+
logger.info(
|
|
1078
|
+
`Selected: ${sessionsToClean.length} item(s) (${formatSize(selectedSize)})`
|
|
1079
|
+
);
|
|
969
1080
|
}
|
|
970
1081
|
const cleanSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
|
|
971
1082
|
if (options.dryRun) {
|
|
@@ -987,7 +1098,7 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
|
|
|
987
1098
|
);
|
|
988
1099
|
for (const session of dryRunFolders) {
|
|
989
1100
|
console.log(
|
|
990
|
-
` ${chalk3.red("Would delete:")} ${session.sessionPath} (${formatSize(session.size)})`
|
|
1101
|
+
` ${chalk3.red("Would delete:")} ${tildify(session.sessionPath)} (${formatSize(session.size)})`
|
|
991
1102
|
);
|
|
992
1103
|
}
|
|
993
1104
|
if (dryRunConfigs.length > 0) {
|
|
@@ -996,29 +1107,34 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
|
|
|
996
1107
|
` ${chalk3.yellow("Would remove from ~/.claude.json:")}`
|
|
997
1108
|
);
|
|
998
1109
|
for (const config of dryRunConfigs) {
|
|
999
|
-
console.log(` - ${config.projectPath}`);
|
|
1110
|
+
console.log(` - ${tildify(config.projectPath)}`);
|
|
1000
1111
|
}
|
|
1001
1112
|
}
|
|
1002
|
-
const autoCleanParts = [];
|
|
1003
1113
|
if (dryRunEnvs.length > 0) {
|
|
1004
|
-
|
|
1114
|
+
console.log();
|
|
1115
|
+
console.log(
|
|
1116
|
+
` ${chalk3.green("Would delete:")} ${dryRunEnvs.length} session-env folder(s)`
|
|
1117
|
+
);
|
|
1005
1118
|
}
|
|
1006
1119
|
if (dryRunTodos.length > 0) {
|
|
1007
|
-
|
|
1120
|
+
console.log();
|
|
1121
|
+
console.log(
|
|
1122
|
+
` ${chalk3.magenta("Would delete:")} ${dryRunTodos.length} todos file(s) (${formatSize(dryRunTodos.reduce((sum, s) => sum + s.size, 0))})`
|
|
1123
|
+
);
|
|
1008
1124
|
}
|
|
1009
1125
|
if (dryRunHistories.length > 0) {
|
|
1010
|
-
autoCleanParts.push(`${dryRunHistories.length} file-history`);
|
|
1011
|
-
}
|
|
1012
|
-
if (autoCleanParts.length > 0) {
|
|
1013
|
-
const autoSize = dryRunEnvs.reduce((sum, s) => sum + s.size, 0) + dryRunTodos.reduce((sum, s) => sum + s.size, 0) + dryRunHistories.reduce((sum, s) => sum + s.size, 0);
|
|
1014
1126
|
console.log();
|
|
1015
1127
|
console.log(
|
|
1016
|
-
` ${chalk3.
|
|
1128
|
+
` ${chalk3.blue("Would delete:")} ${dryRunHistories.length} file-history folder(s) (${formatSize(dryRunHistories.reduce((sum, s) => sum + s.size, 0))})`
|
|
1017
1129
|
);
|
|
1018
1130
|
}
|
|
1019
1131
|
return;
|
|
1020
1132
|
}
|
|
1021
1133
|
if (!options.force) {
|
|
1134
|
+
if (!process.stdout.isTTY) {
|
|
1135
|
+
logger.error("Confirmation requires a TTY. Use -f to skip confirmation.");
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1022
1138
|
console.log();
|
|
1023
1139
|
const action = options.noTrash ? "permanently delete" : "move to trash";
|
|
1024
1140
|
const { confirmed } = await inquirer.prompt([
|
|
@@ -1080,7 +1196,7 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
|
|
|
1080
1196
|
logger.error(`Failed to delete ${cleanResult.errors.length} item(s)`);
|
|
1081
1197
|
if (options.verbose) {
|
|
1082
1198
|
for (const err of cleanResult.errors) {
|
|
1083
|
-
console.log(chalk3.red(` ${err.sessionPath}: ${err.error.message}`));
|
|
1199
|
+
console.log(chalk3.red(` ${tildify(err.sessionPath)}: ${err.error.message}`));
|
|
1084
1200
|
}
|
|
1085
1201
|
}
|
|
1086
1202
|
}
|
|
@@ -1096,12 +1212,12 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
|
|
|
1096
1212
|
// src/commands/watch.ts
|
|
1097
1213
|
import { Command as Command3 } from "commander";
|
|
1098
1214
|
import chalk4 from "chalk";
|
|
1099
|
-
import { existsSync as
|
|
1215
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1100
1216
|
import { homedir as homedir5 } from "os";
|
|
1101
1217
|
import { join as join8, resolve as resolve2 } from "path";
|
|
1102
1218
|
|
|
1103
1219
|
// src/utils/config.ts
|
|
1104
|
-
import { existsSync as
|
|
1220
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1105
1221
|
import { homedir as homedir3 } from "os";
|
|
1106
1222
|
import { dirname as dirname2, join as join6, resolve } from "path";
|
|
1107
1223
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -1116,7 +1232,7 @@ function getAppVersion() {
|
|
|
1116
1232
|
];
|
|
1117
1233
|
for (const packagePath of paths) {
|
|
1118
1234
|
try {
|
|
1119
|
-
if (
|
|
1235
|
+
if (existsSync3(packagePath)) {
|
|
1120
1236
|
const content = readFileSync(packagePath, "utf-8");
|
|
1121
1237
|
const pkg = JSON.parse(content);
|
|
1122
1238
|
return pkg.version;
|
|
@@ -1132,7 +1248,7 @@ function getConfigPath() {
|
|
|
1132
1248
|
}
|
|
1133
1249
|
function loadConfig() {
|
|
1134
1250
|
const configPath = getConfigPath();
|
|
1135
|
-
if (!
|
|
1251
|
+
if (!existsSync3(configPath)) {
|
|
1136
1252
|
return {};
|
|
1137
1253
|
}
|
|
1138
1254
|
try {
|
|
@@ -1145,13 +1261,13 @@ function loadConfig() {
|
|
|
1145
1261
|
function saveConfig(config) {
|
|
1146
1262
|
const configPath = getConfigPath();
|
|
1147
1263
|
const configDir = dirname2(configPath);
|
|
1148
|
-
if (!
|
|
1264
|
+
if (!existsSync3(configDir)) {
|
|
1149
1265
|
mkdirSync(configDir, { recursive: true });
|
|
1150
1266
|
}
|
|
1267
|
+
const { version: _removed, ...cleanConfig } = config;
|
|
1151
1268
|
const configWithVersion = {
|
|
1152
|
-
version: getAppVersion(),
|
|
1153
1269
|
configVersion: CONFIG_VERSION,
|
|
1154
|
-
...
|
|
1270
|
+
...cleanConfig
|
|
1155
1271
|
};
|
|
1156
1272
|
writeFileSync(configPath, JSON.stringify(configWithVersion, null, 2), "utf-8");
|
|
1157
1273
|
}
|
|
@@ -1206,10 +1322,55 @@ function setWatchDepth(depth) {
|
|
|
1206
1322
|
function resetConfig() {
|
|
1207
1323
|
saveConfig({});
|
|
1208
1324
|
}
|
|
1325
|
+
function getIgnorePaths() {
|
|
1326
|
+
const config = loadConfig();
|
|
1327
|
+
return config.ignorePaths;
|
|
1328
|
+
}
|
|
1329
|
+
function addIgnorePath(path) {
|
|
1330
|
+
const config = loadConfig();
|
|
1331
|
+
const resolved = resolve(path.replace(/^~/, homedir3()));
|
|
1332
|
+
const paths = config.ignorePaths ?? [];
|
|
1333
|
+
if (!paths.includes(resolved)) {
|
|
1334
|
+
paths.push(resolved);
|
|
1335
|
+
config.ignorePaths = paths;
|
|
1336
|
+
saveConfig(config);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
function removeIgnorePath(path) {
|
|
1340
|
+
const config = loadConfig();
|
|
1341
|
+
const resolved = resolve(path.replace(/^~/, homedir3()));
|
|
1342
|
+
const paths = config.ignorePaths ?? [];
|
|
1343
|
+
const index = paths.indexOf(resolved);
|
|
1344
|
+
if (index === -1) return false;
|
|
1345
|
+
paths.splice(index, 1);
|
|
1346
|
+
config.ignorePaths = paths;
|
|
1347
|
+
saveConfig(config);
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
1209
1350
|
|
|
1210
1351
|
// src/core/watcher.ts
|
|
1211
1352
|
import { watch } from "chokidar";
|
|
1212
1353
|
import { access as access4 } from "fs/promises";
|
|
1354
|
+
|
|
1355
|
+
// src/core/constants.ts
|
|
1356
|
+
var IGNORED_SYSTEM_PATTERNS = [
|
|
1357
|
+
// All hidden folders (starting with .)
|
|
1358
|
+
/(^|[/\\])\../,
|
|
1359
|
+
// macOS user folders (not project directories)
|
|
1360
|
+
/\/Library(\/|$)/,
|
|
1361
|
+
/\/Applications(\/|$)/,
|
|
1362
|
+
/\/Music(\/|$)/,
|
|
1363
|
+
/\/Movies(\/|$)/,
|
|
1364
|
+
/\/Pictures(\/|$)/,
|
|
1365
|
+
/\/Downloads(\/|$)/,
|
|
1366
|
+
/\/Documents(\/|$)/,
|
|
1367
|
+
/\/Desktop(\/|$)/,
|
|
1368
|
+
/\/Public(\/|$)/,
|
|
1369
|
+
// Development folders
|
|
1370
|
+
/node_modules(\/|$)/
|
|
1371
|
+
];
|
|
1372
|
+
|
|
1373
|
+
// src/core/watcher.ts
|
|
1213
1374
|
var Watcher = class {
|
|
1214
1375
|
options;
|
|
1215
1376
|
debounceMs;
|
|
@@ -1220,16 +1381,23 @@ var Watcher = class {
|
|
|
1220
1381
|
batchedEvents = [];
|
|
1221
1382
|
/** Debounce timer */
|
|
1222
1383
|
debounceTimer = null;
|
|
1384
|
+
/** Paths that errored (to avoid duplicate logs) */
|
|
1385
|
+
erroredPaths = /* @__PURE__ */ new Set();
|
|
1223
1386
|
constructor(options) {
|
|
1224
1387
|
this.options = options;
|
|
1225
1388
|
this.debounceMs = options.debounceMs ?? 1e4;
|
|
1226
1389
|
}
|
|
1227
1390
|
start() {
|
|
1228
1391
|
if (this.watcher) return;
|
|
1392
|
+
const ignored = [...IGNORED_SYSTEM_PATTERNS];
|
|
1393
|
+
if (this.options.ignorePaths) {
|
|
1394
|
+
ignored.push(...this.options.ignorePaths);
|
|
1395
|
+
}
|
|
1229
1396
|
this.watcher = watch(this.options.watchPaths, {
|
|
1230
1397
|
ignoreInitial: true,
|
|
1231
1398
|
persistent: true,
|
|
1232
|
-
depth: this.options.depth ?? 3
|
|
1399
|
+
depth: this.options.depth ?? 3,
|
|
1400
|
+
ignored
|
|
1233
1401
|
});
|
|
1234
1402
|
this.watcher.on("unlinkDir", (path) => {
|
|
1235
1403
|
this.handleDelete(path);
|
|
@@ -1237,6 +1405,13 @@ var Watcher = class {
|
|
|
1237
1405
|
this.watcher.on("addDir", (path) => {
|
|
1238
1406
|
this.handleRecovery(path);
|
|
1239
1407
|
});
|
|
1408
|
+
this.watcher.on("error", (error) => {
|
|
1409
|
+
const pathInfo = error.path;
|
|
1410
|
+
if (pathInfo && !this.erroredPaths.has(pathInfo)) {
|
|
1411
|
+
this.erroredPaths.add(pathInfo);
|
|
1412
|
+
console.error(`[watch] Cannot watch: ${pathInfo} (${error.code})`);
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1240
1415
|
}
|
|
1241
1416
|
stop() {
|
|
1242
1417
|
if (!this.watcher) return;
|
|
@@ -1325,7 +1500,7 @@ var Watcher = class {
|
|
|
1325
1500
|
import { homedir as homedir4 } from "os";
|
|
1326
1501
|
import { join as join7, dirname as dirname3 } from "path";
|
|
1327
1502
|
import { readFile as readFile4, writeFile as writeFile2, unlink, mkdir as mkdir2 } from "fs/promises";
|
|
1328
|
-
import { existsSync as
|
|
1503
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1329
1504
|
import { execSync } from "child_process";
|
|
1330
1505
|
var SERVICE_LABEL = "sooink.ai-session-tidy.watcher";
|
|
1331
1506
|
var PLIST_FILENAME = `${SERVICE_LABEL}.plist`;
|
|
@@ -1337,7 +1512,7 @@ function getNodePath() {
|
|
|
1337
1512
|
}
|
|
1338
1513
|
function getScriptPath() {
|
|
1339
1514
|
const scriptPath = process.argv[1];
|
|
1340
|
-
if (scriptPath &&
|
|
1515
|
+
if (scriptPath && existsSync4(scriptPath)) {
|
|
1341
1516
|
return scriptPath;
|
|
1342
1517
|
}
|
|
1343
1518
|
throw new Error("Could not determine script path");
|
|
@@ -1385,11 +1560,11 @@ var ServiceManager = class {
|
|
|
1385
1560
|
throw new Error("Service management is only supported on macOS");
|
|
1386
1561
|
}
|
|
1387
1562
|
const launchAgentsDir = dirname3(this.plistPath);
|
|
1388
|
-
if (!
|
|
1563
|
+
if (!existsSync4(launchAgentsDir)) {
|
|
1389
1564
|
await mkdir2(launchAgentsDir, { recursive: true });
|
|
1390
1565
|
}
|
|
1391
1566
|
const logDir = join7(homedir4(), ".ai-session-tidy");
|
|
1392
|
-
if (!
|
|
1567
|
+
if (!existsSync4(logDir)) {
|
|
1393
1568
|
await mkdir2(logDir, { recursive: true });
|
|
1394
1569
|
}
|
|
1395
1570
|
const stdoutPath = join7(logDir, "watcher.log");
|
|
@@ -1405,7 +1580,7 @@ var ServiceManager = class {
|
|
|
1405
1580
|
await writeFile2(this.plistPath, plistContent, "utf-8");
|
|
1406
1581
|
}
|
|
1407
1582
|
async uninstall() {
|
|
1408
|
-
if (
|
|
1583
|
+
if (existsSync4(this.plistPath)) {
|
|
1409
1584
|
await unlink(this.plistPath);
|
|
1410
1585
|
}
|
|
1411
1586
|
}
|
|
@@ -1413,9 +1588,18 @@ var ServiceManager = class {
|
|
|
1413
1588
|
if (!this.isSupported()) {
|
|
1414
1589
|
throw new Error("Service management is only supported on macOS");
|
|
1415
1590
|
}
|
|
1416
|
-
if (!
|
|
1591
|
+
if (!existsSync4(this.plistPath)) {
|
|
1417
1592
|
throw new Error('Service not installed. Run "watch start" to install and start.');
|
|
1418
1593
|
}
|
|
1594
|
+
const logDir = join7(homedir4(), ".ai-session-tidy");
|
|
1595
|
+
const stdoutPath = join7(logDir, "watcher.log");
|
|
1596
|
+
const stderrPath = join7(logDir, "watcher.error.log");
|
|
1597
|
+
if (existsSync4(stdoutPath)) {
|
|
1598
|
+
await writeFile2(stdoutPath, "", "utf-8");
|
|
1599
|
+
}
|
|
1600
|
+
if (existsSync4(stderrPath)) {
|
|
1601
|
+
await writeFile2(stderrPath, "", "utf-8");
|
|
1602
|
+
}
|
|
1419
1603
|
try {
|
|
1420
1604
|
execSync(`launchctl load "${this.plistPath}"`, { stdio: "pipe" });
|
|
1421
1605
|
} catch (error) {
|
|
@@ -1429,7 +1613,7 @@ var ServiceManager = class {
|
|
|
1429
1613
|
if (!this.isSupported()) {
|
|
1430
1614
|
throw new Error("Service management is only supported on macOS");
|
|
1431
1615
|
}
|
|
1432
|
-
if (!
|
|
1616
|
+
if (!existsSync4(this.plistPath)) {
|
|
1433
1617
|
return;
|
|
1434
1618
|
}
|
|
1435
1619
|
try {
|
|
@@ -1446,7 +1630,7 @@ var ServiceManager = class {
|
|
|
1446
1630
|
if (!this.isSupported()) {
|
|
1447
1631
|
return info;
|
|
1448
1632
|
}
|
|
1449
|
-
if (!
|
|
1633
|
+
if (!existsSync4(this.plistPath)) {
|
|
1450
1634
|
return info;
|
|
1451
1635
|
}
|
|
1452
1636
|
try {
|
|
@@ -1479,7 +1663,7 @@ var ServiceManager = class {
|
|
|
1479
1663
|
let stdout = "";
|
|
1480
1664
|
let stderr = "";
|
|
1481
1665
|
try {
|
|
1482
|
-
if (
|
|
1666
|
+
if (existsSync4(stdoutPath)) {
|
|
1483
1667
|
const content = await readFile4(stdoutPath, "utf-8");
|
|
1484
1668
|
const logLines = content.split("\n");
|
|
1485
1669
|
stdout = logLines.slice(-lines).join("\n");
|
|
@@ -1487,7 +1671,7 @@ var ServiceManager = class {
|
|
|
1487
1671
|
} catch {
|
|
1488
1672
|
}
|
|
1489
1673
|
try {
|
|
1490
|
-
if (
|
|
1674
|
+
if (existsSync4(stderrPath)) {
|
|
1491
1675
|
const content = await readFile4(stderrPath, "utf-8");
|
|
1492
1676
|
const logLines = content.split("\n");
|
|
1493
1677
|
stderr = logLines.slice(-lines).join("\n");
|
|
@@ -1579,7 +1763,7 @@ var statusCommand = new Command3("status").description("Show watcher service sta
|
|
|
1579
1763
|
if (status.pid) {
|
|
1580
1764
|
console.log(`PID: ${status.pid}`);
|
|
1581
1765
|
}
|
|
1582
|
-
console.log(`Plist: ${status.plistPath}`);
|
|
1766
|
+
console.log(`Plist: ${tildify(status.plistPath)}`);
|
|
1583
1767
|
console.log();
|
|
1584
1768
|
if (options.logs) {
|
|
1585
1769
|
const lines = parseInt(options.logs, 10) || 20;
|
|
@@ -1642,14 +1826,14 @@ async function runWatcher(options) {
|
|
|
1642
1826
|
logger.info(`Using default watch paths.`);
|
|
1643
1827
|
}
|
|
1644
1828
|
}
|
|
1645
|
-
const validPaths = watchPaths.filter((p) =>
|
|
1829
|
+
const validPaths = watchPaths.filter((p) => existsSync5(p));
|
|
1646
1830
|
if (validPaths.length === 0) {
|
|
1647
1831
|
logger.error("No valid watch paths found. Use -p to specify paths.");
|
|
1648
1832
|
return;
|
|
1649
1833
|
}
|
|
1650
1834
|
if (validPaths.length < watchPaths.length) {
|
|
1651
|
-
const invalidPaths = watchPaths.filter((p) => !
|
|
1652
|
-
logger.warn(`Skipping non-existent paths: ${invalidPaths.join(", ")}`);
|
|
1835
|
+
const invalidPaths = watchPaths.filter((p) => !existsSync5(p));
|
|
1836
|
+
logger.warn(`Skipping non-existent paths: ${invalidPaths.map(tildify).join(", ")}`);
|
|
1653
1837
|
}
|
|
1654
1838
|
const allScanners = createAllScanners();
|
|
1655
1839
|
const availableScanners = await getAvailableScanners(allScanners);
|
|
@@ -1661,7 +1845,7 @@ async function runWatcher(options) {
|
|
|
1661
1845
|
logger.info(
|
|
1662
1846
|
`Watching for project deletions (${availableScanners.map((s) => s.name).join(", ")})`
|
|
1663
1847
|
);
|
|
1664
|
-
logger.info(`Watch paths: ${validPaths.join(", ")}`);
|
|
1848
|
+
logger.info(`Watch paths: ${validPaths.map(tildify).join(", ")}`);
|
|
1665
1849
|
logger.info(`Cleanup delay: ${String(delayMinutes)} minute(s)`);
|
|
1666
1850
|
logger.info(`Watch depth: ${String(depth)}`);
|
|
1667
1851
|
if (process.stdout.isTTY) {
|
|
@@ -1673,14 +1857,15 @@ async function runWatcher(options) {
|
|
|
1673
1857
|
watchPaths: validPaths,
|
|
1674
1858
|
delayMs,
|
|
1675
1859
|
depth,
|
|
1860
|
+
ignorePaths: getIgnorePaths(),
|
|
1676
1861
|
onDelete: async (events) => {
|
|
1677
1862
|
if (events.length === 1) {
|
|
1678
|
-
logger.info(`Detected deletion: ${events[0].path}`);
|
|
1863
|
+
logger.info(`Detected deletion: ${tildify(events[0].path)}`);
|
|
1679
1864
|
} else {
|
|
1680
1865
|
logger.info(`Detected ${events.length} deletions (debounced)`);
|
|
1681
1866
|
if (options.verbose) {
|
|
1682
1867
|
for (const event of events) {
|
|
1683
|
-
logger.debug(` - ${event.path}`);
|
|
1868
|
+
logger.debug(` - ${tildify(event.path)}`);
|
|
1684
1869
|
}
|
|
1685
1870
|
}
|
|
1686
1871
|
}
|
|
@@ -1810,15 +1995,25 @@ var listCommand = new Command4("list").description("List detected AI coding tool
|
|
|
1810
1995
|
// src/commands/config.ts
|
|
1811
1996
|
import { Command as Command5 } from "commander";
|
|
1812
1997
|
import inquirer2 from "inquirer";
|
|
1998
|
+
import { homedir as homedir6 } from "os";
|
|
1999
|
+
import { resolve as resolve3 } from "path";
|
|
1813
2000
|
var configCommand = new Command5("config").description(
|
|
1814
2001
|
"Manage configuration"
|
|
1815
2002
|
);
|
|
1816
|
-
var
|
|
1817
|
-
|
|
2003
|
+
var pathCommand = new Command5("path").description("Manage watch paths");
|
|
2004
|
+
pathCommand.command("add <path>").description("Add a watch path").action((path) => {
|
|
2005
|
+
const resolved = resolve3(path.replace(/^~/, homedir6()));
|
|
2006
|
+
const home = homedir6();
|
|
2007
|
+
if (resolved === home || home.startsWith(resolved + "/")) {
|
|
2008
|
+
logger.warn("Warning: Watching home directory is not recommended.");
|
|
2009
|
+
logger.warn("Many system folders will be excluded automatically.");
|
|
2010
|
+
logger.warn("Consider adding specific project folders instead.");
|
|
2011
|
+
console.log();
|
|
2012
|
+
}
|
|
1818
2013
|
addWatchPath(path);
|
|
1819
2014
|
logger.success(`Added: ${path}`);
|
|
1820
2015
|
});
|
|
1821
|
-
|
|
2016
|
+
pathCommand.command("remove <path>").description("Remove a watch path").action((path) => {
|
|
1822
2017
|
const removed = removeWatchPath(path);
|
|
1823
2018
|
if (removed) {
|
|
1824
2019
|
logger.success(`Removed: ${path}`);
|
|
@@ -1826,7 +2021,7 @@ pathsCommand.command("remove <path>").description("Remove a watch path").action(
|
|
|
1826
2021
|
logger.warn(`Path not found: ${path}`);
|
|
1827
2022
|
}
|
|
1828
2023
|
});
|
|
1829
|
-
|
|
2024
|
+
pathCommand.command("list").description("List watch paths").action(() => {
|
|
1830
2025
|
const paths = getWatchPaths();
|
|
1831
2026
|
if (!paths || paths.length === 0) {
|
|
1832
2027
|
logger.info("No watch paths configured.");
|
|
@@ -1834,10 +2029,35 @@ pathsCommand.command("list").description("List watch paths").action(() => {
|
|
|
1834
2029
|
}
|
|
1835
2030
|
console.log();
|
|
1836
2031
|
for (const p of paths) {
|
|
1837
|
-
console.log(` ${p}`);
|
|
2032
|
+
console.log(` ${tildify(p)}`);
|
|
2033
|
+
}
|
|
2034
|
+
});
|
|
2035
|
+
configCommand.addCommand(pathCommand);
|
|
2036
|
+
var ignoreCommand = new Command5("ignore").description("Manage ignore paths");
|
|
2037
|
+
ignoreCommand.command("add <path>").description("Add a path to ignore from watching").action((path) => {
|
|
2038
|
+
addIgnorePath(path);
|
|
2039
|
+
logger.success(`Added to ignore: ${path}`);
|
|
2040
|
+
});
|
|
2041
|
+
ignoreCommand.command("remove <path>").description("Remove a path from ignore list").action((path) => {
|
|
2042
|
+
const removed = removeIgnorePath(path);
|
|
2043
|
+
if (removed) {
|
|
2044
|
+
logger.success(`Removed from ignore: ${path}`);
|
|
2045
|
+
} else {
|
|
2046
|
+
logger.warn(`Path not found: ${path}`);
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
ignoreCommand.command("list").description("List ignored paths").action(() => {
|
|
2050
|
+
const paths = getIgnorePaths();
|
|
2051
|
+
if (!paths || paths.length === 0) {
|
|
2052
|
+
logger.info("No ignore paths configured.");
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
console.log();
|
|
2056
|
+
for (const p of paths) {
|
|
2057
|
+
console.log(` ${tildify(p)}`);
|
|
1838
2058
|
}
|
|
1839
2059
|
});
|
|
1840
|
-
configCommand.addCommand(
|
|
2060
|
+
configCommand.addCommand(ignoreCommand);
|
|
1841
2061
|
var DEFAULT_DELAY_MINUTES2 = 5;
|
|
1842
2062
|
var MAX_DELAY_MINUTES2 = 10;
|
|
1843
2063
|
configCommand.command("delay [minutes]").description(`Get or set watch delay in minutes (default: ${DEFAULT_DELAY_MINUTES2}, max: ${MAX_DELAY_MINUTES2})`).action((minutes) => {
|
|
@@ -1885,6 +2105,10 @@ configCommand.command("show").description("Show all configuration").action(() =>
|
|
|
1885
2105
|
});
|
|
1886
2106
|
configCommand.command("reset").description("Reset configuration to defaults").option("-f, --force", "Skip confirmation prompt").action(async (options) => {
|
|
1887
2107
|
if (!options.force) {
|
|
2108
|
+
if (!process.stdout.isTTY) {
|
|
2109
|
+
logger.error("Confirmation requires a TTY. Use -f to skip confirmation.");
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
1888
2112
|
const { confirmed } = await inquirer2.prompt([
|
|
1889
2113
|
{
|
|
1890
2114
|
type: "confirm",
|