vuer-cli 0.0.4__tar.gz → 0.0.5__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.
Files changed (41) hide show
  1. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/PKG-INFO +36 -6
  2. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/README.md +33 -3
  3. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/pyproject.toml +2 -2
  4. vuer_cli-0.0.5/src/vuer_cli/add.py +78 -0
  5. vuer_cli-0.0.5/src/vuer_cli/envs_publish.py +397 -0
  6. vuer_cli-0.0.5/src/vuer_cli/envs_pull.py +213 -0
  7. vuer_cli-0.0.5/src/vuer_cli/login.py +459 -0
  8. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/main.py +7 -2
  9. vuer_cli-0.0.5/src/vuer_cli/remove.py +97 -0
  10. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/scripts/demcap.py +19 -15
  11. vuer_cli-0.0.5/src/vuer_cli/scripts/mcap_playback.py +661 -0
  12. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/scripts/minimap.py +113 -210
  13. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/scripts/viz_ptc_cams.py +1 -1
  14. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/scripts/viz_ptc_proxie.py +1 -1
  15. vuer_cli-0.0.5/src/vuer_cli/sync.py +356 -0
  16. vuer_cli-0.0.5/src/vuer_cli/upgrade.py +144 -0
  17. vuer_cli-0.0.4/src/vuer_cli/add.py +0 -80
  18. vuer_cli-0.0.4/src/vuer_cli/envs_publish.py +0 -371
  19. vuer_cli-0.0.4/src/vuer_cli/envs_pull.py +0 -206
  20. vuer_cli-0.0.4/src/vuer_cli/remove.py +0 -97
  21. vuer_cli-0.0.4/src/vuer_cli/scripts/vuer_ros_bridge.py +0 -210
  22. vuer_cli-0.0.4/src/vuer_cli/sync.py +0 -350
  23. vuer_cli-0.0.4/src/vuer_cli/upgrade.py +0 -152
  24. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/.gitignore +0 -0
  25. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/CLAUDE.md +0 -0
  26. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/CONTRIBUTING.md +0 -0
  27. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/LICENSE +0 -0
  28. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/commands/add.md +0 -0
  29. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/commands/index.md +0 -0
  30. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/commands/remove.md +0 -0
  31. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/commands/sync.md +0 -0
  32. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/commands/upgrade.md +0 -0
  33. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/concepts.md +0 -0
  34. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/index.md +0 -0
  35. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/introduction.md +0 -0
  36. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/overview.md +0 -0
  37. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/publishing.md +0 -0
  38. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/__init__.py +0 -0
  39. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/mcap_extractor.py +0 -0
  40. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/scripts/ptc_utils.py +0 -0
  41. {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vuer-cli
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: A Python CLI for Vuer, a real-time 3D visualization library
5
5
  Project-URL: Homepage, https://github.com/vuer-ai/vuer-cli
6
6
  Project-URL: Repository, https://github.com/vuer-ai/vuer-cli
@@ -27,7 +27,7 @@ Requires-Dist: mcap>=1.1.0; extra == 'all'
27
27
  Requires-Dist: numpy>=1.24.0; extra == 'all'
28
28
  Requires-Dist: opencv-python>=4.8.0; extra == 'all'
29
29
  Requires-Dist: pandas>=2.0.0; extra == 'all'
30
- Requires-Dist: vuer>=0.0.79; extra == 'all'
30
+ Requires-Dist: vuer>=0.0.81; extra == 'all'
31
31
  Provides-Extra: docs
32
32
  Requires-Dist: furo; extra == 'docs'
33
33
  Requires-Dist: myst-parser; extra == 'docs'
@@ -46,7 +46,7 @@ Provides-Extra: viz
46
46
  Requires-Dist: numpy>=1.24.0; extra == 'viz'
47
47
  Requires-Dist: opencv-python>=4.8.0; extra == 'viz'
48
48
  Requires-Dist: pandas>=2.0.0; extra == 'viz'
49
- Requires-Dist: vuer>=0.0.79; extra == 'viz'
49
+ Requires-Dist: vuer>=0.0.81; extra == 'viz'
50
50
  Description-Content-Type: text/markdown
51
51
 
52
52
  # Vuer Hub Environment Manager
@@ -80,6 +80,7 @@ uv add vuer-cli==0.0.4
80
80
  |----------------|-----------------------------------------------------------------------|
81
81
  | `vuer` | Show top-level help and list available commands |
82
82
  | `vuer --help` | Show detailed CLI help |
83
+ | `login` | Authenticate with Vuer Hub using OAuth Device Flow |
83
84
  | `sync` | Sync all environments from environment.json dependencies (like npm install) |
84
85
  | `add` | Add an environment dependency to environment.json |
85
86
  | `remove` | Remove an environment dependency from environment.json |
@@ -89,18 +90,47 @@ uv add vuer-cli==0.0.4
89
90
 
90
91
  ## Usage
91
92
 
92
- Quick start — configure environment variables
93
+ ### Authentication
93
94
 
94
- `VUER_HUB_URL` is required and has no default. Set it to the base URL of your Vuer Hub API.
95
+ The easiest way to authenticate is using the `login` command:
96
+
97
+ ```bash
98
+ # Set the Hub URL first
99
+ export VUER_HUB_URL="https://hub.vuer.ai/api"
100
+
101
+ # Login with dev environment (default)
102
+ vuer login
103
+
104
+ # Or login with production environment
105
+ vuer login --env production
106
+ ```
107
+
108
+ **After successful authentication:**
109
+ - ✅ Credentials are automatically saved and configured
110
+ - ✅ **Vuer CLI will automatically use the saved credentials** - no manual setup needed!
111
+ - ✅ Works immediately in the current terminal
112
+ - ✅ Works in all new terminals (auto-configured)
113
+ - ✅ Cross-platform support (Windows, macOS, Linux)
114
+
115
+ **How it works:**
116
+ 1. Saves credentials to `~/.vuer/credentials` (used automatically by CLI)
117
+ 2. Creates shell script at `~/.vuer/env.sh` (for manual use if needed)
118
+ 3. **Windows**: Sets user environment variables via `setx`
119
+ 4. **macOS/Linux**: Automatically adds to your shell config (~/.zshrc or ~/.bashrc)
120
+
121
+ **Manual setup (optional):**
122
+
123
+ If you prefer to set environment variables manually:
95
124
 
96
125
  ```bash
97
126
  export VUER_HUB_URL="https://hub.vuer.ai/api"
98
- # Optional: token for private hubs or authenticated operations
99
127
  export VUER_AUTH_TOKEN="eyJhbGci..."
100
128
  # Optional: enable dry-run mode to simulate operations (no network changes)
101
129
  export VUER_CLI_DRY_RUN="1"
102
130
  ```
103
131
 
132
+ ### Basic Commands
133
+
104
134
  ```bash
105
135
  # Sync all environments from environment.json dependencies
106
136
  # Reads environment.json in current directory, validates dependencies,
@@ -29,6 +29,7 @@ uv add vuer-cli==0.0.4
29
29
  |----------------|-----------------------------------------------------------------------|
30
30
  | `vuer` | Show top-level help and list available commands |
31
31
  | `vuer --help` | Show detailed CLI help |
32
+ | `login` | Authenticate with Vuer Hub using OAuth Device Flow |
32
33
  | `sync` | Sync all environments from environment.json dependencies (like npm install) |
33
34
  | `add` | Add an environment dependency to environment.json |
34
35
  | `remove` | Remove an environment dependency from environment.json |
@@ -38,18 +39,47 @@ uv add vuer-cli==0.0.4
38
39
 
39
40
  ## Usage
40
41
 
41
- Quick start — configure environment variables
42
+ ### Authentication
42
43
 
43
- `VUER_HUB_URL` is required and has no default. Set it to the base URL of your Vuer Hub API.
44
+ The easiest way to authenticate is using the `login` command:
45
+
46
+ ```bash
47
+ # Set the Hub URL first
48
+ export VUER_HUB_URL="https://hub.vuer.ai/api"
49
+
50
+ # Login with dev environment (default)
51
+ vuer login
52
+
53
+ # Or login with production environment
54
+ vuer login --env production
55
+ ```
56
+
57
+ **After successful authentication:**
58
+ - ✅ Credentials are automatically saved and configured
59
+ - ✅ **Vuer CLI will automatically use the saved credentials** - no manual setup needed!
60
+ - ✅ Works immediately in the current terminal
61
+ - ✅ Works in all new terminals (auto-configured)
62
+ - ✅ Cross-platform support (Windows, macOS, Linux)
63
+
64
+ **How it works:**
65
+ 1. Saves credentials to `~/.vuer/credentials` (used automatically by CLI)
66
+ 2. Creates shell script at `~/.vuer/env.sh` (for manual use if needed)
67
+ 3. **Windows**: Sets user environment variables via `setx`
68
+ 4. **macOS/Linux**: Automatically adds to your shell config (~/.zshrc or ~/.bashrc)
69
+
70
+ **Manual setup (optional):**
71
+
72
+ If you prefer to set environment variables manually:
44
73
 
45
74
  ```bash
46
75
  export VUER_HUB_URL="https://hub.vuer.ai/api"
47
- # Optional: token for private hubs or authenticated operations
48
76
  export VUER_AUTH_TOKEN="eyJhbGci..."
49
77
  # Optional: enable dry-run mode to simulate operations (no network changes)
50
78
  export VUER_CLI_DRY_RUN="1"
51
79
  ```
52
80
 
81
+ ### Basic Commands
82
+
53
83
  ```bash
54
84
  # Sync all environments from environment.json dependencies
55
85
  # Reads environment.json in current directory, validates dependencies,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vuer-cli"
7
- version = "0.0.4"
7
+ version = "0.0.5"
8
8
  description = "A Python CLI for Vuer, a real-time 3D visualization library"
9
9
  readme = { file = "README.md", "content-type" = "text/markdown" }
10
10
  license = { text = "MIT" }
@@ -43,7 +43,7 @@ mcap = [
43
43
  "opencv-python>=4.8.0",
44
44
  ]
45
45
  viz = [
46
- "vuer>=0.0.79",
46
+ "vuer>=0.0.81",
47
47
  "pandas>=2.0.0",
48
48
  "numpy>=1.24.0",
49
49
  "opencv-python>=4.8.0",
@@ -0,0 +1,78 @@
1
+ """Add command - add an environment spec to environment.json then sync."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from params_proto import proto
7
+
8
+ from .sync import Sync, read_environments_lock
9
+ from .utils import normalize_env_spec, parse_env_spec, print_error
10
+
11
+
12
+ @proto
13
+ class Add:
14
+ """Add an environment to environment.json and run `vuer sync`.
15
+
16
+ Example:
17
+ vuer add some-environment/v1.2.3
18
+ """
19
+
20
+ # Required positional arg: environment spec, e.g. "some-environment/v1.2.3"
21
+ env: str
22
+
23
+ def __call__(self) -> int:
24
+ """Execute add command."""
25
+ try:
26
+ env_spec = self.env
27
+
28
+ name, version = parse_env_spec(env_spec)
29
+ env_spec_normalized = normalize_env_spec(f"{name}/{version}")
30
+
31
+ cwd = Path.cwd()
32
+ lock_path = cwd / "environments-lock.yaml"
33
+
34
+ # Step 2: Check if already present in environments-lock.yaml
35
+ if lock_path.exists():
36
+ existing_deps = read_environments_lock(lock_path)
37
+ if env_spec_normalized in existing_deps:
38
+ print(
39
+ f"[INFO] Environment {env_spec_normalized} already present in {lock_path}"
40
+ )
41
+ return 0
42
+
43
+ # Step 3: Ensure environment.json has this dependency, then run sync
44
+ env_json_path = cwd / "environment.json"
45
+ if env_json_path.exists():
46
+ with env_json_path.open("r", encoding="utf-8") as f:
47
+ try:
48
+ data = json.load(f)
49
+ except json.JSONDecodeError as e:
50
+ raise ValueError(f"Invalid environment.json: {e}") from e
51
+ else:
52
+ data = {}
53
+
54
+ deps = data.get("dependencies")
55
+ if deps is None:
56
+ deps = {}
57
+ if not isinstance(deps, dict):
58
+ raise ValueError("environment.json 'dependencies' field must be an object")
59
+
60
+ # Add or update the dependency
61
+ deps[name] = version
62
+ data["dependencies"] = deps
63
+
64
+ with env_json_path.open("w", encoding="utf-8") as f:
65
+ json.dump(data, f, indent=2, ensure_ascii=False)
66
+ f.write("\n")
67
+
68
+ print(
69
+ f"[INFO] Added {env_spec_normalized} to environment.json dependencies. Running sync..."
70
+ )
71
+ return Sync().run()
72
+
73
+ except (FileNotFoundError, ValueError, RuntimeError) as e:
74
+ print_error(str(e))
75
+ return 1
76
+ except Exception as e:
77
+ print_error(f"Unexpected error: {e}")
78
+ return 1
@@ -0,0 +1,397 @@
1
+ """EnvsPublish command - publish an environment version (npm-style workflow)."""
2
+
3
+ import json
4
+ import tarfile
5
+ import tempfile
6
+ import threading
7
+ import time
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List
11
+
12
+ from params_proto import EnvVar, proto
13
+
14
+ from .utils import is_dry_run, normalize_env_spec, print_error, spinner
15
+
16
+ # -- Configuration with environment variable defaults --
17
+
18
+
19
+ @proto.prefix
20
+ class Hub:
21
+ """Vuer Hub connection settings."""
22
+
23
+ url: str = EnvVar("VUER_HUB_URL", default="") # Base URL of the Vuer Hub API
24
+ auth_token: str = EnvVar(
25
+ "VUER_AUTH_TOKEN", default=""
26
+ ) # JWT token for authentication
27
+
28
+ @staticmethod
29
+ def get_auth_token() -> str:
30
+ """Get auth token from environment variable or credentials file.
31
+
32
+ Returns:
33
+ Auth token string, or empty string if not found
34
+ """
35
+ # First try environment variable
36
+ if Hub.auth_token:
37
+ return Hub.auth_token
38
+
39
+ # Fall back to credentials file
40
+ from .login import load_credentials
41
+ credentials = load_credentials()
42
+ return credentials.get("access_token", "")
43
+
44
+
45
+ # -- Subcommand dataclass --
46
+
47
+
48
+ @dataclass
49
+ class EnvsPublish:
50
+ """Publish environment to registry (npm-style).
51
+
52
+ Reads environment.json, creates tgz archive, and uploads to the hub.
53
+ """
54
+
55
+ directory: str = "." # Directory containing environment.json
56
+ timeout: int = 300 # Request timeout in seconds
57
+ tag: str = "latest" # Version tag
58
+ dry_run: bool = False # Simulate without uploading
59
+
60
+ def __call__(self) -> int:
61
+ """Execute envs-publish command."""
62
+ try:
63
+ dry_run = self.dry_run or is_dry_run()
64
+
65
+ # Get auth token (from env or credentials file)
66
+ auth_token = Hub.get_auth_token() if not dry_run else ""
67
+
68
+ if not dry_run:
69
+ if not Hub.url:
70
+ raise RuntimeError(
71
+ "Missing VUER_HUB_URL. Please set the VUER_HUB_URL environment variable "
72
+ "or pass --hub.url on the command line."
73
+ )
74
+ if not auth_token:
75
+ raise RuntimeError(
76
+ "Missing VUER_AUTH_TOKEN. Please run 'vuer login' to authenticate, "
77
+ "or set the VUER_AUTH_TOKEN environment variable."
78
+ )
79
+
80
+ print(f"[INFO] Reading environment.json from {self.directory}...")
81
+ metadata, envs_metadata = parse_environments_json(self.directory)
82
+ print(f"[INFO] Found package: {metadata['name']}/{metadata['version']}")
83
+
84
+ # Validate dependencies if present
85
+ dependencies = extract_dependencies(envs_metadata)
86
+ if dependencies:
87
+ print(f"[INFO] Validating {len(dependencies)} dependencies...")
88
+ validate_dependencies(dependencies, dry_run, Hub.url, auth_token)
89
+ print("[INFO] All dependencies are valid.")
90
+ else:
91
+ print("[INFO] No dependencies to validate.")
92
+
93
+ print("[INFO] Creating tgz archive...")
94
+ archive_path = create_tgz_archive(self.directory, metadata)
95
+ print(f"[INFO] Archive created: {archive_path}")
96
+
97
+ publish_to_registry(
98
+ archive_path=archive_path,
99
+ metadata=metadata,
100
+ envs_metadata=envs_metadata,
101
+ hub_url=Hub.url,
102
+ auth_token=auth_token,
103
+ timeout=self.timeout,
104
+ dry_run=dry_run,
105
+ )
106
+
107
+ return 0
108
+ except FileNotFoundError as e:
109
+ print_error(str(e))
110
+ return 1
111
+ except ValueError as e:
112
+ print_error(str(e))
113
+ return 1
114
+ except RuntimeError as e:
115
+ # RuntimeError from validate_dependencies already prints error message
116
+ # Only print if it wasn't already printed
117
+ if "Dependency validation failed" not in str(e):
118
+ print_error(str(e))
119
+ return 1
120
+ except Exception as e:
121
+ print_error(f"Unexpected error: {e}")
122
+ return 1
123
+
124
+
125
+ # -- Helper functions --
126
+
127
+
128
+ def parse_environments_json(directory: str) -> tuple[Dict[str, Any], Dict[str, Any]]:
129
+ """Parse environment.json and extract metadata plus full content.
130
+
131
+ Returns:
132
+ (metadata, full_data)
133
+ """
134
+ envs_path = Path(directory) / "environment.json"
135
+ if not envs_path.exists():
136
+ raise FileNotFoundError(f"environment.json not found in {directory}")
137
+
138
+ try:
139
+ with envs_path.open("r", encoding="utf-8") as f:
140
+ data = json.load(f)
141
+ except json.JSONDecodeError as e:
142
+ raise ValueError(f"Invalid environment.json: {e}") from e
143
+
144
+ metadata = {
145
+ "name": data.get("name", ""),
146
+ "version": data.get("version", ""),
147
+ "description": data.get("description", ""),
148
+ "visibility": data.get("visibility", "PUBLIC"),
149
+ "env_type": data.get("env-type", "") or data.get("env_type", ""),
150
+ }
151
+
152
+ if not metadata["name"]:
153
+ raise ValueError("environment.json must contain 'name' field")
154
+ if not metadata["version"]:
155
+ raise ValueError("environment.json must contain 'version' field")
156
+
157
+ return metadata, data
158
+
159
+
160
+ def extract_dependencies(envs_metadata: Dict[str, Any]) -> List[str]:
161
+ """Extract dependencies from environment.json and convert to list format.
162
+
163
+ Args:
164
+ envs_metadata: Full environment.json content
165
+
166
+ Returns:
167
+ List of dependency specs like ["some-dependency/^1.2.3", ...]
168
+ Returns empty list if no dependencies or dependencies is empty.
169
+ """
170
+ deps_dict = envs_metadata.get("dependencies", {})
171
+ if not deps_dict or not isinstance(deps_dict, dict):
172
+ return []
173
+
174
+ dependencies = []
175
+ for name, version_spec in deps_dict.items():
176
+ if not isinstance(version_spec, str):
177
+ version_spec = str(version_spec)
178
+ dependencies.append(normalize_env_spec(f"{name}/{version_spec}"))
179
+
180
+ return dependencies
181
+
182
+
183
+ def validate_dependencies(
184
+ dependencies: List[str],
185
+ dry_run: bool,
186
+ hub_url: str,
187
+ auth_token: str,
188
+ ) -> None:
189
+ """Validate dependencies with backend API.
190
+
191
+ Args:
192
+ dependencies: List of dependency specs like ["name/version", ...]
193
+ dry_run: Whether to run in dry-run mode
194
+ hub_url: Vuer Hub base URL
195
+ auth_token: Authentication token
196
+
197
+ Raises:
198
+ RuntimeError: If validation fails (non-200 status or error in response)
199
+ """
200
+ if dry_run or is_dry_run():
201
+ print("[INFO] (dry-run) Validating dependencies (simulated)...")
202
+ return
203
+
204
+ if not hub_url:
205
+ raise RuntimeError(
206
+ "Missing VUER_HUB_URL. Cannot validate dependencies without hub URL."
207
+ )
208
+
209
+ import requests
210
+
211
+ url = f"{hub_url.rstrip('/')}/environments/dependencies"
212
+ headers = {}
213
+ if auth_token:
214
+ headers["Authorization"] = f"Bearer {auth_token}"
215
+ headers["Content-Type"] = "application/json"
216
+
217
+ payload = {"name_versionId_list": dependencies}
218
+
219
+ try:
220
+ response = requests.post(url, json=payload, headers=headers, timeout=300)
221
+ except requests.exceptions.RequestException as e:
222
+ raise RuntimeError(f"Failed to validate dependencies: {e}") from e
223
+
224
+ status = response.status_code
225
+
226
+ # Handle non-200 status codes
227
+ if status != 200:
228
+ error_msg = ""
229
+ try:
230
+ data = response.json()
231
+ if isinstance(data, dict):
232
+ error_msg = data.get("error") or data.get("message", "")
233
+ if not error_msg:
234
+ error_msg = json.dumps(data, ensure_ascii=False)
235
+ else:
236
+ error_msg = json.dumps(data, ensure_ascii=False)
237
+ except Exception:
238
+ text = (response.text or "").strip()
239
+ error_msg = text if text else "Unknown error"
240
+
241
+ if error_msg:
242
+ print_error(f"Dependency validation failed ({status}): {error_msg}")
243
+ else:
244
+ print_error(f"Dependency validation failed ({status})")
245
+ raise RuntimeError(f"Dependency validation failed with status {status}")
246
+
247
+ # Status 200: check for error field in response body
248
+ try:
249
+ data = response.json()
250
+ if isinstance(data, dict) and "error" in data:
251
+ error_msg = data["error"]
252
+ print_error(f"Dependency validation failed: {error_msg}")
253
+ raise RuntimeError(f"Dependency validation failed: {error_msg}")
254
+ except (json.JSONDecodeError, ValueError):
255
+ # Response is not JSON or doesn't have error field, assume success
256
+ pass
257
+
258
+
259
+ def create_tgz_archive(directory: str, metadata: Dict[str, Any]) -> str:
260
+ """Create a tgz archive from environment files."""
261
+ archive_name = f"{metadata['name']}-{metadata['version']}.tgz"
262
+ temp_dir = Path(tempfile.gettempdir())
263
+ archive_path = str(temp_dir / archive_name)
264
+
265
+ directory_path = Path(directory).resolve()
266
+
267
+ with tarfile.open(archive_path, "w:gz") as tar:
268
+ for file_path in directory_path.rglob("*"):
269
+ if file_path.is_file():
270
+ arcname = file_path.relative_to(directory_path)
271
+ tar.add(file_path, arcname=arcname)
272
+
273
+ return archive_path
274
+
275
+
276
+ def upload_with_progress(
277
+ archive_path: str, metadata: Dict[str, Any], timeout: int
278
+ ) -> None:
279
+ """Simulate an upload in dry-run mode."""
280
+ file_path = Path(archive_path)
281
+ total_size = file_path.stat().st_size
282
+ print(f"[INFO] (dry-run) Uploading {file_path.name} ({total_size} bytes)...")
283
+ time.sleep(min(2.0, max(0.1, total_size / (10 * 1024 * 1024))))
284
+
285
+
286
+ def publish_to_registry(
287
+ archive_path: str,
288
+ metadata: Dict[str, Any],
289
+ envs_metadata: Dict[str, Any],
290
+ hub_url: str,
291
+ auth_token: str,
292
+ timeout: int,
293
+ dry_run: bool,
294
+ ) -> None:
295
+ """Publish package to registry via API."""
296
+ print(f"[INFO] Publishing {metadata['name']}/{metadata['version']} to registry...")
297
+ print(f"[INFO] Archive: {archive_path}")
298
+ print(f"[INFO] Metadata: {json.dumps(metadata, indent=2)}")
299
+ print(f"[INFO] environment.json: {json.dumps(envs_metadata, indent=2)}")
300
+ print(f"[INFO] Hub URL: {hub_url}")
301
+ print(f"[INFO] Timeout: {timeout}s")
302
+
303
+ if dry_run or is_dry_run():
304
+ upload_with_progress(archive_path, metadata, timeout)
305
+ print(
306
+ f"[SUCCESS] (dry-run) Published {metadata['name']}/{metadata['version']} (no network call)."
307
+ )
308
+ return
309
+
310
+ # Import requests lazily to avoid SSL/cert loading in restricted envs.
311
+ import requests
312
+
313
+ url = f"{hub_url.rstrip('/')}/environments/upload"
314
+ file_path = Path(archive_path)
315
+
316
+ with file_path.open("rb") as f:
317
+ files = {
318
+ "package": (file_path.name, f, "application/octet-stream"),
319
+ }
320
+ data = {
321
+ "name": str(metadata["name"]),
322
+ "versionId": str(metadata["version"]),
323
+ "description": str(metadata.get("description", "")),
324
+ "type": str(metadata.get("env_type", "")),
325
+ "visibility": str(metadata.get("visibility", "PUBLIC")),
326
+ }
327
+ # Send full environment.json content as metadata field.
328
+ data["metadata"] = json.dumps(envs_metadata, ensure_ascii=False)
329
+
330
+ headers = {}
331
+ if auth_token:
332
+ headers["Authorization"] = f"Bearer {auth_token}"
333
+
334
+ stop_event = threading.Event()
335
+ spinner_thread = threading.Thread(
336
+ target=spinner,
337
+ args=(f"[INFO] Uploading {file_path.name} ", stop_event),
338
+ daemon=True,
339
+ )
340
+ spinner_thread.start()
341
+ try:
342
+ response = requests.post(
343
+ url,
344
+ data=data,
345
+ files=files,
346
+ headers=headers,
347
+ timeout=timeout,
348
+ )
349
+ finally:
350
+ stop_event.set()
351
+ spinner_thread.join()
352
+
353
+ status = response.status_code
354
+ text = (response.text or "").strip()
355
+
356
+ if status >= 300:
357
+ inline_msg = ""
358
+ try:
359
+ data = response.json()
360
+ if isinstance(data, dict):
361
+ msg = data.get("message")
362
+ err = data.get("error")
363
+ if msg:
364
+ inline_msg = str(msg)
365
+ elif err:
366
+ inline_msg = str(err)
367
+ else:
368
+ inline_msg = json.dumps(data, ensure_ascii=False)
369
+ else:
370
+ inline_msg = json.dumps(data, ensure_ascii=False)
371
+ except Exception:
372
+ inline_msg = text
373
+
374
+ inline_msg = (inline_msg or "").strip()
375
+ if inline_msg:
376
+ raise RuntimeError(f"Publish failed ({status}): {inline_msg}")
377
+ raise RuntimeError(f"Publish failed ({status})")
378
+
379
+ env_id = None
380
+ env_name = metadata.get("name")
381
+ env_version = metadata.get("version")
382
+ try:
383
+ payload = response.json()
384
+ env = payload.get("environment", payload) if isinstance(payload, dict) else {}
385
+ env_id = env.get("environmentId") or env.get("id")
386
+ env_name = env.get("name", env_name)
387
+ env_version = env.get("versionId", env_version)
388
+ except Exception:
389
+ pass
390
+
391
+ print("\n=== Publish Success ===")
392
+ if env_id:
393
+ print(f"ID : {env_id}")
394
+ print(f"Name : {env_name}")
395
+ print(f"Version : {env_version}")
396
+ visibility = metadata.get("visibility", "PUBLIC")
397
+ print(f"Visibility: {visibility}")