@zzusp/ccsm 1.0.1 → 1.0.2

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.
Files changed (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +236 -236
  3. package/bin/cli.mjs +52 -52
  4. package/dist/assets/{DiskUsage-CKhggLs5.js → DiskUsage-BY6XwffG.js} +2 -2
  5. package/dist/assets/DiskUsage-BY6XwffG.js.map +1 -0
  6. package/dist/assets/{ImportPage-wge4VhZ-.js → ImportPage-Cwq5bx7G.js} +2 -2
  7. package/dist/assets/ImportPage-Cwq5bx7G.js.map +1 -0
  8. package/dist/assets/MarkdownContent-BFu7Nkk_.js +2 -0
  9. package/dist/assets/MarkdownContent-BFu7Nkk_.js.map +1 -0
  10. package/dist/assets/{ProjectMemory-Q4XX40j_.js → ProjectMemory-CcE3KbUK.js} +2 -2
  11. package/dist/assets/ProjectMemory-CcE3KbUK.js.map +1 -0
  12. package/dist/assets/index-CrWxV6sb.css +1 -0
  13. package/dist/assets/index-DTbWl1jb.js +11 -0
  14. package/dist/assets/index-DTbWl1jb.js.map +1 -0
  15. package/dist/assets/markdown-Bag5rX3T.js +30 -0
  16. package/dist/assets/markdown-Bag5rX3T.js.map +1 -0
  17. package/dist/index.html +26 -26
  18. package/package.json +85 -83
  19. package/server/index.ts +130 -130
  20. package/server/lib/active-sessions.test.ts +119 -119
  21. package/server/lib/active-sessions.ts +95 -95
  22. package/server/lib/bundle.test.ts +182 -182
  23. package/server/lib/bundle.ts +86 -86
  24. package/server/lib/claude-paths.test.ts +126 -126
  25. package/server/lib/claude-paths.ts +43 -43
  26. package/server/lib/cleanup-suggestions.ts +131 -131
  27. package/server/lib/constants.ts +8 -8
  28. package/server/lib/delete-project.ts +100 -100
  29. package/server/lib/delete.test.ts +244 -244
  30. package/server/lib/delete.ts +192 -192
  31. package/server/lib/disk-usage.ts +81 -81
  32. package/server/lib/encode-cwd.ts +24 -24
  33. package/server/lib/export-bundle.ts +236 -236
  34. package/server/lib/export-import-bundle.test.ts +337 -337
  35. package/server/lib/fs-size.ts +38 -38
  36. package/server/lib/import-bundle.ts +488 -488
  37. package/server/lib/load-memory.ts +120 -120
  38. package/server/lib/load-session.ts +209 -209
  39. package/server/lib/modified-files.test.ts +280 -280
  40. package/server/lib/modified-files.ts +228 -228
  41. package/server/lib/open-folder.ts +47 -47
  42. package/server/lib/parse-jsonl.ts +160 -139
  43. package/server/lib/port.ts +23 -23
  44. package/server/lib/safe-id.test.ts +41 -41
  45. package/server/lib/safe-id.ts +6 -6
  46. package/server/lib/safe-remove.test.ts +73 -73
  47. package/server/lib/safe-remove.ts +25 -25
  48. package/server/lib/scan.ts +289 -286
  49. package/server/lib/search-all.ts +130 -130
  50. package/server/lib/search-session.ts +203 -203
  51. package/server/lib/system-tags.ts +20 -20
  52. package/server/lib/update.ts +67 -67
  53. package/server/lib/version.test.ts +39 -39
  54. package/server/lib/version.ts +117 -117
  55. package/server/routes/disk-cleanup.ts +54 -54
  56. package/server/routes/disk.ts +9 -9
  57. package/server/routes/import.ts +87 -87
  58. package/server/routes/projects.ts +104 -104
  59. package/server/routes/search.ts +79 -79
  60. package/server/routes/sessions.ts +130 -130
  61. package/server/routes/version.ts +34 -34
  62. package/server/types.ts +1 -1
  63. package/shared/constants.ts +7 -7
  64. package/shared/types.ts +513 -511
  65. package/dist/assets/DiskUsage-CKhggLs5.js.map +0 -1
  66. package/dist/assets/ImportPage-wge4VhZ-.js.map +0 -1
  67. package/dist/assets/ProjectMemory-Q4XX40j_.js.map +0 -1
  68. package/dist/assets/index-7aMrnHJG.js +0 -7
  69. package/dist/assets/index-7aMrnHJG.js.map +0 -1
  70. package/dist/assets/index-BOeI_J4B.css +0 -1
@@ -1,119 +1,119 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
-
6
- // active-sessions.ts 是 delete / import "跳过活会话"安全网的事实源头。
7
- // 测试覆盖 isPidAlive 的 POSIX 分支 + buildActiveSessionMap 对死/活 PID 的区分。
8
-
9
- let fakeRoot: string;
10
-
11
- vi.mock('./claude-paths.ts', () => ({
12
- get PATHS() {
13
- const root = process.env.CCSM_TEST_ROOT!;
14
- return {
15
- root,
16
- projects: path.join(root, 'projects'),
17
- fileHistory: path.join(root, 'file-history'),
18
- sessionEnv: path.join(root, 'session-env'),
19
- sessions: path.join(root, 'sessions'),
20
- history: path.join(root, 'history.jsonl'),
21
- };
22
- },
23
- isUnderClaudeRoot(target: string): boolean {
24
- const root = process.env.CCSM_TEST_ROOT!;
25
- const resolved = path.resolve(target);
26
- return resolved === root || resolved.startsWith(root + path.sep);
27
- },
28
- getCacheDir(): string {
29
- return path.join(process.env.CCSM_TEST_ROOT!, '_cache');
30
- },
31
- }));
32
-
33
- beforeEach(() => {
34
- fakeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccsm-active-test-'));
35
- process.env.CCSM_TEST_ROOT = fakeRoot;
36
- fs.mkdirSync(path.join(fakeRoot, 'sessions'), { recursive: true });
37
- });
38
-
39
- afterEach(() => {
40
- vi.restoreAllMocks();
41
- delete process.env.CCSM_TEST_ROOT;
42
- fs.rmSync(fakeRoot, { recursive: true, force: true });
43
- });
44
-
45
- describe('isPidAlive (POSIX)', () => {
46
- it('当前进程 pid 必为活', async () => {
47
- const { isPidAlive } = await import('./active-sessions.ts');
48
- // 仅在非 Windows 平台跑 POSIX 断言;CI 上若在 Windows 这条用 process.platform 跳过
49
- if (process.platform === 'win32') return;
50
- expect(isPidAlive(process.pid)).toBe(true);
51
- });
52
-
53
- it('明显不可能的 pid 不应判活', async () => {
54
- const { isPidAlive } = await import('./active-sessions.ts');
55
- if (process.platform === 'win32') return;
56
- expect(isPidAlive(0)).toBe(false);
57
- expect(isPidAlive(-1)).toBe(false);
58
- expect(isPidAlive(Number.NaN)).toBe(false);
59
- });
60
-
61
- it('一个上限附近、几乎不可能存在的 pid 判死', async () => {
62
- const { isPidAlive } = await import('./active-sessions.ts');
63
- if (process.platform === 'win32') return;
64
- // 4194304 是 Linux 默认 pid_max;macOS 99998;两边都极不可能命中真实进程
65
- expect(isPidAlive(4194303)).toBe(false);
66
- });
67
- });
68
-
69
- describe('readActivePidEntries / buildActiveSessionMap', () => {
70
- function writePidFile(pid: number, sessionId: string): void {
71
- fs.writeFileSync(
72
- path.join(fakeRoot, 'sessions', `${pid}.json`),
73
- JSON.stringify({ pid, sessionId, cwd: '/Users/alice/proj' }),
74
- );
75
- }
76
-
77
- it('活进程被读出且 alive=true、死进程 alive=false', async () => {
78
- const { readActivePidEntries, buildActiveSessionMap } = await import('./active-sessions.ts');
79
- if (process.platform === 'win32') return;
80
-
81
- writePidFile(process.pid, 'sid-live');
82
- writePidFile(4194303, 'sid-dead');
83
-
84
- const entries = readActivePidEntries();
85
- const live = entries.find((e) => e.sessionId === 'sid-live');
86
- const dead = entries.find((e) => e.sessionId === 'sid-dead');
87
- expect(live?.alive).toBe(true);
88
- expect(dead?.alive).toBe(false);
89
-
90
- const map = buildActiveSessionMap();
91
- expect(map.get('sid-live')).toBe(process.pid);
92
- expect(map.has('sid-dead')).toBe(false);
93
- });
94
-
95
- it('PID 文件格式不合(缺字段 / 非 JSON)静默跳过,不抛', async () => {
96
- const { readActivePidEntries } = await import('./active-sessions.ts');
97
- if (process.platform === 'win32') return;
98
-
99
- fs.writeFileSync(path.join(fakeRoot, 'sessions', '111.json'), 'not json');
100
- fs.writeFileSync(
101
- path.join(fakeRoot, 'sessions', '222.json'),
102
- JSON.stringify({ sessionId: 'no-pid-here' }),
103
- );
104
- fs.writeFileSync(
105
- path.join(fakeRoot, 'sessions', '333.json'),
106
- JSON.stringify({ pid: 333 }), // 缺 sessionId
107
- );
108
-
109
- const entries = readActivePidEntries();
110
- expect(entries).toEqual([]);
111
- });
112
-
113
- it('sessions 目录不存在时返回空(不创建副作用)', async () => {
114
- const { readActivePidEntries } = await import('./active-sessions.ts');
115
- fs.rmSync(path.join(fakeRoot, 'sessions'), { recursive: true, force: true });
116
- expect(readActivePidEntries()).toEqual([]);
117
- expect(fs.existsSync(path.join(fakeRoot, 'sessions'))).toBe(false);
118
- });
119
- });
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ // active-sessions.ts 是 delete / import "跳过活会话"安全网的事实源头。
7
+ // 测试覆盖 isPidAlive 的 POSIX 分支 + buildActiveSessionMap 对死/活 PID 的区分。
8
+
9
+ let fakeRoot: string;
10
+
11
+ vi.mock('./claude-paths.ts', () => ({
12
+ get PATHS() {
13
+ const root = process.env.CCSM_TEST_ROOT!;
14
+ return {
15
+ root,
16
+ projects: path.join(root, 'projects'),
17
+ fileHistory: path.join(root, 'file-history'),
18
+ sessionEnv: path.join(root, 'session-env'),
19
+ sessions: path.join(root, 'sessions'),
20
+ history: path.join(root, 'history.jsonl'),
21
+ };
22
+ },
23
+ isUnderClaudeRoot(target: string): boolean {
24
+ const root = process.env.CCSM_TEST_ROOT!;
25
+ const resolved = path.resolve(target);
26
+ return resolved === root || resolved.startsWith(root + path.sep);
27
+ },
28
+ getCacheDir(): string {
29
+ return path.join(process.env.CCSM_TEST_ROOT!, '_cache');
30
+ },
31
+ }));
32
+
33
+ beforeEach(() => {
34
+ fakeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccsm-active-test-'));
35
+ process.env.CCSM_TEST_ROOT = fakeRoot;
36
+ fs.mkdirSync(path.join(fakeRoot, 'sessions'), { recursive: true });
37
+ });
38
+
39
+ afterEach(() => {
40
+ vi.restoreAllMocks();
41
+ delete process.env.CCSM_TEST_ROOT;
42
+ fs.rmSync(fakeRoot, { recursive: true, force: true });
43
+ });
44
+
45
+ describe('isPidAlive (POSIX)', () => {
46
+ it('当前进程 pid 必为活', async () => {
47
+ const { isPidAlive } = await import('./active-sessions.ts');
48
+ // 仅在非 Windows 平台跑 POSIX 断言;CI 上若在 Windows 这条用 process.platform 跳过
49
+ if (process.platform === 'win32') return;
50
+ expect(isPidAlive(process.pid)).toBe(true);
51
+ });
52
+
53
+ it('明显不可能的 pid 不应判活', async () => {
54
+ const { isPidAlive } = await import('./active-sessions.ts');
55
+ if (process.platform === 'win32') return;
56
+ expect(isPidAlive(0)).toBe(false);
57
+ expect(isPidAlive(-1)).toBe(false);
58
+ expect(isPidAlive(Number.NaN)).toBe(false);
59
+ });
60
+
61
+ it('一个上限附近、几乎不可能存在的 pid 判死', async () => {
62
+ const { isPidAlive } = await import('./active-sessions.ts');
63
+ if (process.platform === 'win32') return;
64
+ // 4194304 是 Linux 默认 pid_max;macOS 99998;两边都极不可能命中真实进程
65
+ expect(isPidAlive(4194303)).toBe(false);
66
+ });
67
+ });
68
+
69
+ describe('readActivePidEntries / buildActiveSessionMap', () => {
70
+ function writePidFile(pid: number, sessionId: string): void {
71
+ fs.writeFileSync(
72
+ path.join(fakeRoot, 'sessions', `${pid}.json`),
73
+ JSON.stringify({ pid, sessionId, cwd: '/Users/alice/proj' }),
74
+ );
75
+ }
76
+
77
+ it('活进程被读出且 alive=true、死进程 alive=false', async () => {
78
+ const { readActivePidEntries, buildActiveSessionMap } = await import('./active-sessions.ts');
79
+ if (process.platform === 'win32') return;
80
+
81
+ writePidFile(process.pid, 'sid-live');
82
+ writePidFile(4194303, 'sid-dead');
83
+
84
+ const entries = readActivePidEntries();
85
+ const live = entries.find((e) => e.sessionId === 'sid-live');
86
+ const dead = entries.find((e) => e.sessionId === 'sid-dead');
87
+ expect(live?.alive).toBe(true);
88
+ expect(dead?.alive).toBe(false);
89
+
90
+ const map = buildActiveSessionMap();
91
+ expect(map.get('sid-live')).toBe(process.pid);
92
+ expect(map.has('sid-dead')).toBe(false);
93
+ });
94
+
95
+ it('PID 文件格式不合(缺字段 / 非 JSON)静默跳过,不抛', async () => {
96
+ const { readActivePidEntries } = await import('./active-sessions.ts');
97
+ if (process.platform === 'win32') return;
98
+
99
+ fs.writeFileSync(path.join(fakeRoot, 'sessions', '111.json'), 'not json');
100
+ fs.writeFileSync(
101
+ path.join(fakeRoot, 'sessions', '222.json'),
102
+ JSON.stringify({ sessionId: 'no-pid-here' }),
103
+ );
104
+ fs.writeFileSync(
105
+ path.join(fakeRoot, 'sessions', '333.json'),
106
+ JSON.stringify({ pid: 333 }), // 缺 sessionId
107
+ );
108
+
109
+ const entries = readActivePidEntries();
110
+ expect(entries).toEqual([]);
111
+ });
112
+
113
+ it('sessions 目录不存在时返回空(不创建副作用)', async () => {
114
+ const { readActivePidEntries } = await import('./active-sessions.ts');
115
+ fs.rmSync(path.join(fakeRoot, 'sessions'), { recursive: true, force: true });
116
+ expect(readActivePidEntries()).toEqual([]);
117
+ expect(fs.existsSync(path.join(fakeRoot, 'sessions'))).toBe(false);
118
+ });
119
+ });
@@ -1,95 +1,95 @@
1
- import { execFileSync } from 'node:child_process';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { PATHS } from './claude-paths.ts';
5
-
6
- export interface ActivePidEntry {
7
- pid: number;
8
- sessionId: string;
9
- cwd: string;
10
- alive: boolean;
11
- /** Absolute path to the PID file we read this entry from. */
12
- sourceFile: string;
13
- }
14
-
15
- export function isPidAlive(pid: number): boolean {
16
- if (!Number.isFinite(pid) || pid <= 0) return false;
17
- if (process.platform === 'win32') {
18
- try {
19
- const out = execFileSync(
20
- 'tasklist',
21
- ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'],
22
- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
23
- );
24
- return out.toLowerCase().includes(`"${pid}"`);
25
- } catch {
26
- return false;
27
- }
28
- }
29
- try {
30
- process.kill(pid, 0);
31
- return true;
32
- } catch (err) {
33
- return (err as NodeJS.ErrnoException).code === 'EPERM';
34
- }
35
- }
36
-
37
- /**
38
- * Windows: enumerate every running PID with one `tasklist` call.
39
- * Each call is ~400-700ms; doing it once instead of per-PID turns the
40
- * cost from O(N×tasklist) into O(1×tasklist) for `readActivePidEntries`.
41
- */
42
- function listAlivePidsWindows(): Set<number> {
43
- const set = new Set<number>();
44
- try {
45
- const out = execFileSync('tasklist', ['/NH', '/FO', 'CSV'], {
46
- encoding: 'utf8',
47
- stdio: ['ignore', 'pipe', 'ignore'],
48
- });
49
- for (const line of out.split(/\r?\n/)) {
50
- // Format: "Image Name","PID","Session Name","Session#","Mem Usage"
51
- const m = line.match(/^"[^"]*","(\d+)"/);
52
- if (m) set.add(Number(m[1]));
53
- }
54
- } catch {
55
- /* return whatever we have; callers treat unknown PIDs as dead */
56
- }
57
- return set;
58
- }
59
-
60
- export function readActivePidEntries(): ActivePidEntry[] {
61
- if (!fs.existsSync(PATHS.sessions)) return [];
62
- const alivePids = process.platform === 'win32' ? listAlivePidsWindows() : null;
63
- const entries: ActivePidEntry[] = [];
64
- for (const name of fs.readdirSync(PATHS.sessions)) {
65
- if (!name.endsWith('.json')) continue;
66
- const full = path.join(PATHS.sessions, name);
67
- try {
68
- const obj = JSON.parse(fs.readFileSync(full, 'utf8')) as {
69
- pid?: number;
70
- sessionId?: string;
71
- cwd?: string;
72
- };
73
- if (typeof obj.pid !== 'number' || typeof obj.sessionId !== 'string') continue;
74
- const alive = alivePids ? alivePids.has(obj.pid) : isPidAlive(obj.pid);
75
- entries.push({
76
- pid: obj.pid,
77
- sessionId: obj.sessionId,
78
- cwd: typeof obj.cwd === 'string' ? obj.cwd : '',
79
- alive,
80
- sourceFile: full,
81
- });
82
- } catch {
83
- // skip malformed PID files
84
- }
85
- }
86
- return entries;
87
- }
88
-
89
- export function buildActiveSessionMap(): Map<string, number> {
90
- const map = new Map<string, number>();
91
- for (const e of readActivePidEntries()) {
92
- if (e.alive) map.set(e.sessionId, e.pid);
93
- }
94
- return map;
95
- }
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { PATHS } from './claude-paths.ts';
5
+
6
+ export interface ActivePidEntry {
7
+ pid: number;
8
+ sessionId: string;
9
+ cwd: string;
10
+ alive: boolean;
11
+ /** Absolute path to the PID file we read this entry from. */
12
+ sourceFile: string;
13
+ }
14
+
15
+ export function isPidAlive(pid: number): boolean {
16
+ if (!Number.isFinite(pid) || pid <= 0) return false;
17
+ if (process.platform === 'win32') {
18
+ try {
19
+ const out = execFileSync(
20
+ 'tasklist',
21
+ ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'],
22
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
23
+ );
24
+ return out.toLowerCase().includes(`"${pid}"`);
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ } catch (err) {
33
+ return (err as NodeJS.ErrnoException).code === 'EPERM';
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Windows: enumerate every running PID with one `tasklist` call.
39
+ * Each call is ~400-700ms; doing it once instead of per-PID turns the
40
+ * cost from O(N×tasklist) into O(1×tasklist) for `readActivePidEntries`.
41
+ */
42
+ function listAlivePidsWindows(): Set<number> {
43
+ const set = new Set<number>();
44
+ try {
45
+ const out = execFileSync('tasklist', ['/NH', '/FO', 'CSV'], {
46
+ encoding: 'utf8',
47
+ stdio: ['ignore', 'pipe', 'ignore'],
48
+ });
49
+ for (const line of out.split(/\r?\n/)) {
50
+ // Format: "Image Name","PID","Session Name","Session#","Mem Usage"
51
+ const m = line.match(/^"[^"]*","(\d+)"/);
52
+ if (m) set.add(Number(m[1]));
53
+ }
54
+ } catch {
55
+ /* return whatever we have; callers treat unknown PIDs as dead */
56
+ }
57
+ return set;
58
+ }
59
+
60
+ export function readActivePidEntries(): ActivePidEntry[] {
61
+ if (!fs.existsSync(PATHS.sessions)) return [];
62
+ const alivePids = process.platform === 'win32' ? listAlivePidsWindows() : null;
63
+ const entries: ActivePidEntry[] = [];
64
+ for (const name of fs.readdirSync(PATHS.sessions)) {
65
+ if (!name.endsWith('.json')) continue;
66
+ const full = path.join(PATHS.sessions, name);
67
+ try {
68
+ const obj = JSON.parse(fs.readFileSync(full, 'utf8')) as {
69
+ pid?: number;
70
+ sessionId?: string;
71
+ cwd?: string;
72
+ };
73
+ if (typeof obj.pid !== 'number' || typeof obj.sessionId !== 'string') continue;
74
+ const alive = alivePids ? alivePids.has(obj.pid) : isPidAlive(obj.pid);
75
+ entries.push({
76
+ pid: obj.pid,
77
+ sessionId: obj.sessionId,
78
+ cwd: typeof obj.cwd === 'string' ? obj.cwd : '',
79
+ alive,
80
+ sourceFile: full,
81
+ });
82
+ } catch {
83
+ // skip malformed PID files
84
+ }
85
+ }
86
+ return entries;
87
+ }
88
+
89
+ export function buildActiveSessionMap(): Map<string, number> {
90
+ const map = new Map<string, number>();
91
+ for (const e of readActivePidEntries()) {
92
+ if (e.alive) map.set(e.sessionId, e.pid);
93
+ }
94
+ return map;
95
+ }