claude-bulkhead 0.0.0-stage → 0.2.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/CHANGELOG.md +90 -0
- package/LICENSE +21 -0
- package/README.es.md +498 -0
- package/README.ja.md +498 -0
- package/README.ko.md +498 -0
- package/README.md +497 -2
- package/README.pt-BR.md +498 -0
- package/README.zh-CN.md +498 -0
- package/bin/bulkhead +3 -0
- package/bin/bulkhead-slot +3 -0
- package/bin/bulkhead-statusline +3 -0
- package/hooks/hooks.json +39 -0
- package/package.json +52 -4
- package/src/basics.js +236 -0
- package/src/checkout-guard.js +296 -0
- package/src/cli-util.js +79 -0
- package/src/cli.js +41 -0
- package/src/config-check.js +29 -0
- package/src/config-cli.js +51 -0
- package/src/config.js +236 -0
- package/src/dev-servers.js +223 -0
- package/src/disk-guard.js +200 -0
- package/src/doctor.js +255 -0
- package/src/events.js +108 -0
- package/src/hook.js +94 -0
- package/src/kill-guard.js +435 -0
- package/src/log-cli.js +146 -0
- package/src/memory-guard.js +145 -0
- package/src/memory.js +76 -0
- package/src/orphan-reaper.js +527 -0
- package/src/paths.js +31 -0
- package/src/port-cli.js +59 -0
- package/src/port-guard.js +142 -0
- package/src/processes.js +181 -0
- package/src/reap-cli.js +56 -0
- package/src/remote-sh.js +478 -0
- package/src/remote.js +1034 -0
- package/src/remotes.js +126 -0
- package/src/scan.js +604 -0
- package/src/sessions.js +147 -0
- package/src/shell.js +597 -0
- package/src/slot-cli.js +150 -0
- package/src/slots.js +168 -0
- package/src/status.js +111 -0
- package/src/statusline.js +69 -0
- package/src/top-worker.js +14 -0
- package/src/top.js +305 -0
- package/src/wait-guard.js +263 -0
package/src/sessions.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
// On Windows the desktop app and the CLI are both claude.exe. The app's processes run from
|
|
7
|
+
// ...\app\Claude.exe (or Claude.app on macOS) and its helpers carry --type=.
|
|
8
|
+
const DESKTOP_APP = /[\\/]app[\\/]Claude\.exe|Claude\.app[\\/]Contents[\\/]MacOS[\\/]Claude(?:["\s]|$)/i
|
|
9
|
+
const PID_REUSE_SLACK_MS = 120 * 1000
|
|
10
|
+
|
|
11
|
+
// The native installer runs versions/<x.y.z> through a `claude` link, so the process name can be
|
|
12
|
+
// the version; the command line still starts with claude or names that folder.
|
|
13
|
+
function isClaudeCli(p) {
|
|
14
|
+
if (!p) return false
|
|
15
|
+
if (/--type=/.test(p.cmd) || DESKTOP_APP.test(p.cmd)) return false
|
|
16
|
+
if (/^claude(?:\.exe)?$/i.test(p.name)) return true
|
|
17
|
+
if (/^["']?(?:\S*[\\/])?claude(?:\.exe)?["']?(?:\s|$)|[\\/]claude[\\/]versions[\\/]/i.test(p.cmd)) return true
|
|
18
|
+
return /^(?:node|bun)(?:\.exe)?$/i.test(p.name) && /claude-code[\\/]cli\.js|@anthropic-ai[\\/]claude-code/i.test(p.cmd)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function claudeDir() {
|
|
22
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Claude Code writes ~/.claude/sessions/<pid>.json with the session's cwd and start time.
|
|
26
|
+
function readSessionFiles(dir = path.join(claudeDir(), 'sessions')) {
|
|
27
|
+
const files = new Map()
|
|
28
|
+
let names = []
|
|
29
|
+
try { names = fs.readdirSync(dir) } catch {}
|
|
30
|
+
for (const name of names.filter(n => /^\d+\.json$/.test(n))) {
|
|
31
|
+
try {
|
|
32
|
+
const s = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'))
|
|
33
|
+
if (s.pid) files.set(Number(s.pid), { cwd: s.cwd || null, startedAt: Number(s.startedAt) || 0, status: s.status || null, sessionId: s.sessionId || null })
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
return files
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function linuxCwd(pid) {
|
|
40
|
+
try { return fs.readlinkSync(`/proc/${pid}/cwd`) } catch { return null }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Maps every process to the session whose tree it is in. A child must be younger than its
|
|
44
|
+
// parent, which skips Windows children whose parent PID was reused. On Linux a server that
|
|
45
|
+
// outlived its shell is still matched through CLAUDE_PID.
|
|
46
|
+
function assignOwners(procs, sessionPids) {
|
|
47
|
+
const byPid = new Map(procs.map(p => [p.pid, p]))
|
|
48
|
+
const kids = new Map()
|
|
49
|
+
for (const p of procs) {
|
|
50
|
+
if (!kids.has(p.ppid)) kids.set(p.ppid, [])
|
|
51
|
+
kids.get(p.ppid).push(p)
|
|
52
|
+
}
|
|
53
|
+
const owner = new Map()
|
|
54
|
+
for (const root of sessionPids) {
|
|
55
|
+
const queue = [byPid.get(root)]
|
|
56
|
+
owner.set(root, root)
|
|
57
|
+
for (let i = 0; i < queue.length; i++) {
|
|
58
|
+
for (const c of kids.get(queue[i].pid) || []) {
|
|
59
|
+
if (owner.has(c.pid) || sessionPids.has(c.pid) || c.start < queue[i].start) continue
|
|
60
|
+
owner.set(c.pid, root)
|
|
61
|
+
queue.push(c)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// the shell snapshots each live session is seen using through its own tree
|
|
66
|
+
const known = new Map()
|
|
67
|
+
for (const [pid, root] of owner) {
|
|
68
|
+
const ms = snapshotMs(byPid.get(pid).cmd)
|
|
69
|
+
if (!ms) continue
|
|
70
|
+
if (!known.has(root)) known.set(root, new Set())
|
|
71
|
+
known.get(root).add(ms)
|
|
72
|
+
}
|
|
73
|
+
for (const p of procs) {
|
|
74
|
+
if (owner.has(p.pid)) continue
|
|
75
|
+
const s = byPid.get(p.claudePid)
|
|
76
|
+
if (sessionPids.has(p.claudePid) && p.start >= s.start) owner.set(p.pid, p.claudePid)
|
|
77
|
+
else {
|
|
78
|
+
const bySnapshot = snapshotOwner(p, byPid, sessionPids, known)
|
|
79
|
+
if (bySnapshot) owner.set(p.pid, bySnapshot)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return owner
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function snapshotMs(cmd) {
|
|
86
|
+
const m = /shell-snapshots[\\/]snapshot-\w+-(\d{13})-/.exec(cmd)
|
|
87
|
+
return m ? Number(m[1]) : 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Windows: a server started with `&` loses its parent when the tool shell's wrapper exits.
|
|
91
|
+
// The Claude shell above it still names the session's shell snapshot. Claude Code writes it at
|
|
92
|
+
// the session's first Bash call, which can be minutes after the start, so any session started
|
|
93
|
+
// before it may own it. A session uses one snapshot, so one seen using another is ruled out.
|
|
94
|
+
// We only use it when exactly one session is left.
|
|
95
|
+
function snapshotOwner(p, byPid, sessionPids, known = new Map()) {
|
|
96
|
+
let cur = p
|
|
97
|
+
let ms = 0
|
|
98
|
+
for (let i = 0; cur && i < 30 && !ms; i++) {
|
|
99
|
+
ms = snapshotMs(cur.cmd)
|
|
100
|
+
const par = byPid.get(cur.ppid)
|
|
101
|
+
cur = par && par.start <= cur.start ? par : null
|
|
102
|
+
}
|
|
103
|
+
if (!ms) return null
|
|
104
|
+
for (const [pid, set] of known) if (set.has(ms)) return pid
|
|
105
|
+
const fits = [...sessionPids].filter(pid => byPid.get(pid).start <= ms + 5000 && !known.has(pid))
|
|
106
|
+
return fits.length === 1 ? fits[0] : null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// [{ pid, start, cwd, rss, ports: [{ port, pid, name }] }], oldest first.
|
|
110
|
+
function findSessions({ procs, ports = [] }, files = new Map(), cwdOf = process.platform === 'linux' ? linuxCwd : () => null) {
|
|
111
|
+
const byPid = new Map(procs.map(p => [p.pid, p]))
|
|
112
|
+
const roots = procs.filter(isClaudeCli)
|
|
113
|
+
const owner = assignOwners(procs, new Set(roots.map(p => p.pid)))
|
|
114
|
+
return roots.sort((a, b) => a.start - b.start).map(p => {
|
|
115
|
+
const f = files.get(p.pid)
|
|
116
|
+
const fileMatches = f && (!f.startedAt || !p.start || Math.abs(f.startedAt - p.start) < PID_REUSE_SLACK_MS)
|
|
117
|
+
let rss = 0
|
|
118
|
+
let children = 0
|
|
119
|
+
for (const [pid, root] of owner) {
|
|
120
|
+
if (root !== p.pid) continue
|
|
121
|
+
rss += byPid.get(pid).rss || 0
|
|
122
|
+
if (pid !== p.pid) children++
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
pid: p.pid,
|
|
126
|
+
start: p.start,
|
|
127
|
+
cwd: (fileMatches && f.cwd) || cwdOf(p.pid),
|
|
128
|
+
rss,
|
|
129
|
+
children,
|
|
130
|
+
ports: ports.filter(l => owner.get(l.pid) === p.pid).map(l => ({ ...l, name: byPid.get(l.pid).name })),
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The session a process runs under: its nearest Claude ancestor, or CLAUDE_PID.
|
|
136
|
+
function ownSession(procs, pid = process.pid, env = process.env) {
|
|
137
|
+
const byPid = new Map(procs.map(p => [p.pid, p]))
|
|
138
|
+
let cur = byPid.get(pid)
|
|
139
|
+
for (let i = 0; cur && i < 40; i++) {
|
|
140
|
+
if (isClaudeCli(cur)) return cur.pid
|
|
141
|
+
const par = byPid.get(cur.ppid)
|
|
142
|
+
cur = par && par.start <= cur.start ? par : null
|
|
143
|
+
}
|
|
144
|
+
return Number(env.CLAUDE_PID) || null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { isClaudeCli, readSessionFiles, findSessions, assignOwners, ownSession, claudeDir }
|