jbs-client 0.0.1

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.
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ const childProcess = require("child_process")
4
+ const fs = require("fs")
5
+ const path = require("path")
6
+ const os = require("os")
7
+
8
+ function run(target) {
9
+ const result = childProcess.spawnSync(target, process.argv.slice(2), {
10
+ stdio: "inherit",
11
+ })
12
+ if (result.error) {
13
+ console.error(result.error.message)
14
+ process.exit(1)
15
+ }
16
+ process.exit(typeof result.status === "number" ? result.status : 0)
17
+ }
18
+
19
+ const envPath = process.env.JBS_CLIENT_BIN_PATH
20
+ if (envPath) {
21
+ run(envPath)
22
+ }
23
+
24
+ const scriptPath = fs.realpathSync(__filename)
25
+ const scriptDir = path.dirname(scriptPath)
26
+ const cached = path.join(scriptDir, ".jbs-client")
27
+ if (fs.existsSync(cached)) {
28
+ run(cached)
29
+ }
30
+
31
+ const platformMap = {
32
+ darwin: "darwin",
33
+ linux: "linux",
34
+ win32: "windows",
35
+ }
36
+ const archMap = {
37
+ x64: "x64",
38
+ arm64: "arm64",
39
+ }
40
+
41
+ const platform = platformMap[os.platform()] || os.platform()
42
+ const arch = archMap[os.arch()] || os.arch()
43
+ const packageName = `jbs-client-${platform}-${arch}`
44
+ const binaryName = platform === "windows" ? "jbs-client.exe" : "jbs-client"
45
+
46
+ function findBinary(startDir) {
47
+ let current = startDir
48
+ for (;;) {
49
+ const modules = path.join(current, "node_modules")
50
+ if (fs.existsSync(modules)) {
51
+ const candidate = path.join(modules, packageName, "bin", binaryName)
52
+ if (fs.existsSync(candidate)) {
53
+ return candidate
54
+ }
55
+ }
56
+ const parent = path.dirname(current)
57
+ if (parent === current) {
58
+ return undefined
59
+ }
60
+ current = parent
61
+ }
62
+ }
63
+
64
+ const resolved = findBinary(scriptDir)
65
+ if (!resolved) {
66
+ console.error(`Unable to find installed binary package "${packageName}".`)
67
+ process.exit(1)
68
+ }
69
+
70
+ run(resolved)
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs"
4
+ import os from "node:os"
5
+ import path from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+
10
+ function detectPackageName() {
11
+ const platformMap = {
12
+ darwin: "darwin",
13
+ linux: "linux",
14
+ win32: "windows",
15
+ }
16
+ const archMap = {
17
+ x64: "x64",
18
+ arm64: "arm64",
19
+ }
20
+
21
+ const platform = platformMap[os.platform()] || os.platform()
22
+ const arch = archMap[os.arch()] || os.arch()
23
+ return {
24
+ packageName: `jbs-client-${platform}-${arch}`,
25
+ binaryName: platform === "windows" ? "jbs-client.exe" : "jbs-client",
26
+ }
27
+ }
28
+
29
+ function findBinary() {
30
+ const { packageName, binaryName } = detectPackageName()
31
+ const candidate = path.join(__dirname, "node_modules", packageName, "bin", binaryName)
32
+ if (!fs.existsSync(candidate)) {
33
+ throw new Error(`Missing binary package ${packageName}`)
34
+ }
35
+ return candidate
36
+ }
37
+
38
+ function main() {
39
+ if (os.platform() === "win32") {
40
+ return
41
+ }
42
+
43
+ const binaryPath = findBinary()
44
+ const cached = path.join(__dirname, "bin", ".jbs-client")
45
+ if (fs.existsSync(cached)) {
46
+ fs.unlinkSync(cached)
47
+ }
48
+
49
+ try {
50
+ fs.linkSync(binaryPath, cached)
51
+ } catch {
52
+ fs.copyFileSync(binaryPath, cached)
53
+ }
54
+ fs.chmodSync(cached, 0o755)
55
+ }
56
+
57
+ try {
58
+ main()
59
+ } catch (error) {
60
+ console.error(error instanceof Error ? error.message : String(error))
61
+ process.exit(0)
62
+ }
package/src/app.tsx ADDED
@@ -0,0 +1,239 @@
1
+ import { useEffect, useMemo, useState } from "react"
2
+ import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
3
+ import { ActionMenu } from "./components/action-menu.js"
4
+ import { LogPanel } from "./components/log-panel.js"
5
+ import { ParamForm } from "./components/param-form.js"
6
+ import { buildPayload, menu, validateAction } from "./protocol.js"
7
+ import { appendLog, backToMenu, createInitialState, nextFocus, openAction, previousFocus, setConnectionStatus, updateFormValue, updateSelectedAction } from "./state.js"
8
+ import { connectWebSocket } from "./ws.js"
9
+ import type { AppState } from "./types.js"
10
+ import type { Selection } from "@opentui/core"
11
+
12
+ type AppProps = {
13
+ host: string
14
+ wsPort: number
15
+ connectBackend: boolean
16
+ }
17
+
18
+ type MenuFocusTarget = "content" | "ws-input" | "reconnect"
19
+
20
+ export function App({ host, wsPort, connectBackend }: AppProps) {
21
+ const renderer = useRenderer()
22
+ const { width, height } = useTerminalDimensions()
23
+ const [state, setState] = useState(createInitialState)
24
+ const [validationError, setValidationError] = useState<string | null>(null)
25
+ const [wsUrl, setWsUrl] = useState(`ws://${host}:${wsPort}`)
26
+ const [reconnectVersion, setReconnectVersion] = useState(0)
27
+ const [menuFocusTarget, setMenuFocusTarget] = useState<MenuFocusTarget>("content")
28
+ const [toastMessage, setToastMessage] = useState<string | null>(null)
29
+
30
+ const connection = useMemo(() => {
31
+ return connectWebSocket({
32
+ url: wsUrl,
33
+ enabled: connectBackend,
34
+ onLog: (message: string) => setState((current: AppState) => appendLog(current, message)),
35
+ onStatusChange: (status) => setState((current: AppState) => setConnectionStatus(current, status))
36
+ })
37
+ }, [connectBackend, reconnectVersion, wsUrl])
38
+
39
+ useEffect(() => {
40
+ if (!connectBackend) {
41
+ setState((current: AppState) => appendLog(setConnectionStatus(current, "idle"), "[local] backend integration disabled"))
42
+ }
43
+ return () => connection.dispose()
44
+ }, [connectBackend, connection])
45
+
46
+ useEffect(() => {
47
+ const handleSelection = (selection: Selection) => {
48
+ const text = selection.getSelectedText()
49
+ if (!text) {
50
+ return
51
+ }
52
+ renderer.copyToClipboardOSC52(text)
53
+ setToastMessage(`Copied ${text.length} chars to clipboard`)
54
+ }
55
+
56
+ renderer.on("selection", handleSelection)
57
+ return () => {
58
+ renderer.off("selection", handleSelection)
59
+ }
60
+ }, [renderer])
61
+
62
+ useEffect(() => {
63
+ if (!toastMessage) {
64
+ return
65
+ }
66
+ const timer = setTimeout(() => {
67
+ setToastMessage(null)
68
+ }, 3000)
69
+ return () => clearTimeout(timer)
70
+ }, [toastMessage])
71
+
72
+ const reconnect = () => {
73
+ setValidationError(null)
74
+ setState((current: AppState) => appendLog(current, `[system] reconnect requested: ${wsUrl}`))
75
+ setReconnectVersion((value) => value + 1)
76
+ }
77
+
78
+ const cycleMenuFocus = (backward: boolean) => {
79
+ const order: MenuFocusTarget[] = ["ws-input", "reconnect", "content"]
80
+ const currentIndex = order.indexOf(menuFocusTarget)
81
+ const nextIndex = backward
82
+ ? (currentIndex - 1 + order.length) % order.length
83
+ : (currentIndex + 1) % order.length
84
+ setMenuFocusTarget(order[nextIndex])
85
+ }
86
+
87
+ useKeyboard((key) => {
88
+ if (key.ctrl && key.name === "c") {
89
+ renderer.destroy()
90
+ return
91
+ }
92
+
93
+ if (state.screen === "menu") {
94
+ if (menuFocusTarget === "ws-input") {
95
+ if (key.name === "tab") {
96
+ cycleMenuFocus(Boolean(key.shift))
97
+ return
98
+ }
99
+ if (key.name === "escape") {
100
+ setMenuFocusTarget("content")
101
+ }
102
+ return
103
+ }
104
+
105
+ if (menuFocusTarget === "reconnect") {
106
+ if (key.name === "tab") {
107
+ cycleMenuFocus(Boolean(key.shift))
108
+ return
109
+ }
110
+ if (key.name === "enter" || key.name === "return") {
111
+ reconnect()
112
+ return
113
+ }
114
+ if (key.name === "escape") {
115
+ setMenuFocusTarget("content")
116
+ }
117
+ return
118
+ }
119
+
120
+ if (key.name === "up") {
121
+ setState((current: AppState) => updateSelectedAction(current, (current.selectedActionIndex - 1 + menu.length) % menu.length))
122
+ return
123
+ }
124
+ if (key.name === "down") {
125
+ setState((current: AppState) => updateSelectedAction(current, (current.selectedActionIndex + 1) % menu.length))
126
+ return
127
+ }
128
+ if (key.name === "tab") {
129
+ cycleMenuFocus(Boolean(key.shift))
130
+ return
131
+ }
132
+ if (key.name === "enter" || key.name === "return") {
133
+ setValidationError(null)
134
+ setState((current: AppState) => openAction(current))
135
+ return
136
+ }
137
+ return
138
+ }
139
+
140
+ const inputCount = menu[state.selectedActionIndex].params.length
141
+
142
+ if (key.name === "escape") {
143
+ setValidationError(null)
144
+ setState((current: AppState) => backToMenu(current))
145
+ return
146
+ }
147
+ if (key.name === "tab") {
148
+ setState((current: AppState) => nextFocus(current, inputCount))
149
+ return
150
+ }
151
+ if (key.shift && key.name === "tab") {
152
+ setState((current: AppState) => previousFocus(current, inputCount))
153
+ return
154
+ }
155
+ if ((key.name === "enter" || key.name === "return") && state.focusIndex === inputCount) {
156
+ if (!validateAction(state.selectedActionIndex, state.formValues)) {
157
+ setValidationError("Param Invalid")
158
+ setState((current: AppState) => appendLog(current, "Param Invalid"))
159
+ return
160
+ }
161
+ setValidationError(null)
162
+ const payload = buildPayload(state.selectedActionIndex, state.formValues)
163
+ setState((current: AppState) => appendLog(current, `[request] ${payload}`))
164
+ connection.send(payload)
165
+ }
166
+ })
167
+
168
+ if (width < 100 || height < 30) {
169
+ return <text>Window need to larger than 100x30, current={width}x{height}</text>
170
+ }
171
+
172
+ return (
173
+ <box width="100%" height="100%" flexDirection="column" padding={1} gap={1} position="relative">
174
+ <box border borderColor="#7c3aed" paddingX={1} paddingY={0} flexDirection="row" alignItems="center" gap={1}>
175
+ <text>JBS Client OpenTUI</text>
176
+ </box>
177
+ {toastMessage ? (
178
+ <box position="absolute" top={1} right={2} zIndex={20} border borderColor="#3b82f6" backgroundColor="#172554" paddingX={1} paddingY={0}>
179
+ <text fg="#bfdbfe">{toastMessage}</text>
180
+ </box>
181
+ ) : null}
182
+ <box border borderColor="#2563eb" height={3} minHeight={3} maxHeight={3} paddingX={1} paddingY={0} flexDirection="row" alignItems="center" justifyContent="flex-start" gap={1}>
183
+ <text fg="#93c5fd">WS</text>
184
+ <box flexGrow={1}>
185
+ <input value={wsUrl} onChange={setWsUrl} width="100%" focused={state.screen === "menu" && menuFocusTarget === "ws-input"} />
186
+ </box>
187
+ <box
188
+ border
189
+ borderColor={state.screen === "menu" && menuFocusTarget === "reconnect" ? "#3b82f6" : "#22c55e"}
190
+ paddingX={1}
191
+ paddingY={0}
192
+ flexGrow={0}
193
+ flexShrink={0}
194
+ alignSelf="center"
195
+ width={13}
196
+ focusable
197
+ onMouseDown={() => {
198
+ setMenuFocusTarget("reconnect")
199
+ reconnect()
200
+ }}
201
+ >
202
+ <text fg={state.screen === "menu" && menuFocusTarget === "reconnect" ? "#93c5fd" : "#22c55e"}>Reconnect</text>
203
+ </box>
204
+ <box flexGrow={0} flexShrink={0} width={12}>
205
+ <text fg={state.connectionStatus === "connected" ? "#22c55e" : state.connectionStatus === "error" ? "#f87171" : "#facc15"}>
206
+ {state.connectionStatus}
207
+ </text>
208
+ </box>
209
+ </box>
210
+ <box flexDirection="row" flexGrow={1} gap={1}>
211
+ <box width="50%" flexGrow={1}>
212
+ {state.screen === "menu" ? (
213
+ <ActionMenu
214
+ focused={menuFocusTarget === "content"}
215
+ selectedIndex={state.selectedActionIndex}
216
+ onChange={(index: number) => setState((current: AppState) => updateSelectedAction(current, index))}
217
+ onSelect={(index: number) => {
218
+ setValidationError(null)
219
+ setState((current: AppState) => openAction(current, index))
220
+ }}
221
+ />
222
+ ) : (
223
+ <ParamForm
224
+ actionIndex={state.selectedActionIndex}
225
+ values={state.formValues}
226
+ focusIndex={state.focusIndex}
227
+ focused
228
+ validationError={validationError}
229
+ onChange={(index: number, value: string) => setState((current: AppState) => updateFormValue(current, index, value))}
230
+ />
231
+ )}
232
+ </box>
233
+ <box width="50%" flexGrow={1}>
234
+ <LogPanel logs={state.logs} focused={false} />
235
+ </box>
236
+ </box>
237
+ </box>
238
+ )
239
+ }
@@ -0,0 +1,28 @@
1
+ import { menu } from "../protocol.js"
2
+
3
+ type ActionMenuProps = {
4
+ focused: boolean
5
+ selectedIndex: number
6
+ onChange: (index: number) => void
7
+ onSelect: (index: number) => void
8
+ }
9
+
10
+ export function ActionMenu({ focused, selectedIndex, onChange, onSelect }: ActionMenuProps) {
11
+ return (
12
+ <box border borderStyle="rounded" borderColor="#d946ef" padding={1} flexGrow={1} title="Input the action?">
13
+ <select
14
+ focused={focused}
15
+ height={19}
16
+ selectedIndex={selectedIndex}
17
+ options={menu.map((item) => ({
18
+ name: item.name,
19
+ value: item.name,
20
+ description: `${item.params.length} param${item.params.length === 1 ? "" : "s"}`
21
+ }))}
22
+ onChange={(index) => onChange(index)}
23
+ onSelect={(index) => onSelect(index)}
24
+ />
25
+ <text fg="#9ca3af">Enter opens the form. Up and Down move the selection.</text>
26
+ </box>
27
+ )
28
+ }
@@ -0,0 +1,31 @@
1
+ import { useEffect, useRef } from "react"
2
+ import type { ScrollBoxRenderable } from "@opentui/core"
3
+
4
+ type LogPanelProps = {
5
+ logs: string[]
6
+ focused?: boolean
7
+ }
8
+
9
+ export function LogPanel({ logs, focused = false }: LogPanelProps) {
10
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null)
11
+
12
+ useEffect(() => {
13
+ if (!scrollRef.current) {
14
+ return
15
+ }
16
+ scrollRef.current.scrollTo({ x: 0, y: scrollRef.current.scrollHeight })
17
+ }, [logs])
18
+
19
+ return (
20
+ <box border borderStyle="rounded" borderColor="#26f7ce" padding={1} flexDirection="column" flexGrow={1} title="Logs">
21
+ <scrollbox ref={scrollRef} flexGrow={1} focused={focused} stickyScroll stickyStart="bottom">
22
+ <box flexDirection="column">
23
+ {logs.length === 0 ? <text fg="#6b7280" selectable>Waiting for logs...</text> : null}
24
+ {logs.map((log, index) => (
25
+ <text key={`${index}-${log}`} selectable>{log}</text>
26
+ ))}
27
+ </box>
28
+ </scrollbox>
29
+ </box>
30
+ )
31
+ }
@@ -0,0 +1,73 @@
1
+ import { useRef } from "react"
2
+ import { menu } from "../protocol.js"
3
+
4
+ type ParamFormProps = {
5
+ actionIndex: number
6
+ values: string[]
7
+ focusIndex: number
8
+ focused: boolean
9
+ validationError: string | null
10
+ onChange: (index: number, value: string) => void
11
+ }
12
+
13
+ export function ParamForm({ actionIndex, values, focusIndex, focused, validationError, onChange }: ParamFormProps) {
14
+ const action = menu[actionIndex]
15
+ const submitFocused = focused && focusIndex === action.params.length
16
+ const textareaRefs = useRef<Record<number, { plainText?: string; initialValue?: string } | null>>({})
17
+
18
+ return (
19
+ <box border borderStyle="rounded" borderColor="#d946ef" padding={1} flexDirection="column" gap={1} flexGrow={1} title={action.name}>
20
+ {action.params.length === 0 ? <text fg="#d1d5db">This action does not need parameters.</text> : null}
21
+ {action.params.map((param, index) => (
22
+ <box key={`${action.name}-${param.name}`} flexDirection="column" gap={1}>
23
+ <text fg="#a7f3d0">{param.name}</text>
24
+ {param.inputType === "textarea" ? (
25
+ <line-number minWidth={4} paddingRight={1} fg="#f472b6">
26
+ <textarea
27
+ ref={(instance) => {
28
+ textareaRefs.current[index] = instance
29
+ }}
30
+ initialValue={values[index] ?? ""}
31
+ height={12}
32
+ width="100%"
33
+ focused={focused && focusIndex === index}
34
+ wrapMode="word"
35
+ onContentChange={() => onChange(index, textareaRefs.current[index]?.plainText ?? values[index] ?? "")}
36
+ />
37
+ </line-number>
38
+ ) : param.inputType === "select" ? (
39
+ <select
40
+ options={(param.options ?? []).map((option) => ({
41
+ name: option.name,
42
+ description: option.description,
43
+ value: option.value
44
+ }))}
45
+ selectedIndex={Math.max(0, (param.options ?? []).findIndex((option) => option.value === (values[index] ?? param.value)))}
46
+ focused={focused && focusIndex === index}
47
+ height={4}
48
+ onChange={(selectedIndex, option) => onChange(index, String(option?.value ?? (param.options ?? [])[selectedIndex]?.value ?? param.value))}
49
+ />
50
+ ) : (
51
+ <input
52
+ value={values[index] ?? ""}
53
+ onChange={(value) => onChange(index, value)}
54
+ width="100%"
55
+ focused={focused && focusIndex === index}
56
+ />
57
+ )}
58
+ </box>
59
+ ))}
60
+ <box
61
+ border
62
+ borderStyle={submitFocused ? "double" : "single"}
63
+ borderColor={submitFocused ? "#22c55e" : "#6b7280"}
64
+ paddingX={2}
65
+ paddingY={1}
66
+ >
67
+ <text fg={submitFocused ? "#22c55e" : "#9ca3af"}>[ Submit ]</text>
68
+ </box>
69
+ {validationError ? <text fg="#f87171">{validationError}</text> : null}
70
+ <text fg="#9ca3af">Tab or Shift+Tab changes form focus. Esc returns to the action menu.</text>
71
+ </box>
72
+ )
73
+ }
@@ -0,0 +1,36 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import { testRender } from "@opentui/react/test-utils"
3
+ import { ActionMenu } from "./components/action-menu.js"
4
+ import { LogPanel } from "./components/log-panel.js"
5
+
6
+ let setup: Awaited<ReturnType<typeof testRender>> | undefined
7
+
8
+ afterEach(() => {
9
+ setup?.renderer.destroy()
10
+ setup = undefined
11
+ })
12
+
13
+ describe("components", () => {
14
+ test("ActionMenu renders menu title and options", async () => {
15
+ setup = await testRender(<ActionMenu focused selectedIndex={0} onChange={() => {}} onSelect={() => {}} />, {
16
+ width: 70,
17
+ height: 20
18
+ })
19
+ await setup.renderOnce()
20
+ const frame = setup.captureCharFrame()
21
+ expect(frame).toContain("Input the action?")
22
+ expect(frame).toContain("Watch")
23
+ expect(frame).toContain("Eval")
24
+ })
25
+
26
+ test("LogPanel renders logs and status", async () => {
27
+ setup = await testRender(<LogPanel logs={["hello", "world"]} />, {
28
+ width: 60,
29
+ height: 16
30
+ })
31
+ await setup.renderOnce()
32
+ const frame = setup.captureCharFrame()
33
+ expect(frame).toContain("Logs")
34
+ expect(frame).toContain("world")
35
+ })
36
+ })
package/src/index.tsx ADDED
@@ -0,0 +1,19 @@
1
+ import { createCliRenderer } from "@opentui/core"
2
+ import { createRoot } from "@opentui/react"
3
+ import { App } from "./app.js"
4
+
5
+ function readArg(name: string, fallback: string): string {
6
+ const args = Bun.argv.slice(2)
7
+ const index = args.indexOf(name)
8
+ return index >= 0 && index + 1 < args.length ? args[index + 1] : fallback
9
+ }
10
+
11
+ const host = readArg("--host", "localhost")
12
+ const wsPort = Number.parseInt(readArg("--ws_port", "18000"), 10)
13
+ const connectBackend = readArg("--connect", "true") !== "false"
14
+
15
+ const renderer = await createCliRenderer({
16
+ exitOnCtrlC: false
17
+ })
18
+
19
+ createRoot(renderer).render(<App host={host} wsPort={wsPort} connectBackend={connectBackend} />)
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { buildInitialFormValues, buildPayload, menu, validateAction } from "./protocol.js"
3
+
4
+ describe("protocol", () => {
5
+ test("buildInitialFormValues copies defaults", () => {
6
+ expect(buildInitialFormValues(0)).toEqual(["", "0"])
7
+ expect(buildInitialFormValues(7)[0]).toContain("ApplicationContext")
8
+ expect(buildInitialFormValues(3)[2]).toBe("asm")
9
+ })
10
+
11
+ test("validateAction enforces signature checks", () => {
12
+ expect(validateAction(0, ["com.demo.Service#run", "1"])).toBe(true)
13
+ expect(validateAction(0, ["com.demo.Service.run", "1"])).toBe(false)
14
+ })
15
+
16
+ test("buildPayload serializes change body request", () => {
17
+ const payload = JSON.parse(buildPayload(3, ["a.B#c", "java.lang.String, int", "javassist", "return 1;"])) as Record<string, unknown>
18
+ expect(payload.type).toBe("CHANGE_BODY")
19
+ expect(payload.className).toBe("a.B")
20
+ expect(payload.method).toBe("c")
21
+ expect(payload.paramTypes).toEqual(["java.lang.String", "int"])
22
+ expect(payload.body).toBe("return 1;")
23
+ expect(payload.mode).toBe(0)
24
+ expect(typeof payload.id).toBe("string")
25
+ })
26
+
27
+ test("buildPayload serializes eval and decompile requests", () => {
28
+ const decompilePayload = JSON.parse(buildPayload(5, ["demo.Service"])) as Record<string, unknown>
29
+ expect(decompilePayload.type).toBe("DECOMPILE")
30
+ expect(decompilePayload.className).toBe("demo.Service")
31
+
32
+ const evalPayload = JSON.parse(buildPayload(6, ["1 + 1"])) as Record<string, unknown>
33
+ expect(evalPayload.type).toBe("EVAL")
34
+ expect(evalPayload.body).toBe("1 + 1")
35
+ })
36
+
37
+ test("menu preserves expected action count", () => {
38
+ expect(menu.map((item) => item.name)).toEqual(["Watch", "OuterWatch", "Trace", "ChangeBody", "ChangeResult", "Decompile", "Eval", "Exec", "Reset"])
39
+ })
40
+ })