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,201 @@
1
+ import type { FunctionDefinition } from "./types.js"
2
+
3
+ const engineOptions = [
4
+ { name: "ASM", description: "Default bytecode engine", value: "asm" },
5
+ { name: "Javassist", description: "Source-level transformer", value: "javassist" }
6
+ ]
7
+
8
+ function classAndMethodChecker(value: string): boolean {
9
+ return value.split("#").length === 2
10
+ }
11
+
12
+ function nonEmptyChecker(value: string): boolean {
13
+ return value.trim().length > 0
14
+ }
15
+
16
+ function engineToMode(value: string): number {
17
+ return value === "javassist" ? 0 : 1
18
+ }
19
+
20
+ function randomString(length: number): string {
21
+ const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
22
+ let result = ""
23
+ for (let index = 0; index < length; index += 1) {
24
+ result += alphabet[Math.floor(Math.random() * alphabet.length)]
25
+ }
26
+ return result
27
+ }
28
+
29
+ function commonMap(): Record<string, unknown> {
30
+ return {
31
+ id: randomString(4),
32
+ timestamp: Date.now()
33
+ }
34
+ }
35
+
36
+ function parseParamTypes(value: string): string[] {
37
+ return value
38
+ .split(",")
39
+ .map((part) => part.trim())
40
+ .filter((part) => part.length > 0)
41
+ }
42
+
43
+ function splitSignature(value: string): [string, string] {
44
+ const [className = "", method = ""] = value.split("#")
45
+ return [className, method]
46
+ }
47
+
48
+ export const menu: FunctionDefinition[] = [
49
+ {
50
+ name: "Watch",
51
+ params: [
52
+ { name: "ClassName#MethodName", inputType: "text", checker: classAndMethodChecker, value: "" },
53
+ { name: "MinCost", inputType: "text", checker: () => true, value: "0" }
54
+ ],
55
+ toPayload: (params: string[]) => JSON.stringify({ ...commonMap(), type: "WATCH", signature: params[0], minCost: Number.parseInt(params[1] || "0", 10) || 0 })
56
+ },
57
+ {
58
+ name: "OuterWatch",
59
+ params: [
60
+ { name: "ClassName#MethodName", inputType: "text", checker: classAndMethodChecker, value: "" },
61
+ { name: "InnerClassName#InnerMethodName", inputType: "text", checker: classAndMethodChecker, value: "" }
62
+ ],
63
+ toPayload: (params: string[]) => JSON.stringify({ ...commonMap(), type: "OUTER_WATCH", signature: params[0], innerSignature: params[1] })
64
+ },
65
+ {
66
+ name: "Trace",
67
+ params: [
68
+ { name: "ClassName#MethodName", inputType: "text", checker: classAndMethodChecker, value: "" },
69
+ { name: "MinCost", inputType: "text", checker: () => true, value: "0" },
70
+ { name: "IgnoreSubMethodZeroCost", inputType: "text", checker: () => true, value: "true" }
71
+ ],
72
+ toPayload: (params: string[]) => JSON.stringify({
73
+ ...commonMap(),
74
+ type: "TRACE",
75
+ signature: params[0],
76
+ minCost: Number.parseInt(params[1] || "0", 10) || 0,
77
+ ignoreZero: params[2] === "true"
78
+ })
79
+ },
80
+ {
81
+ name: "ChangeBody",
82
+ params: [
83
+ { name: "ClassName#MethodName", inputType: "text", checker: classAndMethodChecker, value: "" },
84
+ { name: "ParamTypes", inputType: "text", checker: () => true, value: "" },
85
+ { name: "Engine", inputType: "select", checker: () => true, value: "asm", options: engineOptions },
86
+ { name: "Body", inputType: "textarea", checker: () => true, value: "" }
87
+ ],
88
+ toPayload: (params: string[]) => {
89
+ const [className, method] = splitSignature(params[0])
90
+ return JSON.stringify({
91
+ ...commonMap(),
92
+ type: "CHANGE_BODY",
93
+ className,
94
+ method,
95
+ paramTypes: parseParamTypes(params[1]),
96
+ body: params[3],
97
+ mode: engineToMode(params[2])
98
+ })
99
+ }
100
+ },
101
+ {
102
+ name: "ChangeResult",
103
+ params: [
104
+ { name: "ClassName#MethodName", inputType: "text", checker: classAndMethodChecker, value: "" },
105
+ { name: "ParamTypes", inputType: "text", checker: () => true, value: "" },
106
+ { name: "InnerClassName#InnerMethodName", inputType: "text", checker: classAndMethodChecker, value: "" },
107
+ { name: "Engine", inputType: "select", checker: () => true, value: "asm", options: engineOptions },
108
+ { name: "Body", inputType: "textarea", checker: () => true, value: "" }
109
+ ],
110
+ toPayload: (params: string[]) => {
111
+ const [className, method] = splitSignature(params[0])
112
+ const [innerClassName, innerMethod] = splitSignature(params[2])
113
+ return JSON.stringify({
114
+ ...commonMap(),
115
+ type: "CHANGE_RESULT",
116
+ className,
117
+ method,
118
+ paramTypes: parseParamTypes(params[1]),
119
+ innerClassName,
120
+ innerMethod,
121
+ body: params[4],
122
+ mode: engineToMode(params[3])
123
+ })
124
+ }
125
+ },
126
+ {
127
+ name: "Decompile",
128
+ params: [
129
+ { name: "ClassName", inputType: "text", checker: nonEmptyChecker, value: "" }
130
+ ],
131
+ toPayload: (params: string[]) => JSON.stringify({
132
+ ...commonMap(),
133
+ type: "DECOMPILE",
134
+ className: params[0]
135
+ })
136
+ },
137
+ {
138
+ name: "Eval",
139
+ params: [
140
+ {
141
+ name: "Body",
142
+ inputType: "textarea",
143
+ checker: nonEmptyChecker,
144
+ value: "ctx.getBeanDefinitionNames().length"
145
+ }
146
+ ],
147
+ toPayload: (params: string[]) => JSON.stringify({
148
+ ...commonMap(),
149
+ type: "EVAL",
150
+ body: params[0]
151
+ })
152
+ },
153
+ {
154
+ name: "Exec",
155
+ params: [
156
+ {
157
+ name: "Body",
158
+ inputType: "textarea",
159
+ checker: () => true,
160
+ value: `
161
+ try {
162
+ ApplicationContext ctx =
163
+ (ApplicationContext) SpringUtils.getSpringBootApplicationContext();
164
+ Global.info(Arrays.toString(ctx.getBeanDefinitionNames()));
165
+ } catch (Exception e) {
166
+ Global.error(e.toString(), e);
167
+ }
168
+ `
169
+ }
170
+ ],
171
+ toPayload: (params: string[]) => JSON.stringify({
172
+ ...commonMap(),
173
+ type: "EXEC",
174
+ body: `package w;
175
+ import w.Global;
176
+ import w.util.SpringUtils;
177
+ import org.springframework.context.ApplicationContext;
178
+ import java.util.*;
179
+ public class Exec{
180
+ public void exec() {${params[0] ?? ""}}
181
+ }`
182
+ })
183
+ },
184
+ {
185
+ name: "Reset",
186
+ params: [],
187
+ toPayload: () => JSON.stringify({ ...commonMap(), type: "RESET" })
188
+ }
189
+ ]
190
+
191
+ export function buildInitialFormValues(actionIndex: number): string[] {
192
+ return menu[actionIndex].params.map((param: FunctionDefinition["params"][number]) => param.value)
193
+ }
194
+
195
+ export function validateAction(actionIndex: number, values: string[]): boolean {
196
+ return menu[actionIndex].params.every((param: FunctionDefinition["params"][number], index: number) => param.checker(values[index] ?? ""))
197
+ }
198
+
199
+ export function buildPayload(actionIndex: number, values: string[]): string {
200
+ return menu[actionIndex].toPayload(values)
201
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { appendLog, backToMenu, createInitialState, nextFocus, openAction, previousFocus, updateFormValue, updateSelectedAction } from "./state.js"
3
+
4
+ describe("state helpers", () => {
5
+ test("openAction builds form state", () => {
6
+ const initial = updateSelectedAction(createInitialState(), 3)
7
+ const opened = openAction(initial)
8
+ expect(opened.screen).toBe("form")
9
+ expect(opened.focusIndex).toBe(0)
10
+ expect(opened.formValues.length).toBe(4)
11
+ })
12
+
13
+ test("focus wraps across inputs and submit button", () => {
14
+ const opened = openAction(updateSelectedAction(createInitialState(), 0))
15
+ expect(nextFocus(opened, 2).focusIndex).toBe(1)
16
+ expect(nextFocus({ ...opened, focusIndex: 2 }, 2).focusIndex).toBe(0)
17
+ expect(previousFocus(opened, 2).focusIndex).toBe(2)
18
+ })
19
+
20
+ test("updateFormValue and backToMenu keep expected data", () => {
21
+ const opened = openAction(updateSelectedAction(createInitialState(), 5))
22
+ const updated = updateFormValue(opened, 0, "hello")
23
+ expect(updated.formValues[0]).toBe("hello")
24
+ expect(backToMenu(updated).screen).toBe("menu")
25
+ })
26
+
27
+ test("appendLog keeps newest messages first", () => {
28
+ const state = appendLog(appendLog(createInitialState(), "first"), "second")
29
+ expect(state.logs).toEqual(["first", "second"])
30
+ })
31
+ })
package/src/state.ts ADDED
@@ -0,0 +1,77 @@
1
+ import { buildInitialFormValues } from "./protocol.js"
2
+ import type { AppState, ConnectionStatus } from "./types.js"
3
+
4
+ const logMaxLength = 200
5
+
6
+ export function createInitialState(): AppState {
7
+ return {
8
+ screen: "menu",
9
+ selectedActionIndex: 0,
10
+ focusIndex: 0,
11
+ formValues: [],
12
+ logs: [],
13
+ connectionStatus: "idle"
14
+ }
15
+ }
16
+
17
+ export function openAction(state: AppState, actionIndex = state.selectedActionIndex): AppState {
18
+ return {
19
+ ...state,
20
+ screen: "form",
21
+ selectedActionIndex: actionIndex,
22
+ focusIndex: 0,
23
+ formValues: buildInitialFormValues(actionIndex)
24
+ }
25
+ }
26
+
27
+ export function backToMenu(state: AppState): AppState {
28
+ return {
29
+ ...state,
30
+ screen: "menu",
31
+ focusIndex: 0
32
+ }
33
+ }
34
+
35
+ export function updateSelectedAction(state: AppState, actionIndex: number): AppState {
36
+ return {
37
+ ...state,
38
+ selectedActionIndex: actionIndex
39
+ }
40
+ }
41
+
42
+ export function updateFormValue(state: AppState, index: number, value: string): AppState {
43
+ const formValues = [...state.formValues]
44
+ formValues[index] = value
45
+ return {
46
+ ...state,
47
+ formValues
48
+ }
49
+ }
50
+
51
+ export function nextFocus(state: AppState, inputCount: number): AppState {
52
+ return {
53
+ ...state,
54
+ focusIndex: (state.focusIndex + 1) % (inputCount + 1)
55
+ }
56
+ }
57
+
58
+ export function previousFocus(state: AppState, inputCount: number): AppState {
59
+ return {
60
+ ...state,
61
+ focusIndex: (state.focusIndex - 1 + inputCount + 1) % (inputCount + 1)
62
+ }
63
+ }
64
+
65
+ export function appendLog(state: AppState, message: string): AppState {
66
+ return {
67
+ ...state,
68
+ logs: [...state.logs, message].slice(-logMaxLength)
69
+ }
70
+ }
71
+
72
+ export function setConnectionStatus(state: AppState, status: ConnectionStatus): AppState {
73
+ return {
74
+ ...state,
75
+ connectionStatus: status
76
+ }
77
+ }
package/src/types.ts ADDED
@@ -0,0 +1,34 @@
1
+ export type ParamOption = {
2
+ name: string
3
+ description: string
4
+ value: string
5
+ }
6
+
7
+ export type ParamInputType = "text" | "textarea" | "select"
8
+
9
+ export type ParamDefinition = {
10
+ name: string
11
+ inputType: ParamInputType
12
+ checker: (value: string) => boolean
13
+ value: string
14
+ options?: ParamOption[]
15
+ }
16
+
17
+ export type FunctionDefinition = {
18
+ name: string
19
+ params: ParamDefinition[]
20
+ toPayload: (params: string[]) => string
21
+ }
22
+
23
+ export type Screen = "menu" | "form"
24
+
25
+ export type ConnectionStatus = "idle" | "connecting" | "connected" | "disconnected" | "error"
26
+
27
+ export type AppState = {
28
+ screen: Screen
29
+ selectedActionIndex: number
30
+ focusIndex: number
31
+ formValues: string[]
32
+ logs: string[]
33
+ connectionStatus: ConnectionStatus
34
+ }
package/src/ws.ts ADDED
@@ -0,0 +1,63 @@
1
+ export type ConnectionOptions = {
2
+ url: string
3
+ enabled: boolean
4
+ onLog: (message: string) => void
5
+ onStatusChange: (status: "connecting" | "connected" | "disconnected" | "error") => void
6
+ }
7
+
8
+ function formatLog(content: string): string {
9
+ const timestamp = new Date()
10
+ const formatted = timestamp.toISOString().replace("T", " ").slice(0, 19)
11
+ return `[${formatted}] ${content}`
12
+ }
13
+
14
+ export function connectWebSocket(options: ConnectionOptions): { send: (message: string) => void; dispose: () => void } {
15
+ let ws: WebSocket | null = null
16
+ let closed = false
17
+
18
+ if (options.enabled) {
19
+ options.onStatusChange("connecting")
20
+ try {
21
+ ws = new WebSocket(options.url)
22
+ ws.onopen = () => {
23
+ options.onStatusChange("connected")
24
+ options.onLog(formatLog(`WebSocket connected: ${options.url}`))
25
+ }
26
+ ws.onmessage = (event) => {
27
+ try {
28
+ const parsed = JSON.parse(String(event.data)) as { content?: string }
29
+ options.onLog(formatLog(parsed.content ?? String(event.data)))
30
+ } catch {
31
+ options.onLog(formatLog(String(event.data)))
32
+ }
33
+ }
34
+ ws.onerror = () => {
35
+ options.onStatusChange("error")
36
+ options.onLog(formatLog(`WebSocket error: ${options.url}`))
37
+ }
38
+ ws.onclose = () => {
39
+ if (!closed) {
40
+ options.onStatusChange("disconnected")
41
+ options.onLog(formatLog(`WebSocket disconnected: ${options.url}`))
42
+ }
43
+ }
44
+ } catch (error) {
45
+ options.onStatusChange("error")
46
+ options.onLog(formatLog(`WebSocket connect failed (${options.url}): ${String(error)}`))
47
+ }
48
+ }
49
+
50
+ return {
51
+ send(message: string) {
52
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
53
+ options.onLog(formatLog("WebSocket is not connected"))
54
+ return
55
+ }
56
+ ws.send(message)
57
+ },
58
+ dispose() {
59
+ closed = true
60
+ ws?.close()
61
+ }
62
+ }
63
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["ESNext", "DOM"],
4
+ "target": "ESNext",
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "jsx": "react-jsx",
8
+ "jsxImportSource": "@opentui/react",
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "noEmit": true,
12
+ "types": ["bun-types"]
13
+ },
14
+ "include": ["src/**/*", "build.ts"]
15
+ }