@cuongtran001/kanna 1.63.2 → 1.63.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/README.md +5 -9
- package/dist/client/assets/{BoardsPage-BJalB-bc.js → BoardsPage-AckCuswi.js} +1 -1
- package/dist/client/assets/BoardsRoutePage-DzKwKy3f.js +1 -0
- package/dist/client/assets/ChatPage-DsaH8CTz.js +93 -0
- package/dist/client/assets/{CronJobRow-CPbO_jwU.js → CronJobRow-Bx9gRqKW.js} +1 -1
- package/dist/client/assets/{CronJobsPage-Ck3SNgGi.js → CronJobsPage-v0-2X1oX.js} +1 -1
- package/dist/client/assets/{FilePreviewSheet-XEuUtDPn.js → FilePreviewSheet-B6SDxSD0.js} +2 -2
- package/dist/client/assets/{FlowSurface-BCbeMTJe.js → FlowSurface-CLC-ZFar.js} +1 -1
- package/dist/client/assets/{GenUIBlock-BJy_dfs-.js → GenUIBlock-CC79Ujro.js} +2 -2
- package/dist/client/assets/{KannaSocketProvider-D7Lzlt35.js → KannaSocketProvider-B2sHsWP5.js} +1 -1
- package/dist/client/assets/{LocalProjectsPage-9LJkQjhT.js → LocalProjectsPage-DYBkcEAm.js} +1 -1
- package/dist/client/assets/SettingsPage-CZqMkMRW.js +9 -0
- package/dist/client/assets/{StackBoardsRoutePage-t_EaFNG4.js → StackBoardsRoutePage-Clviz0hX.js} +1 -1
- package/dist/client/assets/{ToolCallMessage-RbF9XT2B.js → ToolCallMessage-BfNbWGmc.js} +1 -1
- package/dist/client/assets/{VChartSurface-CKuPelsn.js → VChartSurface-CUbxyBo3.js} +1 -1
- package/dist/client/assets/{WorkflowsPage-B6qUmQA3.js → WorkflowsPage-rengzb97.js} +1 -1
- package/dist/client/assets/{WorkflowsSection-DxO0S4An.js → WorkflowsSection-DuXdLsMO.js} +1 -1
- package/dist/client/assets/{chart-renderer-DjYZvNxh.js → chart-renderer-D7ukB2wg.js} +1 -1
- package/dist/client/assets/{chatSounds-FD2DQzF6.js → chatSounds-BLg2xPDZ.js} +1 -1
- package/dist/client/assets/chevron-left-RbFvmeyK.js +1 -0
- package/dist/client/assets/{clipboard.adapter-GN9nnYak.js → clipboard.adapter-CgdNwn-1.js} +1 -1
- package/dist/client/assets/{columnStyle-DjA7hA9k.js → columnStyle-CMCF0ds6.js} +1 -1
- package/dist/client/assets/{createScopedStore-BoLB9Rhc.js → createScopedStore-BrD5NHWx.js} +1 -1
- package/dist/client/assets/{dist-CimtOoGK.js → dist-DNSTHlSo.js} +1 -1
- package/dist/client/assets/editor-icons-DY4uKeEY.js +1 -0
- package/dist/client/assets/{ellipsis-Cd7sO0LO.js → ellipsis-OHg0x_x1.js} +1 -1
- package/dist/client/assets/{flow-kinds-CW2TU_83.js → flow-kinds-Ki2DP9nP.js} +1 -1
- package/dist/client/assets/{folder-C7sXCaHj.js → folder-CnWzWIG5.js} +1 -1
- package/dist/client/assets/{formatters-wSVyvDFZ.js → formatters-BGORekd-.js} +1 -1
- package/dist/client/assets/{genui-EzBIAYFE.js → genui-C9i6j0XZ.js} +1 -1
- package/dist/client/assets/{globe-xmRpmpma.js → globe-BjR1tyE4.js} +1 -1
- package/dist/client/assets/{host-DCaA9W-Z.js → host-D9tJwZXl.js} +1 -1
- package/dist/client/assets/{index-CDqjh7nt.js → index-CesRxZ0X.js} +7 -7
- package/dist/client/assets/index-CxDE9VOy.css +1 -0
- package/dist/client/assets/{info-D4MMvZFe.js → info-DNYekoE-.js} +1 -1
- package/dist/client/assets/{input-C4fvDiQ4.js → input-DgW9uRZN.js} +1 -1
- package/dist/client/assets/{lexicalToReact-sPUyOWk6.js → lexicalToReact-CZvp9Pgz.js} +3 -3
- package/dist/client/assets/{log-aTaDVF_e.js → log-DnEvbp3E.js} +1 -1
- package/dist/client/assets/{monitor-_CRKcV1h.js → monitor-qbWfUdBG.js} +1 -1
- package/dist/client/assets/{pencil-fNBeUUCf.js → pencil-C965rrpd.js} +1 -1
- package/dist/client/assets/{pendingActionsStore-COTh4Ztq.js → pendingActionsStore-CxhI19V9.js} +1 -1
- package/dist/client/assets/{play-BAmkJfn6.js → play-uofLR7yT.js} +1 -1
- package/dist/client/assets/{plus-c75CkNEO.js → plus-D4CI6rj1.js} +1 -1
- package/dist/client/assets/{popover-CHM4skzh.js → popover-Bc646hKn.js} +1 -1
- package/dist/client/assets/{pushClient-BJH_ITRB.js → pushClient-CxnHu9h4.js} +1 -1
- package/dist/client/assets/runDetached-DfQXHAaY.js +1 -0
- package/dist/client/assets/{segmented-control-DNOwRupT.js → segmented-control-CKGX29Kl.js} +1 -1
- package/dist/client/assets/{select-dLHiAls6.js → select-BjeajpVt.js} +1 -1
- package/dist/client/assets/{settings-header-button-CGKCzFyk.js → settings-header-button-BYvHR6Xl.js} +1 -1
- package/dist/client/assets/{sidebarPendingActions-Dyp80Lv_.js → sidebarPendingActions-xgMTnfRY.js} +1 -1
- package/dist/client/assets/{square-pen-B1IxmScM.js → square-pen-DmppQdTd.js} +1 -1
- package/dist/client/assets/{state-mark-BhTZfcYe.js → state-mark-C3kEL1hX.js} +1 -1
- package/dist/client/assets/{status-pill-CwhJhaMy.js → status-pill-Bo1Nb6HX.js} +1 -1
- package/dist/client/assets/{terminal-Bnj7qFGw.js → terminal-DfCF-J4p.js} +1 -1
- package/dist/client/assets/{timer.adapter-DHt2zwsL.js → timer.adapter-C2RhP4d6.js} +1 -1
- package/dist/client/assets/{tools-3gJcj3hK.js → tools-t5T_3UBg.js} +1 -1
- package/dist/client/assets/{triangle-alert-CWrc0CIO.js → triangle-alert-CYQhobyY.js} +1 -1
- package/dist/client/assets/useAppGlobalState-DUa3pkCF.js +1 -0
- package/dist/client/assets/useKannaState-FcieR5qb.js +5 -0
- package/dist/client/assets/useTheme-D9sKDPEm.js +1 -0
- package/dist/client/index.html +35 -35
- package/package.json +2 -2
- package/src/server/agent-coordinator-types.ts +0 -6
- package/src/server/agent-coordinator.ts +6 -45
- package/src/server/agent.compaction.test.ts +1 -13
- package/src/server/agent.oauth-account-info.test.ts +0 -3
- package/src/server/agent.oauth-release.test.ts +0 -2
- package/src/server/agent.oauth-rotation.test.ts +0 -9
- package/src/server/agent.oauth-token-share-cap.test.ts +0 -1
- package/src/server/agent.openrouter-model.test.ts +0 -2
- package/src/server/agent.openrouter-watchdog.test.ts +0 -3
- package/src/server/agent.sdk-workflow-register.test.ts +2 -64
- package/src/server/agent.test.ts +1 -319
- package/src/server/app-settings.test.ts +14 -17
- package/src/server/app-settings.ts +22 -37
- package/src/server/auto-continue/e2e.test.ts +0 -1
- package/src/server/background-task-output-registry.test.ts +1 -1
- package/src/server/background-task-output-registry.ts +1 -1
- package/src/server/claude-cancel-handler.test.ts +0 -58
- package/src/server/claude-cancel-handler.ts +1 -17
- package/src/server/claude-harness-stream.test.ts +134 -0
- package/src/server/{claude-pty/jsonl-path.test.ts → claude-projects-path.adapter.test.ts} +1 -19
- package/src/server/{claude-pty/jsonl-path.adapter.ts → claude-projects-path.adapter.ts} +0 -11
- package/src/server/claude-send-command.test.ts +0 -1
- package/src/server/claude-session-config-helpers.test.ts +1 -90
- package/src/server/claude-session-config-helpers.ts +1 -38
- package/src/server/claude-session-error-handler.test.ts +0 -1
- package/src/server/claude-session-lifecycle.test.ts +2 -36
- package/src/server/claude-session-lifecycle.ts +2 -8
- package/src/server/claude-session-runner.test.ts +8 -36
- package/src/server/claude-session-runner.ts +1 -26
- package/src/server/claude-session-spawner.test.ts +5 -64
- package/src/server/claude-session-spawner.ts +49 -115
- package/src/server/claude-session-start.test.ts +1 -14
- package/src/server/claude-session-start.ts +0 -8
- package/src/server/claude-session-state-queries.test.ts +1 -2
- package/src/server/claude-session-state.test.ts +0 -1
- package/src/server/claude-session-state.ts +0 -4
- package/src/server/claude-subagent-wiring.test.ts +2 -68
- package/src/server/claude-subagent-wiring.ts +3 -50
- package/src/server/claude-turn-starter-types.ts +0 -3
- package/src/server/claude-turn-starter.test.ts +0 -28
- package/src/server/claude-turn-starter.ts +0 -5
- package/src/server/event-store.test.ts +0 -2
- package/src/server/events.ts +1 -2
- package/src/server/harness-types.ts +1 -2
- package/src/server/import-subagent-drill.e2e.test.ts +1 -1
- package/src/server/imported-subagents-dir.test.ts +1 -1
- package/src/server/imported-subagents-dir.ts +1 -1
- package/src/server/kanna-mcp.test.ts +0 -75
- package/src/server/kanna-mcp.ts +1 -47
- package/src/server/server.test.ts +5 -7
- package/src/server/server.ts +2 -18
- package/src/server/subagent-orchestrator.test.ts +0 -1
- package/src/server/subagent-provider-run.test.ts +48 -10
- package/src/server/subagent-provider-run.ts +12 -3
- package/src/server/tool-callback.test.ts +1 -1
- package/src/server/ws-router-agent-ctrl.test.ts +0 -45
- package/src/server/ws-router-agent-ctrl.ts +1 -31
- package/src/server/ws-router-broadcast.ts +0 -27
- package/src/server/ws-router-defaults.test.ts +2 -3
- package/src/server/ws-router-defaults.ts +2 -3
- package/src/server/ws-router-envelope.ts +0 -15
- package/src/server/ws-router-observability.test.ts +1 -1
- package/src/server/ws-router-settings.test.ts +2 -2
- package/src/server/ws-router.test.ts +2 -2
- package/src/server/ws-router.ts +0 -8
- package/src/shared/app-settings-types.ts +9 -20
- package/src/shared/claude-api-error-classification.ts +1 -1
- package/src/shared/protocol.ts +1 -11
- package/dist/client/assets/BoardsRoutePage-BTTjJX-Y.js +0 -1
- package/dist/client/assets/ChatPage-DH8wtQyD.js +0 -93
- package/dist/client/assets/SettingsPage-CKrZ6dGQ.js +0 -9
- package/dist/client/assets/chevron-left-CJ0qv9kF.js +0 -1
- package/dist/client/assets/editor-icons-CONDQZof.js +0 -1
- package/dist/client/assets/index-C-UYaDk6.css +0 -1
- package/dist/client/assets/runDetached-DqQ3k2ji.js +0 -1
- package/dist/client/assets/useAppGlobalState-DKOGrO5l.js +0 -1
- package/dist/client/assets/useKannaState-D7R_-dM6.js +0 -5
- package/dist/client/assets/useTheme-B57LIKyx.js +0 -1
- package/src/server/agent.pty-rotation.test.ts +0 -322
- package/src/server/claude-pty/auth.test.ts +0 -38
- package/src/server/claude-pty/auth.ts +0 -17
- package/src/server/claude-pty/claude-session-registry.adapter.ts +0 -68
- package/src/server/claude-pty/claude-session-registry.test.ts +0 -149
- package/src/server/claude-pty/driver.test.ts +0 -1153
- package/src/server/claude-pty/driver.ts +0 -829
- package/src/server/claude-pty/env.test.ts +0 -79
- package/src/server/claude-pty/env.ts +0 -20
- package/src/server/claude-pty/jsonl-to-event.test.ts +0 -882
- package/src/server/claude-pty/jsonl-to-event.ts +0 -357
- package/src/server/claude-pty/parity-matrix.test.ts +0 -437
- package/src/server/claude-pty/pid-registry.adapter.ts +0 -164
- package/src/server/claude-pty/pid-registry.test.ts +0 -178
- package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +0 -20
- package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +0 -32
- package/src/server/claude-pty/pty-instance-registry.test.ts +0 -208
- package/src/server/claude-pty/pty-instance-registry.ts +0 -172
- package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +0 -103
- package/src/server/claude-pty/pty-memory-sampler.adapter.ts +0 -86
- package/src/server/claude-pty/pty-process.adapter.ts +0 -60
- package/src/server/claude-pty/pty-process.test.ts +0 -49
- package/src/server/claude-pty/resolve-binary.adapter.ts +0 -106
- package/src/server/claude-pty/resolve-binary.test.ts +0 -118
- package/src/server/claude-pty/runtime-dir.adapter.ts +0 -19
- package/src/server/claude-pty/sandbox/detect.adapter.ts +0 -17
- package/src/server/claude-pty/sandbox/detect.test.ts +0 -17
- package/src/server/claude-pty/sandbox/platform.test.ts +0 -35
- package/src/server/claude-pty/sandbox/platform.ts +0 -27
- package/src/server/claude-pty/sandbox/preflight-io.adapter.ts +0 -14
- package/src/server/claude-pty/sandbox/preflight.test.ts +0 -69
- package/src/server/claude-pty/sandbox/preflight.ts +0 -44
- package/src/server/claude-pty/sandbox/profile-linux.adapter.ts +0 -63
- package/src/server/claude-pty/sandbox/profile-linux.test.ts +0 -90
- package/src/server/claude-pty/sandbox/profile-macos.adapter.ts +0 -72
- package/src/server/claude-pty/sandbox/profile-macos.test.ts +0 -68
- package/src/server/claude-pty/sandbox/wrap.adapter.ts +0 -58
- package/src/server/claude-pty/sandbox/wrap.test.ts +0 -82
- package/src/server/claude-pty/settings-writer.adapter.ts +0 -23
- package/src/server/claude-pty/settings-writer.test.ts +0 -22
- package/src/server/claude-pty/smoke-test-io.adapter.ts +0 -28
- package/src/server/claude-pty/smoke-test.test.ts +0 -199
- package/src/server/claude-pty/smoke-test.ts +0 -182
- package/src/server/claude-pty/tui-control.test.ts +0 -300
- package/src/server/claude-pty/tui-control.ts +0 -174
- package/src/server/claude-pty/tui-source.adapter.test.ts +0 -374
- package/src/server/claude-pty/tui-source.adapter.ts +0 -294
- package/src/server/http-server.adapter.test.ts +0 -18
- package/src/server/http-server.adapter.ts +0 -39
- package/src/server/kanna-mcp-http.test.ts +0 -274
- package/src/server/kanna-mcp-http.ts +0 -194
- package/src/server/kanna-mcp-tools/ask-user-question.test.ts +0 -108
- package/src/server/kanna-mcp-tools/ask-user-question.ts +0 -55
- package/src/server/kanna-mcp-tools/exit-plan-mode.test.ts +0 -61
- package/src/server/kanna-mcp-tools/exit-plan-mode.ts +0 -49
- package/src/server/mcp-zod-compat.adapter.ts +0 -20
- package/src/shared/pty-instance.ts +0 -43
- /package/src/server/{claude-pty/output-ring.test.ts → output-ring.test.ts} +0 -0
- /package/src/server/{claude-pty/output-ring.ts → output-ring.ts} +0 -0
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import process from "node:process"
|
|
3
|
-
import {
|
|
4
|
-
collectTreePids,
|
|
5
|
-
parsePsOutput,
|
|
6
|
-
sampleProcessTreeUsage,
|
|
7
|
-
sumTreeUsage,
|
|
8
|
-
type PsProcessRow,
|
|
9
|
-
} from "./pty-memory-sampler.adapter"
|
|
10
|
-
|
|
11
|
-
describe("parsePsOutput", () => {
|
|
12
|
-
test("parses normal ps output with pid/ppid/rss/pcpu", () => {
|
|
13
|
-
const out = [
|
|
14
|
-
" 1 0 1024 0.0",
|
|
15
|
-
" 100 1 2048 5.2",
|
|
16
|
-
" 101 100 4096 12.5",
|
|
17
|
-
"",
|
|
18
|
-
" 102 100 8192 99.9",
|
|
19
|
-
].join("\n")
|
|
20
|
-
expect(parsePsOutput(out)).toEqual([
|
|
21
|
-
{ pid: 1, ppid: 0, rssKb: 1024, cpuPercent: 0.0 },
|
|
22
|
-
{ pid: 100, ppid: 1, rssKb: 2048, cpuPercent: 5.2 },
|
|
23
|
-
{ pid: 101, ppid: 100, rssKb: 4096, cpuPercent: 12.5 },
|
|
24
|
-
{ pid: 102, ppid: 100, rssKb: 8192, cpuPercent: 99.9 },
|
|
25
|
-
])
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test("skips malformed rows without throwing", () => {
|
|
29
|
-
const out = [
|
|
30
|
-
"100 1 1024 3.0",
|
|
31
|
-
"garbage line here",
|
|
32
|
-
"abc def ghi jkl",
|
|
33
|
-
"200 1 2048 7.5",
|
|
34
|
-
" ",
|
|
35
|
-
"300 1 100",
|
|
36
|
-
].join("\n")
|
|
37
|
-
expect(parsePsOutput(out)).toEqual([
|
|
38
|
-
{ pid: 100, ppid: 1, rssKb: 1024, cpuPercent: 3.0 },
|
|
39
|
-
{ pid: 200, ppid: 1, rssKb: 2048, cpuPercent: 7.5 },
|
|
40
|
-
])
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test("returns empty array for empty input", () => {
|
|
44
|
-
expect(parsePsOutput("")).toEqual([])
|
|
45
|
-
expect(parsePsOutput("\n\n\n")).toEqual([])
|
|
46
|
-
})
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
describe("collectTreePids", () => {
|
|
50
|
-
const rows: PsProcessRow[] = [
|
|
51
|
-
{ pid: 1, ppid: 0, rssKb: 0, cpuPercent: 0 },
|
|
52
|
-
{ pid: 100, ppid: 1, rssKb: 0, cpuPercent: 0 },
|
|
53
|
-
{ pid: 101, ppid: 100, rssKb: 0, cpuPercent: 0 },
|
|
54
|
-
{ pid: 102, ppid: 100, rssKb: 0, cpuPercent: 0 },
|
|
55
|
-
{ pid: 103, ppid: 101, rssKb: 0, cpuPercent: 0 },
|
|
56
|
-
{ pid: 200, ppid: 1, rssKb: 0, cpuPercent: 0 },
|
|
57
|
-
]
|
|
58
|
-
|
|
59
|
-
test("collects root + descendants transitively", () => {
|
|
60
|
-
expect(collectTreePids(rows, 100)).toEqual(new Set([100, 101, 102, 103]))
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test("returns only root when no children", () => {
|
|
64
|
-
expect(collectTreePids(rows, 200)).toEqual(new Set([200]))
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test("returns only root when root absent from rows", () => {
|
|
68
|
-
expect(collectTreePids(rows, 999)).toEqual(new Set([999]))
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
describe("sumTreeUsage", () => {
|
|
73
|
-
const rows: PsProcessRow[] = [
|
|
74
|
-
{ pid: 1, ppid: 0, rssKb: 100, cpuPercent: 1.0 },
|
|
75
|
-
{ pid: 100, ppid: 1, rssKb: 200, cpuPercent: 25.0 },
|
|
76
|
-
{ pid: 101, ppid: 100, rssKb: 300, cpuPercent: 50.5 },
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
test("sums rss (kb->bytes) + cpu for pids in tree", () => {
|
|
80
|
-
const tree = new Set<number>([100, 101])
|
|
81
|
-
expect(sumTreeUsage(rows, tree)).toEqual({
|
|
82
|
-
rssBytes: (200 + 300) * 1024,
|
|
83
|
-
cpuPercent: 25.0 + 50.5,
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test("returns zeros for empty tree", () => {
|
|
88
|
-
expect(sumTreeUsage(rows, new Set())).toEqual({ rssBytes: 0, cpuPercent: 0 })
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
describe("sampleProcessTreeUsage", () => {
|
|
93
|
-
test("returns positive rss + finite cpu for current process", async () => {
|
|
94
|
-
const sample = await sampleProcessTreeUsage(process.pid)
|
|
95
|
-
expect(sample).not.toBeNull()
|
|
96
|
-
expect(sample?.rssBytes).toBeGreaterThan(0)
|
|
97
|
-
expect(Number.isFinite(sample?.cpuPercent ?? Number.NaN)).toBe(true)
|
|
98
|
-
}, 5_000)
|
|
99
|
-
|
|
100
|
-
test("returns null for pid that does not exist", async () => {
|
|
101
|
-
expect(await sampleProcessTreeUsage(2_147_483_646)).toBeNull()
|
|
102
|
-
}, 5_000)
|
|
103
|
-
})
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process"
|
|
2
|
-
import { promisify } from "node:util"
|
|
3
|
-
|
|
4
|
-
const execFileAsync = promisify(execFile)
|
|
5
|
-
|
|
6
|
-
const PS_TIMEOUT_MS = 2000
|
|
7
|
-
|
|
8
|
-
export interface PsProcessRow {
|
|
9
|
-
pid: number
|
|
10
|
-
ppid: number
|
|
11
|
-
rssKb: number
|
|
12
|
-
cpuPercent: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ProcessTreeSample {
|
|
16
|
-
rssBytes: number
|
|
17
|
-
cpuPercent: number
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function parsePsOutput(stdout: string): PsProcessRow[] {
|
|
21
|
-
const rows: PsProcessRow[] = []
|
|
22
|
-
for (const rawLine of stdout.split("\n")) {
|
|
23
|
-
const line = rawLine.trim()
|
|
24
|
-
if (!line) continue
|
|
25
|
-
const parts = line.split(/\s+/)
|
|
26
|
-
if (parts.length < 4) continue
|
|
27
|
-
const pid = Number(parts[0])
|
|
28
|
-
const ppid = Number(parts[1])
|
|
29
|
-
const rssKb = Number(parts[2])
|
|
30
|
-
const cpuPercent = Number(parts[3])
|
|
31
|
-
if (!Number.isFinite(pid) || !Number.isFinite(ppid) || !Number.isFinite(rssKb) || !Number.isFinite(cpuPercent)) continue
|
|
32
|
-
rows.push({ pid, ppid, rssKb, cpuPercent })
|
|
33
|
-
}
|
|
34
|
-
return rows
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function collectTreePids(rows: readonly PsProcessRow[], rootPid: number): Set<number> {
|
|
38
|
-
const childrenByParent = new Map<number, number[]>()
|
|
39
|
-
for (const row of rows) {
|
|
40
|
-
const list = childrenByParent.get(row.ppid)
|
|
41
|
-
if (list) list.push(row.pid)
|
|
42
|
-
else childrenByParent.set(row.ppid, [row.pid])
|
|
43
|
-
}
|
|
44
|
-
const tree = new Set<number>([rootPid])
|
|
45
|
-
const queue: number[] = [rootPid]
|
|
46
|
-
while (queue.length > 0) {
|
|
47
|
-
const next = queue.shift()
|
|
48
|
-
if (next === undefined) break
|
|
49
|
-
const kids = childrenByParent.get(next)
|
|
50
|
-
if (!kids) continue
|
|
51
|
-
for (const kid of kids) {
|
|
52
|
-
if (tree.has(kid)) continue
|
|
53
|
-
tree.add(kid)
|
|
54
|
-
queue.push(kid)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return tree
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function sumTreeUsage(rows: readonly PsProcessRow[], tree: ReadonlySet<number>): ProcessTreeSample {
|
|
61
|
-
let totalKb = 0
|
|
62
|
-
let totalCpu = 0
|
|
63
|
-
for (const row of rows) {
|
|
64
|
-
if (!tree.has(row.pid)) continue
|
|
65
|
-
totalKb += row.rssKb
|
|
66
|
-
totalCpu += row.cpuPercent
|
|
67
|
-
}
|
|
68
|
-
return { rssBytes: totalKb * 1024, cpuPercent: totalCpu }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function sampleProcessTreeUsage(rootPid: number): Promise<ProcessTreeSample | null> {
|
|
72
|
-
let stdout: string
|
|
73
|
-
try {
|
|
74
|
-
const result = await execFileAsync("ps", ["-A", "-o", "pid=,ppid=,rss=,pcpu="], {
|
|
75
|
-
timeout: PS_TIMEOUT_MS,
|
|
76
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
77
|
-
})
|
|
78
|
-
stdout = result.stdout
|
|
79
|
-
} catch {
|
|
80
|
-
return null
|
|
81
|
-
}
|
|
82
|
-
const rows = parsePsOutput(stdout)
|
|
83
|
-
if (!rows.some((r) => r.pid === rootPid)) return null
|
|
84
|
-
const tree = collectTreePids(rows, rootPid)
|
|
85
|
-
return sumTreeUsage(rows, tree)
|
|
86
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
export interface PtyProcess {
|
|
2
|
-
pid: number
|
|
3
|
-
sendInput(data: string): Promise<void>
|
|
4
|
-
resize(cols: number, rows: number): void
|
|
5
|
-
exited: Promise<number>
|
|
6
|
-
close(): void
|
|
7
|
-
kill(signal?: NodeJS.Signals | number): void
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface SpawnPtyProcessArgs {
|
|
11
|
-
command: string
|
|
12
|
-
args: string[]
|
|
13
|
-
cwd: string
|
|
14
|
-
env: NodeJS.ProcessEnv
|
|
15
|
-
cols?: number
|
|
16
|
-
rows?: number
|
|
17
|
-
onOutput?: (chunk: string) => void
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function spawnPtyProcess(opts: SpawnPtyProcessArgs): Promise<PtyProcess> {
|
|
21
|
-
if (typeof Bun.Terminal !== "function") {
|
|
22
|
-
throw new Error("Bun.Terminal not available — requires Bun 1.3.5+")
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const cols = opts.cols ?? 120
|
|
26
|
-
const rows = opts.rows ?? 40
|
|
27
|
-
|
|
28
|
-
const terminal = new Bun.Terminal({
|
|
29
|
-
cols,
|
|
30
|
-
rows,
|
|
31
|
-
name: "xterm-256color",
|
|
32
|
-
data: (_t, data) => {
|
|
33
|
-
if (opts.onOutput) {
|
|
34
|
-
const chunk = Buffer.from(data).toString("utf8")
|
|
35
|
-
opts.onOutput(chunk)
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
const proc = Bun.spawn([opts.command, ...opts.args], {
|
|
41
|
-
cwd: opts.cwd,
|
|
42
|
-
env: opts.env,
|
|
43
|
-
terminal,
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
pid: proc.pid,
|
|
48
|
-
async sendInput(data) { terminal.write(data) },
|
|
49
|
-
resize(newCols, newRows) { terminal.resize(newCols, newRows) },
|
|
50
|
-
exited: proc.exited,
|
|
51
|
-
close() {
|
|
52
|
-
try { terminal.close() } catch { }
|
|
53
|
-
try { proc.kill("SIGTERM") } catch { }
|
|
54
|
-
},
|
|
55
|
-
kill(signal) {
|
|
56
|
-
try { terminal.close() } catch { }
|
|
57
|
-
try { proc.kill(signal ?? "SIGKILL") } catch { }
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { spawnPtyProcess } from "./pty-process.adapter"
|
|
3
|
-
|
|
4
|
-
describe("spawnPtyProcess", () => {
|
|
5
|
-
test(
|
|
6
|
-
"spawns a child process and exits cleanly",
|
|
7
|
-
async () => {
|
|
8
|
-
if (process.platform === "win32") {
|
|
9
|
-
console.log("skip: PTY not supported on Windows")
|
|
10
|
-
return
|
|
11
|
-
}
|
|
12
|
-
if (typeof Bun.Terminal !== "function") {
|
|
13
|
-
console.log("skip: Bun.Terminal not available")
|
|
14
|
-
return
|
|
15
|
-
}
|
|
16
|
-
const handle = await spawnPtyProcess({
|
|
17
|
-
command: "/bin/sh",
|
|
18
|
-
args: ["-c", "echo hello"],
|
|
19
|
-
cwd: "/tmp",
|
|
20
|
-
env: process.env,
|
|
21
|
-
cols: 80,
|
|
22
|
-
rows: 24,
|
|
23
|
-
})
|
|
24
|
-
const exitCode = await handle.exited
|
|
25
|
-
expect(exitCode).toBe(0)
|
|
26
|
-
handle.close()
|
|
27
|
-
},
|
|
28
|
-
30_000,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
test(
|
|
32
|
-
"captures output via onOutput callback",
|
|
33
|
-
async () => {
|
|
34
|
-
if (process.platform === "win32" || typeof Bun.Terminal !== "function") return
|
|
35
|
-
const chunks: string[] = []
|
|
36
|
-
const handle = await spawnPtyProcess({
|
|
37
|
-
command: "/bin/sh",
|
|
38
|
-
args: ["-c", "echo hi"],
|
|
39
|
-
cwd: "/tmp",
|
|
40
|
-
env: process.env,
|
|
41
|
-
onOutput: (chunk) => chunks.push(chunk),
|
|
42
|
-
})
|
|
43
|
-
await handle.exited
|
|
44
|
-
handle.close()
|
|
45
|
-
expect(chunks.join("")).toContain("hi")
|
|
46
|
-
},
|
|
47
|
-
30_000,
|
|
48
|
-
)
|
|
49
|
-
})
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import { promisify } from "node:util"
|
|
4
|
-
import { execFile } from "node:child_process"
|
|
5
|
-
|
|
6
|
-
const execFileAsync = promisify(execFile)
|
|
7
|
-
|
|
8
|
-
export interface ResolveClaudeBinaryArgs {
|
|
9
|
-
env: NodeJS.ProcessEnv
|
|
10
|
-
homeDir: string
|
|
11
|
-
cwd?: string
|
|
12
|
-
platform?: NodeJS.Platform
|
|
13
|
-
arch?: NodeJS.Architecture
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ResolveClaudeBinaryResult {
|
|
17
|
-
path: string
|
|
18
|
-
source: "env-CLAUDE_EXECUTABLE" | "env-CLAUDE_CODE_EXECPATH" | "PATH" | "node_modules"
|
|
19
|
-
triedPaths: string[]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function expandTilde(p: string, home: string): string {
|
|
23
|
-
return p.replace(/^~(?=\/|$)/, home)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function isExecutableFile(p: string): Promise<boolean> {
|
|
27
|
-
try {
|
|
28
|
-
const s = await stat(p)
|
|
29
|
-
return s.isFile()
|
|
30
|
-
} catch {
|
|
31
|
-
return false
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function whichClaude(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): Promise<string | null> {
|
|
36
|
-
const cmd = platform === "win32" ? "where" : "which"
|
|
37
|
-
try {
|
|
38
|
-
const { stdout } = await execFileAsync(cmd, ["claude"], { env, timeout: 2000 })
|
|
39
|
-
const first = stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0)
|
|
40
|
-
if (!first) return null
|
|
41
|
-
if (!(await isExecutableFile(first))) return null
|
|
42
|
-
return first
|
|
43
|
-
} catch {
|
|
44
|
-
return null
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function buildNodeModulesCandidates(cwd: string, platform: NodeJS.Platform, arch: NodeJS.Architecture): string[] {
|
|
49
|
-
const tag = `${platform}-${arch}`
|
|
50
|
-
const pkgDir = `@anthropic-ai/claude-agent-sdk-${tag}`
|
|
51
|
-
const candidates: string[] = []
|
|
52
|
-
let dir = path.resolve(cwd)
|
|
53
|
-
while (true) {
|
|
54
|
-
candidates.push(path.join(dir, "node_modules", pkgDir, "claude"))
|
|
55
|
-
const parent = path.dirname(dir)
|
|
56
|
-
if (parent === dir) break
|
|
57
|
-
dir = parent
|
|
58
|
-
}
|
|
59
|
-
return candidates
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function resolveClaudeBinary(args: ResolveClaudeBinaryArgs): Promise<ResolveClaudeBinaryResult> {
|
|
63
|
-
const env = args.env
|
|
64
|
-
const home = args.homeDir
|
|
65
|
-
const cwd = args.cwd ?? process.cwd()
|
|
66
|
-
const platform = args.platform ?? process.platform
|
|
67
|
-
const arch = args.arch ?? process.arch
|
|
68
|
-
const tried: string[] = []
|
|
69
|
-
|
|
70
|
-
if (env.CLAUDE_EXECUTABLE) {
|
|
71
|
-
const candidate = expandTilde(env.CLAUDE_EXECUTABLE, home)
|
|
72
|
-
tried.push(`CLAUDE_EXECUTABLE=${candidate}`)
|
|
73
|
-
if (await isExecutableFile(candidate)) {
|
|
74
|
-
return { path: candidate, source: "env-CLAUDE_EXECUTABLE", triedPaths: tried }
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (env.CLAUDE_CODE_EXECPATH) {
|
|
79
|
-
const candidate = expandTilde(env.CLAUDE_CODE_EXECPATH, home)
|
|
80
|
-
tried.push(`CLAUDE_CODE_EXECPATH=${candidate}`)
|
|
81
|
-
if (await isExecutableFile(candidate)) {
|
|
82
|
-
return { path: candidate, source: "env-CLAUDE_CODE_EXECPATH", triedPaths: tried }
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const fromPath = await whichClaude(env, platform)
|
|
87
|
-
tried.push(`PATH lookup: ${fromPath ?? "<not found>"}`)
|
|
88
|
-
if (fromPath) {
|
|
89
|
-
return { path: fromPath, source: "PATH", triedPaths: tried }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const candidates = buildNodeModulesCandidates(cwd, platform, arch)
|
|
93
|
-
for (const candidate of candidates) {
|
|
94
|
-
tried.push(candidate)
|
|
95
|
-
if (await isExecutableFile(candidate)) {
|
|
96
|
-
return { path: candidate, source: "node_modules", triedPaths: tried }
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
throw new Error(
|
|
101
|
-
`Unable to locate the \`claude\` CLI binary. Set CLAUDE_EXECUTABLE to an absolute path, `
|
|
102
|
-
+ `install \`@anthropic-ai/claude-code\` globally so \`claude\` is on PATH, or install `
|
|
103
|
-
+ `\`@anthropic-ai/claude-agent-sdk\` so the platform-bundled binary exists in node_modules. `
|
|
104
|
-
+ `Tried:\n - ${tried.join("\n - ")}`,
|
|
105
|
-
)
|
|
106
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
3
|
-
import { tmpdir } from "node:os"
|
|
4
|
-
import path from "node:path"
|
|
5
|
-
import { resolveClaudeBinary } from "./resolve-binary.adapter"
|
|
6
|
-
|
|
7
|
-
describe("resolveClaudeBinary", () => {
|
|
8
|
-
let workDir: string
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
workDir = await mkdtemp(path.join(tmpdir(), "kanna-resolve-binary-"))
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
await rm(workDir, { recursive: true, force: true })
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
async function makeExec(p: string): Promise<void> {
|
|
19
|
-
await mkdir(path.dirname(p), { recursive: true })
|
|
20
|
-
await writeFile(p, "#!/bin/sh\necho fake\n", { encoding: "utf8" })
|
|
21
|
-
await chmod(p, 0o755)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test("returns CLAUDE_EXECUTABLE when set and exists", async () => {
|
|
25
|
-
const bin = path.join(workDir, "fake-claude")
|
|
26
|
-
await makeExec(bin)
|
|
27
|
-
const result = await resolveClaudeBinary({
|
|
28
|
-
env: { CLAUDE_EXECUTABLE: bin, PATH: "" },
|
|
29
|
-
homeDir: workDir,
|
|
30
|
-
cwd: workDir,
|
|
31
|
-
platform: "darwin",
|
|
32
|
-
arch: "arm64",
|
|
33
|
-
})
|
|
34
|
-
expect(result.source).toBe("env-CLAUDE_EXECUTABLE")
|
|
35
|
-
expect(result.path).toBe(bin)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
test("expands tilde in CLAUDE_EXECUTABLE", async () => {
|
|
39
|
-
const bin = path.join(workDir, "tilde-claude")
|
|
40
|
-
await makeExec(bin)
|
|
41
|
-
const result = await resolveClaudeBinary({
|
|
42
|
-
env: { CLAUDE_EXECUTABLE: "~/tilde-claude", PATH: "" },
|
|
43
|
-
homeDir: workDir,
|
|
44
|
-
cwd: workDir,
|
|
45
|
-
platform: "darwin",
|
|
46
|
-
arch: "arm64",
|
|
47
|
-
})
|
|
48
|
-
expect(result.path).toBe(bin)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test("falls back to CLAUDE_CODE_EXECPATH when CLAUDE_EXECUTABLE missing file", async () => {
|
|
52
|
-
const bin = path.join(workDir, "execpath-claude")
|
|
53
|
-
await makeExec(bin)
|
|
54
|
-
const result = await resolveClaudeBinary({
|
|
55
|
-
env: {
|
|
56
|
-
CLAUDE_EXECUTABLE: path.join(workDir, "does-not-exist"),
|
|
57
|
-
CLAUDE_CODE_EXECPATH: bin,
|
|
58
|
-
PATH: "",
|
|
59
|
-
},
|
|
60
|
-
homeDir: workDir,
|
|
61
|
-
cwd: workDir,
|
|
62
|
-
platform: "darwin",
|
|
63
|
-
arch: "arm64",
|
|
64
|
-
})
|
|
65
|
-
expect(result.source).toBe("env-CLAUDE_CODE_EXECPATH")
|
|
66
|
-
expect(result.path).toBe(bin)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test("falls back to node_modules platform-bundled binary", async () => {
|
|
70
|
-
const inner = path.join(workDir, "nested", "deep")
|
|
71
|
-
await mkdir(inner, { recursive: true })
|
|
72
|
-
const bundled = path.join(workDir, "node_modules", "@anthropic-ai", "claude-agent-sdk-darwin-arm64", "claude")
|
|
73
|
-
await makeExec(bundled)
|
|
74
|
-
const result = await resolveClaudeBinary({
|
|
75
|
-
env: { PATH: "" },
|
|
76
|
-
homeDir: workDir,
|
|
77
|
-
cwd: inner,
|
|
78
|
-
platform: "darwin",
|
|
79
|
-
arch: "arm64",
|
|
80
|
-
})
|
|
81
|
-
expect(result.source).toBe("node_modules")
|
|
82
|
-
expect(result.path).toBe(bundled)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test("walks parent dirs when looking up node_modules bundled binary", async () => {
|
|
86
|
-
const inner = path.join(workDir, "a", "b", "c")
|
|
87
|
-
await mkdir(inner, { recursive: true })
|
|
88
|
-
const bundled = path.join(workDir, "node_modules", "@anthropic-ai", "claude-agent-sdk-linux-x64", "claude")
|
|
89
|
-
await makeExec(bundled)
|
|
90
|
-
const result = await resolveClaudeBinary({
|
|
91
|
-
env: { PATH: "" },
|
|
92
|
-
homeDir: workDir,
|
|
93
|
-
cwd: inner,
|
|
94
|
-
platform: "linux",
|
|
95
|
-
arch: "x64",
|
|
96
|
-
})
|
|
97
|
-
expect(result.path).toBe(bundled)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
test("throws with all tried paths when nothing resolves", async () => {
|
|
101
|
-
let err: unknown
|
|
102
|
-
try {
|
|
103
|
-
await resolveClaudeBinary({
|
|
104
|
-
env: { CLAUDE_EXECUTABLE: path.join(workDir, "missing"), PATH: "" },
|
|
105
|
-
homeDir: workDir,
|
|
106
|
-
cwd: workDir,
|
|
107
|
-
platform: "darwin",
|
|
108
|
-
arch: "arm64",
|
|
109
|
-
})
|
|
110
|
-
} catch (caught) {
|
|
111
|
-
err = caught
|
|
112
|
-
}
|
|
113
|
-
expect(err).toBeInstanceOf(Error)
|
|
114
|
-
expect((err as Error).message).toContain("Unable to locate")
|
|
115
|
-
expect((err as Error).message).toContain("CLAUDE_EXECUTABLE")
|
|
116
|
-
expect((err as Error).message).toContain("node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64/claude")
|
|
117
|
-
}, 10_000)
|
|
118
|
-
})
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import { tmpdir } from "node:os"
|
|
4
|
-
|
|
5
|
-
export async function createRuntimeDir(prefix: string): Promise<string> {
|
|
6
|
-
return await mkdtemp(path.join(tmpdir(), prefix))
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export async function writeRuntimeFile(
|
|
10
|
-
filePath: string,
|
|
11
|
-
contents: string,
|
|
12
|
-
options?: { encoding?: BufferEncoding; mode?: number },
|
|
13
|
-
): Promise<void> {
|
|
14
|
-
await writeFile(filePath, contents, options)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function removeRuntimeDir(dir: string): Promise<void> {
|
|
18
|
-
await rm(dir, { recursive: true, force: true })
|
|
19
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process"
|
|
2
|
-
|
|
3
|
-
let cached: boolean | null = null
|
|
4
|
-
|
|
5
|
-
export async function detectBwrap(): Promise<boolean> {
|
|
6
|
-
if (cached !== null) return cached
|
|
7
|
-
cached = await new Promise<boolean>((resolve) => {
|
|
8
|
-
const child = spawn("/usr/bin/which", ["bwrap"], { stdio: ["ignore", "ignore", "ignore"] })
|
|
9
|
-
child.on("close", (code) => resolve(code === 0))
|
|
10
|
-
child.on("error", () => resolve(false))
|
|
11
|
-
})
|
|
12
|
-
return cached
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function resetBwrapCacheForTest(): void {
|
|
16
|
-
cached = null
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { detectBwrap, resetBwrapCacheForTest } from "./detect.adapter"
|
|
3
|
-
|
|
4
|
-
describe("detectBwrap", () => {
|
|
5
|
-
test("returns boolean (real platform check)", async () => {
|
|
6
|
-
resetBwrapCacheForTest()
|
|
7
|
-
const result = await detectBwrap()
|
|
8
|
-
expect(typeof result).toBe("boolean")
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
test("subsequent calls hit cache (same result)", async () => {
|
|
12
|
-
resetBwrapCacheForTest()
|
|
13
|
-
const first = await detectBwrap()
|
|
14
|
-
const second = await detectBwrap()
|
|
15
|
-
expect(second).toBe(first)
|
|
16
|
-
})
|
|
17
|
-
})
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { isSandboxSupported, isSandboxEnabledAsync } from "./platform"
|
|
3
|
-
import { resetBwrapCacheForTest } from "./detect.adapter"
|
|
4
|
-
|
|
5
|
-
describe("isSandboxSupported", () => {
|
|
6
|
-
test("true on darwin", () => {
|
|
7
|
-
expect(isSandboxSupported("darwin")).toBe(true)
|
|
8
|
-
})
|
|
9
|
-
test("false on linux (P4.1)", () => {
|
|
10
|
-
expect(isSandboxSupported("linux")).toBe(false)
|
|
11
|
-
})
|
|
12
|
-
test("false on win32", () => {
|
|
13
|
-
expect(isSandboxSupported("win32")).toBe(false)
|
|
14
|
-
})
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
describe("isSandboxEnabledAsync", () => {
|
|
18
|
-
test("respects env=off on linux", async () => {
|
|
19
|
-
expect(await isSandboxEnabledAsync({ platform: "linux", env: "off" })).toBe(false)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test("linux: depends on bwrap detection (sync no, async maybe yes)", async () => {
|
|
23
|
-
resetBwrapCacheForTest()
|
|
24
|
-
const r = await isSandboxEnabledAsync({ platform: "linux", env: undefined })
|
|
25
|
-
expect(typeof r).toBe("boolean")
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test("darwin: always enabled when env not 'off'", async () => {
|
|
29
|
-
expect(await isSandboxEnabledAsync({ platform: "darwin", env: undefined })).toBe(true)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test("win32: always false", async () => {
|
|
33
|
-
expect(await isSandboxEnabledAsync({ platform: "win32", env: "on" })).toBe(false)
|
|
34
|
-
})
|
|
35
|
-
})
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { detectBwrap } from "./detect.adapter"
|
|
2
|
-
import { log } from "../../../shared/log"
|
|
3
|
-
|
|
4
|
-
export function isSandboxSupported(platform: NodeJS.Platform): boolean {
|
|
5
|
-
return platform === "darwin"
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export async function isSandboxEnabledAsync(args: {
|
|
9
|
-
platform: NodeJS.Platform
|
|
10
|
-
env: string | undefined
|
|
11
|
-
}): Promise<boolean> {
|
|
12
|
-
if (args.env === "off") return false
|
|
13
|
-
if (args.platform === "darwin") return true
|
|
14
|
-
if (args.platform === "linux") {
|
|
15
|
-
const ok = await detectBwrap()
|
|
16
|
-
if (!ok) {
|
|
17
|
-
log.warn(
|
|
18
|
-
"[claude-pty/sandbox] bwrap not found on PATH — PTY OS sandbox is "
|
|
19
|
-
+ "DISABLED (loses defense-in-depth against built-in credential "
|
|
20
|
-
+ "reads). Install bubblewrap (apt/dnf/pacman install bubblewrap) "
|
|
21
|
-
+ "or set KANNA_PTY_SANDBOX=off to silence this warning.",
|
|
22
|
-
)
|
|
23
|
-
}
|
|
24
|
-
return ok
|
|
25
|
-
}
|
|
26
|
-
return false
|
|
27
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process"
|
|
2
|
-
import { writeFile } from "node:fs/promises"
|
|
3
|
-
|
|
4
|
-
export function spawnExitCode(command: string, args: string[]): Promise<number> {
|
|
5
|
-
return new Promise<number>((resolve) => {
|
|
6
|
-
const child = spawn(command, args, { stdio: ["ignore", "ignore", "ignore"] })
|
|
7
|
-
child.on("close", (code) => resolve(code ?? -1))
|
|
8
|
-
child.on("error", () => resolve(-1))
|
|
9
|
-
})
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function writeTextFile(p: string, contents: string): Promise<void> {
|
|
13
|
-
return writeFile(p, contents, "utf8")
|
|
14
|
-
}
|