remotedev-agent 1.3.0__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.
- remotedev_agent-1.3.0/LICENSE +21 -0
- remotedev_agent-1.3.0/MANIFEST.in +3 -0
- remotedev_agent-1.3.0/PKG-INFO +133 -0
- remotedev_agent-1.3.0/README.md +96 -0
- remotedev_agent-1.3.0/pyproject.toml +59 -0
- remotedev_agent-1.3.0/remotedev_agent/__init__.py +3 -0
- remotedev_agent-1.3.0/remotedev_agent/cli.py +265 -0
- remotedev_agent-1.3.0/remotedev_agent/config.py +37 -0
- remotedev_agent-1.3.0/remotedev_agent/daemon.py +541 -0
- remotedev_agent-1.3.0/remotedev_agent/diagnostics.py +205 -0
- remotedev_agent-1.3.0/remotedev_agent/discovery.py +33 -0
- remotedev_agent-1.3.0/remotedev_agent/git_ops.py +34 -0
- remotedev_agent-1.3.0/remotedev_agent/process_manager.py +460 -0
- remotedev_agent-1.3.0/remotedev_agent/pty_manager.py +286 -0
- remotedev_agent-1.3.0/remotedev_agent/sandbox_manager.py +46 -0
- remotedev_agent-1.3.0/remotedev_agent/service/__init__.py +0 -0
- remotedev_agent-1.3.0/remotedev_agent/service/linux.py +102 -0
- remotedev_agent-1.3.0/remotedev_agent/service/macos.py +180 -0
- remotedev_agent-1.3.0/remotedev_agent.egg-info/PKG-INFO +133 -0
- remotedev_agent-1.3.0/remotedev_agent.egg-info/SOURCES.txt +30 -0
- remotedev_agent-1.3.0/remotedev_agent.egg-info/dependency_links.txt +1 -0
- remotedev_agent-1.3.0/remotedev_agent.egg-info/entry_points.txt +2 -0
- remotedev_agent-1.3.0/remotedev_agent.egg-info/requires.txt +9 -0
- remotedev_agent-1.3.0/remotedev_agent.egg-info/top_level.txt +1 -0
- remotedev_agent-1.3.0/setup.cfg +4 -0
- remotedev_agent-1.3.0/tests/test_daemon.py +208 -0
- remotedev_agent-1.3.0/tests/test_diagnostics.py +195 -0
- remotedev_agent-1.3.0/tests/test_discovery.py +29 -0
- remotedev_agent-1.3.0/tests/test_macos_installer.py +100 -0
- remotedev_agent-1.3.0/tests/test_process_manager.py +123 -0
- remotedev_agent-1.3.0/tests/test_pty_manager.py +350 -0
- remotedev_agent-1.3.0/tests/test_scrollback_persistence.py +167 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shaun
|
|
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.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: remotedev-agent
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: Agent daemon for Remote AI Maestro — manages PTY sessions and AI agent subprocesses on remote machines
|
|
5
|
+
Author-email: Shaun <scwj1210@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shaunchew/remote-ai-maestro
|
|
8
|
+
Project-URL: Repository, https://github.com/shaunchew/remote-ai-maestro
|
|
9
|
+
Project-URL: Documentation, https://github.com/shaunchew/remote-ai-maestro#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/shaunchew/remote-ai-maestro/issues
|
|
11
|
+
Keywords: remote,ai,agent,terminal,pty,tmux,claude,codex
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Classifier: Topic :: System :: Shells
|
|
24
|
+
Classifier: Topic :: Terminals :: Terminal Emulators/X Terminals
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: websockets>=13.0
|
|
29
|
+
Requires-Dist: click>=8.0
|
|
30
|
+
Requires-Dist: ptyprocess>=0.7.0
|
|
31
|
+
Requires-Dist: watchdog>=3.0
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-mock>=3.12; extra == "test"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# remotedev-agent
|
|
39
|
+
|
|
40
|
+
Agent daemon for [Remote AI Maestro](https://remote-ai-maestro.thesavvydeveloper.com) -- connects your machine to the platform so you can remotely control AI coding agents (Claude Code, Codex, Aider, Copilot, Cursor) via a browser-based terminal.
|
|
41
|
+
|
|
42
|
+
## What it does
|
|
43
|
+
|
|
44
|
+
- Opens tmux-backed PTY sessions that survive disconnects and daemon restarts
|
|
45
|
+
- Discovers AI agents available on your machine's PATH
|
|
46
|
+
- Streams terminal I/O to the Remote AI Maestro relay over WebSocket
|
|
47
|
+
- Manages sandbox directories (create, clone repos, delete)
|
|
48
|
+
- Installs as a background service (launchd on macOS, systemd on Linux)
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- Python 3.10+
|
|
53
|
+
- tmux installed and on PATH
|
|
54
|
+
- macOS or Linux
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install remotedev-agent
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
1. Register a machine on the [Remote AI Maestro dashboard](https://remote-ai-maestro.thesavvydeveloper.com) to get your machine ID and token.
|
|
65
|
+
|
|
66
|
+
2. Configure the agent:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
remotedev-agent configure \
|
|
70
|
+
--relay-url wss://relay.thesavvydeveloper.com \
|
|
71
|
+
--machine-id YOUR_MACHINE_ID \
|
|
72
|
+
--token YOUR_MACHINE_TOKEN
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
3. Start the agent:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
remotedev-agent start
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The agent connects to the relay and begins accepting terminal sessions from the web dashboard.
|
|
82
|
+
|
|
83
|
+
## Interactive setup
|
|
84
|
+
|
|
85
|
+
If you prefer prompts instead of flags:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
remotedev-agent setup
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Install as a background service
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
remotedev-agent install-service
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This creates a launchd plist (macOS) or systemd unit (Linux) so the agent starts automatically on boot.
|
|
98
|
+
|
|
99
|
+
To remove the service:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
remotedev-agent uninstall-service
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
Config is stored at `~/.remotedev-agent/config.json` with three fields:
|
|
108
|
+
|
|
109
|
+
| Field | Description |
|
|
110
|
+
|-------|-------------|
|
|
111
|
+
| `relay_url` | WebSocket URL of the relay server |
|
|
112
|
+
| `machine_id` | UUID assigned when you register the machine |
|
|
113
|
+
| `token` | Machine authentication token |
|
|
114
|
+
|
|
115
|
+
## How it works
|
|
116
|
+
|
|
117
|
+
The agent daemon maintains a persistent WebSocket connection to the Remote AI Maestro relay server. When a user opens a sandbox terminal in their browser, the relay forwards the request to the agent, which creates a tmux-backed PTY session on your machine. All terminal input/output is streamed in real time through the relay.
|
|
118
|
+
|
|
119
|
+
AI agents (Claude Code, Codex, etc.) are discovered by probing your PATH at startup. Users can invoke any discovered agent from the browser terminal.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Install in editable mode with test dependencies
|
|
125
|
+
pip install -e ".[test]"
|
|
126
|
+
|
|
127
|
+
# Run tests
|
|
128
|
+
pytest
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# remotedev-agent
|
|
2
|
+
|
|
3
|
+
Agent daemon for [Remote AI Maestro](https://remote-ai-maestro.thesavvydeveloper.com) -- connects your machine to the platform so you can remotely control AI coding agents (Claude Code, Codex, Aider, Copilot, Cursor) via a browser-based terminal.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- Opens tmux-backed PTY sessions that survive disconnects and daemon restarts
|
|
8
|
+
- Discovers AI agents available on your machine's PATH
|
|
9
|
+
- Streams terminal I/O to the Remote AI Maestro relay over WebSocket
|
|
10
|
+
- Manages sandbox directories (create, clone repos, delete)
|
|
11
|
+
- Installs as a background service (launchd on macOS, systemd on Linux)
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Python 3.10+
|
|
16
|
+
- tmux installed and on PATH
|
|
17
|
+
- macOS or Linux
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install remotedev-agent
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
1. Register a machine on the [Remote AI Maestro dashboard](https://remote-ai-maestro.thesavvydeveloper.com) to get your machine ID and token.
|
|
28
|
+
|
|
29
|
+
2. Configure the agent:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
remotedev-agent configure \
|
|
33
|
+
--relay-url wss://relay.thesavvydeveloper.com \
|
|
34
|
+
--machine-id YOUR_MACHINE_ID \
|
|
35
|
+
--token YOUR_MACHINE_TOKEN
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. Start the agent:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
remotedev-agent start
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The agent connects to the relay and begins accepting terminal sessions from the web dashboard.
|
|
45
|
+
|
|
46
|
+
## Interactive setup
|
|
47
|
+
|
|
48
|
+
If you prefer prompts instead of flags:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
remotedev-agent setup
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Install as a background service
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
remotedev-agent install-service
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This creates a launchd plist (macOS) or systemd unit (Linux) so the agent starts automatically on boot.
|
|
61
|
+
|
|
62
|
+
To remove the service:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
remotedev-agent uninstall-service
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
Config is stored at `~/.remotedev-agent/config.json` with three fields:
|
|
71
|
+
|
|
72
|
+
| Field | Description |
|
|
73
|
+
|-------|-------------|
|
|
74
|
+
| `relay_url` | WebSocket URL of the relay server |
|
|
75
|
+
| `machine_id` | UUID assigned when you register the machine |
|
|
76
|
+
| `token` | Machine authentication token |
|
|
77
|
+
|
|
78
|
+
## How it works
|
|
79
|
+
|
|
80
|
+
The agent daemon maintains a persistent WebSocket connection to the Remote AI Maestro relay server. When a user opens a sandbox terminal in their browser, the relay forwards the request to the agent, which creates a tmux-backed PTY session on your machine. All terminal input/output is streamed in real time through the relay.
|
|
81
|
+
|
|
82
|
+
AI agents (Claude Code, Codex, etc.) are discovered by probing your PATH at startup. Users can invoke any discovered agent from the browser terminal.
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Install in editable mode with test dependencies
|
|
88
|
+
pip install -e ".[test]"
|
|
89
|
+
|
|
90
|
+
# Run tests
|
|
91
|
+
pytest
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "remotedev-agent"
|
|
3
|
+
version = "1.3.0"
|
|
4
|
+
description = "Agent daemon for Remote AI Maestro — manages PTY sessions and AI agent subprocesses on remote machines"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Shaun", email = "scwj1210@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["remote", "ai", "agent", "terminal", "pty", "tmux", "claude", "codex"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Operating System :: MacOS",
|
|
17
|
+
"Operating System :: POSIX :: Linux",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
"Topic :: System :: Shells",
|
|
25
|
+
"Topic :: Terminals :: Terminal Emulators/X Terminals",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"websockets>=13.0",
|
|
29
|
+
"click>=8.0",
|
|
30
|
+
"ptyprocess>=0.7.0",
|
|
31
|
+
"watchdog>=3.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/shaunchew/remote-ai-maestro"
|
|
36
|
+
Repository = "https://github.com/shaunchew/remote-ai-maestro"
|
|
37
|
+
Documentation = "https://github.com/shaunchew/remote-ai-maestro#readme"
|
|
38
|
+
Issues = "https://github.com/shaunchew/remote-ai-maestro/issues"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
remotedev-agent = "remotedev_agent.cli:cli"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
test = [
|
|
45
|
+
"pytest>=8.0",
|
|
46
|
+
"pytest-asyncio>=0.23",
|
|
47
|
+
"pytest-mock>=3.12",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["setuptools>=68.0"]
|
|
52
|
+
build-backend = "setuptools.build_meta"
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
include = ["remotedev_agent*"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
asyncio_mode = "auto"
|
|
59
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from remotedev_agent.config import AgentConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _package_version() -> str:
|
|
10
|
+
"""Resolve the installed package version. Falls back to the literal in
|
|
11
|
+
pyproject.toml when running from a source checkout without metadata."""
|
|
12
|
+
try:
|
|
13
|
+
from importlib.metadata import version as _v
|
|
14
|
+
return _v("remotedev-agent")
|
|
15
|
+
except Exception:
|
|
16
|
+
return "unknown"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.group()
|
|
20
|
+
@click.version_option(version=_package_version(), prog_name="remotedev-agent")
|
|
21
|
+
def cli():
|
|
22
|
+
"""Remote AI Maestro Agent — connects your machine to Remote AI Maestro."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cli.command()
|
|
26
|
+
@click.option("--relay-url", prompt="Relay URL", help="WebSocket relay URL (e.g. ws://localhost:8001)")
|
|
27
|
+
@click.option("--machine-id", prompt="Machine ID", help="Machine UUID from dashboard")
|
|
28
|
+
@click.option("--token", prompt="Machine token", hide_input=True, help="Machine registration token")
|
|
29
|
+
def setup(relay_url: str, machine_id: str, token: str):
|
|
30
|
+
"""Configure the agent with relay connection details."""
|
|
31
|
+
config = AgentConfig(relay_url=relay_url, machine_id=machine_id, token=token)
|
|
32
|
+
config.save()
|
|
33
|
+
click.echo("Config saved.")
|
|
34
|
+
click.echo("To run hidden in the background (recommended): remotedev-agent install-service")
|
|
35
|
+
click.echo("Or quick-detach for one-off: remotedev-agent start --detach")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@cli.command()
|
|
39
|
+
@click.option("--relay-url", default=None)
|
|
40
|
+
@click.option("--machine-id", default=None)
|
|
41
|
+
@click.option("--token", default=None)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--no-verify", is_flag=True,
|
|
44
|
+
help="Skip the post-config connectivity smoke check.",
|
|
45
|
+
)
|
|
46
|
+
def configure(relay_url: str | None, machine_id: str | None, token: str | None, no_verify: bool):
|
|
47
|
+
"""Non-interactive configuration (for scripting/EC2 user-data).
|
|
48
|
+
|
|
49
|
+
By default, runs a 10-second connectivity check against the relay after
|
|
50
|
+
saving — surfaces token mismatches or network problems immediately
|
|
51
|
+
instead of letting the daemon retry-loop in silence. Pass `--no-verify`
|
|
52
|
+
to skip (e.g. when configuring offline before bringing the network up).
|
|
53
|
+
"""
|
|
54
|
+
if not all([relay_url, machine_id, token]):
|
|
55
|
+
click.echo("All of --relay-url, --machine-id, --token are required.", err=True)
|
|
56
|
+
raise SystemExit(1)
|
|
57
|
+
config = AgentConfig(relay_url=relay_url, machine_id=machine_id, token=token)
|
|
58
|
+
config.save()
|
|
59
|
+
click.echo("Config saved.")
|
|
60
|
+
if no_verify:
|
|
61
|
+
return
|
|
62
|
+
# Smoke check — informational. We don't exit non-zero on failure because
|
|
63
|
+
# configure is a config-write op (the user may be configuring on one
|
|
64
|
+
# network and connecting from another). The output is enough to catch
|
|
65
|
+
# the common cases without blocking the install.
|
|
66
|
+
from remotedev_agent.diagnostics import probe_ws_connect, summarize, probe_tcp_v4, probe_tcp_v6
|
|
67
|
+
click.echo("Verifying connectivity (10s)...")
|
|
68
|
+
async def _check():
|
|
69
|
+
v4 = await probe_tcp_v4(relay_url)
|
|
70
|
+
v6 = await probe_tcp_v6(relay_url)
|
|
71
|
+
ws = await probe_ws_connect(relay_url, machine_id, token)
|
|
72
|
+
return [v4, v6, ws]
|
|
73
|
+
try:
|
|
74
|
+
probes = asyncio.run(_check())
|
|
75
|
+
except Exception as e:
|
|
76
|
+
click.echo(f" (smoke check skipped: {e})")
|
|
77
|
+
return
|
|
78
|
+
click.echo(summarize(probes))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@cli.command()
|
|
82
|
+
def diagnose():
|
|
83
|
+
"""Run a full self-diagnostic against the configured relay.
|
|
84
|
+
|
|
85
|
+
Output is designed to be pasted into a bug report. Probes:
|
|
86
|
+
- DNS A/AAAA records
|
|
87
|
+
- raw TCP connect over IPv4 and IPv6 (isolates blackholed-v6 networks)
|
|
88
|
+
- full WebSocket handshake against /ws/agent/{id} with the saved token
|
|
89
|
+
(distinguishes auth failures from network problems)
|
|
90
|
+
"""
|
|
91
|
+
from remotedev_agent.diagnostics import run_full_diagnose, format_report, summarize
|
|
92
|
+
try:
|
|
93
|
+
config = AgentConfig.load()
|
|
94
|
+
except FileNotFoundError as e:
|
|
95
|
+
click.echo(str(e), err=True)
|
|
96
|
+
raise SystemExit(1)
|
|
97
|
+
click.echo(f"Diagnosing connectivity for machine {config.machine_id}")
|
|
98
|
+
click.echo(f"Relay URL: {config.relay_url}\n")
|
|
99
|
+
probes = asyncio.run(run_full_diagnose(config))
|
|
100
|
+
click.echo(format_report(probes))
|
|
101
|
+
click.echo("")
|
|
102
|
+
click.echo(summarize(probes))
|
|
103
|
+
# Exit non-zero if the WS probe failed — makes diagnose usable in scripts
|
|
104
|
+
# ("if remotedev-agent diagnose; then ... fi").
|
|
105
|
+
if not probes[-1].ok:
|
|
106
|
+
raise SystemExit(2)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@cli.command()
|
|
110
|
+
@click.option(
|
|
111
|
+
"--detach", "-d", is_flag=True,
|
|
112
|
+
help="Run in the background, detached from the terminal. Logs go to ~/.remotedev/agent.log.",
|
|
113
|
+
)
|
|
114
|
+
def start(detach: bool):
|
|
115
|
+
"""Start the agent daemon. Use --detach to run in the background."""
|
|
116
|
+
from remotedev_agent import process_manager
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
config = AgentConfig.load()
|
|
120
|
+
except FileNotFoundError as e:
|
|
121
|
+
click.echo(str(e), err=True)
|
|
122
|
+
click.echo("Run 'remotedev-agent setup' or 'remotedev-agent configure ...' first.", err=True)
|
|
123
|
+
raise SystemExit(1)
|
|
124
|
+
|
|
125
|
+
def _run():
|
|
126
|
+
# Imported post-fork to avoid copying daemon-side resources (websockets,
|
|
127
|
+
# asyncio, ptyprocess) into the short-lived parent.
|
|
128
|
+
from remotedev_agent.daemon import AgentDaemon
|
|
129
|
+
daemon = AgentDaemon(config)
|
|
130
|
+
asyncio.run(daemon.run())
|
|
131
|
+
|
|
132
|
+
if not detach:
|
|
133
|
+
click.echo(f"Starting agent for machine {config.machine_id}...")
|
|
134
|
+
_run()
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
pid = process_manager.detach_and_run(_run)
|
|
139
|
+
except RuntimeError as e:
|
|
140
|
+
click.echo(str(e), err=True)
|
|
141
|
+
raise SystemExit(1)
|
|
142
|
+
click.echo(f"Agent running in background (PID {pid}).")
|
|
143
|
+
click.echo(f"Logs: {process_manager.LOG_FILE}")
|
|
144
|
+
click.echo("View: remotedev-agent logs -f")
|
|
145
|
+
click.echo("Stop: remotedev-agent stop")
|
|
146
|
+
click.echo("Status: remotedev-agent status")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@cli.command("stop")
|
|
150
|
+
def cmd_stop():
|
|
151
|
+
"""Stop the detached agent (no effect on tmux sandbox sessions)."""
|
|
152
|
+
from remotedev_agent import process_manager
|
|
153
|
+
svc = process_manager.service_status()
|
|
154
|
+
if svc["running"]:
|
|
155
|
+
click.echo(
|
|
156
|
+
f"Agent is running as a {svc['kind']} service, not in detached mode.\n"
|
|
157
|
+
"Stop the service with 'remotedev-agent uninstall-service' "
|
|
158
|
+
f"(or '{'launchctl unload ~/Library/LaunchAgents/' + process_manager.LAUNCHD_LABEL + '.plist' if svc['kind'] == 'launchd' else 'systemctl --user stop ' + process_manager.SYSTEMD_UNIT}').",
|
|
159
|
+
err=True,
|
|
160
|
+
)
|
|
161
|
+
raise SystemExit(1)
|
|
162
|
+
ok, message = process_manager.stop()
|
|
163
|
+
click.echo(message)
|
|
164
|
+
# Stale-PID cleanup is a successful no-op for callers, not a failure.
|
|
165
|
+
if not ok and "stale" not in message.lower() and "not running" not in message.lower():
|
|
166
|
+
raise SystemExit(1)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@cli.command("status")
|
|
170
|
+
def cmd_status():
|
|
171
|
+
"""Show whether the agent is running (detached or as a service)."""
|
|
172
|
+
from remotedev_agent import process_manager
|
|
173
|
+
info = process_manager.status()
|
|
174
|
+
if info["service_running"]:
|
|
175
|
+
click.echo(f"Agent is running as a {info['service_kind']} service.")
|
|
176
|
+
elif info["detached_running"]:
|
|
177
|
+
click.echo(f"Agent is running in detached mode (PID {info['pid']}).")
|
|
178
|
+
elif info["pid"]:
|
|
179
|
+
click.echo(f"Agent is NOT running (stale PID {info['pid']} in PID file).")
|
|
180
|
+
else:
|
|
181
|
+
click.echo("Agent is NOT running.")
|
|
182
|
+
if info["service_kind"]:
|
|
183
|
+
click.echo(f"Service: {info['service_kind']} ({'running' if info['service_running'] else 'stopped/not loaded'})")
|
|
184
|
+
click.echo(f"PID file: {info['pid_file']}")
|
|
185
|
+
click.echo(f"Log file: {info['log_file']}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@cli.command()
|
|
189
|
+
@click.option("--follow", "-f", is_flag=True, help="Stream new log lines as they arrive.")
|
|
190
|
+
@click.option("--lines", "-n", type=click.IntRange(min=1), default=200, show_default=True, help="Number of recent lines to print.")
|
|
191
|
+
def logs(follow: bool, lines: int):
|
|
192
|
+
"""View agent logs without affecting any running session."""
|
|
193
|
+
from remotedev_agent import process_manager
|
|
194
|
+
process_manager.tail_log(lines=lines, follow=follow)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@cli.command()
|
|
198
|
+
def restart():
|
|
199
|
+
"""Restart the agent (works for both detached and service mode)."""
|
|
200
|
+
import subprocess
|
|
201
|
+
import sys
|
|
202
|
+
from remotedev_agent import process_manager
|
|
203
|
+
svc = process_manager.service_status()
|
|
204
|
+
if svc["kind"] == "launchd":
|
|
205
|
+
# `kickstart -k` is the atomic stop+restart on macOS; falls back to
|
|
206
|
+
# legacy stop/start if the modern domain syntax isn't supported.
|
|
207
|
+
uid = os.getuid() if hasattr(os, "getuid") else None
|
|
208
|
+
target = f"gui/{uid}/{process_manager.LAUNCHD_LABEL}" if uid is not None else process_manager.LAUNCHD_LABEL
|
|
209
|
+
res = subprocess.run(["launchctl", "kickstart", "-k", target], capture_output=True, text=True)
|
|
210
|
+
if res.returncode == 0:
|
|
211
|
+
click.echo("Service restarted (launchd).")
|
|
212
|
+
return
|
|
213
|
+
stop_res = subprocess.run(["launchctl", "stop", process_manager.LAUNCHD_LABEL], capture_output=True, text=True)
|
|
214
|
+
start_res = subprocess.run(["launchctl", "start", process_manager.LAUNCHD_LABEL], capture_output=True, text=True)
|
|
215
|
+
if start_res.returncode == 0:
|
|
216
|
+
click.echo("Service restarted (launchd, fallback path).")
|
|
217
|
+
else:
|
|
218
|
+
click.echo(f"Failed to restart launchd service: {start_res.stderr.strip() or stop_res.stderr.strip() or res.stderr.strip()}", err=True)
|
|
219
|
+
raise SystemExit(1)
|
|
220
|
+
return
|
|
221
|
+
if svc["kind"] == "systemd":
|
|
222
|
+
# If the unit exists at all, prefer systemctl restart — even if the
|
|
223
|
+
# unit is currently inactive/failed, restart will bring it up cleanly
|
|
224
|
+
# and avoid forking a competing detached agent.
|
|
225
|
+
subprocess.run(["systemctl", "--user", "restart", process_manager.SYSTEMD_UNIT], check=True)
|
|
226
|
+
click.echo("Service restarted (systemd).")
|
|
227
|
+
return
|
|
228
|
+
process_manager.stop()
|
|
229
|
+
subprocess.run([sys.executable, "-m", "remotedev_agent.cli", "start", "--detach"], check=False)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@cli.command("install-service")
|
|
233
|
+
def install_service():
|
|
234
|
+
"""Install as a background service (launchd on macOS, systemd on Linux)."""
|
|
235
|
+
import platform
|
|
236
|
+
system = platform.system()
|
|
237
|
+
if system == "Darwin":
|
|
238
|
+
from remotedev_agent.service.macos import install
|
|
239
|
+
install()
|
|
240
|
+
elif system == "Linux":
|
|
241
|
+
from remotedev_agent.service.linux import install
|
|
242
|
+
install()
|
|
243
|
+
else:
|
|
244
|
+
click.echo(f"Unsupported platform: {system}", err=True)
|
|
245
|
+
raise SystemExit(1)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@cli.command("uninstall-service")
|
|
249
|
+
def uninstall_service():
|
|
250
|
+
"""Uninstall the background service."""
|
|
251
|
+
import platform
|
|
252
|
+
system = platform.system()
|
|
253
|
+
if system == "Darwin":
|
|
254
|
+
from remotedev_agent.service.macos import uninstall
|
|
255
|
+
uninstall()
|
|
256
|
+
elif system == "Linux":
|
|
257
|
+
from remotedev_agent.service.linux import uninstall
|
|
258
|
+
uninstall()
|
|
259
|
+
else:
|
|
260
|
+
click.echo(f"Unsupported platform: {system}", err=True)
|
|
261
|
+
raise SystemExit(1)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
cli()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
CONFIG_DIR = Path.home() / ".remotedev"
|
|
7
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class AgentConfig:
|
|
12
|
+
relay_url: str
|
|
13
|
+
machine_id: str
|
|
14
|
+
token: str
|
|
15
|
+
sandboxes_root: str = ""
|
|
16
|
+
|
|
17
|
+
def __post_init__(self):
|
|
18
|
+
if not self.sandboxes_root:
|
|
19
|
+
self.sandboxes_root = str(Path.home() / "Documents")
|
|
20
|
+
|
|
21
|
+
def save(self):
|
|
22
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
os.chmod(CONFIG_DIR, 0o700)
|
|
24
|
+
CONFIG_FILE.write_text(json.dumps({
|
|
25
|
+
"relay_url": self.relay_url,
|
|
26
|
+
"machine_id": self.machine_id,
|
|
27
|
+
"token": self.token,
|
|
28
|
+
"sandboxes_root": self.sandboxes_root,
|
|
29
|
+
}, indent=2))
|
|
30
|
+
os.chmod(CONFIG_FILE, 0o600)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def load(cls) -> "AgentConfig":
|
|
34
|
+
if not CONFIG_FILE.exists():
|
|
35
|
+
raise FileNotFoundError(f"Config not found at {CONFIG_FILE}. Run 'remotedev-agent setup' first.")
|
|
36
|
+
data = json.loads(CONFIG_FILE.read_text())
|
|
37
|
+
return cls(**data)
|