@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.
- 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 +85 -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
|
@@ -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
|
+
}
|