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.
Files changed (48) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/LICENSE +21 -0
  3. package/README.es.md +498 -0
  4. package/README.ja.md +498 -0
  5. package/README.ko.md +498 -0
  6. package/README.md +497 -2
  7. package/README.pt-BR.md +498 -0
  8. package/README.zh-CN.md +498 -0
  9. package/bin/bulkhead +3 -0
  10. package/bin/bulkhead-slot +3 -0
  11. package/bin/bulkhead-statusline +3 -0
  12. package/hooks/hooks.json +39 -0
  13. package/package.json +52 -4
  14. package/src/basics.js +236 -0
  15. package/src/checkout-guard.js +296 -0
  16. package/src/cli-util.js +79 -0
  17. package/src/cli.js +41 -0
  18. package/src/config-check.js +29 -0
  19. package/src/config-cli.js +51 -0
  20. package/src/config.js +236 -0
  21. package/src/dev-servers.js +223 -0
  22. package/src/disk-guard.js +200 -0
  23. package/src/doctor.js +255 -0
  24. package/src/events.js +108 -0
  25. package/src/hook.js +94 -0
  26. package/src/kill-guard.js +435 -0
  27. package/src/log-cli.js +146 -0
  28. package/src/memory-guard.js +145 -0
  29. package/src/memory.js +76 -0
  30. package/src/orphan-reaper.js +527 -0
  31. package/src/paths.js +31 -0
  32. package/src/port-cli.js +59 -0
  33. package/src/port-guard.js +142 -0
  34. package/src/processes.js +181 -0
  35. package/src/reap-cli.js +56 -0
  36. package/src/remote-sh.js +478 -0
  37. package/src/remote.js +1034 -0
  38. package/src/remotes.js +126 -0
  39. package/src/scan.js +604 -0
  40. package/src/sessions.js +147 -0
  41. package/src/shell.js +597 -0
  42. package/src/slot-cli.js +150 -0
  43. package/src/slots.js +168 -0
  44. package/src/status.js +111 -0
  45. package/src/statusline.js +69 -0
  46. package/src/top-worker.js +14 -0
  47. package/src/top.js +305 -0
  48. package/src/wait-guard.js +263 -0
@@ -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 }