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.
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/PKG-INFO +36 -6
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/README.md +33 -3
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/pyproject.toml +2 -2
- vuer_cli-0.0.5/src/vuer_cli/add.py +78 -0
- vuer_cli-0.0.5/src/vuer_cli/envs_publish.py +397 -0
- vuer_cli-0.0.5/src/vuer_cli/envs_pull.py +213 -0
- vuer_cli-0.0.5/src/vuer_cli/login.py +459 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/main.py +7 -2
- vuer_cli-0.0.5/src/vuer_cli/remove.py +97 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/scripts/demcap.py +19 -15
- vuer_cli-0.0.5/src/vuer_cli/scripts/mcap_playback.py +661 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/scripts/minimap.py +113 -210
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/scripts/viz_ptc_cams.py +1 -1
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/scripts/viz_ptc_proxie.py +1 -1
- vuer_cli-0.0.5/src/vuer_cli/sync.py +356 -0
- vuer_cli-0.0.5/src/vuer_cli/upgrade.py +144 -0
- vuer_cli-0.0.4/src/vuer_cli/add.py +0 -80
- vuer_cli-0.0.4/src/vuer_cli/envs_publish.py +0 -371
- vuer_cli-0.0.4/src/vuer_cli/envs_pull.py +0 -206
- vuer_cli-0.0.4/src/vuer_cli/remove.py +0 -97
- vuer_cli-0.0.4/src/vuer_cli/scripts/vuer_ros_bridge.py +0 -210
- vuer_cli-0.0.4/src/vuer_cli/sync.py +0 -350
- vuer_cli-0.0.4/src/vuer_cli/upgrade.py +0 -152
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/.gitignore +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/CLAUDE.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/CONTRIBUTING.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/LICENSE +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/commands/add.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/commands/index.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/commands/remove.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/commands/sync.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/commands/upgrade.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/concepts.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/index.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/introduction.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/overview.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/docs/publishing.md +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/__init__.py +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/mcap_extractor.py +0 -0
- {vuer_cli-0.0.4 → vuer_cli-0.0.5}/src/vuer_cli/scripts/ptc_utils.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
93
|
+
### Authentication
|
|
93
94
|
|
|
94
|
-
|
|
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
|
-
|
|
42
|
+
### Authentication
|
|
42
43
|
|
|
43
|
-
|
|
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.
|
|
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.
|
|
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}")
|