@lizard-build/sdk 0.1.47 → 0.1.49
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 +18 -0
- package/dist/index.d.mts +139 -3
- package/dist/index.d.ts +139 -3
- package/dist/index.js +164 -1
- package/dist/index.mjs +163 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,6 +59,24 @@ const result = await sandbox.process.exec('printf "hello\\n"', {
|
|
|
59
59
|
if (result.exitCode !== 0) throw new Error(result.stderr)
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
## Desktop (computer use)
|
|
63
|
+
|
|
64
|
+
The `desktop` template runs XFCE and Chromium you can watch in a browser and an agent can drive:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { Sandbox } from '@lizard-build/sdk'
|
|
68
|
+
|
|
69
|
+
const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
|
|
70
|
+
const { url, viewOnlyUrl } = await sandbox.desktop.start() // open `url` in a browser
|
|
71
|
+
await sandbox.desktop.openUrl('https://example.com')
|
|
72
|
+
await sandbox.desktop.click(640, 400)
|
|
73
|
+
await sandbox.desktop.type('hello')
|
|
74
|
+
await sandbox.desktop.press('Return')
|
|
75
|
+
const png: Uint8Array = await sandbox.desktop.screenshot()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Treat `url` like a credential: anyone with it can see and control the desktop. Share `viewOnlyUrl` when someone only needs to watch. See the [desktop guide](../../docs/sandboxes.md#desktop-computer-use).
|
|
79
|
+
|
|
62
80
|
## Run Python
|
|
63
81
|
|
|
64
82
|
Run Python with the `interpreter` template. Each process command starts a separate Python process, so Python variables do not survive between calls. Save intermediate results to files. The API returns stdout, stderr, and an exit code; it does not return typed notebook results or chart objects.
|
package/dist/index.d.mts
CHANGED
|
@@ -401,6 +401,8 @@ interface ProcessResult {
|
|
|
401
401
|
stdout: string;
|
|
402
402
|
stderr: string;
|
|
403
403
|
exitCode: number;
|
|
404
|
+
/** Set when the platform cut the output off at its per-stream limit (512 KiB). */
|
|
405
|
+
truncated?: boolean;
|
|
404
406
|
}
|
|
405
407
|
interface ProcessOpts {
|
|
406
408
|
timeoutMs?: number;
|
|
@@ -432,7 +434,9 @@ declare class Process {
|
|
|
432
434
|
* The command runs in a shell inside the Lizard sandbox and returns
|
|
433
435
|
* stdout, stderr, and the exit code when it finishes.
|
|
434
436
|
*
|
|
435
|
-
* @param cmd Shell command to run inside the sandbox
|
|
437
|
+
* @param cmd Shell command to run inside the sandbox, or an argv array that is
|
|
438
|
+
* executed directly with no shell — nothing in it is interpreted, so it is the
|
|
439
|
+
* safe way to pass untrusted text as an argument.
|
|
436
440
|
* @param opts Optional execution options — environment variables, working
|
|
437
441
|
* directory, user, and timeout.
|
|
438
442
|
*
|
|
@@ -442,6 +446,11 @@ declare class Process {
|
|
|
442
446
|
* console.log(result.stdout)
|
|
443
447
|
* ```
|
|
444
448
|
*
|
|
449
|
+
* @example Pass untrusted text as an argument, not through a shell:
|
|
450
|
+
* ```ts
|
|
451
|
+
* await sandbox.process.exec(['grep', '-rn', '--', userQuery, '/workspace'])
|
|
452
|
+
* ```
|
|
453
|
+
*
|
|
445
454
|
* @example Run with a custom working directory and env vars:
|
|
446
455
|
* ```ts
|
|
447
456
|
* const result = await sandbox.process.exec('npm test', {
|
|
@@ -450,7 +459,7 @@ declare class Process {
|
|
|
450
459
|
* })
|
|
451
460
|
* ```
|
|
452
461
|
*/
|
|
453
|
-
exec(cmd: string, opts?: ProcessOpts): Promise<ProcessResult>;
|
|
462
|
+
exec(cmd: string | string[], opts?: ProcessOpts): Promise<ProcessResult>;
|
|
454
463
|
/**
|
|
455
464
|
* Read an SSE exec stream, handing each line to the caller as it arrives while
|
|
456
465
|
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
@@ -642,6 +651,120 @@ declare class Watcher {
|
|
|
642
651
|
close(): Promise<void>;
|
|
643
652
|
}
|
|
644
653
|
|
|
654
|
+
/**
|
|
655
|
+
* State of a sandbox's desktop, and the URLs that stream it.
|
|
656
|
+
*
|
|
657
|
+
* **Both URLs are credentials.** The port is published on a public hostname, and each
|
|
658
|
+
* URL carries the stream token and a VNC password. Anyone holding `url` can see and
|
|
659
|
+
* control the desktop; anyone holding `viewOnlyUrl` can watch it. Do not log them or
|
|
660
|
+
* put them anywhere you would not put an API key.
|
|
661
|
+
*/
|
|
662
|
+
interface DesktopInfo {
|
|
663
|
+
running: boolean;
|
|
664
|
+
/** Screen width in pixels, or `null` when the desktop has never been started. */
|
|
665
|
+
width: number | null;
|
|
666
|
+
height: number | null;
|
|
667
|
+
/** noVNC page with full control (mouse and keyboard). Treat like a credential. */
|
|
668
|
+
url: string;
|
|
669
|
+
/** noVNC page that can only watch; enforced by the VNC server, not the page. */
|
|
670
|
+
viewOnlyUrl: string;
|
|
671
|
+
}
|
|
672
|
+
interface DesktopStartOpts {
|
|
673
|
+
/** Screen width, 640–3840. Pass together with `height`; default 1280x800. */
|
|
674
|
+
width?: number;
|
|
675
|
+
/** Screen height, 480–2160. */
|
|
676
|
+
height?: number;
|
|
677
|
+
}
|
|
678
|
+
type MouseButton = 'left' | 'right' | 'middle';
|
|
679
|
+
type ScrollDirection = 'up' | 'down' | 'left' | 'right';
|
|
680
|
+
interface ClickOpts {
|
|
681
|
+
/** Default `'left'`. */
|
|
682
|
+
button?: MouseButton;
|
|
683
|
+
/** Double-click instead of a single click. */
|
|
684
|
+
double?: boolean;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* A graphical desktop in the sandbox, for computer-use agents and for people to watch.
|
|
688
|
+
*
|
|
689
|
+
* Only the `desktop` template has one (Xvfb + XFCE + Chromium on `DISPLAY=:1`); on
|
|
690
|
+
* any other template the calls fail with `code === 'DESKTOP_NOT_SUPPORTED'`.
|
|
691
|
+
*
|
|
692
|
+
* Input and screenshots run inside the sandbox with `xdotool` and `scrot`. Every
|
|
693
|
+
* argument goes to the sandbox as an argv array and is never parsed by a shell, so
|
|
694
|
+
* text typed on behalf of a model or a user cannot run commands.
|
|
695
|
+
*
|
|
696
|
+
* Access via `sandbox.desktop`.
|
|
697
|
+
*
|
|
698
|
+
* @example
|
|
699
|
+
* ```ts
|
|
700
|
+
* const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
|
|
701
|
+
* const { url } = await sandbox.desktop.start() // open in a browser to watch/drive
|
|
702
|
+
* await sandbox.desktop.openUrl('https://example.com')
|
|
703
|
+
* await sandbox.desktop.click(640, 400)
|
|
704
|
+
* await sandbox.desktop.type('hello')
|
|
705
|
+
* await sandbox.desktop.press('Return')
|
|
706
|
+
* const png = await sandbox.desktop.screenshot()
|
|
707
|
+
* ```
|
|
708
|
+
*/
|
|
709
|
+
declare class Desktop {
|
|
710
|
+
private readonly sandboxId;
|
|
711
|
+
private readonly config;
|
|
712
|
+
private readonly process;
|
|
713
|
+
private readonly fs;
|
|
714
|
+
constructor(sandboxId: string, config: ConnectionConfig, process: Process, fs: Fs);
|
|
715
|
+
private get client();
|
|
716
|
+
private get path();
|
|
717
|
+
/**
|
|
718
|
+
* Start the desktop, publish its stream and return the URLs. Idempotent: calling
|
|
719
|
+
* it on a running desktop returns the same URLs (the size of a running desktop is
|
|
720
|
+
* not changed).
|
|
721
|
+
*/
|
|
722
|
+
start(opts?: DesktopStartOpts): Promise<DesktopInfo>;
|
|
723
|
+
/** The desktop's state and URLs, without starting it. */
|
|
724
|
+
info(): Promise<DesktopInfo>;
|
|
725
|
+
/** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
|
|
726
|
+
stop(): Promise<void>;
|
|
727
|
+
/**
|
|
728
|
+
* A PNG of the whole screen. The desktop must be running.
|
|
729
|
+
*
|
|
730
|
+
* @example
|
|
731
|
+
* ```ts
|
|
732
|
+
* import { writeFileSync } from 'node:fs'
|
|
733
|
+
* writeFileSync('screen.png', await sandbox.desktop.screenshot())
|
|
734
|
+
* ```
|
|
735
|
+
*/
|
|
736
|
+
screenshot(): Promise<Uint8Array>;
|
|
737
|
+
/** Click at (x, y). */
|
|
738
|
+
click(x: number, y: number, opts?: ClickOpts): Promise<void>;
|
|
739
|
+
/** Move the pointer to (x, y) without clicking. */
|
|
740
|
+
moveMouse(x: number, y: number): Promise<void>;
|
|
741
|
+
/** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
|
|
742
|
+
drag(fromX: number, fromY: number, toX: number, toY: number): Promise<void>;
|
|
743
|
+
/**
|
|
744
|
+
* Type text into the focused window, as keystrokes. The text is passed as a
|
|
745
|
+
* single argument after `--`, never through a shell.
|
|
746
|
+
*/
|
|
747
|
+
type(text: string): Promise<void>;
|
|
748
|
+
/**
|
|
749
|
+
* Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
|
|
750
|
+
* `'alt+Tab'`. An array presses them one after another.
|
|
751
|
+
*/
|
|
752
|
+
press(keys: string | string[]): Promise<void>;
|
|
753
|
+
/** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
|
|
754
|
+
scroll(direction: ScrollDirection, amount?: number): Promise<void>;
|
|
755
|
+
/** Where the pointer is now. */
|
|
756
|
+
cursorPosition(): Promise<{
|
|
757
|
+
x: number;
|
|
758
|
+
y: number;
|
|
759
|
+
}>;
|
|
760
|
+
/**
|
|
761
|
+
* Open a URL in a new Chromium window on the desktop. Returns once Chromium has
|
|
762
|
+
* been launched, not when the page has loaded — take a screenshot to see it.
|
|
763
|
+
*/
|
|
764
|
+
openUrl(url: string): Promise<void>;
|
|
765
|
+
private run;
|
|
766
|
+
}
|
|
767
|
+
|
|
645
768
|
interface SandboxInfo {
|
|
646
769
|
sandboxId: string;
|
|
647
770
|
template: string;
|
|
@@ -837,6 +960,19 @@ declare class Sandbox extends SandboxClient {
|
|
|
837
960
|
* ```
|
|
838
961
|
*/
|
|
839
962
|
readonly process: Process;
|
|
963
|
+
/**
|
|
964
|
+
* Drive the graphical desktop of a `desktop`-template sandbox: stream it to a
|
|
965
|
+
* browser, take screenshots, click and type.
|
|
966
|
+
*
|
|
967
|
+
* @example
|
|
968
|
+
* ```ts
|
|
969
|
+
* const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
|
|
970
|
+
* const { url } = await sandbox.desktop.start() // a credential: anyone with it controls the desktop
|
|
971
|
+
* await sandbox.desktop.click(100, 200)
|
|
972
|
+
* const png = await sandbox.desktop.screenshot()
|
|
973
|
+
* ```
|
|
974
|
+
*/
|
|
975
|
+
readonly desktop: Desktop;
|
|
840
976
|
protected readonly connectionConfig: ConnectionConfig;
|
|
841
977
|
constructor(opts: {
|
|
842
978
|
sandboxId: string;
|
|
@@ -1813,4 +1949,4 @@ declare class LizardCLI {
|
|
|
1813
1949
|
}): Promise<CLIResult>;
|
|
1814
1950
|
}
|
|
1815
1951
|
|
|
1816
|
-
export { type Account, type Addon, type AddonType, AddonsAPI, type ApiKey, type ApiKeyScope, ApiKeysAPI, AuthenticationError, type Balance, BillingAPI, type CLIOptions, type CLIResult, type CodeContext, CodeSandbox, ConfigApplyError, type ConfigResult, ConflictError, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateApiKeyOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type CreateWorkspaceOpts, type CreatedApiKey, type DeployEvent, DeployHandle, type DomainInfo, type DomainStatus, DomainsAPI, Execution, ExecutionError, type FileInfo, type FsOpts, GitHubAPI, type ListTransactionsOpts, Lizard, LizardCLI, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, MetricsAPI, NotFoundError, PlatformClient, type ProcessOpts, type ProcessResult, type Project, type ProjectConfig, type ProjectCost, ProjectsAPI, type RawMetrics, type Region, RegionsAPI, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type SandboxSnapshot, type ScaleOpts, type Secret, SecretsAPI, type Service, type ServiceMetrics, type ServiceUpdate, ServicesAPI, type SetSecretOpts, type SnapshotOpts, type SnapshotWaitOpts, StorageAPI, type StreamEvent, TimeoutError, type Transaction, type TransactionPage, Volume, type VolumeInfo, type Workspace, WorkspacesAPI, resolveProjectId };
|
|
1952
|
+
export { type Account, type Addon, type AddonType, AddonsAPI, type ApiKey, type ApiKeyScope, ApiKeysAPI, AuthenticationError, type Balance, BillingAPI, type CLIOptions, type CLIResult, type ClickOpts, type CodeContext, CodeSandbox, ConfigApplyError, type ConfigResult, ConflictError, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateApiKeyOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type CreateWorkspaceOpts, type CreatedApiKey, type DeployEvent, DeployHandle, Desktop, type DesktopInfo, type DesktopStartOpts, type DomainInfo, type DomainStatus, DomainsAPI, Execution, ExecutionError, type FileInfo, type FsOpts, GitHubAPI, type ListTransactionsOpts, Lizard, LizardCLI, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, MetricsAPI, type MouseButton, NotFoundError, PlatformClient, type ProcessOpts, type ProcessResult, type Project, type ProjectConfig, type ProjectCost, ProjectsAPI, type RawMetrics, type Region, RegionsAPI, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type SandboxSnapshot, type ScaleOpts, type ScrollDirection, type Secret, SecretsAPI, type Service, type ServiceMetrics, type ServiceUpdate, ServicesAPI, type SetSecretOpts, type SnapshotOpts, type SnapshotWaitOpts, StorageAPI, type StreamEvent, TimeoutError, type Transaction, type TransactionPage, Volume, type VolumeInfo, type Workspace, WorkspacesAPI, resolveProjectId };
|
package/dist/index.d.ts
CHANGED
|
@@ -401,6 +401,8 @@ interface ProcessResult {
|
|
|
401
401
|
stdout: string;
|
|
402
402
|
stderr: string;
|
|
403
403
|
exitCode: number;
|
|
404
|
+
/** Set when the platform cut the output off at its per-stream limit (512 KiB). */
|
|
405
|
+
truncated?: boolean;
|
|
404
406
|
}
|
|
405
407
|
interface ProcessOpts {
|
|
406
408
|
timeoutMs?: number;
|
|
@@ -432,7 +434,9 @@ declare class Process {
|
|
|
432
434
|
* The command runs in a shell inside the Lizard sandbox and returns
|
|
433
435
|
* stdout, stderr, and the exit code when it finishes.
|
|
434
436
|
*
|
|
435
|
-
* @param cmd Shell command to run inside the sandbox
|
|
437
|
+
* @param cmd Shell command to run inside the sandbox, or an argv array that is
|
|
438
|
+
* executed directly with no shell — nothing in it is interpreted, so it is the
|
|
439
|
+
* safe way to pass untrusted text as an argument.
|
|
436
440
|
* @param opts Optional execution options — environment variables, working
|
|
437
441
|
* directory, user, and timeout.
|
|
438
442
|
*
|
|
@@ -442,6 +446,11 @@ declare class Process {
|
|
|
442
446
|
* console.log(result.stdout)
|
|
443
447
|
* ```
|
|
444
448
|
*
|
|
449
|
+
* @example Pass untrusted text as an argument, not through a shell:
|
|
450
|
+
* ```ts
|
|
451
|
+
* await sandbox.process.exec(['grep', '-rn', '--', userQuery, '/workspace'])
|
|
452
|
+
* ```
|
|
453
|
+
*
|
|
445
454
|
* @example Run with a custom working directory and env vars:
|
|
446
455
|
* ```ts
|
|
447
456
|
* const result = await sandbox.process.exec('npm test', {
|
|
@@ -450,7 +459,7 @@ declare class Process {
|
|
|
450
459
|
* })
|
|
451
460
|
* ```
|
|
452
461
|
*/
|
|
453
|
-
exec(cmd: string, opts?: ProcessOpts): Promise<ProcessResult>;
|
|
462
|
+
exec(cmd: string | string[], opts?: ProcessOpts): Promise<ProcessResult>;
|
|
454
463
|
/**
|
|
455
464
|
* Read an SSE exec stream, handing each line to the caller as it arrives while
|
|
456
465
|
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
@@ -642,6 +651,120 @@ declare class Watcher {
|
|
|
642
651
|
close(): Promise<void>;
|
|
643
652
|
}
|
|
644
653
|
|
|
654
|
+
/**
|
|
655
|
+
* State of a sandbox's desktop, and the URLs that stream it.
|
|
656
|
+
*
|
|
657
|
+
* **Both URLs are credentials.** The port is published on a public hostname, and each
|
|
658
|
+
* URL carries the stream token and a VNC password. Anyone holding `url` can see and
|
|
659
|
+
* control the desktop; anyone holding `viewOnlyUrl` can watch it. Do not log them or
|
|
660
|
+
* put them anywhere you would not put an API key.
|
|
661
|
+
*/
|
|
662
|
+
interface DesktopInfo {
|
|
663
|
+
running: boolean;
|
|
664
|
+
/** Screen width in pixels, or `null` when the desktop has never been started. */
|
|
665
|
+
width: number | null;
|
|
666
|
+
height: number | null;
|
|
667
|
+
/** noVNC page with full control (mouse and keyboard). Treat like a credential. */
|
|
668
|
+
url: string;
|
|
669
|
+
/** noVNC page that can only watch; enforced by the VNC server, not the page. */
|
|
670
|
+
viewOnlyUrl: string;
|
|
671
|
+
}
|
|
672
|
+
interface DesktopStartOpts {
|
|
673
|
+
/** Screen width, 640–3840. Pass together with `height`; default 1280x800. */
|
|
674
|
+
width?: number;
|
|
675
|
+
/** Screen height, 480–2160. */
|
|
676
|
+
height?: number;
|
|
677
|
+
}
|
|
678
|
+
type MouseButton = 'left' | 'right' | 'middle';
|
|
679
|
+
type ScrollDirection = 'up' | 'down' | 'left' | 'right';
|
|
680
|
+
interface ClickOpts {
|
|
681
|
+
/** Default `'left'`. */
|
|
682
|
+
button?: MouseButton;
|
|
683
|
+
/** Double-click instead of a single click. */
|
|
684
|
+
double?: boolean;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* A graphical desktop in the sandbox, for computer-use agents and for people to watch.
|
|
688
|
+
*
|
|
689
|
+
* Only the `desktop` template has one (Xvfb + XFCE + Chromium on `DISPLAY=:1`); on
|
|
690
|
+
* any other template the calls fail with `code === 'DESKTOP_NOT_SUPPORTED'`.
|
|
691
|
+
*
|
|
692
|
+
* Input and screenshots run inside the sandbox with `xdotool` and `scrot`. Every
|
|
693
|
+
* argument goes to the sandbox as an argv array and is never parsed by a shell, so
|
|
694
|
+
* text typed on behalf of a model or a user cannot run commands.
|
|
695
|
+
*
|
|
696
|
+
* Access via `sandbox.desktop`.
|
|
697
|
+
*
|
|
698
|
+
* @example
|
|
699
|
+
* ```ts
|
|
700
|
+
* const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
|
|
701
|
+
* const { url } = await sandbox.desktop.start() // open in a browser to watch/drive
|
|
702
|
+
* await sandbox.desktop.openUrl('https://example.com')
|
|
703
|
+
* await sandbox.desktop.click(640, 400)
|
|
704
|
+
* await sandbox.desktop.type('hello')
|
|
705
|
+
* await sandbox.desktop.press('Return')
|
|
706
|
+
* const png = await sandbox.desktop.screenshot()
|
|
707
|
+
* ```
|
|
708
|
+
*/
|
|
709
|
+
declare class Desktop {
|
|
710
|
+
private readonly sandboxId;
|
|
711
|
+
private readonly config;
|
|
712
|
+
private readonly process;
|
|
713
|
+
private readonly fs;
|
|
714
|
+
constructor(sandboxId: string, config: ConnectionConfig, process: Process, fs: Fs);
|
|
715
|
+
private get client();
|
|
716
|
+
private get path();
|
|
717
|
+
/**
|
|
718
|
+
* Start the desktop, publish its stream and return the URLs. Idempotent: calling
|
|
719
|
+
* it on a running desktop returns the same URLs (the size of a running desktop is
|
|
720
|
+
* not changed).
|
|
721
|
+
*/
|
|
722
|
+
start(opts?: DesktopStartOpts): Promise<DesktopInfo>;
|
|
723
|
+
/** The desktop's state and URLs, without starting it. */
|
|
724
|
+
info(): Promise<DesktopInfo>;
|
|
725
|
+
/** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
|
|
726
|
+
stop(): Promise<void>;
|
|
727
|
+
/**
|
|
728
|
+
* A PNG of the whole screen. The desktop must be running.
|
|
729
|
+
*
|
|
730
|
+
* @example
|
|
731
|
+
* ```ts
|
|
732
|
+
* import { writeFileSync } from 'node:fs'
|
|
733
|
+
* writeFileSync('screen.png', await sandbox.desktop.screenshot())
|
|
734
|
+
* ```
|
|
735
|
+
*/
|
|
736
|
+
screenshot(): Promise<Uint8Array>;
|
|
737
|
+
/** Click at (x, y). */
|
|
738
|
+
click(x: number, y: number, opts?: ClickOpts): Promise<void>;
|
|
739
|
+
/** Move the pointer to (x, y) without clicking. */
|
|
740
|
+
moveMouse(x: number, y: number): Promise<void>;
|
|
741
|
+
/** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
|
|
742
|
+
drag(fromX: number, fromY: number, toX: number, toY: number): Promise<void>;
|
|
743
|
+
/**
|
|
744
|
+
* Type text into the focused window, as keystrokes. The text is passed as a
|
|
745
|
+
* single argument after `--`, never through a shell.
|
|
746
|
+
*/
|
|
747
|
+
type(text: string): Promise<void>;
|
|
748
|
+
/**
|
|
749
|
+
* Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
|
|
750
|
+
* `'alt+Tab'`. An array presses them one after another.
|
|
751
|
+
*/
|
|
752
|
+
press(keys: string | string[]): Promise<void>;
|
|
753
|
+
/** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
|
|
754
|
+
scroll(direction: ScrollDirection, amount?: number): Promise<void>;
|
|
755
|
+
/** Where the pointer is now. */
|
|
756
|
+
cursorPosition(): Promise<{
|
|
757
|
+
x: number;
|
|
758
|
+
y: number;
|
|
759
|
+
}>;
|
|
760
|
+
/**
|
|
761
|
+
* Open a URL in a new Chromium window on the desktop. Returns once Chromium has
|
|
762
|
+
* been launched, not when the page has loaded — take a screenshot to see it.
|
|
763
|
+
*/
|
|
764
|
+
openUrl(url: string): Promise<void>;
|
|
765
|
+
private run;
|
|
766
|
+
}
|
|
767
|
+
|
|
645
768
|
interface SandboxInfo {
|
|
646
769
|
sandboxId: string;
|
|
647
770
|
template: string;
|
|
@@ -837,6 +960,19 @@ declare class Sandbox extends SandboxClient {
|
|
|
837
960
|
* ```
|
|
838
961
|
*/
|
|
839
962
|
readonly process: Process;
|
|
963
|
+
/**
|
|
964
|
+
* Drive the graphical desktop of a `desktop`-template sandbox: stream it to a
|
|
965
|
+
* browser, take screenshots, click and type.
|
|
966
|
+
*
|
|
967
|
+
* @example
|
|
968
|
+
* ```ts
|
|
969
|
+
* const sandbox = await Sandbox.create('desktop', { project: 'my-project' })
|
|
970
|
+
* const { url } = await sandbox.desktop.start() // a credential: anyone with it controls the desktop
|
|
971
|
+
* await sandbox.desktop.click(100, 200)
|
|
972
|
+
* const png = await sandbox.desktop.screenshot()
|
|
973
|
+
* ```
|
|
974
|
+
*/
|
|
975
|
+
readonly desktop: Desktop;
|
|
840
976
|
protected readonly connectionConfig: ConnectionConfig;
|
|
841
977
|
constructor(opts: {
|
|
842
978
|
sandboxId: string;
|
|
@@ -1813,4 +1949,4 @@ declare class LizardCLI {
|
|
|
1813
1949
|
}): Promise<CLIResult>;
|
|
1814
1950
|
}
|
|
1815
1951
|
|
|
1816
|
-
export { type Account, type Addon, type AddonType, AddonsAPI, type ApiKey, type ApiKeyScope, ApiKeysAPI, AuthenticationError, type Balance, BillingAPI, type CLIOptions, type CLIResult, type CodeContext, CodeSandbox, ConfigApplyError, type ConfigResult, ConflictError, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateApiKeyOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type CreateWorkspaceOpts, type CreatedApiKey, type DeployEvent, DeployHandle, type DomainInfo, type DomainStatus, DomainsAPI, Execution, ExecutionError, type FileInfo, type FsOpts, GitHubAPI, type ListTransactionsOpts, Lizard, LizardCLI, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, MetricsAPI, NotFoundError, PlatformClient, type ProcessOpts, type ProcessResult, type Project, type ProjectConfig, type ProjectCost, ProjectsAPI, type RawMetrics, type Region, RegionsAPI, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type SandboxSnapshot, type ScaleOpts, type Secret, SecretsAPI, type Service, type ServiceMetrics, type ServiceUpdate, ServicesAPI, type SetSecretOpts, type SnapshotOpts, type SnapshotWaitOpts, StorageAPI, type StreamEvent, TimeoutError, type Transaction, type TransactionPage, Volume, type VolumeInfo, type Workspace, WorkspacesAPI, resolveProjectId };
|
|
1952
|
+
export { type Account, type Addon, type AddonType, AddonsAPI, type ApiKey, type ApiKeyScope, ApiKeysAPI, AuthenticationError, type Balance, BillingAPI, type CLIOptions, type CLIResult, type ClickOpts, type CodeContext, CodeSandbox, ConfigApplyError, type ConfigResult, ConflictError, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateApiKeyOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type CreateWorkspaceOpts, type CreatedApiKey, type DeployEvent, DeployHandle, Desktop, type DesktopInfo, type DesktopStartOpts, type DomainInfo, type DomainStatus, DomainsAPI, Execution, ExecutionError, type FileInfo, type FsOpts, GitHubAPI, type ListTransactionsOpts, Lizard, LizardCLI, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, MetricsAPI, type MouseButton, NotFoundError, PlatformClient, type ProcessOpts, type ProcessResult, type Project, type ProjectConfig, type ProjectCost, ProjectsAPI, type RawMetrics, type Region, RegionsAPI, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type SandboxSnapshot, type ScaleOpts, type ScrollDirection, type Secret, SecretsAPI, type Service, type ServiceMetrics, type ServiceUpdate, ServicesAPI, type SetSecretOpts, type SnapshotOpts, type SnapshotWaitOpts, StorageAPI, type StreamEvent, TimeoutError, type Transaction, type TransactionPage, Volume, type VolumeInfo, type Workspace, WorkspacesAPI, resolveProjectId };
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
ConfigApplyError: () => ConfigApplyError,
|
|
29
29
|
ConflictError: () => ConflictError,
|
|
30
30
|
DeployHandle: () => DeployHandle,
|
|
31
|
+
Desktop: () => Desktop,
|
|
31
32
|
DomainsAPI: () => DomainsAPI,
|
|
32
33
|
GitHubAPI: () => GitHubAPI,
|
|
33
34
|
Lizard: () => Lizard,
|
|
@@ -512,7 +513,9 @@ var Process = class {
|
|
|
512
513
|
* The command runs in a shell inside the Lizard sandbox and returns
|
|
513
514
|
* stdout, stderr, and the exit code when it finishes.
|
|
514
515
|
*
|
|
515
|
-
* @param cmd Shell command to run inside the sandbox
|
|
516
|
+
* @param cmd Shell command to run inside the sandbox, or an argv array that is
|
|
517
|
+
* executed directly with no shell — nothing in it is interpreted, so it is the
|
|
518
|
+
* safe way to pass untrusted text as an argument.
|
|
516
519
|
* @param opts Optional execution options — environment variables, working
|
|
517
520
|
* directory, user, and timeout.
|
|
518
521
|
*
|
|
@@ -522,6 +525,11 @@ var Process = class {
|
|
|
522
525
|
* console.log(result.stdout)
|
|
523
526
|
* ```
|
|
524
527
|
*
|
|
528
|
+
* @example Pass untrusted text as an argument, not through a shell:
|
|
529
|
+
* ```ts
|
|
530
|
+
* await sandbox.process.exec(['grep', '-rn', '--', userQuery, '/workspace'])
|
|
531
|
+
* ```
|
|
532
|
+
*
|
|
525
533
|
* @example Run with a custom working directory and env vars:
|
|
526
534
|
* ```ts
|
|
527
535
|
* const result = await sandbox.process.exec('npm test', {
|
|
@@ -838,6 +846,159 @@ var Watcher = class {
|
|
|
838
846
|
}
|
|
839
847
|
};
|
|
840
848
|
|
|
849
|
+
// src/sandbox/desktop.ts
|
|
850
|
+
var BUTTONS = { left: "1", middle: "2", right: "3" };
|
|
851
|
+
var SCROLL_BUTTONS = { up: "4", down: "5", left: "6", right: "7" };
|
|
852
|
+
var SCREENSHOT_PATH = /^\/tmp\/lizard-screenshot-[A-Za-z0-9_-]+\.png$/;
|
|
853
|
+
var TYPE_DELAY_MS = 12;
|
|
854
|
+
var Desktop = class {
|
|
855
|
+
constructor(sandboxId, config, process2, fs) {
|
|
856
|
+
this.sandboxId = sandboxId;
|
|
857
|
+
this.config = config;
|
|
858
|
+
this.process = process2;
|
|
859
|
+
this.fs = fs;
|
|
860
|
+
}
|
|
861
|
+
get client() {
|
|
862
|
+
return new PlatformClient(this.config);
|
|
863
|
+
}
|
|
864
|
+
get path() {
|
|
865
|
+
return `/api/sandboxes/${encodeURIComponent(this.sandboxId)}/desktop`;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Start the desktop, publish its stream and return the URLs. Idempotent: calling
|
|
869
|
+
* it on a running desktop returns the same URLs (the size of a running desktop is
|
|
870
|
+
* not changed).
|
|
871
|
+
*/
|
|
872
|
+
start(opts = {}) {
|
|
873
|
+
const body = {};
|
|
874
|
+
if (opts.width !== void 0) body.width = opts.width;
|
|
875
|
+
if (opts.height !== void 0) body.height = opts.height;
|
|
876
|
+
return this.client.post(this.path, body);
|
|
877
|
+
}
|
|
878
|
+
/** The desktop's state and URLs, without starting it. */
|
|
879
|
+
info() {
|
|
880
|
+
return this.client.get(this.path);
|
|
881
|
+
}
|
|
882
|
+
/** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
|
|
883
|
+
async stop() {
|
|
884
|
+
await this.client.delete(this.path);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* A PNG of the whole screen. The desktop must be running.
|
|
888
|
+
*
|
|
889
|
+
* @example
|
|
890
|
+
* ```ts
|
|
891
|
+
* import { writeFileSync } from 'node:fs'
|
|
892
|
+
* writeFileSync('screen.png', await sandbox.desktop.screenshot())
|
|
893
|
+
* ```
|
|
894
|
+
*/
|
|
895
|
+
async screenshot() {
|
|
896
|
+
const r = await this.run(["lizard-desktop", "screenshot", "--path"]);
|
|
897
|
+
const path = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
|
|
898
|
+
if (!SCREENSHOT_PATH.test(path)) {
|
|
899
|
+
throw new LizardError(`Screenshot returned an unexpected path: ${path.slice(0, 200)}`);
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
return await this.fs.readBytes(path);
|
|
903
|
+
} finally {
|
|
904
|
+
await this.fs.remove(path).catch(() => {
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/** Click at (x, y). */
|
|
909
|
+
async click(x, y, opts = {}) {
|
|
910
|
+
const button = BUTTONS[opts.button ?? "left"];
|
|
911
|
+
if (!button) throw new LizardError(`Unknown mouse button: ${String(opts.button)}`);
|
|
912
|
+
const repeat = opts.double ? ["--repeat", "2", "--delay", "100"] : [];
|
|
913
|
+
await this.run(["xdotool", "mousemove", coord(x), coord(y), "click", ...repeat, button]);
|
|
914
|
+
}
|
|
915
|
+
/** Move the pointer to (x, y) without clicking. */
|
|
916
|
+
async moveMouse(x, y) {
|
|
917
|
+
await this.run(["xdotool", "mousemove", coord(x), coord(y)]);
|
|
918
|
+
}
|
|
919
|
+
/** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
|
|
920
|
+
async drag(fromX, fromY, toX, toY) {
|
|
921
|
+
await this.run([
|
|
922
|
+
"xdotool",
|
|
923
|
+
"mousemove",
|
|
924
|
+
coord(fromX),
|
|
925
|
+
coord(fromY),
|
|
926
|
+
"mousedown",
|
|
927
|
+
"1",
|
|
928
|
+
"sleep",
|
|
929
|
+
"0.1",
|
|
930
|
+
"mousemove",
|
|
931
|
+
coord(toX),
|
|
932
|
+
coord(toY),
|
|
933
|
+
"sleep",
|
|
934
|
+
"0.1",
|
|
935
|
+
"mouseup",
|
|
936
|
+
"1"
|
|
937
|
+
]);
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Type text into the focused window, as keystrokes. The text is passed as a
|
|
941
|
+
* single argument after `--`, never through a shell.
|
|
942
|
+
*/
|
|
943
|
+
async type(text) {
|
|
944
|
+
if (!text) return;
|
|
945
|
+
const timeoutMs = Math.min(6e5, Math.max(6e4, text.length * TYPE_DELAY_MS * 3));
|
|
946
|
+
await this.run(["xdotool", "type", "--delay", String(TYPE_DELAY_MS), "--", text], timeoutMs);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
|
|
950
|
+
* `'alt+Tab'`. An array presses them one after another.
|
|
951
|
+
*/
|
|
952
|
+
async press(keys) {
|
|
953
|
+
const list = Array.isArray(keys) ? keys : [keys];
|
|
954
|
+
if (list.length === 0 || list.some((k) => typeof k !== "string" || !k)) {
|
|
955
|
+
throw new LizardError("press() needs at least one non-empty key");
|
|
956
|
+
}
|
|
957
|
+
await this.run(["xdotool", "key", "--", ...list]);
|
|
958
|
+
}
|
|
959
|
+
/** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
|
|
960
|
+
async scroll(direction, amount = 3) {
|
|
961
|
+
const button = SCROLL_BUTTONS[direction];
|
|
962
|
+
if (!button) throw new LizardError(`Unknown scroll direction: ${String(direction)}`);
|
|
963
|
+
if (!Number.isInteger(amount) || amount < 1) throw new LizardError("scroll amount must be a positive integer");
|
|
964
|
+
await this.run(["xdotool", "click", "--repeat", String(amount), "--delay", "30", button]);
|
|
965
|
+
}
|
|
966
|
+
/** Where the pointer is now. */
|
|
967
|
+
async cursorPosition() {
|
|
968
|
+
const r = await this.run(["xdotool", "getmouselocation", "--shell"]);
|
|
969
|
+
const x = /^X=(\d+)$/m.exec(r.stdout);
|
|
970
|
+
const y = /^Y=(\d+)$/m.exec(r.stdout);
|
|
971
|
+
if (!x || !y) throw new LizardError(`Unexpected xdotool output: ${r.stdout.trim()}`);
|
|
972
|
+
return { x: Number(x[1]), y: Number(y[1]) };
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Open a URL in a new Chromium window on the desktop. Returns once Chromium has
|
|
976
|
+
* been launched, not when the page has loaded — take a screenshot to see it.
|
|
977
|
+
*/
|
|
978
|
+
async openUrl(url) {
|
|
979
|
+
await this.run([
|
|
980
|
+
"sh",
|
|
981
|
+
"-c",
|
|
982
|
+
'setsid chromium --new-window "$1" >/dev/null 2>&1 < /dev/null &',
|
|
983
|
+
"sh",
|
|
984
|
+
url
|
|
985
|
+
]);
|
|
986
|
+
}
|
|
987
|
+
async run(argv, timeoutMs) {
|
|
988
|
+
const r = await this.process.exec(argv, timeoutMs ? { timeoutMs } : void 0);
|
|
989
|
+
if (r.exitCode !== 0) {
|
|
990
|
+
const err = new LizardError(`${argv[0]} ${argv[1] ?? ""} failed (exit ${r.exitCode}): ${(r.stderr || r.stdout).trim()}`);
|
|
991
|
+
if (r.exitCode === 127) err.code = "DESKTOP_NOT_SUPPORTED";
|
|
992
|
+
throw err;
|
|
993
|
+
}
|
|
994
|
+
return r;
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
function coord(n) {
|
|
998
|
+
if (!Number.isFinite(n) || n < 0) throw new LizardError(`Invalid screen coordinate: ${n}`);
|
|
999
|
+
return String(Math.round(n));
|
|
1000
|
+
}
|
|
1001
|
+
|
|
841
1002
|
// src/sandbox/client.ts
|
|
842
1003
|
var SandboxClient = class {
|
|
843
1004
|
static async createSandbox(template, timeoutMs, opts) {
|
|
@@ -939,6 +1100,7 @@ var Sandbox = class extends SandboxClient {
|
|
|
939
1100
|
this.connectionConfig = new ConnectionConfig(opts);
|
|
940
1101
|
this.fs = new Fs(this.sandboxId, this.connectionConfig);
|
|
941
1102
|
this.process = new Process(this.sandboxId, this.connectionConfig);
|
|
1103
|
+
this.desktop = new Desktop(this.sandboxId, this.connectionConfig, this.process, this.fs);
|
|
942
1104
|
}
|
|
943
1105
|
static async create(templateOrOpts, opts) {
|
|
944
1106
|
const template = typeof templateOrOpts === "string" ? templateOrOpts : templateOrOpts?.template ?? this.defaultTemplate;
|
|
@@ -1988,6 +2150,7 @@ async function* readLines(body) {
|
|
|
1988
2150
|
ConfigApplyError,
|
|
1989
2151
|
ConflictError,
|
|
1990
2152
|
DeployHandle,
|
|
2153
|
+
Desktop,
|
|
1991
2154
|
DomainsAPI,
|
|
1992
2155
|
GitHubAPI,
|
|
1993
2156
|
Lizard,
|
package/dist/index.mjs
CHANGED
|
@@ -461,7 +461,9 @@ var Process = class {
|
|
|
461
461
|
* The command runs in a shell inside the Lizard sandbox and returns
|
|
462
462
|
* stdout, stderr, and the exit code when it finishes.
|
|
463
463
|
*
|
|
464
|
-
* @param cmd Shell command to run inside the sandbox
|
|
464
|
+
* @param cmd Shell command to run inside the sandbox, or an argv array that is
|
|
465
|
+
* executed directly with no shell — nothing in it is interpreted, so it is the
|
|
466
|
+
* safe way to pass untrusted text as an argument.
|
|
465
467
|
* @param opts Optional execution options — environment variables, working
|
|
466
468
|
* directory, user, and timeout.
|
|
467
469
|
*
|
|
@@ -471,6 +473,11 @@ var Process = class {
|
|
|
471
473
|
* console.log(result.stdout)
|
|
472
474
|
* ```
|
|
473
475
|
*
|
|
476
|
+
* @example Pass untrusted text as an argument, not through a shell:
|
|
477
|
+
* ```ts
|
|
478
|
+
* await sandbox.process.exec(['grep', '-rn', '--', userQuery, '/workspace'])
|
|
479
|
+
* ```
|
|
480
|
+
*
|
|
474
481
|
* @example Run with a custom working directory and env vars:
|
|
475
482
|
* ```ts
|
|
476
483
|
* const result = await sandbox.process.exec('npm test', {
|
|
@@ -787,6 +794,159 @@ var Watcher = class {
|
|
|
787
794
|
}
|
|
788
795
|
};
|
|
789
796
|
|
|
797
|
+
// src/sandbox/desktop.ts
|
|
798
|
+
var BUTTONS = { left: "1", middle: "2", right: "3" };
|
|
799
|
+
var SCROLL_BUTTONS = { up: "4", down: "5", left: "6", right: "7" };
|
|
800
|
+
var SCREENSHOT_PATH = /^\/tmp\/lizard-screenshot-[A-Za-z0-9_-]+\.png$/;
|
|
801
|
+
var TYPE_DELAY_MS = 12;
|
|
802
|
+
var Desktop = class {
|
|
803
|
+
constructor(sandboxId, config, process2, fs) {
|
|
804
|
+
this.sandboxId = sandboxId;
|
|
805
|
+
this.config = config;
|
|
806
|
+
this.process = process2;
|
|
807
|
+
this.fs = fs;
|
|
808
|
+
}
|
|
809
|
+
get client() {
|
|
810
|
+
return new PlatformClient(this.config);
|
|
811
|
+
}
|
|
812
|
+
get path() {
|
|
813
|
+
return `/api/sandboxes/${encodeURIComponent(this.sandboxId)}/desktop`;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Start the desktop, publish its stream and return the URLs. Idempotent: calling
|
|
817
|
+
* it on a running desktop returns the same URLs (the size of a running desktop is
|
|
818
|
+
* not changed).
|
|
819
|
+
*/
|
|
820
|
+
start(opts = {}) {
|
|
821
|
+
const body = {};
|
|
822
|
+
if (opts.width !== void 0) body.width = opts.width;
|
|
823
|
+
if (opts.height !== void 0) body.height = opts.height;
|
|
824
|
+
return this.client.post(this.path, body);
|
|
825
|
+
}
|
|
826
|
+
/** The desktop's state and URLs, without starting it. */
|
|
827
|
+
info() {
|
|
828
|
+
return this.client.get(this.path);
|
|
829
|
+
}
|
|
830
|
+
/** Stop the desktop and unpublish its stream. Its URLs stay the same for the next start. */
|
|
831
|
+
async stop() {
|
|
832
|
+
await this.client.delete(this.path);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* A PNG of the whole screen. The desktop must be running.
|
|
836
|
+
*
|
|
837
|
+
* @example
|
|
838
|
+
* ```ts
|
|
839
|
+
* import { writeFileSync } from 'node:fs'
|
|
840
|
+
* writeFileSync('screen.png', await sandbox.desktop.screenshot())
|
|
841
|
+
* ```
|
|
842
|
+
*/
|
|
843
|
+
async screenshot() {
|
|
844
|
+
const r = await this.run(["lizard-desktop", "screenshot", "--path"]);
|
|
845
|
+
const path = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
|
|
846
|
+
if (!SCREENSHOT_PATH.test(path)) {
|
|
847
|
+
throw new LizardError(`Screenshot returned an unexpected path: ${path.slice(0, 200)}`);
|
|
848
|
+
}
|
|
849
|
+
try {
|
|
850
|
+
return await this.fs.readBytes(path);
|
|
851
|
+
} finally {
|
|
852
|
+
await this.fs.remove(path).catch(() => {
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/** Click at (x, y). */
|
|
857
|
+
async click(x, y, opts = {}) {
|
|
858
|
+
const button = BUTTONS[opts.button ?? "left"];
|
|
859
|
+
if (!button) throw new LizardError(`Unknown mouse button: ${String(opts.button)}`);
|
|
860
|
+
const repeat = opts.double ? ["--repeat", "2", "--delay", "100"] : [];
|
|
861
|
+
await this.run(["xdotool", "mousemove", coord(x), coord(y), "click", ...repeat, button]);
|
|
862
|
+
}
|
|
863
|
+
/** Move the pointer to (x, y) without clicking. */
|
|
864
|
+
async moveMouse(x, y) {
|
|
865
|
+
await this.run(["xdotool", "mousemove", coord(x), coord(y)]);
|
|
866
|
+
}
|
|
867
|
+
/** Press the left button at (fromX, fromY), move to (toX, toY) and release. */
|
|
868
|
+
async drag(fromX, fromY, toX, toY) {
|
|
869
|
+
await this.run([
|
|
870
|
+
"xdotool",
|
|
871
|
+
"mousemove",
|
|
872
|
+
coord(fromX),
|
|
873
|
+
coord(fromY),
|
|
874
|
+
"mousedown",
|
|
875
|
+
"1",
|
|
876
|
+
"sleep",
|
|
877
|
+
"0.1",
|
|
878
|
+
"mousemove",
|
|
879
|
+
coord(toX),
|
|
880
|
+
coord(toY),
|
|
881
|
+
"sleep",
|
|
882
|
+
"0.1",
|
|
883
|
+
"mouseup",
|
|
884
|
+
"1"
|
|
885
|
+
]);
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Type text into the focused window, as keystrokes. The text is passed as a
|
|
889
|
+
* single argument after `--`, never through a shell.
|
|
890
|
+
*/
|
|
891
|
+
async type(text) {
|
|
892
|
+
if (!text) return;
|
|
893
|
+
const timeoutMs = Math.min(6e5, Math.max(6e4, text.length * TYPE_DELAY_MS * 3));
|
|
894
|
+
await this.run(["xdotool", "type", "--delay", String(TYPE_DELAY_MS), "--", text], timeoutMs);
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Press a key or a chord, in xdotool keysym syntax: `'Return'`, `'ctrl+l'`,
|
|
898
|
+
* `'alt+Tab'`. An array presses them one after another.
|
|
899
|
+
*/
|
|
900
|
+
async press(keys) {
|
|
901
|
+
const list = Array.isArray(keys) ? keys : [keys];
|
|
902
|
+
if (list.length === 0 || list.some((k) => typeof k !== "string" || !k)) {
|
|
903
|
+
throw new LizardError("press() needs at least one non-empty key");
|
|
904
|
+
}
|
|
905
|
+
await this.run(["xdotool", "key", "--", ...list]);
|
|
906
|
+
}
|
|
907
|
+
/** Scroll with the mouse wheel at the pointer's current position. `amount` is wheel clicks. */
|
|
908
|
+
async scroll(direction, amount = 3) {
|
|
909
|
+
const button = SCROLL_BUTTONS[direction];
|
|
910
|
+
if (!button) throw new LizardError(`Unknown scroll direction: ${String(direction)}`);
|
|
911
|
+
if (!Number.isInteger(amount) || amount < 1) throw new LizardError("scroll amount must be a positive integer");
|
|
912
|
+
await this.run(["xdotool", "click", "--repeat", String(amount), "--delay", "30", button]);
|
|
913
|
+
}
|
|
914
|
+
/** Where the pointer is now. */
|
|
915
|
+
async cursorPosition() {
|
|
916
|
+
const r = await this.run(["xdotool", "getmouselocation", "--shell"]);
|
|
917
|
+
const x = /^X=(\d+)$/m.exec(r.stdout);
|
|
918
|
+
const y = /^Y=(\d+)$/m.exec(r.stdout);
|
|
919
|
+
if (!x || !y) throw new LizardError(`Unexpected xdotool output: ${r.stdout.trim()}`);
|
|
920
|
+
return { x: Number(x[1]), y: Number(y[1]) };
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Open a URL in a new Chromium window on the desktop. Returns once Chromium has
|
|
924
|
+
* been launched, not when the page has loaded — take a screenshot to see it.
|
|
925
|
+
*/
|
|
926
|
+
async openUrl(url) {
|
|
927
|
+
await this.run([
|
|
928
|
+
"sh",
|
|
929
|
+
"-c",
|
|
930
|
+
'setsid chromium --new-window "$1" >/dev/null 2>&1 < /dev/null &',
|
|
931
|
+
"sh",
|
|
932
|
+
url
|
|
933
|
+
]);
|
|
934
|
+
}
|
|
935
|
+
async run(argv, timeoutMs) {
|
|
936
|
+
const r = await this.process.exec(argv, timeoutMs ? { timeoutMs } : void 0);
|
|
937
|
+
if (r.exitCode !== 0) {
|
|
938
|
+
const err = new LizardError(`${argv[0]} ${argv[1] ?? ""} failed (exit ${r.exitCode}): ${(r.stderr || r.stdout).trim()}`);
|
|
939
|
+
if (r.exitCode === 127) err.code = "DESKTOP_NOT_SUPPORTED";
|
|
940
|
+
throw err;
|
|
941
|
+
}
|
|
942
|
+
return r;
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
function coord(n) {
|
|
946
|
+
if (!Number.isFinite(n) || n < 0) throw new LizardError(`Invalid screen coordinate: ${n}`);
|
|
947
|
+
return String(Math.round(n));
|
|
948
|
+
}
|
|
949
|
+
|
|
790
950
|
// src/sandbox/client.ts
|
|
791
951
|
var SandboxClient = class {
|
|
792
952
|
static async createSandbox(template, timeoutMs, opts) {
|
|
@@ -888,6 +1048,7 @@ var Sandbox = class extends SandboxClient {
|
|
|
888
1048
|
this.connectionConfig = new ConnectionConfig(opts);
|
|
889
1049
|
this.fs = new Fs(this.sandboxId, this.connectionConfig);
|
|
890
1050
|
this.process = new Process(this.sandboxId, this.connectionConfig);
|
|
1051
|
+
this.desktop = new Desktop(this.sandboxId, this.connectionConfig, this.process, this.fs);
|
|
891
1052
|
}
|
|
892
1053
|
static async create(templateOrOpts, opts) {
|
|
893
1054
|
const template = typeof templateOrOpts === "string" ? templateOrOpts : templateOrOpts?.template ?? this.defaultTemplate;
|
|
@@ -1936,6 +2097,7 @@ export {
|
|
|
1936
2097
|
ConfigApplyError,
|
|
1937
2098
|
ConflictError,
|
|
1938
2099
|
DeployHandle,
|
|
2100
|
+
Desktop,
|
|
1939
2101
|
DomainsAPI,
|
|
1940
2102
|
GitHubAPI,
|
|
1941
2103
|
Lizard,
|
package/package.json
CHANGED