vastly 0.2.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.
@@ -0,0 +1,14 @@
1
+ *.claude
2
+
3
+ # User config (created on first run, never committed)
4
+ .vastly.json
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.pyc
9
+ *.pyo
10
+ *.egg-info/
11
+ dist/
12
+ build/
13
+ .venv/
14
+ .pytest_cache/
vastly-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: vastly
3
+ Version: 0.2.0
4
+ Summary: Connect to Vast.ai GPU instances -- sync SSH configs, set up your project, and open your IDE.
5
+ Project-URL: Repository, https://github.com/seamusfallows/vastly
6
+ Project-URL: Issues, https://github.com/seamusfallows/vastly/issues
7
+ Author: Seamus Fallows
8
+ Keywords: gpu,remote-development,ssh,vast.ai
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Vastly
24
+
25
+ Connect to Vast.ai GPU instances from your terminal: sync SSH configs, set up your project remotely, and open your IDE in one command.
26
+
27
+ ## Prerequisites
28
+
29
+ - Python 3.9+
30
+ - [Vast.ai CLI](https://vast.ai/docs/cli/getting-started) (`pip install vastai`) with API key configured
31
+ - Git
32
+ - SSH
33
+ - [VS Code](https://code.visualstudio.com) or [Cursor](https://cursor.com) with the Remote-SSH extension
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ pip install vastly
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ ```sh
44
+ cd your-project # any local git repo
45
+ vst # checks setup -> opens IDE (sets up on first run)
46
+ ```
47
+
48
+ ```sh
49
+ vst 1xRTX4090-TW # target a specific instance by name
50
+ vst --no-setup # open IDE on the remote without cloning or installing anything
51
+ vst --version # show version
52
+ ```
53
+
54
+ ## How It Works
55
+
56
+ ### 1. Sync
57
+
58
+ Calls the Vast.ai API and writes an SSH config for each running instance to `~/.ssh/vast.d/`. On first run, adds `Include vast.d/*` to `~/.ssh/config`.
59
+
60
+ Instances are named by GPU and region (e.g. `1xRTX4090-TW`, `2xA100-US`). Duplicates get the instance ID appended (`1xRTX4090-TW-12345`).
61
+
62
+ ### 2. Select
63
+
64
+ One instance is selected automatically. Multiple instances prompt you to pick one or select all. You can also pass the name directly: `vst 1xRTX4090-TW`.
65
+
66
+ ### 3. Setup (first run only)
67
+
68
+ For each selected instance, `vst` checks if the project has already been set up by looking for a marker file at `~/.vastly/setup/<repo>.json` on the instance. If the marker exists, it skips straight to opening the IDE.
69
+
70
+ On first run (no marker), `vst` reads the remote URL from your local git repo, copies a setup script to the instance, and runs it.
71
+
72
+ The setup script ([setup-remote.sh](src/vastly/data/setup-remote.sh)):
73
+
74
+ - Disables auto-tmux (on by default, configurable)
75
+ - Configures git identity from your local `git config`
76
+ - Adds `github.com` to SSH known hosts
77
+ - Clones your repo into the workspace
78
+ - Installs Python dependencies (auto-detected)
79
+ - Runs any configured post-install commands
80
+ - Writes VS Code settings and patches `.bashrc`
81
+ - Writes a setup marker (`~/.vastly/setup/<repo>.json`) so setup is skipped next time
82
+
83
+ **Dependency auto-detection** (checked in order):
84
+
85
+ 1. `uv.lock` or `[tool.uv]` in pyproject.toml -- `uv sync` (installs uv if needed)
86
+ 2. `[project]` in pyproject.toml -- `pip install -e .`
87
+ 3. `requirements*.txt` -- `pip install -r` for each file
88
+ 4. `setup.py` -- `pip install -e .`
89
+
90
+ Override with `installCommand` in your config.
91
+
92
+ ### 4. Open
93
+
94
+ Launches your IDE via Remote-SSH at the project directory. If already open, focuses the existing window.
95
+
96
+ ## Configuration
97
+
98
+ On first run, `vst` creates `~/.vastly.json` with defaults:
99
+
100
+ ```jsonc
101
+ {
102
+ // "code" (VS Code) or "cursor"
103
+ "ide": "code",
104
+
105
+ // Path to SSH private key. null = use your SSH config or ssh-agent
106
+ "sshKeyPath": null,
107
+
108
+ // SSH user on remote instances
109
+ "sshUser": "root",
110
+
111
+ // Ports to forward to localhost. Set to [] to disable
112
+ // Local ports auto-increment when multiple instances are running
113
+ "portForwards": [
114
+ { "local": 8080, "remote": 8080 }
115
+ ],
116
+
117
+ // Remote directory where projects are cloned
118
+ "workspace": "/workspace",
119
+
120
+ // Creates ~/.no_auto_tmux to prevent auto-tmux on Vast images
121
+ "disableAutoTmux": true,
122
+
123
+ // Which git remote to read the repo URL from
124
+ "gitRemote": "origin",
125
+
126
+ // Commands to run after dependency install
127
+ // e.g. ["curl -fsSL https://claude.ai/install.sh | bash"]
128
+ "postInstall": [],
129
+
130
+ // Override auto-detected install method. null = auto-detect
131
+ // e.g. "uv sync", "pip install -e '.[dev]'", "conda env update -f environment.yml"
132
+ "installCommand": null
133
+ }
134
+ ```
135
+
136
+ ## Troubleshooting
137
+
138
+ **"Missing: vastai CLI"** -- `pip install vastai`, then `vastai set api-key <key>`.
139
+
140
+ **SSH connection timeout** -- Instance may still be booting. Setup retries 3 times. Run `vastai show instances` to check status.
141
+
142
+ **"Not in a git repo"** -- `vst` reads the remote URL from your local repo. Run from inside a git repo, or use `--no-setup`.
vastly-0.2.0/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Vastly
2
+
3
+ Connect to Vast.ai GPU instances from your terminal: sync SSH configs, set up your project remotely, and open your IDE in one command.
4
+
5
+ ## Prerequisites
6
+
7
+ - Python 3.9+
8
+ - [Vast.ai CLI](https://vast.ai/docs/cli/getting-started) (`pip install vastai`) with API key configured
9
+ - Git
10
+ - SSH
11
+ - [VS Code](https://code.visualstudio.com) or [Cursor](https://cursor.com) with the Remote-SSH extension
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ pip install vastly
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```sh
22
+ cd your-project # any local git repo
23
+ vst # checks setup -> opens IDE (sets up on first run)
24
+ ```
25
+
26
+ ```sh
27
+ vst 1xRTX4090-TW # target a specific instance by name
28
+ vst --no-setup # open IDE on the remote without cloning or installing anything
29
+ vst --version # show version
30
+ ```
31
+
32
+ ## How It Works
33
+
34
+ ### 1. Sync
35
+
36
+ Calls the Vast.ai API and writes an SSH config for each running instance to `~/.ssh/vast.d/`. On first run, adds `Include vast.d/*` to `~/.ssh/config`.
37
+
38
+ Instances are named by GPU and region (e.g. `1xRTX4090-TW`, `2xA100-US`). Duplicates get the instance ID appended (`1xRTX4090-TW-12345`).
39
+
40
+ ### 2. Select
41
+
42
+ One instance is selected automatically. Multiple instances prompt you to pick one or select all. You can also pass the name directly: `vst 1xRTX4090-TW`.
43
+
44
+ ### 3. Setup (first run only)
45
+
46
+ For each selected instance, `vst` checks if the project has already been set up by looking for a marker file at `~/.vastly/setup/<repo>.json` on the instance. If the marker exists, it skips straight to opening the IDE.
47
+
48
+ On first run (no marker), `vst` reads the remote URL from your local git repo, copies a setup script to the instance, and runs it.
49
+
50
+ The setup script ([setup-remote.sh](src/vastly/data/setup-remote.sh)):
51
+
52
+ - Disables auto-tmux (on by default, configurable)
53
+ - Configures git identity from your local `git config`
54
+ - Adds `github.com` to SSH known hosts
55
+ - Clones your repo into the workspace
56
+ - Installs Python dependencies (auto-detected)
57
+ - Runs any configured post-install commands
58
+ - Writes VS Code settings and patches `.bashrc`
59
+ - Writes a setup marker (`~/.vastly/setup/<repo>.json`) so setup is skipped next time
60
+
61
+ **Dependency auto-detection** (checked in order):
62
+
63
+ 1. `uv.lock` or `[tool.uv]` in pyproject.toml -- `uv sync` (installs uv if needed)
64
+ 2. `[project]` in pyproject.toml -- `pip install -e .`
65
+ 3. `requirements*.txt` -- `pip install -r` for each file
66
+ 4. `setup.py` -- `pip install -e .`
67
+
68
+ Override with `installCommand` in your config.
69
+
70
+ ### 4. Open
71
+
72
+ Launches your IDE via Remote-SSH at the project directory. If already open, focuses the existing window.
73
+
74
+ ## Configuration
75
+
76
+ On first run, `vst` creates `~/.vastly.json` with defaults:
77
+
78
+ ```jsonc
79
+ {
80
+ // "code" (VS Code) or "cursor"
81
+ "ide": "code",
82
+
83
+ // Path to SSH private key. null = use your SSH config or ssh-agent
84
+ "sshKeyPath": null,
85
+
86
+ // SSH user on remote instances
87
+ "sshUser": "root",
88
+
89
+ // Ports to forward to localhost. Set to [] to disable
90
+ // Local ports auto-increment when multiple instances are running
91
+ "portForwards": [
92
+ { "local": 8080, "remote": 8080 }
93
+ ],
94
+
95
+ // Remote directory where projects are cloned
96
+ "workspace": "/workspace",
97
+
98
+ // Creates ~/.no_auto_tmux to prevent auto-tmux on Vast images
99
+ "disableAutoTmux": true,
100
+
101
+ // Which git remote to read the repo URL from
102
+ "gitRemote": "origin",
103
+
104
+ // Commands to run after dependency install
105
+ // e.g. ["curl -fsSL https://claude.ai/install.sh | bash"]
106
+ "postInstall": [],
107
+
108
+ // Override auto-detected install method. null = auto-detect
109
+ // e.g. "uv sync", "pip install -e '.[dev]'", "conda env update -f environment.yml"
110
+ "installCommand": null
111
+ }
112
+ ```
113
+
114
+ ## Troubleshooting
115
+
116
+ **"Missing: vastai CLI"** -- `pip install vastai`, then `vastai set api-key <key>`.
117
+
118
+ **SSH connection timeout** -- Instance may still be booting. Setup retries 3 times. Run `vastai show instances` to check status.
119
+
120
+ **"Not in a git repo"** -- `vst` reads the remote URL from your local repo. Run from inside a git repo, or use `--no-setup`.
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "vastly"
7
+ version = "0.2.0"
8
+ description = "Connect to Vast.ai GPU instances -- sync SSH configs, set up your project, and open your IDE."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [{ name = "Seamus Fallows" }]
12
+ keywords = ["vast.ai", "gpu", "ssh", "remote-development"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development",
25
+ ]
26
+
27
+ [project.urls]
28
+ Repository = "https://github.com/seamusfallows/vastly"
29
+ Issues = "https://github.com/seamusfallows/vastly/issues"
30
+
31
+ [project.scripts]
32
+ vst = "vastly.cli:main"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/vastly"]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,6 @@
1
+ """Allow running vastly as ``python -m vastly``."""
2
+
3
+ from vastly.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,124 @@
1
+ """CLI entry point for the `vst` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import shutil
7
+ import subprocess
8
+
9
+ from vastly import __version__
10
+ from vastly.config import load_config
11
+ from vastly.ide import check_ide, open_ide
12
+ from vastly.instance import get_synced_instances, select_instance, show_table
13
+ from vastly.remote import convert_to_ssh_url, setup_instances
14
+ from vastly.ssh import run_ssh
15
+
16
+
17
+ def _check_prerequisites(*, need_ide: bool = False, ide: str) -> bool:
18
+ """Verify required tools are available."""
19
+ ok = True
20
+
21
+ if not shutil.which("vastai"):
22
+ print("\033[31mMissing: vastai CLI. Install with: pip install vastai\033[0m")
23
+ ok = False
24
+ if not shutil.which("git"):
25
+ print("\033[31mMissing: git. Install from https://git-scm.com\033[0m")
26
+ ok = False
27
+ if not shutil.which("ssh"):
28
+ print("\033[31mMissing: ssh.\033[0m")
29
+ ok = False
30
+ if need_ide and not check_ide(ide):
31
+ urls = {"code": "https://code.visualstudio.com", "cursor": "https://cursor.com"}
32
+ url = urls.get(ide, "")
33
+ hint = f" Download from {url}" if url else ""
34
+ print(f"\033[31mMissing: {ide}.{hint}\033[0m")
35
+ ok = False
36
+
37
+ return ok
38
+
39
+
40
+ def _local_repo_info(git_remote: str) -> tuple[str, str] | None:
41
+ """Return (repo_url, repo_name) from the local git repo, or None."""
42
+ try:
43
+ repo_url = subprocess.run(
44
+ ["git", "remote", "get-url", git_remote],
45
+ capture_output=True, text=True,
46
+ ).stdout.strip()
47
+ except FileNotFoundError:
48
+ return None
49
+ if not repo_url:
50
+ return None
51
+ repo_url = convert_to_ssh_url(repo_url)
52
+ repo_name = repo_url.rsplit("/", 1)[-1].removesuffix(".git")
53
+ return repo_url, repo_name
54
+
55
+
56
+ def _connect(name: str | None, no_setup: bool) -> None:
57
+ """Main connect flow -- sync instances, check setup, run setup if needed, open IDE."""
58
+ config = load_config()
59
+
60
+ if not _check_prerequisites(need_ide=True, ide=config["ide"]):
61
+ return
62
+
63
+ instances = get_synced_instances(config)
64
+ if not instances:
65
+ return
66
+
67
+ show_table(instances)
68
+
69
+ selected = select_instance(instances, name)
70
+ if not selected:
71
+ return
72
+
73
+ repo_info = _local_repo_info(config["gitRemote"])
74
+
75
+ for inst in selected:
76
+ inst_name = inst["name"]
77
+
78
+ if no_setup or not repo_info:
79
+ # Skip setup -- open workspace root
80
+ if not repo_info and not no_setup:
81
+ print(f" \033[33mNot in a git repo. Tip: run vst from inside a git repo to auto-setup.\033[0m")
82
+ print(f" \033[32mOpening {config['workspace']}\033[0m")
83
+ open_ide(config["ide"], inst_name, config["workspace"])
84
+ continue
85
+
86
+ repo_url, repo_name = repo_info
87
+ remote_path = f"{config['workspace']}/{repo_name}"
88
+
89
+ # Check if project is already set up via marker file
90
+ result = run_ssh(inst_name, f"test -f ~/.vastly/setup/{repo_name}.json && echo done")
91
+
92
+ if result.returncode != 0:
93
+ print(f" \033[31m{inst_name}: unreachable via SSH.\033[0m")
94
+ continue
95
+
96
+ if result.stdout.strip() == "done":
97
+ print(f" \033[32mOpening {remote_path}\033[0m")
98
+ open_ide(config["ide"], inst_name, remote_path)
99
+ continue
100
+
101
+ # Not set up yet -- run setup
102
+ success = setup_instances([inst], repo_url, repo_name, config)
103
+ if success:
104
+ open_ide(config["ide"], inst_name, remote_path)
105
+
106
+
107
+ def main() -> None:
108
+ """Parse arguments and run the connect flow."""
109
+ parser = argparse.ArgumentParser(
110
+ prog="vst",
111
+ description="Connect to a Vast.ai instance -- sync SSH, set up your project, and open your IDE.",
112
+ epilog=(
113
+ "prerequisites: vastai CLI (pip install vastai), git, ssh, VS Code or Cursor\n"
114
+ "config: ~/.vastly.json (created on first run)\n"
115
+ "docs: https://github.com/seamusfallows/vastly"
116
+ ),
117
+ formatter_class=argparse.RawDescriptionHelpFormatter,
118
+ )
119
+ parser.add_argument("name", nargs="?", help="instance name (tab-complete from ~/.ssh/vast.d/)")
120
+ parser.add_argument("--no-setup", action="store_true", help="open IDE without cloning or installing")
121
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
122
+
123
+ args = parser.parse_args()
124
+ _connect(args.name, args.no_setup)
@@ -0,0 +1,61 @@
1
+ """Load and manage ~/.vastly.json configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from importlib import resources
8
+ from pathlib import Path
9
+
10
+ CONFIG_PATH = Path.home() / ".vastly.json"
11
+
12
+ DEFAULTS = {
13
+ "ide": "code",
14
+ "sshUser": "root",
15
+ "workspace": "/workspace",
16
+ "disableAutoTmux": True,
17
+ "gitRemote": "origin",
18
+ }
19
+
20
+
21
+ def load_config(path: Path | None = None) -> dict:
22
+ """Load config from disk, creating from template if missing.
23
+
24
+ Returns a dict with all keys populated (user values override defaults).
25
+ """
26
+ path = path or CONFIG_PATH
27
+
28
+ if not path.exists():
29
+ template = resources.files("vastly.data").joinpath(".vastly.template.json")
30
+ path.write_text(template.read_text(encoding="utf-8"), encoding="utf-8")
31
+ print(f"Created config at {path}")
32
+ print("Edit it to change IDE, SSH key, port forwarding, and more.")
33
+
34
+ try:
35
+ raw = json.loads(path.read_text(encoding="utf-8"))
36
+ except json.JSONDecodeError as e:
37
+ print(f"\033[31mInvalid JSON in {path}: {e}\033[0m")
38
+ print("Fix the file or delete it to regenerate from template.")
39
+ sys.exit(1)
40
+
41
+ port_forwards = raw.get("portForwards")
42
+ if port_forwards is None:
43
+ port_forwards = [{"local": 8080, "remote": 8080}]
44
+
45
+ post_install = raw.get("postInstall")
46
+ if isinstance(post_install, str):
47
+ post_install = [post_install]
48
+ elif not post_install:
49
+ post_install = []
50
+
51
+ return {
52
+ "ide": raw.get("ide") or DEFAULTS["ide"],
53
+ "sshKeyPath": raw.get("sshKeyPath"),
54
+ "sshUser": raw.get("sshUser") or DEFAULTS["sshUser"],
55
+ "portForwards": list(port_forwards),
56
+ "workspace": raw.get("workspace") or DEFAULTS["workspace"],
57
+ "disableAutoTmux": raw.get("disableAutoTmux", DEFAULTS["disableAutoTmux"]),
58
+ "gitRemote": raw.get("gitRemote") or DEFAULTS["gitRemote"],
59
+ "postInstall": list(post_install),
60
+ "installCommand": raw.get("installCommand"),
61
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "ide": "code",
3
+ "sshKeyPath": null,
4
+ "sshUser": "root",
5
+ "portForwards": [
6
+ { "local": 8080, "remote": 8080 }
7
+ ],
8
+ "workspace": "/workspace",
9
+ "disableAutoTmux": true,
10
+ "gitRemote": "origin",
11
+ "postInstall": [],
12
+ "installCommand": null
13
+ }
File without changes