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.
- package/README.md +324 -0
- package/dist/bridge-contract.test.js +124 -63
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +241 -46
- package/dist/cli-parsing.test.js +34 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +323 -28
- package/dist/coord-map.d.ts +14 -0
- package/dist/coord-map.d.ts.map +1 -0
- package/dist/coord-map.js +75 -0
- package/dist/coord-map.test.d.ts +2 -0
- package/dist/coord-map.test.d.ts.map +1 -0
- package/dist/coord-map.test.js +157 -0
- package/dist/darwin-arm64/usecomputer.node +0 -0
- package/dist/darwin-x64/usecomputer.node +0 -0
- package/dist/debug-point-image.d.ts +8 -0
- package/dist/debug-point-image.d.ts.map +1 -0
- package/dist/debug-point-image.js +43 -0
- package/dist/debug-point-image.test.d.ts +2 -0
- package/dist/debug-point-image.test.d.ts.map +1 -0
- package/dist/debug-point-image.test.js +44 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/lib.d.ts +26 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +88 -0
- package/dist/native-click-smoke.test.js +69 -29
- package/dist/native-lib.d.ts +59 -1
- package/dist/native-lib.d.ts.map +1 -1
- package/dist/terminal-table.d.ts +10 -0
- package/dist/terminal-table.d.ts.map +1 -0
- package/dist/terminal-table.js +55 -0
- package/dist/terminal-table.test.d.ts +2 -0
- package/dist/terminal-table.test.d.ts.map +1 -0
- package/dist/terminal-table.test.js +41 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -4
- package/src/bridge-contract.test.ts +140 -69
- package/src/bridge.ts +293 -53
- package/src/cli-parsing.test.ts +61 -0
- package/src/cli.ts +393 -32
- package/src/coord-map.test.ts +178 -0
- package/src/coord-map.ts +105 -0
- package/src/debug-point-image.test.ts +50 -0
- package/src/debug-point-image.ts +69 -0
- package/src/index.ts +3 -1
- package/src/lib.ts +125 -0
- package/src/native-click-smoke.test.ts +81 -63
- package/src/native-lib.ts +39 -1
- package/src/terminal-table.test.ts +44 -0
- package/src/terminal-table.ts +88 -0
- package/src/types.ts +50 -0
- package/zig/src/lib.zig +1258 -267
- package/zig/src/scroll.zig +213 -0
- package/zig/src/window.zig +123 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Validates screenshot coord-map parsing and reverse mapping edge cases.
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { mapPointFromCoordMap, mapPointToCoordMap, parseCoordMapOrThrow } from './coord-map.js';
|
|
4
|
+
describe('coord-map reverse mapping', () => {
|
|
5
|
+
test('maps full-display scaled screenshot coordinates to desktop coordinates', () => {
|
|
6
|
+
const coordMap = parseCoordMapOrThrow('0,0,1600,900,1568,882');
|
|
7
|
+
const mapped = [
|
|
8
|
+
mapPointFromCoordMap({ point: { x: 0, y: 0 }, coordMap }),
|
|
9
|
+
mapPointFromCoordMap({ point: { x: 1567, y: 881 }, coordMap }),
|
|
10
|
+
mapPointFromCoordMap({ point: { x: 784, y: 441 }, coordMap }),
|
|
11
|
+
];
|
|
12
|
+
expect(mapped).toMatchInlineSnapshot(`
|
|
13
|
+
[
|
|
14
|
+
{
|
|
15
|
+
"x": 0,
|
|
16
|
+
"y": 0,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"x": 1599,
|
|
20
|
+
"y": 899,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"x": 800,
|
|
24
|
+
"y": 450,
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
`);
|
|
28
|
+
});
|
|
29
|
+
test('maps correctly when display origin is non-zero', () => {
|
|
30
|
+
const coordMap = parseCoordMapOrThrow('-1728,120,1728,1117,1568,1014');
|
|
31
|
+
const mapped = [
|
|
32
|
+
mapPointFromCoordMap({ point: { x: 0, y: 0 }, coordMap }),
|
|
33
|
+
mapPointFromCoordMap({ point: { x: 1567, y: 1013 }, coordMap }),
|
|
34
|
+
];
|
|
35
|
+
expect(mapped).toMatchInlineSnapshot(`
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
"x": -1728,
|
|
39
|
+
"y": 120,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"x": -1,
|
|
43
|
+
"y": 1236,
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
`);
|
|
47
|
+
});
|
|
48
|
+
test('maps region capture coordinates including display offset', () => {
|
|
49
|
+
const coordMap = parseCoordMapOrThrow('2200,80,640,360,640,360');
|
|
50
|
+
const mapped = [
|
|
51
|
+
mapPointFromCoordMap({ point: { x: 0, y: 0 }, coordMap }),
|
|
52
|
+
mapPointFromCoordMap({ point: { x: 639, y: 359 }, coordMap }),
|
|
53
|
+
mapPointFromCoordMap({ point: { x: 320, y: 180 }, coordMap }),
|
|
54
|
+
];
|
|
55
|
+
expect(mapped).toMatchInlineSnapshot(`
|
|
56
|
+
[
|
|
57
|
+
{
|
|
58
|
+
"x": 2200,
|
|
59
|
+
"y": 80,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"x": 2839,
|
|
63
|
+
"y": 439,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"x": 2520,
|
|
67
|
+
"y": 260,
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
`);
|
|
71
|
+
});
|
|
72
|
+
test('clamps out-of-bounds screenshot coordinates to capture bounds', () => {
|
|
73
|
+
const coordMap = parseCoordMapOrThrow('500,400,300,200,150,100');
|
|
74
|
+
const mapped = [
|
|
75
|
+
mapPointFromCoordMap({ point: { x: -10, y: -20 }, coordMap }),
|
|
76
|
+
mapPointFromCoordMap({ point: { x: 150, y: 100 }, coordMap }),
|
|
77
|
+
mapPointFromCoordMap({ point: { x: 200, y: 1000 }, coordMap }),
|
|
78
|
+
];
|
|
79
|
+
expect(mapped).toMatchInlineSnapshot(`
|
|
80
|
+
[
|
|
81
|
+
{
|
|
82
|
+
"x": 500,
|
|
83
|
+
"y": 400,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"x": 799,
|
|
87
|
+
"y": 599,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"x": 799,
|
|
91
|
+
"y": 599,
|
|
92
|
+
},
|
|
93
|
+
]
|
|
94
|
+
`);
|
|
95
|
+
});
|
|
96
|
+
test('maps desktop coordinates back into screenshot image coordinates', () => {
|
|
97
|
+
const coordMap = parseCoordMapOrThrow('0,0,1720,1440,1568,1313');
|
|
98
|
+
const mapped = [
|
|
99
|
+
mapPointToCoordMap({ point: { x: 0, y: 0 }, coordMap }),
|
|
100
|
+
mapPointToCoordMap({ point: { x: 1719, y: 1439 }, coordMap }),
|
|
101
|
+
mapPointToCoordMap({ point: { x: 230, y: 614 }, coordMap }),
|
|
102
|
+
];
|
|
103
|
+
expect(mapped).toMatchInlineSnapshot(`
|
|
104
|
+
[
|
|
105
|
+
{
|
|
106
|
+
"x": 0,
|
|
107
|
+
"y": 0,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"x": 1567,
|
|
111
|
+
"y": 1312,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"x": 210,
|
|
115
|
+
"y": 560,
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
`);
|
|
119
|
+
});
|
|
120
|
+
test('round-trips screenshot coordinates through desktop space', () => {
|
|
121
|
+
const coordMap = parseCoordMapOrThrow('0,0,1720,1440,1568,1313');
|
|
122
|
+
const roundTrip = [
|
|
123
|
+
{ x: 0, y: 0 },
|
|
124
|
+
{ x: 210, y: 560 },
|
|
125
|
+
{ x: 1567, y: 1312 },
|
|
126
|
+
].map((point) => {
|
|
127
|
+
return mapPointToCoordMap({
|
|
128
|
+
point: mapPointFromCoordMap({ point, coordMap }),
|
|
129
|
+
coordMap,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
expect(roundTrip).toMatchInlineSnapshot(`
|
|
133
|
+
[
|
|
134
|
+
{
|
|
135
|
+
"x": 0,
|
|
136
|
+
"y": 0,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"x": 210,
|
|
140
|
+
"y": 560,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"x": 1567,
|
|
144
|
+
"y": 1312,
|
|
145
|
+
},
|
|
146
|
+
]
|
|
147
|
+
`);
|
|
148
|
+
});
|
|
149
|
+
test('rejects invalid coord-map payloads', () => {
|
|
150
|
+
expect(() => {
|
|
151
|
+
parseCoordMapOrThrow('0,0,10,10,20');
|
|
152
|
+
}).toThrowError('Option --coord-map must be x,y,width,height,imageWidth,imageHeight');
|
|
153
|
+
expect(() => {
|
|
154
|
+
parseCoordMapOrThrow('0,0,0,10,20,20');
|
|
155
|
+
}).toThrowError('Option --coord-map must have positive width and height values');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Point } from './types.js';
|
|
2
|
+
export declare function drawDebugPointOnImage({ imagePath, point, imageWidth, imageHeight, }: {
|
|
3
|
+
imagePath: string;
|
|
4
|
+
point: Point;
|
|
5
|
+
imageWidth: number;
|
|
6
|
+
imageHeight: number;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=debug-point-image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug-point-image.d.ts","sourceRoot":"","sources":["../src/debug-point-image.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AA2CvC,wBAAsB,qBAAqB,CAAC,EAC1C,SAAS,EACT,KAAK,EACL,UAAU,EACV,WAAW,GACZ,EAAE;IACD,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,KAAK,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Draws visible debug markers onto screenshots to validate coord-map targeting.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
async function loadSharp() {
|
|
7
|
+
try {
|
|
8
|
+
return require('sharp');
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
throw new Error('Optional dependency `sharp` is required for `debug-point`. Install it with `pnpm add sharp --save-optional`.', {
|
|
12
|
+
cause: error,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function createMarkerSvg({ point, imageWidth, imageHeight, }) {
|
|
17
|
+
const radius = 10;
|
|
18
|
+
const crosshairRadius = 22;
|
|
19
|
+
const ringRadius = 18;
|
|
20
|
+
return [
|
|
21
|
+
`<svg width="${String(imageWidth)}" height="${String(imageHeight)}" xmlns="http://www.w3.org/2000/svg">`,
|
|
22
|
+
' <g>',
|
|
23
|
+
` <circle cx="${String(point.x)}" cy="${String(point.y)}" r="${String(ringRadius)}" fill="none" stroke="white" stroke-width="4" opacity="0.95" />`,
|
|
24
|
+
` <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" />`,
|
|
25
|
+
` <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" />`,
|
|
26
|
+
` <circle cx="${String(point.x)}" cy="${String(point.y)}" r="${String(ringRadius)}" fill="none" stroke="#ff2d2d" stroke-width="2" />`,
|
|
27
|
+
` <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" />`,
|
|
28
|
+
` <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" />`,
|
|
29
|
+
` <circle cx="${String(point.x)}" cy="${String(point.y)}" r="${String(radius)}" fill="#ff2d2d" stroke="white" stroke-width="3" />`,
|
|
30
|
+
' </g>',
|
|
31
|
+
'</svg>',
|
|
32
|
+
].join('\n');
|
|
33
|
+
}
|
|
34
|
+
export async function drawDebugPointOnImage({ imagePath, point, imageWidth, imageHeight, }) {
|
|
35
|
+
const sharpModule = await loadSharp();
|
|
36
|
+
const markerSvg = createMarkerSvg({ point, imageWidth, imageHeight });
|
|
37
|
+
const output = await sharpModule(imagePath)
|
|
38
|
+
.composite([{ input: Buffer.from(markerSvg) }])
|
|
39
|
+
.png()
|
|
40
|
+
.toBuffer();
|
|
41
|
+
fs.mkdirSync(path.dirname(imagePath), { recursive: true });
|
|
42
|
+
fs.writeFileSync(imagePath, output);
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug-point-image.test.d.ts","sourceRoot":"","sources":["../src/debug-point-image.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Validates that debug-point image overlays draw a visible red marker.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, expect, test } from 'vitest';
|
|
5
|
+
import { drawDebugPointOnImage } from './debug-point-image.js';
|
|
6
|
+
describe('drawDebugPointOnImage', () => {
|
|
7
|
+
test('draws a red marker at the requested point', async () => {
|
|
8
|
+
const sharpModule = await import('sharp');
|
|
9
|
+
const sharp = sharpModule.default;
|
|
10
|
+
const filePath = path.join(process.cwd(), 'tmp', 'debug-point-image-test.png');
|
|
11
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
12
|
+
const baseImage = await sharp({
|
|
13
|
+
create: {
|
|
14
|
+
width: 40,
|
|
15
|
+
height: 30,
|
|
16
|
+
channels: 4,
|
|
17
|
+
background: { r: 255, g: 255, b: 255, alpha: 1 },
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
.png()
|
|
21
|
+
.toBuffer();
|
|
22
|
+
fs.writeFileSync(filePath, baseImage);
|
|
23
|
+
await drawDebugPointOnImage({
|
|
24
|
+
imagePath: filePath,
|
|
25
|
+
point: { x: 20, y: 15 },
|
|
26
|
+
imageWidth: 40,
|
|
27
|
+
imageHeight: 30,
|
|
28
|
+
});
|
|
29
|
+
const result = await sharp(filePath)
|
|
30
|
+
.raw()
|
|
31
|
+
.toBuffer({ resolveWithObject: true });
|
|
32
|
+
const channels = result.info.channels;
|
|
33
|
+
const index = (15 * result.info.width + 20) * channels;
|
|
34
|
+
const pixel = Array.from(result.data.slice(index, index + channels));
|
|
35
|
+
expect(pixel).toMatchInlineSnapshot(`
|
|
36
|
+
[
|
|
37
|
+
255,
|
|
38
|
+
45,
|
|
39
|
+
45,
|
|
40
|
+
255,
|
|
41
|
+
]
|
|
42
|
+
`);
|
|
43
|
+
});
|
|
44
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { createCli } from './cli.js';
|
|
2
2
|
export { createBridge, createBridgeFromNative } from './bridge.js';
|
|
3
|
+
export * from './lib.js';
|
|
4
|
+
export * from './coord-map.js';
|
|
3
5
|
export * from './types.js';
|
|
4
6
|
export * from './command-parsers.js';
|
|
5
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAClE,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAClE,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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
|
export { createCli } from './cli.js';
|
|
3
3
|
export { createBridge, createBridgeFromNative } from './bridge.js';
|
|
4
|
+
export * from './lib.js';
|
|
5
|
+
export * from './coord-map.js';
|
|
4
6
|
export * from './types.js';
|
|
5
7
|
export * from './command-parsers.js';
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NativeModule } from './native-lib.js';
|
|
2
|
+
import type { DisplayInfo, Point, ScreenshotResult, WindowInfo } from './types.js';
|
|
3
|
+
export type NativeScreenshotInput = Parameters<NativeModule['screenshot']>[0];
|
|
4
|
+
export type NativeClickInput = Parameters<NativeModule['click']>[0];
|
|
5
|
+
export type NativeTypeTextInput = Parameters<NativeModule['typeText']>[0];
|
|
6
|
+
export type NativePressInput = Parameters<NativeModule['press']>[0];
|
|
7
|
+
export type NativeScrollInput = Parameters<NativeModule['scroll']>[0];
|
|
8
|
+
export type NativeDragInput = Parameters<NativeModule['drag']>[0];
|
|
9
|
+
export type NativeMouseButtonInput = Parameters<NativeModule['mouseDown']>[0];
|
|
10
|
+
export type NativeClipboardSetInput = Parameters<NativeModule['clipboardSet']>[0];
|
|
11
|
+
export declare function screenshot(input: NativeScreenshotInput): Promise<ScreenshotResult>;
|
|
12
|
+
export declare function click(input: NativeClickInput): Promise<void>;
|
|
13
|
+
export declare function typeText(input: NativeTypeTextInput): Promise<void>;
|
|
14
|
+
export declare function press(input: NativePressInput): Promise<void>;
|
|
15
|
+
export declare function scroll(input: NativeScrollInput): Promise<void>;
|
|
16
|
+
export declare function drag(input: NativeDragInput): Promise<void>;
|
|
17
|
+
export declare function hover(input: Point): Promise<void>;
|
|
18
|
+
export declare function mouseMove(input: Point): Promise<void>;
|
|
19
|
+
export declare function mouseDown(input: NativeMouseButtonInput): Promise<void>;
|
|
20
|
+
export declare function mouseUp(input: NativeMouseButtonInput): Promise<void>;
|
|
21
|
+
export declare function mousePosition(): Promise<Point>;
|
|
22
|
+
export declare function displayList(): Promise<DisplayInfo[]>;
|
|
23
|
+
export declare function windowList(): Promise<WindowInfo[]>;
|
|
24
|
+
export declare function clipboardGet(): Promise<string>;
|
|
25
|
+
export declare function clipboardSet(input: NativeClipboardSetInput): Promise<void>;
|
|
26
|
+
//# sourceMappingURL=lib.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,KAAK,EACV,WAAW,EAEX,KAAK,EACL,gBAAgB,EAChB,UAAU,EACX,MAAM,YAAY,CAAA;AAInB,MAAM,MAAM,qBAAqB,GAAG,UAAU,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7E,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACnE,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACzE,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACnE,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACrE,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACjE,MAAM,MAAM,sBAAsB,GAAG,UAAU,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7E,MAAM,MAAM,uBAAuB,GAAG,UAAU,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAEjF,wBAAsB,UAAU,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAQxF;AAED,wBAAsB,KAAK,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOlE;AAED,wBAAsB,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxE;AAED,wBAAsB,KAAK,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMlE;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMpE;AAED,wBAAsB,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhE;AAED,wBAAsB,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvD;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3D;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAI5E;AAED,wBAAsB,OAAO,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1E;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,KAAK,CAAC,CAEpD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAE1D;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAExD;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAEpD;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhF"}
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Public library helpers that expose the native automation commands as plain functions.
|
|
2
|
+
import { createBridge } from './bridge.js';
|
|
3
|
+
const bridge = createBridge();
|
|
4
|
+
export async function screenshot(input) {
|
|
5
|
+
return bridge.screenshot({
|
|
6
|
+
path: input.path ?? undefined,
|
|
7
|
+
display: input.display ?? undefined,
|
|
8
|
+
window: input.window ?? undefined,
|
|
9
|
+
region: input.region ?? undefined,
|
|
10
|
+
annotate: input.annotate ?? undefined,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export async function click(input) {
|
|
14
|
+
return bridge.click({
|
|
15
|
+
point: input.point,
|
|
16
|
+
button: normalizeMouseButton(input.button),
|
|
17
|
+
count: input.count ?? 1,
|
|
18
|
+
modifiers: [],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export async function typeText(input) {
|
|
22
|
+
return bridge.typeText({
|
|
23
|
+
text: input.text,
|
|
24
|
+
delayMs: input.delayMs ?? undefined,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export async function press(input) {
|
|
28
|
+
return bridge.press({
|
|
29
|
+
key: input.key,
|
|
30
|
+
count: input.count ?? 1,
|
|
31
|
+
delayMs: input.delayMs ?? undefined,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export async function scroll(input) {
|
|
35
|
+
return bridge.scroll({
|
|
36
|
+
direction: normalizeDirection(input.direction),
|
|
37
|
+
amount: input.amount,
|
|
38
|
+
at: input.at ?? undefined,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export async function drag(input) {
|
|
42
|
+
return bridge.drag({
|
|
43
|
+
from: input.from,
|
|
44
|
+
to: input.to,
|
|
45
|
+
durationMs: input.durationMs ?? undefined,
|
|
46
|
+
button: normalizeMouseButton(input.button),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export async function hover(input) {
|
|
50
|
+
return bridge.hover(input);
|
|
51
|
+
}
|
|
52
|
+
export async function mouseMove(input) {
|
|
53
|
+
return bridge.mouseMove(input);
|
|
54
|
+
}
|
|
55
|
+
export async function mouseDown(input) {
|
|
56
|
+
return bridge.mouseDown({
|
|
57
|
+
button: normalizeMouseButton(input.button),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export async function mouseUp(input) {
|
|
61
|
+
return bridge.mouseUp({
|
|
62
|
+
button: normalizeMouseButton(input.button),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export async function mousePosition() {
|
|
66
|
+
return bridge.mousePosition();
|
|
67
|
+
}
|
|
68
|
+
export async function displayList() {
|
|
69
|
+
return bridge.displayList();
|
|
70
|
+
}
|
|
71
|
+
export async function windowList() {
|
|
72
|
+
return bridge.windowList();
|
|
73
|
+
}
|
|
74
|
+
export async function clipboardGet() {
|
|
75
|
+
return bridge.clipboardGet();
|
|
76
|
+
}
|
|
77
|
+
export async function clipboardSet(input) {
|
|
78
|
+
return bridge.clipboardSet(input);
|
|
79
|
+
}
|
|
80
|
+
function normalizeMouseButton(input) {
|
|
81
|
+
return input ?? 'left';
|
|
82
|
+
}
|
|
83
|
+
function normalizeDirection(input) {
|
|
84
|
+
if (input === 'up' || input === 'down' || input === 'left' || input === 'right') {
|
|
85
|
+
return input;
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Invalid direction "${input}". Expected up, down, left, or right`);
|
|
88
|
+
}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
// Optional host smoke test for
|
|
1
|
+
// Optional host smoke test for direct native mouse methods.
|
|
2
2
|
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { native } from './native-lib.js';
|
|
4
5
|
const runNativeSmoke = process.env.USECOMPUTER_NATIVE_SMOKE === '1';
|
|
6
|
+
const displayListSchema = z.array(z.object({
|
|
7
|
+
id: z.number(),
|
|
8
|
+
index: z.number(),
|
|
9
|
+
name: z.string(),
|
|
10
|
+
x: z.number(),
|
|
11
|
+
y: z.number(),
|
|
12
|
+
width: z.number(),
|
|
13
|
+
height: z.number(),
|
|
14
|
+
scale: z.number(),
|
|
15
|
+
isPrimary: z.boolean(),
|
|
16
|
+
}));
|
|
5
17
|
describe('native click smoke', () => {
|
|
6
18
|
const smokeTest = runNativeSmoke ? test : test.skip;
|
|
7
19
|
smokeTest('executes click command without crashing', () => {
|
|
@@ -9,42 +21,37 @@ describe('native click smoke', () => {
|
|
|
9
21
|
if (!native) {
|
|
10
22
|
return;
|
|
11
23
|
}
|
|
12
|
-
const response = native.
|
|
24
|
+
const response = native.click({
|
|
13
25
|
point: { x: 10, y: 10 },
|
|
14
26
|
button: 'left',
|
|
15
27
|
count: 1,
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
expect(parsed).toMatchInlineSnapshot(`
|
|
28
|
+
});
|
|
29
|
+
expect(response).toMatchInlineSnapshot(`
|
|
19
30
|
{
|
|
20
|
-
"
|
|
31
|
+
"error": null,
|
|
21
32
|
"ok": true,
|
|
22
33
|
}
|
|
23
34
|
`);
|
|
24
|
-
expect(
|
|
35
|
+
expect(response.ok).toBe(true);
|
|
25
36
|
});
|
|
26
37
|
smokeTest('executes mouse-move/down/up/position/hover/drag without crashing', () => {
|
|
27
38
|
expect(native).toBeTruthy();
|
|
28
39
|
if (!native) {
|
|
29
40
|
return;
|
|
30
41
|
}
|
|
31
|
-
const moveResponse =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
x: 24,
|
|
40
|
-
y: 24,
|
|
41
|
-
})));
|
|
42
|
-
const dragResponse = JSON.parse(native.execute('drag', JSON.stringify({
|
|
43
|
-
from: { x: 24, y: 24 },
|
|
44
|
-
to: { x: 30, y: 30 },
|
|
42
|
+
const moveResponse = native.mouseMove({ x: 0, y: 0 });
|
|
43
|
+
const downResponse = native.mouseDown({ button: 'left' });
|
|
44
|
+
const upResponse = native.mouseUp({ button: 'left' });
|
|
45
|
+
const positionResponse = native.mousePosition();
|
|
46
|
+
const hoverResponse = native.hover({ x: 0, y: 0 });
|
|
47
|
+
const dragResponse = native.drag({
|
|
48
|
+
from: { x: 0, y: 0 },
|
|
49
|
+
to: { x: 0, y: 0 },
|
|
45
50
|
button: 'left',
|
|
46
51
|
durationMs: 10,
|
|
47
|
-
})
|
|
52
|
+
});
|
|
53
|
+
const typeResponse = native.typeText({ text: 'h', delayMs: 1 });
|
|
54
|
+
const pressResponse = native.press({ key: 'backspace', count: 1, delayMs: 1 });
|
|
48
55
|
expect({
|
|
49
56
|
moveResponse,
|
|
50
57
|
downResponse,
|
|
@@ -52,33 +59,44 @@ describe('native click smoke', () => {
|
|
|
52
59
|
positionResponse,
|
|
53
60
|
hoverResponse,
|
|
54
61
|
dragResponse,
|
|
62
|
+
typeResponse,
|
|
63
|
+
pressResponse,
|
|
55
64
|
}).toMatchInlineSnapshot(`
|
|
56
65
|
{
|
|
57
66
|
"downResponse": {
|
|
58
|
-
"
|
|
67
|
+
"error": null,
|
|
59
68
|
"ok": true,
|
|
60
69
|
},
|
|
61
70
|
"dragResponse": {
|
|
62
|
-
"
|
|
71
|
+
"error": null,
|
|
63
72
|
"ok": true,
|
|
64
73
|
},
|
|
65
74
|
"hoverResponse": {
|
|
66
|
-
"
|
|
75
|
+
"error": null,
|
|
67
76
|
"ok": true,
|
|
68
77
|
},
|
|
69
78
|
"moveResponse": {
|
|
70
|
-
"
|
|
79
|
+
"error": null,
|
|
71
80
|
"ok": true,
|
|
72
81
|
},
|
|
73
82
|
"positionResponse": {
|
|
74
83
|
"data": {
|
|
75
|
-
"x":
|
|
76
|
-
"y":
|
|
84
|
+
"x": 0,
|
|
85
|
+
"y": 0,
|
|
77
86
|
},
|
|
87
|
+
"error": null,
|
|
88
|
+
"ok": true,
|
|
89
|
+
},
|
|
90
|
+
"pressResponse": {
|
|
91
|
+
"error": null,
|
|
92
|
+
"ok": true,
|
|
93
|
+
},
|
|
94
|
+
"typeResponse": {
|
|
95
|
+
"error": null,
|
|
78
96
|
"ok": true,
|
|
79
97
|
},
|
|
80
98
|
"upResponse": {
|
|
81
|
-
"
|
|
99
|
+
"error": null,
|
|
82
100
|
"ok": true,
|
|
83
101
|
},
|
|
84
102
|
}
|
|
@@ -89,5 +107,27 @@ describe('native click smoke', () => {
|
|
|
89
107
|
expect(positionResponse.ok).toBe(true);
|
|
90
108
|
expect(hoverResponse.ok).toBe(true);
|
|
91
109
|
expect(dragResponse.ok).toBe(true);
|
|
110
|
+
expect(typeResponse.ok).toBe(true);
|
|
111
|
+
expect(pressResponse.ok).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
smokeTest('returns display payload for desktop list command', () => {
|
|
114
|
+
expect(native).toBeTruthy();
|
|
115
|
+
if (!native) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const result = native.displayList();
|
|
119
|
+
expect(result.ok).toBe(true);
|
|
120
|
+
if (!result.ok || !result.data) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const parsedJson = JSON.parse(result.data);
|
|
124
|
+
const parsed = displayListSchema.safeParse(parsedJson);
|
|
125
|
+
expect(parsed.success).toBe(true);
|
|
126
|
+
if (!parsed.success) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
expect(parsed.data.length).toBeGreaterThan(0);
|
|
130
|
+
expect(parsed.data[0]?.width).toBeGreaterThan(0);
|
|
131
|
+
expect(parsed.data[0]?.height).toBeGreaterThan(0);
|
|
92
132
|
});
|
|
93
133
|
});
|
package/dist/native-lib.d.ts
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
|
+
import type { MouseButton, NativeCommandResult, NativeDataResult, Point, Region } from './types.js';
|
|
2
|
+
type NativeScreenshotOutput = {
|
|
3
|
+
path: string;
|
|
4
|
+
desktopIndex: number;
|
|
5
|
+
captureX: number;
|
|
6
|
+
captureY: number;
|
|
7
|
+
captureWidth: number;
|
|
8
|
+
captureHeight: number;
|
|
9
|
+
imageWidth: number;
|
|
10
|
+
imageHeight: number;
|
|
11
|
+
};
|
|
1
12
|
export interface NativeModule {
|
|
2
|
-
|
|
13
|
+
screenshot(input: {
|
|
14
|
+
path: string | null;
|
|
15
|
+
display: number | null;
|
|
16
|
+
window: number | null;
|
|
17
|
+
region: Region | null;
|
|
18
|
+
annotate: boolean | null;
|
|
19
|
+
}): NativeDataResult<NativeScreenshotOutput>;
|
|
20
|
+
click(input: {
|
|
21
|
+
point: Point;
|
|
22
|
+
button: MouseButton | null;
|
|
23
|
+
count: number | null;
|
|
24
|
+
}): NativeCommandResult;
|
|
25
|
+
typeText(input: {
|
|
26
|
+
text: string;
|
|
27
|
+
delayMs: number | null;
|
|
28
|
+
}): NativeCommandResult;
|
|
29
|
+
press(input: {
|
|
30
|
+
key: string;
|
|
31
|
+
count: number | null;
|
|
32
|
+
delayMs: number | null;
|
|
33
|
+
}): NativeCommandResult;
|
|
34
|
+
scroll(input: {
|
|
35
|
+
direction: string;
|
|
36
|
+
amount: number;
|
|
37
|
+
at: Point | null;
|
|
38
|
+
}): NativeCommandResult;
|
|
39
|
+
drag(input: {
|
|
40
|
+
from: Point;
|
|
41
|
+
to: Point;
|
|
42
|
+
durationMs: number | null;
|
|
43
|
+
button: MouseButton | null;
|
|
44
|
+
}): NativeCommandResult;
|
|
45
|
+
hover(input: Point): NativeCommandResult;
|
|
46
|
+
mouseMove(input: Point): NativeCommandResult;
|
|
47
|
+
mouseDown(input: {
|
|
48
|
+
button: MouseButton | null;
|
|
49
|
+
}): NativeCommandResult;
|
|
50
|
+
mouseUp(input: {
|
|
51
|
+
button: MouseButton | null;
|
|
52
|
+
}): NativeCommandResult;
|
|
53
|
+
mousePosition(): NativeDataResult<Point>;
|
|
54
|
+
displayList(): NativeDataResult<string>;
|
|
55
|
+
windowList(): NativeDataResult<string>;
|
|
56
|
+
clipboardGet(): NativeDataResult<string>;
|
|
57
|
+
clipboardSet(input: {
|
|
58
|
+
text: string;
|
|
59
|
+
}): NativeCommandResult;
|
|
3
60
|
}
|
|
4
61
|
export declare const native: NativeModule | null;
|
|
62
|
+
export {};
|
|
5
63
|
//# sourceMappingURL=native-lib.d.ts.map
|
package/dist/native-lib.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-lib.d.ts","sourceRoot":"","sources":["../src/native-lib.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"native-lib.d.ts","sourceRoot":"","sources":["../src/native-lib.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,EACL,MAAM,EACP,MAAM,YAAY,CAAA;AAEnB,KAAK,sBAAsB,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAID,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,KAAK,EAAE;QAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;QACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;QACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;QACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;QACrB,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAA;KACzB,GAAG,gBAAgB,CAAC,sBAAsB,CAAC,CAAA;IAC5C,KAAK,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,mBAAmB,CAAA;IACrG,QAAQ,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,mBAAmB,CAAA;IAC9E,KAAK,CAAC,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,mBAAmB,CAAA;IAChG,MAAM,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,KAAK,GAAG,IAAI,CAAA;KAAE,GAAG,mBAAmB,CAAA;IAC3F,IAAI,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,EAAE,EAAE,KAAK,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;KAAE,GAAG,mBAAmB,CAAA;IACnH,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,mBAAmB,CAAA;IACxC,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,mBAAmB,CAAA;IAC5C,SAAS,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;KAAE,GAAG,mBAAmB,CAAA;IACrE,OAAO,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;KAAE,GAAG,mBAAmB,CAAA;IACnE,aAAa,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAA;IACxC,WAAW,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAA;IACvC,UAAU,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAA;IACtC,YAAY,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAA;IACxC,YAAY,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,mBAAmB,CAAA;CAC3D;AA4BD,eAAO,MAAM,MAAM,qBAAqB,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type TableColumn<Row> = {
|
|
2
|
+
header: string;
|
|
3
|
+
align?: 'left' | 'right';
|
|
4
|
+
value: (row: Row) => string;
|
|
5
|
+
};
|
|
6
|
+
export declare function renderAlignedTable<Row>({ rows, columns, }: {
|
|
7
|
+
rows: Row[];
|
|
8
|
+
columns: TableColumn<Row>[];
|
|
9
|
+
}): string[];
|
|
10
|
+
//# sourceMappingURL=terminal-table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-table.d.ts","sourceRoot":"","sources":["../src/terminal-table.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,CAAC,GAAG,IAAI;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACxB,KAAK,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,CAAA;CAC5B,CAAA;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,EACtC,IAAI,EACJ,OAAO,GACR,EAAE;IACD,IAAI,EAAE,GAAG,EAAE,CAAA;IACX,OAAO,EAAE,WAAW,CAAC,GAAG,CAAC,EAAE,CAAA;CAC5B,GAAG,MAAM,EAAE,CAoEX"}
|