usecomputer 0.0.3 → 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 (57) hide show
  1. package/README.md +324 -0
  2. package/dist/bridge-contract.test.js +124 -63
  3. package/dist/bridge.d.ts.map +1 -1
  4. package/dist/bridge.js +241 -46
  5. package/dist/cli-parsing.test.js +34 -11
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +323 -28
  8. package/dist/coord-map.d.ts +14 -0
  9. package/dist/coord-map.d.ts.map +1 -0
  10. package/dist/coord-map.js +75 -0
  11. package/dist/coord-map.test.d.ts +2 -0
  12. package/dist/coord-map.test.d.ts.map +1 -0
  13. package/dist/coord-map.test.js +157 -0
  14. package/dist/darwin-arm64/usecomputer.node +0 -0
  15. package/dist/darwin-x64/usecomputer.node +0 -0
  16. package/dist/debug-point-image.d.ts +8 -0
  17. package/dist/debug-point-image.d.ts.map +1 -0
  18. package/dist/debug-point-image.js +43 -0
  19. package/dist/debug-point-image.test.d.ts +2 -0
  20. package/dist/debug-point-image.test.d.ts.map +1 -0
  21. package/dist/debug-point-image.test.js +44 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +3 -1
  25. package/dist/lib.d.ts +26 -0
  26. package/dist/lib.d.ts.map +1 -0
  27. package/dist/lib.js +88 -0
  28. package/dist/native-click-smoke.test.js +69 -29
  29. package/dist/native-lib.d.ts +59 -1
  30. package/dist/native-lib.d.ts.map +1 -1
  31. package/dist/terminal-table.d.ts +10 -0
  32. package/dist/terminal-table.d.ts.map +1 -0
  33. package/dist/terminal-table.js +55 -0
  34. package/dist/terminal-table.test.d.ts +2 -0
  35. package/dist/terminal-table.test.d.ts.map +1 -0
  36. package/dist/terminal-table.test.js +41 -0
  37. package/dist/types.d.ts +45 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +16 -4
  40. package/src/bridge-contract.test.ts +140 -69
  41. package/src/bridge.ts +293 -53
  42. package/src/cli-parsing.test.ts +61 -0
  43. package/src/cli.ts +393 -32
  44. package/src/coord-map.test.ts +178 -0
  45. package/src/coord-map.ts +105 -0
  46. package/src/debug-point-image.test.ts +50 -0
  47. package/src/debug-point-image.ts +69 -0
  48. package/src/index.ts +3 -1
  49. package/src/lib.ts +125 -0
  50. package/src/native-click-smoke.test.ts +81 -63
  51. package/src/native-lib.ts +39 -1
  52. package/src/terminal-table.test.ts +44 -0
  53. package/src/terminal-table.ts +88 -0
  54. package/src/types.ts +50 -0
  55. package/zig/src/lib.zig +1258 -267
  56. package/zig/src/scroll.zig +213 -0
  57. package/zig/src/window.zig +123 -0
@@ -1,10 +1,25 @@
1
- // Optional host smoke test for the real native click command.
1
+ // Optional host smoke test for direct native mouse methods.
2
2
 
3
3
  import { describe, expect, test } from 'vitest'
4
+ import { z } from 'zod'
4
5
  import { native } from './native-lib.js'
5
6
 
6
7
  const runNativeSmoke = process.env.USECOMPUTER_NATIVE_SMOKE === '1'
7
8
 
9
+ const displayListSchema = z.array(
10
+ z.object({
11
+ id: z.number(),
12
+ index: z.number(),
13
+ name: z.string(),
14
+ x: z.number(),
15
+ y: z.number(),
16
+ width: z.number(),
17
+ height: z.number(),
18
+ scale: z.number(),
19
+ isPrimary: z.boolean(),
20
+ }),
21
+ )
22
+
8
23
  describe('native click smoke', () => {
9
24
  const smokeTest = runNativeSmoke ? test : test.skip
10
25
 
@@ -14,23 +29,19 @@ describe('native click smoke', () => {
14
29
  return
15
30
  }
16
31
 
17
- const response = native.execute(
18
- 'click',
19
- JSON.stringify({
20
- point: { x: 10, y: 10 },
21
- button: 'left',
22
- count: 1,
23
- }),
24
- )
32
+ const response = native.click({
33
+ point: { x: 10, y: 10 },
34
+ button: 'left',
35
+ count: 1,
36
+ })
25
37
 
26
- const parsed = JSON.parse(response) as { ok: boolean; data?: null; error?: string }
27
- expect(parsed).toMatchInlineSnapshot(`
38
+ expect(response).toMatchInlineSnapshot(`
28
39
  {
29
- "data": null,
40
+ "error": null,
30
41
  "ok": true,
31
42
  }
32
43
  `)
33
- expect(typeof parsed.ok).toBe('boolean')
44
+ expect(response.ok).toBe(true)
34
45
  })
35
46
 
36
47
  smokeTest('executes mouse-move/down/up/position/hover/drag without crashing', () => {
@@ -39,49 +50,19 @@ describe('native click smoke', () => {
39
50
  return
40
51
  }
41
52
 
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 }
53
+ const moveResponse = native.mouseMove({ x: 0, y: 0 })
54
+ const downResponse = native.mouseDown({ button: 'left' })
55
+ const upResponse = native.mouseUp({ button: 'left' })
56
+ const positionResponse = native.mousePosition()
57
+ const hoverResponse = native.hover({ x: 0, y: 0 })
58
+ const dragResponse = native.drag({
59
+ from: { x: 0, y: 0 },
60
+ to: { x: 0, y: 0 },
61
+ button: 'left',
62
+ durationMs: 10,
63
+ })
64
+ const typeResponse = native.typeText({ text: 'h', delayMs: 1 })
65
+ const pressResponse = native.press({ key: 'backspace', count: 1, delayMs: 1 })
85
66
 
86
67
  expect({
87
68
  moveResponse,
@@ -90,33 +71,44 @@ describe('native click smoke', () => {
90
71
  positionResponse,
91
72
  hoverResponse,
92
73
  dragResponse,
74
+ typeResponse,
75
+ pressResponse,
93
76
  }).toMatchInlineSnapshot(`
94
77
  {
95
78
  "downResponse": {
96
- "data": null,
79
+ "error": null,
97
80
  "ok": true,
98
81
  },
99
82
  "dragResponse": {
100
- "data": null,
83
+ "error": null,
101
84
  "ok": true,
102
85
  },
103
86
  "hoverResponse": {
104
- "data": null,
87
+ "error": null,
105
88
  "ok": true,
106
89
  },
107
90
  "moveResponse": {
108
- "data": null,
91
+ "error": null,
109
92
  "ok": true,
110
93
  },
111
94
  "positionResponse": {
112
95
  "data": {
113
- "x": 20,
114
- "y": 20,
96
+ "x": 0,
97
+ "y": 0,
115
98
  },
99
+ "error": null,
100
+ "ok": true,
101
+ },
102
+ "pressResponse": {
103
+ "error": null,
104
+ "ok": true,
105
+ },
106
+ "typeResponse": {
107
+ "error": null,
116
108
  "ok": true,
117
109
  },
118
110
  "upResponse": {
119
- "data": null,
111
+ "error": null,
120
112
  "ok": true,
121
113
  },
122
114
  }
@@ -127,5 +119,31 @@ describe('native click smoke', () => {
127
119
  expect(positionResponse.ok).toBe(true)
128
120
  expect(hoverResponse.ok).toBe(true)
129
121
  expect(dragResponse.ok).toBe(true)
122
+ expect(typeResponse.ok).toBe(true)
123
+ expect(pressResponse.ok).toBe(true)
124
+ })
125
+
126
+ smokeTest('returns display payload for desktop list command', () => {
127
+ expect(native).toBeTruthy()
128
+ if (!native) {
129
+ return
130
+ }
131
+
132
+ const result = native.displayList()
133
+ expect(result.ok).toBe(true)
134
+ if (!result.ok || !result.data) {
135
+ return
136
+ }
137
+
138
+ const parsedJson: unknown = JSON.parse(result.data)
139
+ const parsed = displayListSchema.safeParse(parsedJson)
140
+ expect(parsed.success).toBe(true)
141
+ if (!parsed.success) {
142
+ return
143
+ }
144
+
145
+ expect(parsed.data.length).toBeGreaterThan(0)
146
+ expect(parsed.data[0]?.width).toBeGreaterThan(0)
147
+ expect(parsed.data[0]?.height).toBeGreaterThan(0)
130
148
  })
131
149
  })
package/src/native-lib.ts CHANGED
@@ -2,11 +2,49 @@
2
2
 
3
3
  import os from 'node:os'
4
4
  import { createRequire } from 'node:module'
5
+ import type {
6
+ MouseButton,
7
+ NativeCommandResult,
8
+ NativeDataResult,
9
+ Point,
10
+ Region,
11
+ } from './types.js'
12
+
13
+ type NativeScreenshotOutput = {
14
+ path: string
15
+ desktopIndex: number
16
+ captureX: number
17
+ captureY: number
18
+ captureWidth: number
19
+ captureHeight: number
20
+ imageWidth: number
21
+ imageHeight: number
22
+ }
5
23
 
6
24
  const require = createRequire(import.meta.url)
7
25
 
8
26
  export interface NativeModule {
9
- execute(command: string, payloadJson: string): string
27
+ screenshot(input: {
28
+ path: string | null
29
+ display: number | null
30
+ window: number | null
31
+ region: Region | null
32
+ annotate: boolean | null
33
+ }): NativeDataResult<NativeScreenshotOutput>
34
+ click(input: { point: Point; button: MouseButton | null; count: number | null }): NativeCommandResult
35
+ typeText(input: { text: string; delayMs: number | null }): NativeCommandResult
36
+ press(input: { key: string; count: number | null; delayMs: number | null }): NativeCommandResult
37
+ scroll(input: { direction: string; amount: number; at: Point | null }): NativeCommandResult
38
+ drag(input: { from: Point; to: Point; durationMs: number | null; button: MouseButton | null }): NativeCommandResult
39
+ hover(input: Point): NativeCommandResult
40
+ mouseMove(input: Point): NativeCommandResult
41
+ mouseDown(input: { button: MouseButton | null }): NativeCommandResult
42
+ mouseUp(input: { button: MouseButton | null }): NativeCommandResult
43
+ mousePosition(): NativeDataResult<Point>
44
+ displayList(): NativeDataResult<string>
45
+ windowList(): NativeDataResult<string>
46
+ clipboardGet(): NativeDataResult<string>
47
+ clipboardSet(input: { text: string }): NativeCommandResult
10
48
  }
11
49
 
12
50
  function loadCandidate(path: string): NativeModule | null {
@@ -0,0 +1,44 @@
1
+ // Tests aligned terminal table formatting for deterministic CLI rendering.
2
+
3
+ import { describe, expect, test } from 'vitest'
4
+ import { renderAlignedTable } from './terminal-table.js'
5
+
6
+ describe('terminal table', () => {
7
+ test('renders aligned columns for mixed widths', () => {
8
+ const lines = renderAlignedTable({
9
+ rows: [
10
+ { id: 2, app: 'Zed', size: '1720x1440' },
11
+ { id: 102, app: 'Google Chrome', size: '3440x1440' },
12
+ ],
13
+ columns: [
14
+ {
15
+ header: 'id',
16
+ align: 'right',
17
+ value: (row) => {
18
+ return String(row.id)
19
+ },
20
+ },
21
+ {
22
+ header: 'app',
23
+ value: (row) => {
24
+ return row.app
25
+ },
26
+ },
27
+ {
28
+ header: 'size',
29
+ align: 'right',
30
+ value: (row) => {
31
+ return row.size
32
+ },
33
+ },
34
+ ],
35
+ })
36
+
37
+ expect(lines.join('\n')).toMatchInlineSnapshot(`
38
+ " id app size
39
+ --- ------------- ---------
40
+ 2 Zed 1720x1440
41
+ 102 Google Chrome 3440x1440"
42
+ `)
43
+ })
44
+ })
@@ -0,0 +1,88 @@
1
+ // Generic aligned terminal table renderer for CLI command output.
2
+
3
+ export type TableColumn<Row> = {
4
+ header: string
5
+ align?: 'left' | 'right'
6
+ value: (row: Row) => string
7
+ }
8
+
9
+ export function renderAlignedTable<Row>({
10
+ rows,
11
+ columns,
12
+ }: {
13
+ rows: Row[]
14
+ columns: TableColumn<Row>[]
15
+ }): string[] {
16
+ if (columns.length === 0) {
17
+ return []
18
+ }
19
+
20
+ const widthByColumn = columns.map((column) => {
21
+ const rowWidth = rows.reduce((maxWidth, row) => {
22
+ const width = printableWidth(column.value(row))
23
+ return Math.max(maxWidth, width)
24
+ }, 0)
25
+ return Math.max(printableWidth(column.header), rowWidth)
26
+ })
27
+
28
+ const formatCell = ({
29
+ value,
30
+ width,
31
+ align,
32
+ }: {
33
+ value: string
34
+ width: number
35
+ align: 'left' | 'right'
36
+ }): string => {
37
+ const currentWidth = printableWidth(value)
38
+ const padSize = Math.max(0, width - currentWidth)
39
+ const padding = ' '.repeat(padSize)
40
+ if (align === 'right') {
41
+ return `${padding}${value}`
42
+ }
43
+ return `${value}${padding}`
44
+ }
45
+
46
+ const renderRow = ({
47
+ values,
48
+ }: {
49
+ values: string[]
50
+ }): string => {
51
+ return values.map((value, index) => {
52
+ const column = columns[index]
53
+ if (!column) {
54
+ return value
55
+ }
56
+ return formatCell({
57
+ value,
58
+ width: widthByColumn[index] ?? value.length,
59
+ align: column.align ?? 'left',
60
+ })
61
+ }).join(' ')
62
+ }
63
+
64
+ const header = renderRow({
65
+ values: columns.map((column) => {
66
+ return column.header
67
+ }),
68
+ })
69
+
70
+ const divider = widthByColumn.map((width) => {
71
+ return '-'.repeat(width)
72
+ }).join(' ')
73
+
74
+ const lines = rows.map((row) => {
75
+ return renderRow({
76
+ values: columns.map((column) => {
77
+ return column.value(row)
78
+ }),
79
+ })
80
+ })
81
+
82
+ return [header, divider, ...lines]
83
+ }
84
+
85
+ function printableWidth(value: string): number {
86
+ const ansiStripped = value.replace(/\u001b\[[0-9;]*m/g, '')
87
+ return ansiStripped.length
88
+ }
package/src/types.ts CHANGED
@@ -16,8 +16,18 @@ export type Region = {
16
16
  height: number
17
17
  }
18
18
 
19
+ export type CoordMap = {
20
+ captureX: number
21
+ captureY: number
22
+ captureWidth: number
23
+ captureHeight: number
24
+ imageWidth: number
25
+ imageHeight: number
26
+ }
27
+
19
28
  export type DisplayInfo = {
20
29
  id: number
30
+ index: number
21
31
  name: string
22
32
  x: number
23
33
  y: number
@@ -27,15 +37,37 @@ export type DisplayInfo = {
27
37
  isPrimary: boolean
28
38
  }
29
39
 
40
+ export type WindowInfo = {
41
+ id: number
42
+ ownerPid: number
43
+ ownerName: string
44
+ title: string
45
+ x: number
46
+ y: number
47
+ width: number
48
+ height: number
49
+ desktopIndex: number
50
+ }
51
+
30
52
  export type ScreenshotInput = {
31
53
  path?: string
32
54
  display?: number
55
+ window?: number
33
56
  region?: Region
34
57
  annotate?: boolean
35
58
  }
36
59
 
37
60
  export type ScreenshotResult = {
38
61
  path: string
62
+ desktopIndex: number
63
+ captureX: number
64
+ captureY: number
65
+ captureWidth: number
66
+ captureHeight: number
67
+ imageWidth: number
68
+ imageHeight: number
69
+ coordMap: string
70
+ hint: string
39
71
  }
40
72
 
41
73
  export type ClickInput = {
@@ -69,6 +101,23 @@ export type DragInput = {
69
101
  button: MouseButton
70
102
  }
71
103
 
104
+ export type NativeErrorObject = {
105
+ code: string
106
+ message: string
107
+ command: string
108
+ }
109
+
110
+ export type NativeCommandResult = {
111
+ ok: boolean
112
+ error?: NativeErrorObject
113
+ }
114
+
115
+ export type NativeDataResult<T> = {
116
+ ok: boolean
117
+ data?: T
118
+ error?: NativeErrorObject
119
+ }
120
+
72
121
  export interface UseComputerBridge {
73
122
  screenshot(input: ScreenshotInput): Promise<ScreenshotResult>
74
123
  click(input: ClickInput): Promise<void>
@@ -82,6 +131,7 @@ export interface UseComputerBridge {
82
131
  mouseUp(input: { button: MouseButton }): Promise<void>
83
132
  mousePosition(): Promise<Point>
84
133
  displayList(): Promise<DisplayInfo[]>
134
+ windowList(): Promise<WindowInfo[]>
85
135
  clipboardGet(): Promise<string>
86
136
  clipboardSet(input: { text: string }): Promise<void>
87
137
  }