shell-use 0.0.1__tar.gz → 0.0.1b2__tar.gz
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.
- shell_use-0.0.1b2/.gitignore +6 -0
- shell_use-0.0.1b2/PKG-INFO +91 -0
- shell_use-0.0.1b2/README.md +76 -0
- shell_use-0.0.1b2/pyproject.toml +29 -0
- shell_use-0.0.1b2/src/shell_use/__init__.py +40 -0
- shell_use-0.0.1b2/src/shell_use/_config.py +55 -0
- shell_use-0.0.1b2/src/shell_use/_protocol.py +20 -0
- shell_use-0.0.1b2/src/shell_use/_transport.py +104 -0
- shell_use-0.0.1b2/src/shell_use/client.py +378 -0
- shell_use-0.0.1b2/src/shell_use/errors.py +56 -0
- shell_use-0.0.1b2/src/shell_use/types.py +48 -0
- shell_use-0.0.1/.gitignore +0 -4
- shell_use-0.0.1/LICENSE +0 -21
- shell_use-0.0.1/PKG-INFO +0 -25
- shell_use-0.0.1/README.md +0 -5
- shell_use-0.0.1/build.log +0 -5
- shell_use-0.0.1/pyproject.toml +0 -30
- shell_use-0.0.1/src/shell_use/__init__.py +0 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shell-use
|
|
3
|
+
Version: 0.0.1b2
|
|
4
|
+
Summary: Python client for the shell-use terminal daemon
|
|
5
|
+
Project-URL: Homepage, https://github.com/microsoft/shell-use
|
|
6
|
+
Project-URL: Repository, https://github.com/microsoft/shell-use
|
|
7
|
+
Author: Microsoft
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: automation,pty,shell-use,terminal,testing,tui
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# shell-use (Python)
|
|
17
|
+
|
|
18
|
+
A Python client for the [`shell-use`](https://github.com/microsoft/shell-use) terminal daemon.
|
|
19
|
+
|
|
20
|
+
The `shell-use` binary must be on your `PATH` (or point to it with the `SHELL_USE_BIN` environment variable or the `binary=` argument). The client talks to the per-session daemon directly over its local socket (a named pipe on Windows, a Unix socket elsewhere) and starts the daemon automatically.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pip install shell-use
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Requires Python 3.8+.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import asyncio
|
|
34
|
+
from shell_use import ShellUse
|
|
35
|
+
|
|
36
|
+
async def main():
|
|
37
|
+
async with ShellUse() as su:
|
|
38
|
+
await su.open()
|
|
39
|
+
await su.submit("echo hello")
|
|
40
|
+
await su.wait_command()
|
|
41
|
+
await su.expect_text("hello")
|
|
42
|
+
await su.expect_exit_code(0)
|
|
43
|
+
|
|
44
|
+
asyncio.run(main())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Drive a full-screen TUI:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
async with ShellUse("vim-session") as su:
|
|
51
|
+
await su.run("vim", "file.txt")
|
|
52
|
+
await su.wait_idle()
|
|
53
|
+
await su.press("i")
|
|
54
|
+
await su.type("some text")
|
|
55
|
+
await su.press("Escape", ":", "w", "q", "Enter")
|
|
56
|
+
await su.wait_exit()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Errors
|
|
60
|
+
|
|
61
|
+
Every failure maps to one of the daemon's exit codes:
|
|
62
|
+
|
|
63
|
+
| Exception | Exit code | Meaning |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `ExpectationError` | 1 | an `expect`/`wait` condition was not met |
|
|
66
|
+
| `UsageError` | 2 | invalid argument (e.g. a bad regex) |
|
|
67
|
+
| `NoSessionError` | 3 | no active session |
|
|
68
|
+
| `DaemonError` | 4 | daemon could not be reached or started |
|
|
69
|
+
| `VersionMismatchError` | 4 | the daemon's version differs from this package |
|
|
70
|
+
| `InternalError` | 5 | internal daemon error |
|
|
71
|
+
|
|
72
|
+
All derive from `ShellUseError`. `wait_*` and `expect_*` raise `ExpectationError` on failure.
|
|
73
|
+
|
|
74
|
+
On its first call, a client checks that the running daemon's version matches the
|
|
75
|
+
package version and raises `VersionMismatchError` if they differ. Stop the daemon
|
|
76
|
+
(`daemon_stop`) so it restarts with the current binary, or point `SHELL_USE_BIN`
|
|
77
|
+
at a matching one.
|
|
78
|
+
|
|
79
|
+
## API
|
|
80
|
+
|
|
81
|
+
`ShellUse(session="default", *, binary=None, home=None)` mirrors the CLI: `open` / `run`, `type` / `write`, `submit`, `press` / `keys`, `mouse.click|move|down|up|drag|scroll`, `resize`, `signal` / `kill`, `state`, `text`, `cells`, `get` (+ `get_command` / `get_output` / `get_exit_code` / `get_cwd` / `get_cursor` / `get_size`), `screenshot`, `wait_text` / `wait_idle` / `wait_command` / `wait_exit`, `expect_text` / `expect_exit_code` / `expect_output` / `expect_snapshot`, and `close`.
|
|
82
|
+
|
|
83
|
+
Module-level helpers: `sessions()`, `close_all()`, `daemon_status()`, `daemon_stop()`, `get_recording()`.
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
| Variable | Purpose |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `SHELL_USE_BIN` | path to the `shell-use` binary |
|
|
90
|
+
| `SHELL_USE_SESSION` | default session name |
|
|
91
|
+
| `SHELL_USE_HOME` | daemon state directory (sockets, pids) |
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# shell-use (Python)
|
|
2
|
+
|
|
3
|
+
A Python client for the [`shell-use`](https://github.com/microsoft/shell-use) terminal daemon.
|
|
4
|
+
|
|
5
|
+
The `shell-use` binary must be on your `PATH` (or point to it with the `SHELL_USE_BIN` environment variable or the `binary=` argument). The client talks to the per-session daemon directly over its local socket (a named pipe on Windows, a Unix socket elsewhere) and starts the daemon automatically.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install shell-use
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Python 3.8+.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import asyncio
|
|
19
|
+
from shell_use import ShellUse
|
|
20
|
+
|
|
21
|
+
async def main():
|
|
22
|
+
async with ShellUse() as su:
|
|
23
|
+
await su.open()
|
|
24
|
+
await su.submit("echo hello")
|
|
25
|
+
await su.wait_command()
|
|
26
|
+
await su.expect_text("hello")
|
|
27
|
+
await su.expect_exit_code(0)
|
|
28
|
+
|
|
29
|
+
asyncio.run(main())
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Drive a full-screen TUI:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
async with ShellUse("vim-session") as su:
|
|
36
|
+
await su.run("vim", "file.txt")
|
|
37
|
+
await su.wait_idle()
|
|
38
|
+
await su.press("i")
|
|
39
|
+
await su.type("some text")
|
|
40
|
+
await su.press("Escape", ":", "w", "q", "Enter")
|
|
41
|
+
await su.wait_exit()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Errors
|
|
45
|
+
|
|
46
|
+
Every failure maps to one of the daemon's exit codes:
|
|
47
|
+
|
|
48
|
+
| Exception | Exit code | Meaning |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `ExpectationError` | 1 | an `expect`/`wait` condition was not met |
|
|
51
|
+
| `UsageError` | 2 | invalid argument (e.g. a bad regex) |
|
|
52
|
+
| `NoSessionError` | 3 | no active session |
|
|
53
|
+
| `DaemonError` | 4 | daemon could not be reached or started |
|
|
54
|
+
| `VersionMismatchError` | 4 | the daemon's version differs from this package |
|
|
55
|
+
| `InternalError` | 5 | internal daemon error |
|
|
56
|
+
|
|
57
|
+
All derive from `ShellUseError`. `wait_*` and `expect_*` raise `ExpectationError` on failure.
|
|
58
|
+
|
|
59
|
+
On its first call, a client checks that the running daemon's version matches the
|
|
60
|
+
package version and raises `VersionMismatchError` if they differ. Stop the daemon
|
|
61
|
+
(`daemon_stop`) so it restarts with the current binary, or point `SHELL_USE_BIN`
|
|
62
|
+
at a matching one.
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
`ShellUse(session="default", *, binary=None, home=None)` mirrors the CLI: `open` / `run`, `type` / `write`, `submit`, `press` / `keys`, `mouse.click|move|down|up|drag|scroll`, `resize`, `signal` / `kill`, `state`, `text`, `cells`, `get` (+ `get_command` / `get_output` / `get_exit_code` / `get_cwd` / `get_cursor` / `get_size`), `screenshot`, `wait_text` / `wait_idle` / `wait_command` / `wait_exit`, `expect_text` / `expect_exit_code` / `expect_output` / `expect_snapshot`, and `close`.
|
|
67
|
+
|
|
68
|
+
Module-level helpers: `sessions()`, `close_all()`, `daemon_status()`, `daemon_stop()`, `get_recording()`.
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
| Variable | Purpose |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `SHELL_USE_BIN` | path to the `shell-use` binary |
|
|
75
|
+
| `SHELL_USE_SESSION` | default session name |
|
|
76
|
+
| `SHELL_USE_HOME` | daemon state directory (sockets, pids) |
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shell-use"
|
|
7
|
+
version = "0.0.1-beta.2"
|
|
8
|
+
description = "Python client for the shell-use terminal daemon"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Microsoft" }]
|
|
13
|
+
keywords = ["terminal", "pty", "automation", "testing", "tui", "shell-use"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/microsoft/shell-use"
|
|
23
|
+
Repository = "https://github.com/microsoft/shell-use"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/shell_use"]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.sdist]
|
|
29
|
+
include = ["src/shell_use", "README.md"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ._config import VERSION as __version__
|
|
4
|
+
from .client import (
|
|
5
|
+
ShellUse,
|
|
6
|
+
close_all,
|
|
7
|
+
daemon_status,
|
|
8
|
+
daemon_stop,
|
|
9
|
+
get_recording,
|
|
10
|
+
sessions,
|
|
11
|
+
)
|
|
12
|
+
from .errors import (
|
|
13
|
+
DaemonError,
|
|
14
|
+
ExpectationError,
|
|
15
|
+
InternalError,
|
|
16
|
+
NoSessionError,
|
|
17
|
+
ShellUseError,
|
|
18
|
+
UsageError,
|
|
19
|
+
VersionMismatchError,
|
|
20
|
+
)
|
|
21
|
+
from .types import Cell, State
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"ShellUse",
|
|
25
|
+
"sessions",
|
|
26
|
+
"close_all",
|
|
27
|
+
"daemon_status",
|
|
28
|
+
"daemon_stop",
|
|
29
|
+
"get_recording",
|
|
30
|
+
"ShellUseError",
|
|
31
|
+
"ExpectationError",
|
|
32
|
+
"UsageError",
|
|
33
|
+
"NoSessionError",
|
|
34
|
+
"DaemonError",
|
|
35
|
+
"VersionMismatchError",
|
|
36
|
+
"InternalError",
|
|
37
|
+
"Cell",
|
|
38
|
+
"State",
|
|
39
|
+
"__version__",
|
|
40
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
VERSION = "0.0.1-beta.2"
|
|
9
|
+
|
|
10
|
+
DEFAULT_COLS = 80
|
|
11
|
+
DEFAULT_ROWS = 30
|
|
12
|
+
|
|
13
|
+
IS_WINDOWS = sys.platform == "win32"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def resolve_session(session: Optional[str]) -> str:
|
|
17
|
+
return session or os.environ.get("SHELL_USE_SESSION") or "default"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_binary(binary: Optional[str]) -> str:
|
|
21
|
+
return binary or os.environ.get("SHELL_USE_BIN") or "shell-use"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_home(home: Optional[str]) -> Optional[str]:
|
|
25
|
+
return home or os.environ.get("SHELL_USE_HOME") or None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def home_dir(home: Optional[str]) -> Path:
|
|
29
|
+
return Path(home) if home else Path.home() / ".shell-use"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def socket_path(session: str, home: Optional[str]) -> str:
|
|
33
|
+
if IS_WINDOWS:
|
|
34
|
+
return rf"\\.\pipe\shell-use-{session}.sock"
|
|
35
|
+
return str(home_dir(home) / f"{session}.sock")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _cache_dir() -> Path:
|
|
39
|
+
if IS_WINDOWS:
|
|
40
|
+
base = os.environ.get("LOCALAPPDATA")
|
|
41
|
+
return Path(base) if base else Path.home() / "AppData" / "Local"
|
|
42
|
+
if sys.platform == "darwin":
|
|
43
|
+
return Path.home() / "Library" / "Caches"
|
|
44
|
+
xdg = os.environ.get("XDG_CACHE_HOME")
|
|
45
|
+
return Path(xdg) if xdg else Path.home() / ".cache"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def recording_dir(home: Optional[str]) -> Path:
|
|
49
|
+
if home:
|
|
50
|
+
return Path(home) / "recordings"
|
|
51
|
+
return _cache_dir() / "shell-use"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def recording_path(session: str, home: Optional[str]) -> Path:
|
|
55
|
+
return recording_dir(home) / f"{session}.cast"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
from .errors import make_error
|
|
6
|
+
|
|
7
|
+
EnvLike = Union[Mapping[str, str], Iterable[Tuple[str, str]], None]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def unwrap(resp: Dict[str, Any]) -> Any:
|
|
11
|
+
if resp.get("ok"):
|
|
12
|
+
return resp.get("data")
|
|
13
|
+
raise make_error(resp.get("kind"), resp.get("message") or "shell-use error")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def env_pairs(env: EnvLike) -> List[List[str]]:
|
|
17
|
+
if env is None:
|
|
18
|
+
return []
|
|
19
|
+
items = env.items() if isinstance(env, Mapping) else env
|
|
20
|
+
return [[str(k), str(v)] for k, v in items]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from . import _config as cfg
|
|
9
|
+
from .errors import DaemonError
|
|
10
|
+
|
|
11
|
+
_Streams = Tuple[asyncio.StreamReader, asyncio.StreamWriter]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def _open(session: str, home: Optional[str]) -> _Streams:
|
|
15
|
+
path = cfg.socket_path(session, home)
|
|
16
|
+
if cfg.IS_WINDOWS:
|
|
17
|
+
loop = asyncio.get_running_loop()
|
|
18
|
+
create = getattr(loop, "create_pipe_connection", None)
|
|
19
|
+
if create is None:
|
|
20
|
+
raise DaemonError(
|
|
21
|
+
"named-pipe client requires the Proactor event loop on Windows "
|
|
22
|
+
"(the default since Python 3.8)"
|
|
23
|
+
)
|
|
24
|
+
reader = asyncio.StreamReader()
|
|
25
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
26
|
+
transport, _ = await create(lambda: protocol, path)
|
|
27
|
+
writer = asyncio.StreamWriter(transport, protocol, reader, loop)
|
|
28
|
+
return reader, writer
|
|
29
|
+
return await asyncio.open_unix_connection(path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def _close(writer: asyncio.StreamWriter) -> None:
|
|
33
|
+
writer.close()
|
|
34
|
+
try:
|
|
35
|
+
await writer.wait_closed()
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def can_connect(session: str, home: Optional[str]) -> bool:
|
|
41
|
+
try:
|
|
42
|
+
_, writer = await _open(session, home)
|
|
43
|
+
except (FileNotFoundError, ConnectionRefusedError, OSError):
|
|
44
|
+
return False
|
|
45
|
+
await _close(writer)
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def ensure_daemon(session: str, home: Optional[str], binary: str) -> None:
|
|
50
|
+
if await can_connect(session, home):
|
|
51
|
+
return
|
|
52
|
+
env = dict(os.environ)
|
|
53
|
+
if home:
|
|
54
|
+
env["SHELL_USE_HOME"] = home
|
|
55
|
+
try:
|
|
56
|
+
proc = await asyncio.create_subprocess_exec(
|
|
57
|
+
binary,
|
|
58
|
+
"--session",
|
|
59
|
+
session,
|
|
60
|
+
"daemon",
|
|
61
|
+
"status",
|
|
62
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
63
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
64
|
+
env=env,
|
|
65
|
+
)
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
raise DaemonError(
|
|
68
|
+
f"could not find the '{binary}' binary on PATH; "
|
|
69
|
+
"set SHELL_USE_BIN or pass binary="
|
|
70
|
+
)
|
|
71
|
+
await proc.wait()
|
|
72
|
+
for _ in range(100):
|
|
73
|
+
if await can_connect(session, home):
|
|
74
|
+
return
|
|
75
|
+
await asyncio.sleep(0.05)
|
|
76
|
+
raise DaemonError(f"daemon for session '{session}' did not become ready")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def request(
|
|
80
|
+
session: str,
|
|
81
|
+
home: Optional[str],
|
|
82
|
+
binary: str,
|
|
83
|
+
payload: Dict[str, Any],
|
|
84
|
+
*,
|
|
85
|
+
autostart: bool = True,
|
|
86
|
+
) -> Dict[str, Any]:
|
|
87
|
+
if autostart:
|
|
88
|
+
await ensure_daemon(session, home, binary)
|
|
89
|
+
try:
|
|
90
|
+
reader, writer = await _open(session, home)
|
|
91
|
+
except (FileNotFoundError, ConnectionRefusedError, OSError) as e:
|
|
92
|
+
raise DaemonError(f"could not connect to session '{session}': {e}")
|
|
93
|
+
try:
|
|
94
|
+
writer.write(json.dumps(payload).encode("utf-8") + b"\n")
|
|
95
|
+
await writer.drain()
|
|
96
|
+
line = await reader.readline()
|
|
97
|
+
finally:
|
|
98
|
+
await _close(writer)
|
|
99
|
+
if not line:
|
|
100
|
+
raise DaemonError("daemon closed the connection without responding")
|
|
101
|
+
try:
|
|
102
|
+
return json.loads(line.decode("utf-8"))
|
|
103
|
+
except json.JSONDecodeError as e:
|
|
104
|
+
raise DaemonError(f"invalid response from daemon: {e}")
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from . import _config as cfg
|
|
7
|
+
from . import _transport as transport
|
|
8
|
+
from ._protocol import EnvLike, env_pairs, unwrap
|
|
9
|
+
from .errors import VersionMismatchError
|
|
10
|
+
from .types import Cell, State
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_version(daemon_version: Optional[str]) -> None:
|
|
14
|
+
if daemon_version != cfg.VERSION:
|
|
15
|
+
raise VersionMismatchError(
|
|
16
|
+
f"shell-use version mismatch: client {cfg.VERSION}, daemon "
|
|
17
|
+
f"{daemon_version or 'unknown'}. Ensure the shell-use binary matches the "
|
|
18
|
+
"shell-use package version, or stop the daemon (daemon_stop) so it "
|
|
19
|
+
"restarts with the current binary."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Mouse:
|
|
24
|
+
def __init__(self, client: "ShellUse") -> None:
|
|
25
|
+
self._c = client
|
|
26
|
+
|
|
27
|
+
async def click(
|
|
28
|
+
self,
|
|
29
|
+
x: Optional[int] = None,
|
|
30
|
+
y: Optional[int] = None,
|
|
31
|
+
*,
|
|
32
|
+
on_text: Optional[str] = None,
|
|
33
|
+
button: int = 0,
|
|
34
|
+
clicks: int = 1,
|
|
35
|
+
) -> None:
|
|
36
|
+
await self._c.send(
|
|
37
|
+
{
|
|
38
|
+
"kind": "mouse",
|
|
39
|
+
"action": {
|
|
40
|
+
"op": "click",
|
|
41
|
+
"x": x,
|
|
42
|
+
"y": y,
|
|
43
|
+
"on_text": on_text,
|
|
44
|
+
"button": button,
|
|
45
|
+
"clicks": clicks,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def move(self, x: int, y: int) -> None:
|
|
51
|
+
await self._c.send({"kind": "mouse", "action": {"op": "move", "x": x, "y": y}})
|
|
52
|
+
|
|
53
|
+
async def down(self, x: int, y: int, *, button: int = 0) -> None:
|
|
54
|
+
await self._c.send(
|
|
55
|
+
{"kind": "mouse", "action": {"op": "down", "x": x, "y": y, "button": button}}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
async def up(self, x: int, y: int, *, button: int = 0) -> None:
|
|
59
|
+
await self._c.send(
|
|
60
|
+
{"kind": "mouse", "action": {"op": "up", "x": x, "y": y, "button": button}}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def drag(
|
|
64
|
+
self, x1: int, y1: int, x2: int, y2: int, *, button: int = 0
|
|
65
|
+
) -> None:
|
|
66
|
+
await self._c.send(
|
|
67
|
+
{
|
|
68
|
+
"kind": "mouse",
|
|
69
|
+
"action": {
|
|
70
|
+
"op": "drag",
|
|
71
|
+
"x1": x1,
|
|
72
|
+
"y1": y1,
|
|
73
|
+
"x2": x2,
|
|
74
|
+
"y2": y2,
|
|
75
|
+
"button": button,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def scroll(self, direction: str, *, amount: int = 3) -> None:
|
|
81
|
+
await self._c.send(
|
|
82
|
+
{
|
|
83
|
+
"kind": "mouse",
|
|
84
|
+
"action": {"op": "scroll", "direction": direction, "amount": amount},
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ShellUse:
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
session: Optional[str] = None,
|
|
93
|
+
*,
|
|
94
|
+
binary: Optional[str] = None,
|
|
95
|
+
home: Optional[str] = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
self._session = cfg.resolve_session(session)
|
|
98
|
+
self._binary = cfg.resolve_binary(binary)
|
|
99
|
+
self._home = cfg.resolve_home(home)
|
|
100
|
+
self._version_checked = False
|
|
101
|
+
self.mouse = _Mouse(self)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def session(self) -> str:
|
|
105
|
+
return self._session
|
|
106
|
+
|
|
107
|
+
async def send(self, payload: Dict[str, Any]) -> Any:
|
|
108
|
+
await self._check_version()
|
|
109
|
+
resp = await transport.request(self._session, self._home, self._binary, payload)
|
|
110
|
+
return unwrap(resp)
|
|
111
|
+
|
|
112
|
+
async def _check_version(self) -> None:
|
|
113
|
+
if self._version_checked:
|
|
114
|
+
return
|
|
115
|
+
resp = await transport.request(
|
|
116
|
+
self._session, self._home, self._binary, {"kind": "status"}
|
|
117
|
+
)
|
|
118
|
+
data = unwrap(resp)
|
|
119
|
+
check_version(data.get("version") if isinstance(data, dict) else None)
|
|
120
|
+
self._version_checked = True
|
|
121
|
+
|
|
122
|
+
async def open(
|
|
123
|
+
self,
|
|
124
|
+
*,
|
|
125
|
+
shell: Optional[str] = None,
|
|
126
|
+
cols: int = cfg.DEFAULT_COLS,
|
|
127
|
+
rows: int = cfg.DEFAULT_ROWS,
|
|
128
|
+
cwd: Optional[str] = None,
|
|
129
|
+
env: EnvLike = None,
|
|
130
|
+
) -> Dict[str, Any]:
|
|
131
|
+
return await self.send(
|
|
132
|
+
{
|
|
133
|
+
"kind": "open",
|
|
134
|
+
"shell": shell,
|
|
135
|
+
"program": None,
|
|
136
|
+
"cols": cols,
|
|
137
|
+
"rows": rows,
|
|
138
|
+
"cwd": cwd,
|
|
139
|
+
"env": env_pairs(env),
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
async def run(
|
|
144
|
+
self,
|
|
145
|
+
program: str,
|
|
146
|
+
*args: str,
|
|
147
|
+
cols: int = cfg.DEFAULT_COLS,
|
|
148
|
+
rows: int = cfg.DEFAULT_ROWS,
|
|
149
|
+
cwd: Optional[str] = None,
|
|
150
|
+
env: EnvLike = None,
|
|
151
|
+
) -> Dict[str, Any]:
|
|
152
|
+
return await self.send(
|
|
153
|
+
{
|
|
154
|
+
"kind": "open",
|
|
155
|
+
"shell": None,
|
|
156
|
+
"program": [program, *args],
|
|
157
|
+
"cols": cols,
|
|
158
|
+
"rows": rows,
|
|
159
|
+
"cwd": cwd,
|
|
160
|
+
"env": env_pairs(env),
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
async def close(self) -> None:
|
|
165
|
+
if not await transport.can_connect(self._session, self._home):
|
|
166
|
+
return
|
|
167
|
+
resp = await transport.request(
|
|
168
|
+
self._session, self._home, self._binary, {"kind": "close"}, autostart=False
|
|
169
|
+
)
|
|
170
|
+
unwrap(resp)
|
|
171
|
+
|
|
172
|
+
async def type(self, text: str) -> None:
|
|
173
|
+
await self.send({"kind": "write", "data": text})
|
|
174
|
+
|
|
175
|
+
async def write(self, data: str) -> None:
|
|
176
|
+
await self.send({"kind": "write", "data": data})
|
|
177
|
+
|
|
178
|
+
async def submit(self, text: Optional[str] = None) -> None:
|
|
179
|
+
await self.send({"kind": "submit", "data": text})
|
|
180
|
+
|
|
181
|
+
async def press(self, *keys: str) -> None:
|
|
182
|
+
await self.send({"kind": "press", "keys": list(keys)})
|
|
183
|
+
|
|
184
|
+
async def keys(self, combo: str) -> None:
|
|
185
|
+
await self.send({"kind": "press", "keys": [combo]})
|
|
186
|
+
|
|
187
|
+
async def resize(self, cols: int, rows: int) -> None:
|
|
188
|
+
await self.send({"kind": "resize", "cols": cols, "rows": rows})
|
|
189
|
+
|
|
190
|
+
async def signal(self, name: str) -> None:
|
|
191
|
+
await self.send({"kind": "signal", "name": name})
|
|
192
|
+
|
|
193
|
+
async def kill(self) -> None:
|
|
194
|
+
await self.send({"kind": "signal", "name": "KILL"})
|
|
195
|
+
|
|
196
|
+
async def state(self) -> State:
|
|
197
|
+
return State.from_dict(await self.send({"kind": "state"}))
|
|
198
|
+
|
|
199
|
+
async def text(self, *, full: bool = False) -> str:
|
|
200
|
+
return (await self.send({"kind": "text", "full": full}))["text"]
|
|
201
|
+
|
|
202
|
+
async def cells(self, x: int, y: int, w: int = 1, h: int = 1) -> List[Cell]:
|
|
203
|
+
data = await self.send({"kind": "cells", "x": x, "y": y, "w": w, "h": h})
|
|
204
|
+
return [Cell(**c) for c in data["cells"]]
|
|
205
|
+
|
|
206
|
+
async def get(self, field: str) -> Any:
|
|
207
|
+
return (await self.send({"kind": "get", "field": field}))["value"]
|
|
208
|
+
|
|
209
|
+
async def get_command(self) -> Optional[str]:
|
|
210
|
+
return await self.get("command")
|
|
211
|
+
|
|
212
|
+
async def get_output(self) -> Optional[str]:
|
|
213
|
+
return await self.get("output")
|
|
214
|
+
|
|
215
|
+
async def get_exit_code(self) -> Optional[int]:
|
|
216
|
+
return await self.get("exit-code")
|
|
217
|
+
|
|
218
|
+
async def get_cwd(self) -> Optional[str]:
|
|
219
|
+
return await self.get("cwd")
|
|
220
|
+
|
|
221
|
+
async def get_cursor(self) -> Dict[str, int]:
|
|
222
|
+
return await self.get("cursor")
|
|
223
|
+
|
|
224
|
+
async def get_size(self) -> Dict[str, int]:
|
|
225
|
+
return await self.get("size")
|
|
226
|
+
|
|
227
|
+
async def screenshot(self, path: Optional[str] = None, *, full: bool = False) -> str:
|
|
228
|
+
data = await self.send({"kind": "screenshot", "full": full, "path": path})
|
|
229
|
+
return data.get("path") or data.get("text")
|
|
230
|
+
|
|
231
|
+
async def wait_text(
|
|
232
|
+
self,
|
|
233
|
+
text: str,
|
|
234
|
+
*,
|
|
235
|
+
regex: bool = False,
|
|
236
|
+
full: bool = False,
|
|
237
|
+
not_: bool = False,
|
|
238
|
+
timeout: int = 5000,
|
|
239
|
+
) -> None:
|
|
240
|
+
await self.send(
|
|
241
|
+
{
|
|
242
|
+
"kind": "wait_text",
|
|
243
|
+
"text": text,
|
|
244
|
+
"regex": regex,
|
|
245
|
+
"full": full,
|
|
246
|
+
"timeout_ms": timeout,
|
|
247
|
+
"not": not_,
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
async def wait_idle(self, *, timeout: int = 5000) -> None:
|
|
252
|
+
await self.send({"kind": "wait_idle", "timeout_ms": timeout})
|
|
253
|
+
|
|
254
|
+
async def wait_command(self, *, timeout: int = 30000) -> None:
|
|
255
|
+
await self.send({"kind": "wait_command", "timeout_ms": timeout})
|
|
256
|
+
|
|
257
|
+
async def wait_exit(self, *, timeout: int = 30000) -> None:
|
|
258
|
+
await self.send({"kind": "wait_exit", "timeout_ms": timeout})
|
|
259
|
+
|
|
260
|
+
async def expect_text(
|
|
261
|
+
self,
|
|
262
|
+
text: str,
|
|
263
|
+
*,
|
|
264
|
+
regex: bool = False,
|
|
265
|
+
full: bool = False,
|
|
266
|
+
strict: bool = True,
|
|
267
|
+
not_: bool = False,
|
|
268
|
+
fg: Optional[str] = None,
|
|
269
|
+
bg: Optional[str] = None,
|
|
270
|
+
timeout: int = 5000,
|
|
271
|
+
) -> None:
|
|
272
|
+
await self.send(
|
|
273
|
+
{
|
|
274
|
+
"kind": "expect_text",
|
|
275
|
+
"text": text,
|
|
276
|
+
"regex": regex,
|
|
277
|
+
"full": full,
|
|
278
|
+
"strict": strict,
|
|
279
|
+
"not": not_,
|
|
280
|
+
"fg": fg,
|
|
281
|
+
"bg": bg,
|
|
282
|
+
"timeout_ms": timeout,
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
async def expect_exit_code(self, code: int) -> None:
|
|
287
|
+
await self.send({"kind": "expect_exit_code", "code": code})
|
|
288
|
+
|
|
289
|
+
async def expect_output(self, text: str, *, regex: bool = False) -> None:
|
|
290
|
+
await self.send({"kind": "expect_output", "text": text, "regex": regex})
|
|
291
|
+
|
|
292
|
+
async def expect_snapshot(
|
|
293
|
+
self, name: str, *, update: bool = False, include_colors: bool = False
|
|
294
|
+
) -> str:
|
|
295
|
+
return (
|
|
296
|
+
await self.send(
|
|
297
|
+
{
|
|
298
|
+
"kind": "snapshot",
|
|
299
|
+
"name": name,
|
|
300
|
+
"update": update,
|
|
301
|
+
"include_colors": include_colors,
|
|
302
|
+
"cwd": os.getcwd(),
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
)["status"]
|
|
306
|
+
|
|
307
|
+
async def __aenter__(self) -> "ShellUse":
|
|
308
|
+
return self
|
|
309
|
+
|
|
310
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
311
|
+
await self.close()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
async def sessions(*, home: Optional[str] = None) -> List[str]:
|
|
315
|
+
h = cfg.resolve_home(home)
|
|
316
|
+
directory = cfg.home_dir(h)
|
|
317
|
+
out: List[str] = []
|
|
318
|
+
if directory.is_dir():
|
|
319
|
+
for entry in sorted(directory.iterdir()):
|
|
320
|
+
if entry.suffix == ".pid":
|
|
321
|
+
name = entry.stem
|
|
322
|
+
if await transport.can_connect(name, h):
|
|
323
|
+
out.append(name)
|
|
324
|
+
return out
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
async def close_all(*, binary: Optional[str] = None, home: Optional[str] = None) -> None:
|
|
328
|
+
h = cfg.resolve_home(home)
|
|
329
|
+
b = cfg.resolve_binary(binary)
|
|
330
|
+
for name in await sessions(home=h):
|
|
331
|
+
try:
|
|
332
|
+
await transport.request(name, h, b, {"kind": "close"}, autostart=False)
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
async def daemon_status(
|
|
338
|
+
session: Optional[str] = None,
|
|
339
|
+
*,
|
|
340
|
+
binary: Optional[str] = None,
|
|
341
|
+
home: Optional[str] = None,
|
|
342
|
+
) -> Dict[str, Any]:
|
|
343
|
+
s = cfg.resolve_session(session)
|
|
344
|
+
h = cfg.resolve_home(home)
|
|
345
|
+
b = cfg.resolve_binary(binary)
|
|
346
|
+
return unwrap(await transport.request(s, h, b, {"kind": "status"}))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
async def daemon_stop(
|
|
350
|
+
session: Optional[str] = None,
|
|
351
|
+
*,
|
|
352
|
+
binary: Optional[str] = None,
|
|
353
|
+
home: Optional[str] = None,
|
|
354
|
+
) -> None:
|
|
355
|
+
s = cfg.resolve_session(session)
|
|
356
|
+
h = cfg.resolve_home(home)
|
|
357
|
+
b = cfg.resolve_binary(binary)
|
|
358
|
+
if not await transport.can_connect(s, h):
|
|
359
|
+
return
|
|
360
|
+
unwrap(await transport.request(s, h, b, {"kind": "shutdown"}, autostart=False))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
async def get_recording(
|
|
364
|
+
session: Optional[str] = None, *, home: Optional[str] = None
|
|
365
|
+
) -> str:
|
|
366
|
+
import asyncio
|
|
367
|
+
|
|
368
|
+
s = cfg.resolve_session(session)
|
|
369
|
+
h = cfg.resolve_home(home)
|
|
370
|
+
path = cfg.recording_path(s, h)
|
|
371
|
+
loop = asyncio.get_running_loop()
|
|
372
|
+
try:
|
|
373
|
+
data = await loop.run_in_executor(None, path.read_bytes)
|
|
374
|
+
except FileNotFoundError:
|
|
375
|
+
from .errors import NoSessionError
|
|
376
|
+
|
|
377
|
+
raise NoSessionError(f"no recording for session '{s}'")
|
|
378
|
+
return data.decode("utf-8", errors="replace")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ShellUseError(Exception):
|
|
7
|
+
kind: str = "internal"
|
|
8
|
+
exit_code: int = 5
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str) -> None:
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
self.message = message
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExpectationError(ShellUseError):
|
|
16
|
+
kind = "assertion"
|
|
17
|
+
exit_code = 1
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UsageError(ShellUseError):
|
|
21
|
+
kind = "usage"
|
|
22
|
+
exit_code = 2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NoSessionError(ShellUseError):
|
|
26
|
+
kind = "no_session"
|
|
27
|
+
exit_code = 3
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DaemonError(ShellUseError):
|
|
31
|
+
kind = "daemon"
|
|
32
|
+
exit_code = 4
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class VersionMismatchError(ShellUseError):
|
|
36
|
+
kind = "version_mismatch"
|
|
37
|
+
exit_code = 4
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class InternalError(ShellUseError):
|
|
41
|
+
kind = "internal"
|
|
42
|
+
exit_code = 5
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_BY_KIND = {
|
|
46
|
+
"assertion": ExpectationError,
|
|
47
|
+
"usage": UsageError,
|
|
48
|
+
"no_session": NoSessionError,
|
|
49
|
+
"daemon": DaemonError,
|
|
50
|
+
"internal": InternalError,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def make_error(kind: Optional[str], message: str) -> ShellUseError:
|
|
55
|
+
"""Construct the typed error for a daemon ``kind`` string."""
|
|
56
|
+
return _BY_KIND.get(kind or "", InternalError)(message)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, Optional, Union
|
|
5
|
+
|
|
6
|
+
Color = Union[str, int]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Cell:
|
|
11
|
+
x: int
|
|
12
|
+
y: int
|
|
13
|
+
char: str
|
|
14
|
+
fg: Color
|
|
15
|
+
bg: Color
|
|
16
|
+
bold: bool
|
|
17
|
+
italic: bool
|
|
18
|
+
underline: bool
|
|
19
|
+
inverse: bool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class State:
|
|
24
|
+
cols: int
|
|
25
|
+
rows: int
|
|
26
|
+
cursor: Dict[str, int]
|
|
27
|
+
cwd: Optional[str]
|
|
28
|
+
last_command: Optional[str]
|
|
29
|
+
last_exit: Optional[int]
|
|
30
|
+
exited: Optional[int]
|
|
31
|
+
ready: bool
|
|
32
|
+
text: str
|
|
33
|
+
session_shell: Optional[str]
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_dict(cls, d: Dict[str, Any]) -> "State":
|
|
37
|
+
return cls(
|
|
38
|
+
cols=d.get("cols", 0),
|
|
39
|
+
rows=d.get("rows", 0),
|
|
40
|
+
cursor=d.get("cursor", {"x": 0, "y": 0}),
|
|
41
|
+
cwd=d.get("cwd"),
|
|
42
|
+
last_command=d.get("last_command"),
|
|
43
|
+
last_exit=d.get("last_exit"),
|
|
44
|
+
exited=d.get("exited"),
|
|
45
|
+
ready=d.get("ready", False),
|
|
46
|
+
text=d.get("text", ""),
|
|
47
|
+
session_shell=d.get("session_shell"),
|
|
48
|
+
)
|
shell_use-0.0.1/.gitignore
DELETED
shell_use-0.0.1/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) Microsoft Corporation.
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE
|
shell_use-0.0.1/PKG-INFO
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: shell-use
|
|
3
|
-
Version: 0.0.1
|
|
4
|
-
Summary: A headless terminal CLI + daemon for driving, asserting on, and recording shells
|
|
5
|
-
Project-URL: Homepage, https://github.com/microsoft/shell-use
|
|
6
|
-
Project-URL: Repository, https://github.com/microsoft/shell-use
|
|
7
|
-
Project-URL: Issues, https://github.com/microsoft/shell-use/issues
|
|
8
|
-
Author: Microsoft Corporation
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: cli,pty,shell,terminal,testing,tui
|
|
12
|
-
Classifier: Development Status :: 1 - Planning
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Topic :: Software Development :: Testing
|
|
17
|
-
Classifier: Topic :: Terminals
|
|
18
|
-
Requires-Python: >=3.8
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
20
|
-
|
|
21
|
-
# shell-use
|
|
22
|
-
|
|
23
|
-
`shell-use` is a Rust-powered CLI for controlling, inspecting, testing, and recording shell sessions and terminal apps.
|
|
24
|
-
|
|
25
|
-
This package reserves the `shell-use` name on PyPI. See the project repository for the actual tool and installation instructions: https://github.com/microsoft/shell-use
|
shell_use-0.0.1/README.md
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
# shell-use
|
|
2
|
-
|
|
3
|
-
`shell-use` is a Rust-powered CLI for controlling, inspecting, testing, and recording shell sessions and terminal apps.
|
|
4
|
-
|
|
5
|
-
This package reserves the `shell-use` name on PyPI. See the project repository for the actual tool and installation instructions: https://github.com/microsoft/shell-use
|
shell_use-0.0.1/build.log
DELETED
shell_use-0.0.1/pyproject.toml
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "shell-use"
|
|
7
|
-
version = "0.0.1"
|
|
8
|
-
description = "A headless terminal CLI + daemon for driving, asserting on, and recording shells"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.8"
|
|
11
|
-
license = "MIT"
|
|
12
|
-
license-files = ["LICENSE"]
|
|
13
|
-
authors = [{ name = "Microsoft Corporation" }]
|
|
14
|
-
keywords = ["terminal", "shell", "cli", "tui", "pty", "testing"]
|
|
15
|
-
classifiers = [
|
|
16
|
-
"Development Status :: 1 - Planning",
|
|
17
|
-
"Intended Audience :: Developers",
|
|
18
|
-
"Operating System :: OS Independent",
|
|
19
|
-
"Programming Language :: Python :: 3",
|
|
20
|
-
"Topic :: Software Development :: Testing",
|
|
21
|
-
"Topic :: Terminals",
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
[project.urls]
|
|
25
|
-
Homepage = "https://github.com/microsoft/shell-use"
|
|
26
|
-
Repository = "https://github.com/microsoft/shell-use"
|
|
27
|
-
Issues = "https://github.com/microsoft/shell-use/issues"
|
|
28
|
-
|
|
29
|
-
[tool.hatch.build.targets.wheel]
|
|
30
|
-
packages = ["src/shell_use"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.1"
|