usecomputer 0.0.1 → 0.0.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 +18 -0
- package/README.md +37 -0
- package/bin.js +3 -1
- package/build.zig +52 -0
- package/build.zig.zon +21 -0
- package/dist/bridge-contract.test.d.ts +2 -0
- package/dist/bridge-contract.test.d.ts.map +1 -0
- package/dist/bridge-contract.test.js +74 -0
- package/dist/bridge.d.ts +7 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +130 -0
- package/dist/cli-parsing.test.d.ts +2 -0
- package/dist/cli-parsing.test.d.ts.map +1 -0
- package/dist/cli-parsing.test.js +30 -0
- package/dist/cli.d.ts +5 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +286 -335
- package/dist/command-parsers.d.ts +6 -0
- package/dist/command-parsers.d.ts.map +1 -0
- package/dist/command-parsers.js +54 -0
- package/dist/command-parsers.test.d.ts +2 -0
- package/dist/command-parsers.test.d.ts.map +1 -0
- package/dist/command-parsers.test.js +44 -0
- package/dist/darwin-arm64/usecomputer.node +0 -0
- package/dist/darwin-x64/usecomputer.node +0 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/native-click-smoke.test.d.ts +2 -0
- package/dist/native-click-smoke.test.d.ts.map +1 -0
- package/dist/native-click-smoke.test.js +93 -0
- package/dist/native-lib.cjs +33 -0
- package/dist/native-lib.d.cts +7 -0
- package/dist/native-lib.d.ts +5 -0
- package/dist/native-lib.d.ts.map +1 -0
- package/dist/native-lib.js +27 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +23 -12
- package/src/bridge-contract.test.ts +85 -0
- package/src/bridge.ts +159 -0
- package/src/cli.ts +329 -473
- package/src/command-parsers.test.ts +50 -0
- package/src/command-parsers.ts +60 -0
- package/src/index.ts +5 -4
- package/src/native-click-smoke.test.ts +131 -0
- package/src/native-lib.ts +38 -0
- package/src/types.ts +87 -0
- package/zig/src/lib.zig +367 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Tests for parsing coordinates, regions, directions, and keyboard modifiers.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import { parseDirection, parseModifiers, parsePoint, parseRegion } from './command-parsers.js'
|
|
5
|
+
|
|
6
|
+
describe('command parsers', () => {
|
|
7
|
+
test('parses x,y points', () => {
|
|
8
|
+
const result = parsePoint('100,200')
|
|
9
|
+
expect(result).toMatchInlineSnapshot(`
|
|
10
|
+
{
|
|
11
|
+
"x": 100,
|
|
12
|
+
"y": 200,
|
|
13
|
+
}
|
|
14
|
+
`)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('rejects invalid points', () => {
|
|
18
|
+
const result = parsePoint('100')
|
|
19
|
+
expect(result instanceof Error).toBe(true)
|
|
20
|
+
expect(result instanceof Error ? result.message : '').toMatchInlineSnapshot(`"Invalid point "100". Expected x,y"`)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('parses x,y,width,height regions', () => {
|
|
24
|
+
const result = parseRegion('10,20,300,400')
|
|
25
|
+
expect(result).toMatchInlineSnapshot(`
|
|
26
|
+
{
|
|
27
|
+
"height": 400,
|
|
28
|
+
"width": 300,
|
|
29
|
+
"x": 10,
|
|
30
|
+
"y": 20,
|
|
31
|
+
}
|
|
32
|
+
`)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('parses modifiers with normalization', () => {
|
|
36
|
+
expect(parseModifiers(' CMD,shift, alt ')).toMatchInlineSnapshot(`
|
|
37
|
+
[
|
|
38
|
+
"cmd",
|
|
39
|
+
"shift",
|
|
40
|
+
"alt",
|
|
41
|
+
]
|
|
42
|
+
`)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('validates scroll direction', () => {
|
|
46
|
+
expect(parseDirection('down')).toBe('down')
|
|
47
|
+
const invalid = parseDirection('diagonal')
|
|
48
|
+
expect(invalid instanceof Error).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Parser helpers for CLI values such as coordinates, regions, and key modifiers.
|
|
2
|
+
|
|
3
|
+
import type { Point, Region, ScrollDirection } from './types.js'
|
|
4
|
+
|
|
5
|
+
export function parsePoint(input: string): Error | Point {
|
|
6
|
+
const parts = input.split(',').map((value) => {
|
|
7
|
+
return value.trim()
|
|
8
|
+
})
|
|
9
|
+
if (parts.length !== 2) {
|
|
10
|
+
return new Error(`Invalid point \"${input}\". Expected x,y`)
|
|
11
|
+
}
|
|
12
|
+
const x = Number(parts[0])
|
|
13
|
+
const y = Number(parts[1])
|
|
14
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
15
|
+
return new Error(`Invalid point \"${input}\". Coordinates must be numbers`)
|
|
16
|
+
}
|
|
17
|
+
return { x, y }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseRegion(input: string): Error | Region {
|
|
21
|
+
const parts = input.split(',').map((value) => {
|
|
22
|
+
return value.trim()
|
|
23
|
+
})
|
|
24
|
+
if (parts.length !== 4) {
|
|
25
|
+
return new Error(`Invalid region \"${input}\". Expected x,y,width,height`)
|
|
26
|
+
}
|
|
27
|
+
const x = Number(parts[0])
|
|
28
|
+
const y = Number(parts[1])
|
|
29
|
+
const width = Number(parts[2])
|
|
30
|
+
const height = Number(parts[3])
|
|
31
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) {
|
|
32
|
+
return new Error(`Invalid region \"${input}\". Values must be numbers`)
|
|
33
|
+
}
|
|
34
|
+
if (width <= 0 || height <= 0) {
|
|
35
|
+
return new Error(`Invalid region \"${input}\". Width and height must be greater than 0`)
|
|
36
|
+
}
|
|
37
|
+
return { x, y, width, height }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseModifiers(input?: string): string[] {
|
|
41
|
+
if (!input) {
|
|
42
|
+
return []
|
|
43
|
+
}
|
|
44
|
+
return input
|
|
45
|
+
.split(',')
|
|
46
|
+
.map((value) => {
|
|
47
|
+
return value.trim().toLowerCase()
|
|
48
|
+
})
|
|
49
|
+
.filter((value) => {
|
|
50
|
+
return value.length > 0
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseDirection(input: string): Error | ScrollDirection {
|
|
55
|
+
const normalized = input.trim().toLowerCase()
|
|
56
|
+
if (normalized === 'up' || normalized === 'down' || normalized === 'left' || normalized === 'right') {
|
|
57
|
+
return normalized
|
|
58
|
+
}
|
|
59
|
+
return new Error(`Invalid direction \"${input}\". Expected up, down, left, or right`)
|
|
60
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Controls any desktop via accessibility APIs, mouse, keyboard, and screen capture.
|
|
3
|
-
// Cross-platform: macOS, Linux, Windows.
|
|
1
|
+
// Public API exports for usecomputer parser, bridge, and CLI modules.
|
|
4
2
|
|
|
5
|
-
export {}
|
|
3
|
+
export { createCli } from './cli.js'
|
|
4
|
+
export { createBridge, createBridgeFromNative } from './bridge.js'
|
|
5
|
+
export * from './types.js'
|
|
6
|
+
export * from './command-parsers.js'
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Optional host smoke test for the real native click command.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import { native } from './native-lib.js'
|
|
5
|
+
|
|
6
|
+
const runNativeSmoke = process.env.USECOMPUTER_NATIVE_SMOKE === '1'
|
|
7
|
+
|
|
8
|
+
describe('native click smoke', () => {
|
|
9
|
+
const smokeTest = runNativeSmoke ? test : test.skip
|
|
10
|
+
|
|
11
|
+
smokeTest('executes click command without crashing', () => {
|
|
12
|
+
expect(native).toBeTruthy()
|
|
13
|
+
if (!native) {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const response = native.execute(
|
|
18
|
+
'click',
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
point: { x: 10, y: 10 },
|
|
21
|
+
button: 'left',
|
|
22
|
+
count: 1,
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const parsed = JSON.parse(response) as { ok: boolean; data?: null; error?: string }
|
|
27
|
+
expect(parsed).toMatchInlineSnapshot(`
|
|
28
|
+
{
|
|
29
|
+
"data": null,
|
|
30
|
+
"ok": true,
|
|
31
|
+
}
|
|
32
|
+
`)
|
|
33
|
+
expect(typeof parsed.ok).toBe('boolean')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
smokeTest('executes mouse-move/down/up/position/hover/drag without crashing', () => {
|
|
37
|
+
expect(native).toBeTruthy()
|
|
38
|
+
if (!native) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const moveResponse = JSON.parse(
|
|
43
|
+
native.execute(
|
|
44
|
+
'mouse-move',
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
x: 20,
|
|
47
|
+
y: 20,
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
50
|
+
) as { ok: boolean; data?: null; error?: string }
|
|
51
|
+
|
|
52
|
+
const downResponse = JSON.parse(
|
|
53
|
+
native.execute('mouse-down', JSON.stringify({ button: 'left' })),
|
|
54
|
+
) as { ok: boolean; data?: null; error?: string }
|
|
55
|
+
|
|
56
|
+
const upResponse = JSON.parse(
|
|
57
|
+
native.execute('mouse-up', JSON.stringify({ button: 'left' })),
|
|
58
|
+
) as { ok: boolean; data?: null; error?: string }
|
|
59
|
+
|
|
60
|
+
const positionResponse = JSON.parse(
|
|
61
|
+
native.execute('mouse-position', JSON.stringify({})),
|
|
62
|
+
) as { ok: boolean; data?: { x: number; y: number }; error?: string }
|
|
63
|
+
|
|
64
|
+
const hoverResponse = JSON.parse(
|
|
65
|
+
native.execute(
|
|
66
|
+
'hover',
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
x: 24,
|
|
69
|
+
y: 24,
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
) as { ok: boolean; data?: null; error?: string }
|
|
73
|
+
|
|
74
|
+
const dragResponse = JSON.parse(
|
|
75
|
+
native.execute(
|
|
76
|
+
'drag',
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
from: { x: 24, y: 24 },
|
|
79
|
+
to: { x: 30, y: 30 },
|
|
80
|
+
button: 'left',
|
|
81
|
+
durationMs: 10,
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
) as { ok: boolean; data?: null; error?: string }
|
|
85
|
+
|
|
86
|
+
expect({
|
|
87
|
+
moveResponse,
|
|
88
|
+
downResponse,
|
|
89
|
+
upResponse,
|
|
90
|
+
positionResponse,
|
|
91
|
+
hoverResponse,
|
|
92
|
+
dragResponse,
|
|
93
|
+
}).toMatchInlineSnapshot(`
|
|
94
|
+
{
|
|
95
|
+
"downResponse": {
|
|
96
|
+
"data": null,
|
|
97
|
+
"ok": true,
|
|
98
|
+
},
|
|
99
|
+
"dragResponse": {
|
|
100
|
+
"data": null,
|
|
101
|
+
"ok": true,
|
|
102
|
+
},
|
|
103
|
+
"hoverResponse": {
|
|
104
|
+
"data": null,
|
|
105
|
+
"ok": true,
|
|
106
|
+
},
|
|
107
|
+
"moveResponse": {
|
|
108
|
+
"data": null,
|
|
109
|
+
"ok": true,
|
|
110
|
+
},
|
|
111
|
+
"positionResponse": {
|
|
112
|
+
"data": {
|
|
113
|
+
"x": 20,
|
|
114
|
+
"y": 20,
|
|
115
|
+
},
|
|
116
|
+
"ok": true,
|
|
117
|
+
},
|
|
118
|
+
"upResponse": {
|
|
119
|
+
"data": null,
|
|
120
|
+
"ok": true,
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
`)
|
|
124
|
+
expect(moveResponse.ok).toBe(true)
|
|
125
|
+
expect(downResponse.ok).toBe(true)
|
|
126
|
+
expect(upResponse.ok).toBe(true)
|
|
127
|
+
expect(positionResponse.ok).toBe(true)
|
|
128
|
+
expect(hoverResponse.ok).toBe(true)
|
|
129
|
+
expect(dragResponse.ok).toBe(true)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// ESM native loader for the usecomputer Zig addon using createRequire.
|
|
2
|
+
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url)
|
|
7
|
+
|
|
8
|
+
export interface NativeModule {
|
|
9
|
+
execute(command: string, payloadJson: string): string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function loadCandidate(path: string): NativeModule | null {
|
|
13
|
+
try {
|
|
14
|
+
return require(path) as NativeModule
|
|
15
|
+
} catch {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadNativeModule(): NativeModule | null {
|
|
21
|
+
const dev = loadCandidate('../zig-out/lib/usecomputer.node')
|
|
22
|
+
if (dev) {
|
|
23
|
+
return dev
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const platform = os.platform()
|
|
27
|
+
const arch = os.arch()
|
|
28
|
+
const target = `${platform}-${arch}`
|
|
29
|
+
|
|
30
|
+
const packaged = loadCandidate(`../dist/${target}/usecomputer.node`)
|
|
31
|
+
if (packaged) {
|
|
32
|
+
return packaged
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const native = loadNativeModule()
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Shared types for usecomputer command parsing and backend bridge calls.
|
|
2
|
+
|
|
3
|
+
export type MouseButton = 'left' | 'right' | 'middle'
|
|
4
|
+
|
|
5
|
+
export type ScrollDirection = 'up' | 'down' | 'left' | 'right'
|
|
6
|
+
|
|
7
|
+
export type Point = {
|
|
8
|
+
x: number
|
|
9
|
+
y: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type Region = {
|
|
13
|
+
x: number
|
|
14
|
+
y: number
|
|
15
|
+
width: number
|
|
16
|
+
height: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type DisplayInfo = {
|
|
20
|
+
id: number
|
|
21
|
+
name: string
|
|
22
|
+
x: number
|
|
23
|
+
y: number
|
|
24
|
+
width: number
|
|
25
|
+
height: number
|
|
26
|
+
scale: number
|
|
27
|
+
isPrimary: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ScreenshotInput = {
|
|
31
|
+
path?: string
|
|
32
|
+
display?: number
|
|
33
|
+
region?: Region
|
|
34
|
+
annotate?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ScreenshotResult = {
|
|
38
|
+
path: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ClickInput = {
|
|
42
|
+
point: Point
|
|
43
|
+
button: MouseButton
|
|
44
|
+
count: number
|
|
45
|
+
modifiers: string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type TypeInput = {
|
|
49
|
+
text: string
|
|
50
|
+
delayMs?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type PressInput = {
|
|
54
|
+
key: string
|
|
55
|
+
count: number
|
|
56
|
+
delayMs?: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ScrollInput = {
|
|
60
|
+
direction: ScrollDirection
|
|
61
|
+
amount: number
|
|
62
|
+
at?: Point
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type DragInput = {
|
|
66
|
+
from: Point
|
|
67
|
+
to: Point
|
|
68
|
+
durationMs?: number
|
|
69
|
+
button: MouseButton
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface UseComputerBridge {
|
|
73
|
+
screenshot(input: ScreenshotInput): Promise<ScreenshotResult>
|
|
74
|
+
click(input: ClickInput): Promise<void>
|
|
75
|
+
typeText(input: TypeInput): Promise<void>
|
|
76
|
+
press(input: PressInput): Promise<void>
|
|
77
|
+
scroll(input: ScrollInput): Promise<void>
|
|
78
|
+
drag(input: DragInput): Promise<void>
|
|
79
|
+
hover(input: Point): Promise<void>
|
|
80
|
+
mouseMove(input: Point): Promise<void>
|
|
81
|
+
mouseDown(input: { button: MouseButton }): Promise<void>
|
|
82
|
+
mouseUp(input: { button: MouseButton }): Promise<void>
|
|
83
|
+
mousePosition(): Promise<Point>
|
|
84
|
+
displayList(): Promise<DisplayInfo[]>
|
|
85
|
+
clipboardGet(): Promise<string>
|
|
86
|
+
clipboardSet(input: { text: string }): Promise<void>
|
|
87
|
+
}
|