usecomputer 0.0.2 → 0.0.4

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 (59) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +338 -0
  3. package/build.zig +1 -0
  4. package/dist/bridge-contract.test.js +124 -63
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/bridge.js +241 -46
  7. package/dist/cli-parsing.test.js +34 -11
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +328 -22
  10. package/dist/coord-map.d.ts +14 -0
  11. package/dist/coord-map.d.ts.map +1 -0
  12. package/dist/coord-map.js +75 -0
  13. package/dist/coord-map.test.d.ts +2 -0
  14. package/dist/coord-map.test.d.ts.map +1 -0
  15. package/dist/coord-map.test.js +157 -0
  16. package/dist/darwin-arm64/usecomputer.node +0 -0
  17. package/dist/darwin-x64/usecomputer.node +0 -0
  18. package/dist/debug-point-image.d.ts +8 -0
  19. package/dist/debug-point-image.d.ts.map +1 -0
  20. package/dist/debug-point-image.js +43 -0
  21. package/dist/debug-point-image.test.d.ts +2 -0
  22. package/dist/debug-point-image.test.d.ts.map +1 -0
  23. package/dist/debug-point-image.test.js +44 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -1
  27. package/dist/lib.d.ts +26 -0
  28. package/dist/lib.d.ts.map +1 -0
  29. package/dist/lib.js +88 -0
  30. package/dist/native-click-smoke.test.js +69 -29
  31. package/dist/native-lib.d.ts +59 -1
  32. package/dist/native-lib.d.ts.map +1 -1
  33. package/dist/terminal-table.d.ts +10 -0
  34. package/dist/terminal-table.d.ts.map +1 -0
  35. package/dist/terminal-table.js +55 -0
  36. package/dist/terminal-table.test.d.ts +2 -0
  37. package/dist/terminal-table.test.d.ts.map +1 -0
  38. package/dist/terminal-table.test.js +41 -0
  39. package/dist/types.d.ts +45 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +16 -4
  42. package/src/bridge-contract.test.ts +140 -69
  43. package/src/bridge.ts +293 -53
  44. package/src/cli-parsing.test.ts +61 -0
  45. package/src/cli.ts +401 -25
  46. package/src/coord-map.test.ts +178 -0
  47. package/src/coord-map.ts +105 -0
  48. package/src/debug-point-image.test.ts +50 -0
  49. package/src/debug-point-image.ts +69 -0
  50. package/src/index.ts +3 -1
  51. package/src/lib.ts +125 -0
  52. package/src/native-click-smoke.test.ts +81 -63
  53. package/src/native-lib.ts +39 -1
  54. package/src/terminal-table.test.ts +44 -0
  55. package/src/terminal-table.ts +88 -0
  56. package/src/types.ts +50 -0
  57. package/zig/src/lib.zig +1280 -163
  58. package/zig/src/scroll.zig +213 -0
  59. package/zig/src/window.zig +123 -0
@@ -0,0 +1,178 @@
1
+ // Validates screenshot coord-map parsing and reverse mapping edge cases.
2
+
3
+ import { describe, expect, test } from 'vitest'
4
+ import { mapPointFromCoordMap, mapPointToCoordMap, parseCoordMapOrThrow } from './coord-map.js'
5
+
6
+ describe('coord-map reverse mapping', () => {
7
+ test('maps full-display scaled screenshot coordinates to desktop coordinates', () => {
8
+ const coordMap = parseCoordMapOrThrow('0,0,1600,900,1568,882')
9
+
10
+ const mapped = [
11
+ mapPointFromCoordMap({ point: { x: 0, y: 0 }, coordMap }),
12
+ mapPointFromCoordMap({ point: { x: 1567, y: 881 }, coordMap }),
13
+ mapPointFromCoordMap({ point: { x: 784, y: 441 }, coordMap }),
14
+ ]
15
+
16
+ expect(mapped).toMatchInlineSnapshot(`
17
+ [
18
+ {
19
+ "x": 0,
20
+ "y": 0,
21
+ },
22
+ {
23
+ "x": 1599,
24
+ "y": 899,
25
+ },
26
+ {
27
+ "x": 800,
28
+ "y": 450,
29
+ },
30
+ ]
31
+ `)
32
+ })
33
+
34
+ test('maps correctly when display origin is non-zero', () => {
35
+ const coordMap = parseCoordMapOrThrow('-1728,120,1728,1117,1568,1014')
36
+
37
+ const mapped = [
38
+ mapPointFromCoordMap({ point: { x: 0, y: 0 }, coordMap }),
39
+ mapPointFromCoordMap({ point: { x: 1567, y: 1013 }, coordMap }),
40
+ ]
41
+
42
+ expect(mapped).toMatchInlineSnapshot(`
43
+ [
44
+ {
45
+ "x": -1728,
46
+ "y": 120,
47
+ },
48
+ {
49
+ "x": -1,
50
+ "y": 1236,
51
+ },
52
+ ]
53
+ `)
54
+ })
55
+
56
+ test('maps region capture coordinates including display offset', () => {
57
+ const coordMap = parseCoordMapOrThrow('2200,80,640,360,640,360')
58
+
59
+ const mapped = [
60
+ mapPointFromCoordMap({ point: { x: 0, y: 0 }, coordMap }),
61
+ mapPointFromCoordMap({ point: { x: 639, y: 359 }, coordMap }),
62
+ mapPointFromCoordMap({ point: { x: 320, y: 180 }, coordMap }),
63
+ ]
64
+
65
+ expect(mapped).toMatchInlineSnapshot(`
66
+ [
67
+ {
68
+ "x": 2200,
69
+ "y": 80,
70
+ },
71
+ {
72
+ "x": 2839,
73
+ "y": 439,
74
+ },
75
+ {
76
+ "x": 2520,
77
+ "y": 260,
78
+ },
79
+ ]
80
+ `)
81
+ })
82
+
83
+ test('clamps out-of-bounds screenshot coordinates to capture bounds', () => {
84
+ const coordMap = parseCoordMapOrThrow('500,400,300,200,150,100')
85
+
86
+ const mapped = [
87
+ mapPointFromCoordMap({ point: { x: -10, y: -20 }, coordMap }),
88
+ mapPointFromCoordMap({ point: { x: 150, y: 100 }, coordMap }),
89
+ mapPointFromCoordMap({ point: { x: 200, y: 1000 }, coordMap }),
90
+ ]
91
+
92
+ expect(mapped).toMatchInlineSnapshot(`
93
+ [
94
+ {
95
+ "x": 500,
96
+ "y": 400,
97
+ },
98
+ {
99
+ "x": 799,
100
+ "y": 599,
101
+ },
102
+ {
103
+ "x": 799,
104
+ "y": 599,
105
+ },
106
+ ]
107
+ `)
108
+ })
109
+
110
+ test('maps desktop coordinates back into screenshot image coordinates', () => {
111
+ const coordMap = parseCoordMapOrThrow('0,0,1720,1440,1568,1313')
112
+
113
+ const mapped = [
114
+ mapPointToCoordMap({ point: { x: 0, y: 0 }, coordMap }),
115
+ mapPointToCoordMap({ point: { x: 1719, y: 1439 }, coordMap }),
116
+ mapPointToCoordMap({ point: { x: 230, y: 614 }, coordMap }),
117
+ ]
118
+
119
+ expect(mapped).toMatchInlineSnapshot(`
120
+ [
121
+ {
122
+ "x": 0,
123
+ "y": 0,
124
+ },
125
+ {
126
+ "x": 1567,
127
+ "y": 1312,
128
+ },
129
+ {
130
+ "x": 210,
131
+ "y": 560,
132
+ },
133
+ ]
134
+ `)
135
+ })
136
+
137
+ test('round-trips screenshot coordinates through desktop space', () => {
138
+ const coordMap = parseCoordMapOrThrow('0,0,1720,1440,1568,1313')
139
+
140
+ const roundTrip = [
141
+ { x: 0, y: 0 },
142
+ { x: 210, y: 560 },
143
+ { x: 1567, y: 1312 },
144
+ ].map((point) => {
145
+ return mapPointToCoordMap({
146
+ point: mapPointFromCoordMap({ point, coordMap }),
147
+ coordMap,
148
+ })
149
+ })
150
+
151
+ expect(roundTrip).toMatchInlineSnapshot(`
152
+ [
153
+ {
154
+ "x": 0,
155
+ "y": 0,
156
+ },
157
+ {
158
+ "x": 210,
159
+ "y": 560,
160
+ },
161
+ {
162
+ "x": 1567,
163
+ "y": 1312,
164
+ },
165
+ ]
166
+ `)
167
+ })
168
+
169
+ test('rejects invalid coord-map payloads', () => {
170
+ expect(() => {
171
+ parseCoordMapOrThrow('0,0,10,10,20')
172
+ }).toThrowError('Option --coord-map must be x,y,width,height,imageWidth,imageHeight')
173
+
174
+ expect(() => {
175
+ parseCoordMapOrThrow('0,0,0,10,20,20')
176
+ }).toThrowError('Option --coord-map must have positive width and height values')
177
+ })
178
+ })
@@ -0,0 +1,105 @@
1
+ // Shared coord-map helpers for converting screenshot-space pixels to desktop coordinates.
2
+
3
+ import type { CoordMap, Point, Region } from './types.js'
4
+
5
+ export function parseCoordMapOrThrow(input?: string): CoordMap | undefined {
6
+ if (!input) {
7
+ return undefined
8
+ }
9
+
10
+ const values = input.split(',').map((value) => {
11
+ return Number(value.trim())
12
+ })
13
+ if (values.length !== 6 || values.some((value) => {
14
+ return !Number.isFinite(value)
15
+ })) {
16
+ throw new Error('Option --coord-map must be x,y,width,height,imageWidth,imageHeight')
17
+ }
18
+
19
+ const [captureX, captureY, captureWidth, captureHeight, imageWidth, imageHeight] = values
20
+ if (captureWidth <= 0 || captureHeight <= 0 || imageWidth <= 0 || imageHeight <= 0) {
21
+ throw new Error('Option --coord-map must have positive width and height values')
22
+ }
23
+
24
+ return {
25
+ captureX,
26
+ captureY,
27
+ captureWidth,
28
+ captureHeight,
29
+ imageWidth,
30
+ imageHeight,
31
+ }
32
+ }
33
+
34
+ export function mapPointFromCoordMap({
35
+ point,
36
+ coordMap,
37
+ }: {
38
+ point: Point
39
+ coordMap?: CoordMap
40
+ }): Point {
41
+ if (!coordMap) {
42
+ return point
43
+ }
44
+
45
+ const imageWidthSpan = Math.max(coordMap.imageWidth - 1, 1)
46
+ const imageHeightSpan = Math.max(coordMap.imageHeight - 1, 1)
47
+ const captureWidthSpan = Math.max(coordMap.captureWidth - 1, 0)
48
+ const captureHeightSpan = Math.max(coordMap.captureHeight - 1, 0)
49
+ const maxCaptureX = coordMap.captureX + captureWidthSpan
50
+ const maxCaptureY = coordMap.captureY + captureHeightSpan
51
+ const mappedX = coordMap.captureX + (point.x / imageWidthSpan) * captureWidthSpan
52
+ const mappedY = coordMap.captureY + (point.y / imageHeightSpan) * captureHeightSpan
53
+ const clampedX = Math.max(coordMap.captureX, Math.min(maxCaptureX, mappedX))
54
+ const clampedY = Math.max(coordMap.captureY, Math.min(maxCaptureY, mappedY))
55
+
56
+ return {
57
+ x: Math.round(clampedX),
58
+ y: Math.round(clampedY),
59
+ }
60
+ }
61
+
62
+ export function mapPointToCoordMap({
63
+ point,
64
+ coordMap,
65
+ }: {
66
+ point: Point
67
+ coordMap?: CoordMap
68
+ }): Point {
69
+ if (!coordMap) {
70
+ return point
71
+ }
72
+
73
+ const captureWidthSpan = Math.max(coordMap.captureWidth - 1, 1)
74
+ const captureHeightSpan = Math.max(coordMap.captureHeight - 1, 1)
75
+ const imageWidthSpan = Math.max(coordMap.imageWidth - 1, 0)
76
+ const imageHeightSpan = Math.max(coordMap.imageHeight - 1, 0)
77
+ const relativeX = (point.x - coordMap.captureX) / captureWidthSpan
78
+ const relativeY = (point.y - coordMap.captureY) / captureHeightSpan
79
+ const mappedX = relativeX * imageWidthSpan
80
+ const mappedY = relativeY * imageHeightSpan
81
+ const clampedX = Math.max(0, Math.min(imageWidthSpan, mappedX))
82
+ const clampedY = Math.max(0, Math.min(imageHeightSpan, mappedY))
83
+
84
+ return {
85
+ x: Math.round(clampedX),
86
+ y: Math.round(clampedY),
87
+ }
88
+ }
89
+
90
+ export function getRegionFromCoordMap({
91
+ coordMap,
92
+ }: {
93
+ coordMap?: CoordMap
94
+ }): Region | undefined {
95
+ if (!coordMap) {
96
+ return undefined
97
+ }
98
+
99
+ return {
100
+ x: coordMap.captureX,
101
+ y: coordMap.captureY,
102
+ width: coordMap.captureWidth,
103
+ height: coordMap.captureHeight,
104
+ }
105
+ }
@@ -0,0 +1,50 @@
1
+ // Validates that debug-point image overlays draw a visible red marker.
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { describe, expect, test } from 'vitest'
6
+ import { drawDebugPointOnImage } from './debug-point-image.js'
7
+
8
+ describe('drawDebugPointOnImage', () => {
9
+ test('draws a red marker at the requested point', async () => {
10
+ const sharpModule = await import('sharp')
11
+ const sharp = sharpModule.default
12
+ const filePath = path.join(process.cwd(), 'tmp', 'debug-point-image-test.png')
13
+
14
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
15
+ const baseImage = await sharp({
16
+ create: {
17
+ width: 40,
18
+ height: 30,
19
+ channels: 4,
20
+ background: { r: 255, g: 255, b: 255, alpha: 1 },
21
+ },
22
+ })
23
+ .png()
24
+ .toBuffer()
25
+ fs.writeFileSync(filePath, baseImage)
26
+
27
+ await drawDebugPointOnImage({
28
+ imagePath: filePath,
29
+ point: { x: 20, y: 15 },
30
+ imageWidth: 40,
31
+ imageHeight: 30,
32
+ })
33
+
34
+ const result = await sharp(filePath)
35
+ .raw()
36
+ .toBuffer({ resolveWithObject: true })
37
+ const channels = result.info.channels
38
+ const index = (15 * result.info.width + 20) * channels
39
+ const pixel = Array.from(result.data.slice(index, index + channels))
40
+
41
+ expect(pixel).toMatchInlineSnapshot(`
42
+ [
43
+ 255,
44
+ 45,
45
+ 45,
46
+ 255,
47
+ ]
48
+ `)
49
+ })
50
+ })
@@ -0,0 +1,69 @@
1
+ // Draws visible debug markers onto screenshots to validate coord-map targeting.
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { createRequire } from 'node:module'
6
+ import type { Point } from './types.js'
7
+
8
+ type SharpModule = typeof import('sharp')
9
+ const require = createRequire(import.meta.url)
10
+
11
+ async function loadSharp(): Promise<SharpModule> {
12
+ try {
13
+ return require('sharp') as SharpModule
14
+ } catch (error) {
15
+ throw new Error('Optional dependency `sharp` is required for `debug-point`. Install it with `pnpm add sharp --save-optional`.', {
16
+ cause: error,
17
+ })
18
+ }
19
+ }
20
+
21
+ function createMarkerSvg({
22
+ point,
23
+ imageWidth,
24
+ imageHeight,
25
+ }: {
26
+ point: Point
27
+ imageWidth: number
28
+ imageHeight: number
29
+ }): string {
30
+ const radius = 10
31
+ const crosshairRadius = 22
32
+ const ringRadius = 18
33
+
34
+ return [
35
+ `<svg width="${String(imageWidth)}" height="${String(imageHeight)}" xmlns="http://www.w3.org/2000/svg">`,
36
+ ' <g>',
37
+ ` <circle cx="${String(point.x)}" cy="${String(point.y)}" r="${String(ringRadius)}" fill="none" stroke="white" stroke-width="4" opacity="0.95" />`,
38
+ ` <line x1="${String(point.x - crosshairRadius)}" y1="${String(point.y)}" x2="${String(point.x + crosshairRadius)}" y2="${String(point.y)}" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.95" />`,
39
+ ` <line x1="${String(point.x)}" y1="${String(point.y - crosshairRadius)}" x2="${String(point.x)}" y2="${String(point.y + crosshairRadius)}" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.95" />`,
40
+ ` <circle cx="${String(point.x)}" cy="${String(point.y)}" r="${String(ringRadius)}" fill="none" stroke="#ff2d2d" stroke-width="2" />`,
41
+ ` <line x1="${String(point.x - crosshairRadius)}" y1="${String(point.y)}" x2="${String(point.x + crosshairRadius)}" y2="${String(point.y)}" stroke="#ff2d2d" stroke-width="3" stroke-linecap="round" />`,
42
+ ` <line x1="${String(point.x)}" y1="${String(point.y - crosshairRadius)}" x2="${String(point.x)}" y2="${String(point.y + crosshairRadius)}" stroke="#ff2d2d" stroke-width="3" stroke-linecap="round" />`,
43
+ ` <circle cx="${String(point.x)}" cy="${String(point.y)}" r="${String(radius)}" fill="#ff2d2d" stroke="white" stroke-width="3" />`,
44
+ ' </g>',
45
+ '</svg>',
46
+ ].join('\n')
47
+ }
48
+
49
+ export async function drawDebugPointOnImage({
50
+ imagePath,
51
+ point,
52
+ imageWidth,
53
+ imageHeight,
54
+ }: {
55
+ imagePath: string
56
+ point: Point
57
+ imageWidth: number
58
+ imageHeight: number
59
+ }): Promise<void> {
60
+ const sharpModule = await loadSharp()
61
+ const markerSvg = createMarkerSvg({ point, imageWidth, imageHeight })
62
+ const output = await sharpModule(imagePath)
63
+ .composite([{ input: Buffer.from(markerSvg) }])
64
+ .png()
65
+ .toBuffer()
66
+
67
+ fs.mkdirSync(path.dirname(imagePath), { recursive: true })
68
+ fs.writeFileSync(imagePath, output)
69
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
- // Public API exports for usecomputer parser, bridge, and CLI modules.
1
+ // Public API exports for usecomputer library helpers, parser, bridge, and CLI modules.
2
2
 
3
3
  export { createCli } from './cli.js'
4
4
  export { createBridge, createBridgeFromNative } from './bridge.js'
5
+ export * from './lib.js'
6
+ export * from './coord-map.js'
5
7
  export * from './types.js'
6
8
  export * from './command-parsers.js'
package/src/lib.ts ADDED
@@ -0,0 +1,125 @@
1
+ // Public library helpers that expose the native automation commands as plain functions.
2
+
3
+ import { createBridge } from './bridge.js'
4
+ import type { NativeModule } from './native-lib.js'
5
+ import type {
6
+ DisplayInfo,
7
+ MouseButton,
8
+ Point,
9
+ ScreenshotResult,
10
+ WindowInfo,
11
+ } from './types.js'
12
+
13
+ const bridge = createBridge()
14
+
15
+ export type NativeScreenshotInput = Parameters<NativeModule['screenshot']>[0]
16
+ export type NativeClickInput = Parameters<NativeModule['click']>[0]
17
+ export type NativeTypeTextInput = Parameters<NativeModule['typeText']>[0]
18
+ export type NativePressInput = Parameters<NativeModule['press']>[0]
19
+ export type NativeScrollInput = Parameters<NativeModule['scroll']>[0]
20
+ export type NativeDragInput = Parameters<NativeModule['drag']>[0]
21
+ export type NativeMouseButtonInput = Parameters<NativeModule['mouseDown']>[0]
22
+ export type NativeClipboardSetInput = Parameters<NativeModule['clipboardSet']>[0]
23
+
24
+ export async function screenshot(input: NativeScreenshotInput): Promise<ScreenshotResult> {
25
+ return bridge.screenshot({
26
+ path: input.path ?? undefined,
27
+ display: input.display ?? undefined,
28
+ window: input.window ?? undefined,
29
+ region: input.region ?? undefined,
30
+ annotate: input.annotate ?? undefined,
31
+ })
32
+ }
33
+
34
+ export async function click(input: NativeClickInput): Promise<void> {
35
+ return bridge.click({
36
+ point: input.point,
37
+ button: normalizeMouseButton(input.button),
38
+ count: input.count ?? 1,
39
+ modifiers: [],
40
+ })
41
+ }
42
+
43
+ export async function typeText(input: NativeTypeTextInput): Promise<void> {
44
+ return bridge.typeText({
45
+ text: input.text,
46
+ delayMs: input.delayMs ?? undefined,
47
+ })
48
+ }
49
+
50
+ export async function press(input: NativePressInput): Promise<void> {
51
+ return bridge.press({
52
+ key: input.key,
53
+ count: input.count ?? 1,
54
+ delayMs: input.delayMs ?? undefined,
55
+ })
56
+ }
57
+
58
+ export async function scroll(input: NativeScrollInput): Promise<void> {
59
+ return bridge.scroll({
60
+ direction: normalizeDirection(input.direction),
61
+ amount: input.amount,
62
+ at: input.at ?? undefined,
63
+ })
64
+ }
65
+
66
+ export async function drag(input: NativeDragInput): Promise<void> {
67
+ return bridge.drag({
68
+ from: input.from,
69
+ to: input.to,
70
+ durationMs: input.durationMs ?? undefined,
71
+ button: normalizeMouseButton(input.button),
72
+ })
73
+ }
74
+
75
+ export async function hover(input: Point): Promise<void> {
76
+ return bridge.hover(input)
77
+ }
78
+
79
+ export async function mouseMove(input: Point): Promise<void> {
80
+ return bridge.mouseMove(input)
81
+ }
82
+
83
+ export async function mouseDown(input: NativeMouseButtonInput): Promise<void> {
84
+ return bridge.mouseDown({
85
+ button: normalizeMouseButton(input.button),
86
+ })
87
+ }
88
+
89
+ export async function mouseUp(input: NativeMouseButtonInput): Promise<void> {
90
+ return bridge.mouseUp({
91
+ button: normalizeMouseButton(input.button),
92
+ })
93
+ }
94
+
95
+ export async function mousePosition(): Promise<Point> {
96
+ return bridge.mousePosition()
97
+ }
98
+
99
+ export async function displayList(): Promise<DisplayInfo[]> {
100
+ return bridge.displayList()
101
+ }
102
+
103
+ export async function windowList(): Promise<WindowInfo[]> {
104
+ return bridge.windowList()
105
+ }
106
+
107
+ export async function clipboardGet(): Promise<string> {
108
+ return bridge.clipboardGet()
109
+ }
110
+
111
+ export async function clipboardSet(input: NativeClipboardSetInput): Promise<void> {
112
+ return bridge.clipboardSet(input)
113
+ }
114
+
115
+ function normalizeMouseButton(input: MouseButton | null): MouseButton {
116
+ return input ?? 'left'
117
+ }
118
+
119
+ function normalizeDirection(input: string): 'up' | 'down' | 'left' | 'right' {
120
+ if (input === 'up' || input === 'down' || input === 'left' || input === 'right') {
121
+ return input
122
+ }
123
+
124
+ throw new Error(`Invalid direction "${input}". Expected up, down, left, or right`)
125
+ }