@zzusp/ccsm 1.0.1 → 1.0.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/LICENSE +21 -21
- package/README.md +236 -236
- package/bin/cli.mjs +52 -52
- package/dist/assets/{DiskUsage-CKhggLs5.js → DiskUsage-BY6XwffG.js} +2 -2
- package/dist/assets/DiskUsage-BY6XwffG.js.map +1 -0
- package/dist/assets/{ImportPage-wge4VhZ-.js → ImportPage-Cwq5bx7G.js} +2 -2
- package/dist/assets/ImportPage-Cwq5bx7G.js.map +1 -0
- package/dist/assets/MarkdownContent-BFu7Nkk_.js +2 -0
- package/dist/assets/MarkdownContent-BFu7Nkk_.js.map +1 -0
- package/dist/assets/{ProjectMemory-Q4XX40j_.js → ProjectMemory-CcE3KbUK.js} +2 -2
- package/dist/assets/ProjectMemory-CcE3KbUK.js.map +1 -0
- package/dist/assets/index-CrWxV6sb.css +1 -0
- package/dist/assets/index-DTbWl1jb.js +11 -0
- package/dist/assets/index-DTbWl1jb.js.map +1 -0
- package/dist/assets/markdown-Bag5rX3T.js +30 -0
- package/dist/assets/markdown-Bag5rX3T.js.map +1 -0
- package/dist/index.html +26 -26
- package/package.json +81 -83
- package/server/index.ts +130 -130
- package/server/lib/active-sessions.test.ts +119 -119
- package/server/lib/active-sessions.ts +95 -95
- package/server/lib/bundle.test.ts +182 -182
- package/server/lib/bundle.ts +86 -86
- package/server/lib/claude-paths.test.ts +126 -126
- package/server/lib/claude-paths.ts +43 -43
- package/server/lib/cleanup-suggestions.ts +131 -131
- package/server/lib/constants.ts +8 -8
- package/server/lib/delete-project.ts +100 -100
- package/server/lib/delete.test.ts +244 -244
- package/server/lib/delete.ts +192 -192
- package/server/lib/disk-usage.ts +81 -81
- package/server/lib/encode-cwd.ts +24 -24
- package/server/lib/export-bundle.ts +236 -236
- package/server/lib/export-import-bundle.test.ts +337 -337
- package/server/lib/fs-size.ts +38 -38
- package/server/lib/import-bundle.ts +488 -488
- package/server/lib/load-memory.ts +120 -120
- package/server/lib/load-session.ts +209 -209
- package/server/lib/modified-files.test.ts +280 -280
- package/server/lib/modified-files.ts +228 -228
- package/server/lib/open-folder.ts +47 -47
- package/server/lib/parse-jsonl.ts +160 -139
- package/server/lib/port.ts +23 -23
- package/server/lib/safe-id.test.ts +41 -41
- package/server/lib/safe-id.ts +6 -6
- package/server/lib/safe-remove.test.ts +73 -73
- package/server/lib/safe-remove.ts +25 -25
- package/server/lib/scan.ts +289 -286
- package/server/lib/search-all.ts +130 -130
- package/server/lib/search-session.ts +203 -203
- package/server/lib/system-tags.ts +20 -20
- package/server/lib/update.ts +67 -67
- package/server/lib/version.test.ts +39 -39
- package/server/lib/version.ts +117 -117
- package/server/routes/disk-cleanup.ts +54 -54
- package/server/routes/disk.ts +9 -9
- package/server/routes/import.ts +87 -87
- package/server/routes/projects.ts +104 -104
- package/server/routes/search.ts +79 -79
- package/server/routes/sessions.ts +130 -130
- package/server/routes/version.ts +34 -34
- package/server/types.ts +1 -1
- package/shared/constants.ts +7 -7
- package/shared/types.ts +513 -511
- package/dist/assets/DiskUsage-CKhggLs5.js.map +0 -1
- package/dist/assets/ImportPage-wge4VhZ-.js.map +0 -1
- package/dist/assets/ProjectMemory-Q4XX40j_.js.map +0 -1
- package/dist/assets/index-7aMrnHJG.js +0 -7
- package/dist/assets/index-7aMrnHJG.js.map +0 -1
- package/dist/assets/index-BOeI_J4B.css +0 -1
package/server/lib/delete.ts
CHANGED
|
@@ -1,192 +1,192 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import readline from 'node:readline';
|
|
5
|
-
import { isUnderClaudeRoot, PATHS } from './claude-paths.ts';
|
|
6
|
-
import { RECENT_ACTIVITY_WINDOW_MS } from './constants.ts';
|
|
7
|
-
import { dirSize, fileSize } from './fs-size.ts';
|
|
8
|
-
import { isSafeId } from './safe-id.ts';
|
|
9
|
-
import { safeRemove } from './safe-remove.ts';
|
|
10
|
-
import {
|
|
11
|
-
buildActiveSessionMap,
|
|
12
|
-
readActivePidEntries,
|
|
13
|
-
} from './active-sessions.ts';
|
|
14
|
-
import type {
|
|
15
|
-
DeletedItem,
|
|
16
|
-
DeleteRequestItem,
|
|
17
|
-
DeleteResult,
|
|
18
|
-
RelatedBytes,
|
|
19
|
-
SkippedItem,
|
|
20
|
-
} from '../types.ts';
|
|
21
|
-
|
|
22
|
-
export type { DeleteRequestItem, DeleteResult } from '../types.ts';
|
|
23
|
-
|
|
24
|
-
const HISTORY_TMP_SUFFIX = '.tmp-clean';
|
|
25
|
-
|
|
26
|
-
export async function deleteSessions(items: DeleteRequestItem[]): Promise<DeleteResult> {
|
|
27
|
-
const liveMap = buildActiveSessionMap();
|
|
28
|
-
const deleted: DeletedItem[] = [];
|
|
29
|
-
const skipped: SkippedItem[] = [];
|
|
30
|
-
const targetIds = new Set<string>();
|
|
31
|
-
|
|
32
|
-
for (const item of items) {
|
|
33
|
-
if (!isSafeId(item.projectId) || !isSafeId(item.sessionId)) {
|
|
34
|
-
skipped.push({ ...item, reason: 'invalid id' });
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const projectDir = path.join(PATHS.projects, item.projectId);
|
|
39
|
-
const jsonlPath = path.join(projectDir, `${item.sessionId}.jsonl`);
|
|
40
|
-
const subdirPath = path.join(projectDir, item.sessionId);
|
|
41
|
-
const fhPath = path.join(PATHS.fileHistory, item.sessionId);
|
|
42
|
-
const sePath = path.join(PATHS.sessionEnv, item.sessionId);
|
|
43
|
-
|
|
44
|
-
const escaped = [jsonlPath, subdirPath, fhPath, sePath].find(
|
|
45
|
-
(p) => !isUnderClaudeRoot(p),
|
|
46
|
-
);
|
|
47
|
-
if (escaped) {
|
|
48
|
-
skipped.push({ ...item, reason: `path escapes ~/.claude: ${escaped}` });
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (liveMap.has(item.sessionId)) {
|
|
53
|
-
skipped.push({
|
|
54
|
-
...item,
|
|
55
|
-
reason: `live PID ${liveMap.get(item.sessionId)} owns this session`,
|
|
56
|
-
});
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (isRecentlyActive(jsonlPath)) {
|
|
61
|
-
skipped.push({
|
|
62
|
-
...item,
|
|
63
|
-
reason: 'jsonl modified within the last 5 minutes — could still be in use',
|
|
64
|
-
});
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (!fs.existsSync(jsonlPath) && !fs.existsSync(subdirPath)) {
|
|
69
|
-
skipped.push({ ...item, reason: 'no files for this session' });
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const related: RelatedBytes = {
|
|
74
|
-
jsonl: fileSize(jsonlPath),
|
|
75
|
-
subdir: dirSize(subdirPath),
|
|
76
|
-
fileHistory: dirSize(fhPath),
|
|
77
|
-
sessionEnv: dirSize(sePath),
|
|
78
|
-
};
|
|
79
|
-
const cleaned: string[] = [];
|
|
80
|
-
|
|
81
|
-
if (safeRemove(jsonlPath)) cleaned.push('projects/<id>.jsonl');
|
|
82
|
-
if (safeRemove(subdirPath)) cleaned.push('projects/<id>/');
|
|
83
|
-
if (safeRemove(fhPath)) cleaned.push('file-history/<id>/');
|
|
84
|
-
if (safeRemove(sePath)) cleaned.push('session-env/<id>/');
|
|
85
|
-
|
|
86
|
-
deleted.push({
|
|
87
|
-
...item,
|
|
88
|
-
freedBytes:
|
|
89
|
-
related.jsonl + related.subdir + related.fileHistory + related.sessionEnv,
|
|
90
|
-
cleaned,
|
|
91
|
-
relatedBytes: related,
|
|
92
|
-
});
|
|
93
|
-
targetIds.add(item.sessionId);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
let historyLinesRemoved = 0;
|
|
97
|
-
if (targetIds.size > 0) {
|
|
98
|
-
historyLinesRemoved = await rewriteHistoryWithout(targetIds);
|
|
99
|
-
cleanupDeadPidFiles(targetIds);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { deleted, skipped, historyLinesRemoved };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function isRecentlyActive(jsonlPath: string): boolean {
|
|
106
|
-
try {
|
|
107
|
-
return Date.now() - fs.statSync(jsonlPath).mtimeMs < RECENT_ACTIVITY_WINDOW_MS;
|
|
108
|
-
} catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function rewriteHistoryWithout(sessionIds: Set<string>): Promise<number> {
|
|
114
|
-
if (!fs.existsSync(PATHS.history)) return 0;
|
|
115
|
-
|
|
116
|
-
const tmpPath = PATHS.history + HISTORY_TMP_SUFFIX;
|
|
117
|
-
if (fs.existsSync(tmpPath)) fs.rmSync(tmpPath, { force: true });
|
|
118
|
-
|
|
119
|
-
let removed = 0;
|
|
120
|
-
try {
|
|
121
|
-
const out = fs.createWriteStream(tmpPath, { encoding: 'utf8' });
|
|
122
|
-
const rl = readline.createInterface({
|
|
123
|
-
input: fs.createReadStream(PATHS.history, { encoding: 'utf8' }),
|
|
124
|
-
crlfDelay: Infinity,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
for await (const raw of rl) {
|
|
128
|
-
if (!raw) {
|
|
129
|
-
out.write(os.EOL);
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
let drop = false;
|
|
133
|
-
try {
|
|
134
|
-
const obj = JSON.parse(raw) as { sessionId?: unknown };
|
|
135
|
-
if (typeof obj.sessionId === 'string' && sessionIds.has(obj.sessionId)) {
|
|
136
|
-
drop = true;
|
|
137
|
-
}
|
|
138
|
-
} catch {
|
|
139
|
-
/* keep malformed lines */
|
|
140
|
-
}
|
|
141
|
-
if (drop) {
|
|
142
|
-
removed += 1;
|
|
143
|
-
} else {
|
|
144
|
-
out.write(raw);
|
|
145
|
-
out.write(os.EOL);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
await new Promise<void>((resolve, reject) => {
|
|
149
|
-
out.end((err: Error | null | undefined) => (err ? reject(err) : resolve()));
|
|
150
|
-
});
|
|
151
|
-
} catch (err) {
|
|
152
|
-
fs.rmSync(tmpPath, { force: true });
|
|
153
|
-
throw err;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (removed === 0) {
|
|
157
|
-
fs.rmSync(tmpPath, { force: true });
|
|
158
|
-
return 0;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Windows-safe atomic-ish replace: backup original, swap tmp in, drop backup.
|
|
162
|
-
// unlink + rename is the alternative but loses recoverability mid-failure.
|
|
163
|
-
const backup = PATHS.history + '.bak-' + Date.now();
|
|
164
|
-
fs.renameSync(PATHS.history, backup);
|
|
165
|
-
try {
|
|
166
|
-
fs.renameSync(tmpPath, PATHS.history);
|
|
167
|
-
fs.rmSync(backup, { force: true });
|
|
168
|
-
} catch (err) {
|
|
169
|
-
if (fs.existsSync(backup)) {
|
|
170
|
-
try {
|
|
171
|
-
fs.renameSync(backup, PATHS.history);
|
|
172
|
-
} catch {
|
|
173
|
-
/* keep backup for manual recovery */
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
fs.rmSync(tmpPath, { force: true });
|
|
177
|
-
throw err;
|
|
178
|
-
}
|
|
179
|
-
return removed;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function cleanupDeadPidFiles(sessionIds: Set<string>): void {
|
|
183
|
-
for (const entry of readActivePidEntries()) {
|
|
184
|
-
if (!sessionIds.has(entry.sessionId)) continue;
|
|
185
|
-
if (entry.alive) continue;
|
|
186
|
-
try {
|
|
187
|
-
fs.rmSync(entry.sourceFile, { force: true });
|
|
188
|
-
} catch {
|
|
189
|
-
/* ignore */
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import readline from 'node:readline';
|
|
5
|
+
import { isUnderClaudeRoot, PATHS } from './claude-paths.ts';
|
|
6
|
+
import { RECENT_ACTIVITY_WINDOW_MS } from './constants.ts';
|
|
7
|
+
import { dirSize, fileSize } from './fs-size.ts';
|
|
8
|
+
import { isSafeId } from './safe-id.ts';
|
|
9
|
+
import { safeRemove } from './safe-remove.ts';
|
|
10
|
+
import {
|
|
11
|
+
buildActiveSessionMap,
|
|
12
|
+
readActivePidEntries,
|
|
13
|
+
} from './active-sessions.ts';
|
|
14
|
+
import type {
|
|
15
|
+
DeletedItem,
|
|
16
|
+
DeleteRequestItem,
|
|
17
|
+
DeleteResult,
|
|
18
|
+
RelatedBytes,
|
|
19
|
+
SkippedItem,
|
|
20
|
+
} from '../types.ts';
|
|
21
|
+
|
|
22
|
+
export type { DeleteRequestItem, DeleteResult } from '../types.ts';
|
|
23
|
+
|
|
24
|
+
const HISTORY_TMP_SUFFIX = '.tmp-clean';
|
|
25
|
+
|
|
26
|
+
export async function deleteSessions(items: DeleteRequestItem[]): Promise<DeleteResult> {
|
|
27
|
+
const liveMap = buildActiveSessionMap();
|
|
28
|
+
const deleted: DeletedItem[] = [];
|
|
29
|
+
const skipped: SkippedItem[] = [];
|
|
30
|
+
const targetIds = new Set<string>();
|
|
31
|
+
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
if (!isSafeId(item.projectId) || !isSafeId(item.sessionId)) {
|
|
34
|
+
skipped.push({ ...item, reason: 'invalid id' });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const projectDir = path.join(PATHS.projects, item.projectId);
|
|
39
|
+
const jsonlPath = path.join(projectDir, `${item.sessionId}.jsonl`);
|
|
40
|
+
const subdirPath = path.join(projectDir, item.sessionId);
|
|
41
|
+
const fhPath = path.join(PATHS.fileHistory, item.sessionId);
|
|
42
|
+
const sePath = path.join(PATHS.sessionEnv, item.sessionId);
|
|
43
|
+
|
|
44
|
+
const escaped = [jsonlPath, subdirPath, fhPath, sePath].find(
|
|
45
|
+
(p) => !isUnderClaudeRoot(p),
|
|
46
|
+
);
|
|
47
|
+
if (escaped) {
|
|
48
|
+
skipped.push({ ...item, reason: `path escapes ~/.claude: ${escaped}` });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (liveMap.has(item.sessionId)) {
|
|
53
|
+
skipped.push({
|
|
54
|
+
...item,
|
|
55
|
+
reason: `live PID ${liveMap.get(item.sessionId)} owns this session`,
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isRecentlyActive(jsonlPath)) {
|
|
61
|
+
skipped.push({
|
|
62
|
+
...item,
|
|
63
|
+
reason: 'jsonl modified within the last 5 minutes — could still be in use',
|
|
64
|
+
});
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(jsonlPath) && !fs.existsSync(subdirPath)) {
|
|
69
|
+
skipped.push({ ...item, reason: 'no files for this session' });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const related: RelatedBytes = {
|
|
74
|
+
jsonl: fileSize(jsonlPath),
|
|
75
|
+
subdir: dirSize(subdirPath),
|
|
76
|
+
fileHistory: dirSize(fhPath),
|
|
77
|
+
sessionEnv: dirSize(sePath),
|
|
78
|
+
};
|
|
79
|
+
const cleaned: string[] = [];
|
|
80
|
+
|
|
81
|
+
if (safeRemove(jsonlPath)) cleaned.push('projects/<id>.jsonl');
|
|
82
|
+
if (safeRemove(subdirPath)) cleaned.push('projects/<id>/');
|
|
83
|
+
if (safeRemove(fhPath)) cleaned.push('file-history/<id>/');
|
|
84
|
+
if (safeRemove(sePath)) cleaned.push('session-env/<id>/');
|
|
85
|
+
|
|
86
|
+
deleted.push({
|
|
87
|
+
...item,
|
|
88
|
+
freedBytes:
|
|
89
|
+
related.jsonl + related.subdir + related.fileHistory + related.sessionEnv,
|
|
90
|
+
cleaned,
|
|
91
|
+
relatedBytes: related,
|
|
92
|
+
});
|
|
93
|
+
targetIds.add(item.sessionId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let historyLinesRemoved = 0;
|
|
97
|
+
if (targetIds.size > 0) {
|
|
98
|
+
historyLinesRemoved = await rewriteHistoryWithout(targetIds);
|
|
99
|
+
cleanupDeadPidFiles(targetIds);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { deleted, skipped, historyLinesRemoved };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isRecentlyActive(jsonlPath: string): boolean {
|
|
106
|
+
try {
|
|
107
|
+
return Date.now() - fs.statSync(jsonlPath).mtimeMs < RECENT_ACTIVITY_WINDOW_MS;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function rewriteHistoryWithout(sessionIds: Set<string>): Promise<number> {
|
|
114
|
+
if (!fs.existsSync(PATHS.history)) return 0;
|
|
115
|
+
|
|
116
|
+
const tmpPath = PATHS.history + HISTORY_TMP_SUFFIX;
|
|
117
|
+
if (fs.existsSync(tmpPath)) fs.rmSync(tmpPath, { force: true });
|
|
118
|
+
|
|
119
|
+
let removed = 0;
|
|
120
|
+
try {
|
|
121
|
+
const out = fs.createWriteStream(tmpPath, { encoding: 'utf8' });
|
|
122
|
+
const rl = readline.createInterface({
|
|
123
|
+
input: fs.createReadStream(PATHS.history, { encoding: 'utf8' }),
|
|
124
|
+
crlfDelay: Infinity,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
for await (const raw of rl) {
|
|
128
|
+
if (!raw) {
|
|
129
|
+
out.write(os.EOL);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
let drop = false;
|
|
133
|
+
try {
|
|
134
|
+
const obj = JSON.parse(raw) as { sessionId?: unknown };
|
|
135
|
+
if (typeof obj.sessionId === 'string' && sessionIds.has(obj.sessionId)) {
|
|
136
|
+
drop = true;
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
/* keep malformed lines */
|
|
140
|
+
}
|
|
141
|
+
if (drop) {
|
|
142
|
+
removed += 1;
|
|
143
|
+
} else {
|
|
144
|
+
out.write(raw);
|
|
145
|
+
out.write(os.EOL);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
await new Promise<void>((resolve, reject) => {
|
|
149
|
+
out.end((err: Error | null | undefined) => (err ? reject(err) : resolve()));
|
|
150
|
+
});
|
|
151
|
+
} catch (err) {
|
|
152
|
+
fs.rmSync(tmpPath, { force: true });
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (removed === 0) {
|
|
157
|
+
fs.rmSync(tmpPath, { force: true });
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Windows-safe atomic-ish replace: backup original, swap tmp in, drop backup.
|
|
162
|
+
// unlink + rename is the alternative but loses recoverability mid-failure.
|
|
163
|
+
const backup = PATHS.history + '.bak-' + Date.now();
|
|
164
|
+
fs.renameSync(PATHS.history, backup);
|
|
165
|
+
try {
|
|
166
|
+
fs.renameSync(tmpPath, PATHS.history);
|
|
167
|
+
fs.rmSync(backup, { force: true });
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (fs.existsSync(backup)) {
|
|
170
|
+
try {
|
|
171
|
+
fs.renameSync(backup, PATHS.history);
|
|
172
|
+
} catch {
|
|
173
|
+
/* keep backup for manual recovery */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
fs.rmSync(tmpPath, { force: true });
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
return removed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function cleanupDeadPidFiles(sessionIds: Set<string>): void {
|
|
183
|
+
for (const entry of readActivePidEntries()) {
|
|
184
|
+
if (!sessionIds.has(entry.sessionId)) continue;
|
|
185
|
+
if (entry.alive) continue;
|
|
186
|
+
try {
|
|
187
|
+
fs.rmSync(entry.sourceFile, { force: true });
|
|
188
|
+
} catch {
|
|
189
|
+
/* ignore */
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
package/server/lib/disk-usage.ts
CHANGED
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
import { scanProjectsForDisk } from './scan.ts';
|
|
2
|
-
import type {
|
|
3
|
-
DiskUsage,
|
|
4
|
-
DiskUsageMonthRow,
|
|
5
|
-
DiskUsageProjectRow,
|
|
6
|
-
DiskUsageTopSession,
|
|
7
|
-
RelatedBytes,
|
|
8
|
-
} from '../types.ts';
|
|
9
|
-
|
|
10
|
-
const TOP_N = 20;
|
|
11
|
-
|
|
12
|
-
export async function computeDiskUsage(): Promise<DiskUsage> {
|
|
13
|
-
const projects = await scanProjectsForDisk();
|
|
14
|
-
|
|
15
|
-
const byProject: DiskUsageProjectRow[] = [];
|
|
16
|
-
const monthMap = new Map<string, { bytes: number; count: number }>();
|
|
17
|
-
const flat: Array<{
|
|
18
|
-
projectId: string;
|
|
19
|
-
sessionId: string;
|
|
20
|
-
title: string;
|
|
21
|
-
customTitle: string | null;
|
|
22
|
-
bytes: number;
|
|
23
|
-
lastAt: string | null;
|
|
24
|
-
}> = [];
|
|
25
|
-
|
|
26
|
-
for (const p of projects) {
|
|
27
|
-
byProject.push({
|
|
28
|
-
projectId: p.id,
|
|
29
|
-
decodedCwd: p.decodedCwd,
|
|
30
|
-
totalBytes: p.totalBytes,
|
|
31
|
-
sessionCount: p.sessionCount,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
for (const s of p.sessions) {
|
|
35
|
-
const total = sessionTotal(s.relatedBytes);
|
|
36
|
-
const month = s.lastAt ? s.lastAt.slice(0, 7) : 'unknown';
|
|
37
|
-
const acc = monthMap.get(month) ?? { bytes: 0, count: 0 };
|
|
38
|
-
acc.bytes += total;
|
|
39
|
-
acc.count += 1;
|
|
40
|
-
monthMap.set(month, acc);
|
|
41
|
-
flat.push({
|
|
42
|
-
projectId: p.id,
|
|
43
|
-
sessionId: s.id,
|
|
44
|
-
title: s.title,
|
|
45
|
-
customTitle: s.customTitle,
|
|
46
|
-
bytes: total,
|
|
47
|
-
lastAt: s.lastAt,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
byProject.sort((a, b) => b.totalBytes - a.totalBytes);
|
|
53
|
-
|
|
54
|
-
const byMonth: DiskUsageMonthRow[] = [...monthMap.entries()]
|
|
55
|
-
.map(([month, v]) => ({ month, totalBytes: v.bytes, sessionCount: v.count }))
|
|
56
|
-
.sort((a, b) => a.month.localeCompare(b.month));
|
|
57
|
-
|
|
58
|
-
const topSessions: DiskUsageTopSession[] = flat
|
|
59
|
-
.sort((a, b) => b.bytes - a.bytes)
|
|
60
|
-
.slice(0, TOP_N)
|
|
61
|
-
.map((f) => ({
|
|
62
|
-
projectId: f.projectId,
|
|
63
|
-
sessionId: f.sessionId,
|
|
64
|
-
title: f.title,
|
|
65
|
-
customTitle: f.customTitle,
|
|
66
|
-
totalBytes: f.bytes,
|
|
67
|
-
lastAt: f.lastAt,
|
|
68
|
-
}));
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
byProject,
|
|
72
|
-
byMonth,
|
|
73
|
-
topSessions,
|
|
74
|
-
totalBytes: byProject.reduce((acc, r) => acc + r.totalBytes, 0),
|
|
75
|
-
totalSessions: flat.length,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function sessionTotal(r: RelatedBytes): number {
|
|
80
|
-
return r.jsonl + r.subdir + r.fileHistory + r.sessionEnv;
|
|
81
|
-
}
|
|
1
|
+
import { scanProjectsForDisk } from './scan.ts';
|
|
2
|
+
import type {
|
|
3
|
+
DiskUsage,
|
|
4
|
+
DiskUsageMonthRow,
|
|
5
|
+
DiskUsageProjectRow,
|
|
6
|
+
DiskUsageTopSession,
|
|
7
|
+
RelatedBytes,
|
|
8
|
+
} from '../types.ts';
|
|
9
|
+
|
|
10
|
+
const TOP_N = 20;
|
|
11
|
+
|
|
12
|
+
export async function computeDiskUsage(): Promise<DiskUsage> {
|
|
13
|
+
const projects = await scanProjectsForDisk();
|
|
14
|
+
|
|
15
|
+
const byProject: DiskUsageProjectRow[] = [];
|
|
16
|
+
const monthMap = new Map<string, { bytes: number; count: number }>();
|
|
17
|
+
const flat: Array<{
|
|
18
|
+
projectId: string;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
title: string;
|
|
21
|
+
customTitle: string | null;
|
|
22
|
+
bytes: number;
|
|
23
|
+
lastAt: string | null;
|
|
24
|
+
}> = [];
|
|
25
|
+
|
|
26
|
+
for (const p of projects) {
|
|
27
|
+
byProject.push({
|
|
28
|
+
projectId: p.id,
|
|
29
|
+
decodedCwd: p.decodedCwd,
|
|
30
|
+
totalBytes: p.totalBytes,
|
|
31
|
+
sessionCount: p.sessionCount,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
for (const s of p.sessions) {
|
|
35
|
+
const total = sessionTotal(s.relatedBytes);
|
|
36
|
+
const month = s.lastAt ? s.lastAt.slice(0, 7) : 'unknown';
|
|
37
|
+
const acc = monthMap.get(month) ?? { bytes: 0, count: 0 };
|
|
38
|
+
acc.bytes += total;
|
|
39
|
+
acc.count += 1;
|
|
40
|
+
monthMap.set(month, acc);
|
|
41
|
+
flat.push({
|
|
42
|
+
projectId: p.id,
|
|
43
|
+
sessionId: s.id,
|
|
44
|
+
title: s.title,
|
|
45
|
+
customTitle: s.customTitle,
|
|
46
|
+
bytes: total,
|
|
47
|
+
lastAt: s.lastAt,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
byProject.sort((a, b) => b.totalBytes - a.totalBytes);
|
|
53
|
+
|
|
54
|
+
const byMonth: DiskUsageMonthRow[] = [...monthMap.entries()]
|
|
55
|
+
.map(([month, v]) => ({ month, totalBytes: v.bytes, sessionCount: v.count }))
|
|
56
|
+
.sort((a, b) => a.month.localeCompare(b.month));
|
|
57
|
+
|
|
58
|
+
const topSessions: DiskUsageTopSession[] = flat
|
|
59
|
+
.sort((a, b) => b.bytes - a.bytes)
|
|
60
|
+
.slice(0, TOP_N)
|
|
61
|
+
.map((f) => ({
|
|
62
|
+
projectId: f.projectId,
|
|
63
|
+
sessionId: f.sessionId,
|
|
64
|
+
title: f.title,
|
|
65
|
+
customTitle: f.customTitle,
|
|
66
|
+
totalBytes: f.bytes,
|
|
67
|
+
lastAt: f.lastAt,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
byProject,
|
|
72
|
+
byMonth,
|
|
73
|
+
topSessions,
|
|
74
|
+
totalBytes: byProject.reduce((acc, r) => acc + r.totalBytes, 0),
|
|
75
|
+
totalSessions: flat.length,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sessionTotal(r: RelatedBytes): number {
|
|
80
|
+
return r.jsonl + r.subdir + r.fileHistory + r.sessionEnv;
|
|
81
|
+
}
|
package/server/lib/encode-cwd.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
const WIN_DRIVE_DOUBLE_DASH = /^([A-Za-z])--/;
|
|
4
|
-
|
|
5
|
-
export function decodeCwd(encoded: string): string {
|
|
6
|
-
if (WIN_DRIVE_DOUBLE_DASH.test(encoded)) {
|
|
7
|
-
const drive = encoded[0]!.toUpperCase();
|
|
8
|
-
const rest = encoded.slice(3).replace(/-/g, '\\');
|
|
9
|
-
return `${drive}:\\${rest}`;
|
|
10
|
-
}
|
|
11
|
-
if (encoded.startsWith('-')) {
|
|
12
|
-
return '/' + encoded.slice(1).replace(/-/g, '/');
|
|
13
|
-
}
|
|
14
|
-
return encoded;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function encodeCwd(cwd: string): string {
|
|
18
|
-
if (path.isAbsolute(cwd) && /^[A-Za-z]:[\\/]/.test(cwd)) {
|
|
19
|
-
const drive = cwd[0]!.toUpperCase();
|
|
20
|
-
const rest = cwd.slice(3).replace(/[\\/]/g, '-');
|
|
21
|
-
return `${drive}--${rest}`;
|
|
22
|
-
}
|
|
23
|
-
return cwd.replace(/\//g, '-');
|
|
24
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const WIN_DRIVE_DOUBLE_DASH = /^([A-Za-z])--/;
|
|
4
|
+
|
|
5
|
+
export function decodeCwd(encoded: string): string {
|
|
6
|
+
if (WIN_DRIVE_DOUBLE_DASH.test(encoded)) {
|
|
7
|
+
const drive = encoded[0]!.toUpperCase();
|
|
8
|
+
const rest = encoded.slice(3).replace(/-/g, '\\');
|
|
9
|
+
return `${drive}:\\${rest}`;
|
|
10
|
+
}
|
|
11
|
+
if (encoded.startsWith('-')) {
|
|
12
|
+
return '/' + encoded.slice(1).replace(/-/g, '/');
|
|
13
|
+
}
|
|
14
|
+
return encoded;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function encodeCwd(cwd: string): string {
|
|
18
|
+
if (path.isAbsolute(cwd) && /^[A-Za-z]:[\\/]/.test(cwd)) {
|
|
19
|
+
const drive = cwd[0]!.toUpperCase();
|
|
20
|
+
const rest = cwd.slice(3).replace(/[\\/]/g, '-');
|
|
21
|
+
return `${drive}--${rest}`;
|
|
22
|
+
}
|
|
23
|
+
return cwd.replace(/\//g, '-');
|
|
24
|
+
}
|