vuer-cli 0.0.4__py3-none-any.whl → 0.0.6__py3-none-any.whl
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/add.py +66 -68
- vuer_cli/envs_publish.py +335 -309
- vuer_cli/envs_pull.py +177 -170
- vuer_cli/login.py +459 -0
- vuer_cli/main.py +7 -2
- vuer_cli/remove.py +84 -84
- vuer_cli/scripts/demcap.py +19 -15
- vuer_cli/scripts/mcap_playback.py +661 -0
- vuer_cli/scripts/minimap.py +113 -210
- vuer_cli/scripts/viz_ptc_cams.py +1 -1
- vuer_cli/scripts/viz_ptc_proxie.py +1 -1
- vuer_cli/sync.py +314 -308
- vuer_cli/upgrade.py +118 -126
- {vuer_cli-0.0.4.dist-info → vuer_cli-0.0.6.dist-info}/METADATA +38 -9
- vuer_cli-0.0.6.dist-info/RECORD +22 -0
- vuer_cli/scripts/vuer_ros_bridge.py +0 -210
- vuer_cli-0.0.4.dist-info/RECORD +0 -21
- {vuer_cli-0.0.4.dist-info → vuer_cli-0.0.6.dist-info}/WHEEL +0 -0
- {vuer_cli-0.0.4.dist-info → vuer_cli-0.0.6.dist-info}/entry_points.txt +0 -0
- {vuer_cli-0.0.4.dist-info → vuer_cli-0.0.6.dist-info}/licenses/LICENSE +0 -0
vuer_cli/login.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Login command - authenticate with Vuer Hub using OAuth Device Flow."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
|
|
11
|
+
from .utils import print_error
|
|
12
|
+
|
|
13
|
+
# Environment URLs mapping
|
|
14
|
+
ENV_URLS = {
|
|
15
|
+
"dev": "https://staging-auth.vuer.ai",
|
|
16
|
+
"production": "https://auth.vuer.ai", # Update with actual production URL
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Login:
|
|
22
|
+
"""Authenticate with Vuer Hub using OAuth Device Flow.
|
|
23
|
+
|
|
24
|
+
Opens browser for user authentication and saves credentials locally.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
env: str = "dev" # Environment to authenticate with (dev or production)
|
|
28
|
+
|
|
29
|
+
def __call__(self) -> int:
|
|
30
|
+
"""Execute login command."""
|
|
31
|
+
try:
|
|
32
|
+
# Validate environment
|
|
33
|
+
if self.env not in ENV_URLS:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Invalid environment '{self.env}'. Must be one of: {', '.join(ENV_URLS.keys())}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
auth_url = ENV_URLS[self.env]
|
|
39
|
+
print(f"[INFO] Authenticating with {self.env} environment...")
|
|
40
|
+
print(f"[INFO] Auth server: {auth_url}")
|
|
41
|
+
|
|
42
|
+
# Generate device secret hash (random string for this session)
|
|
43
|
+
device_secret_hash = secrets.token_urlsafe(16)
|
|
44
|
+
|
|
45
|
+
# Step 1: Start device flow
|
|
46
|
+
print("[INFO] Starting device authentication flow...")
|
|
47
|
+
device_data = start_device_flow(auth_url, device_secret_hash)
|
|
48
|
+
|
|
49
|
+
user_code = device_data["user_code"]
|
|
50
|
+
verification_uri = device_data["verification_uri"]
|
|
51
|
+
verification_uri_complete = device_data.get(
|
|
52
|
+
"verification_uri_complete")
|
|
53
|
+
polling_url = device_data["polling_url"]
|
|
54
|
+
expires_in = device_data["expires_in"]
|
|
55
|
+
interval = device_data.get("interval", 5)
|
|
56
|
+
|
|
57
|
+
# Step 2: Display instructions to user
|
|
58
|
+
print("\n" + "=" * 60)
|
|
59
|
+
print("AUTHENTICATION REQUIRED")
|
|
60
|
+
print("=" * 60)
|
|
61
|
+
print(f"\nPlease visit the following URL to authenticate:")
|
|
62
|
+
print(f"\n {verification_uri}\n")
|
|
63
|
+
print(f"And enter this code: {user_code}")
|
|
64
|
+
print(f"\nThis code will expire in {expires_in} seconds.")
|
|
65
|
+
print("=" * 60 + "\n")
|
|
66
|
+
|
|
67
|
+
# Step 3: Poll for authentication
|
|
68
|
+
print("[INFO] Waiting for authentication...")
|
|
69
|
+
print("[INFO] (Press Ctrl+C to cancel)\n")
|
|
70
|
+
|
|
71
|
+
token_data = poll_for_token(
|
|
72
|
+
polling_url=polling_url,
|
|
73
|
+
device_secret_hash=device_secret_hash,
|
|
74
|
+
interval=interval,
|
|
75
|
+
expires_in=expires_in,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
access_token = token_data["access_token"]
|
|
79
|
+
refresh_token = token_data.get("refresh_token", "")
|
|
80
|
+
token_type = token_data.get("token_type", "Bearer")
|
|
81
|
+
|
|
82
|
+
# Step 4: Save credentials to environment file
|
|
83
|
+
env_file_path = save_credentials(
|
|
84
|
+
access_token=access_token,
|
|
85
|
+
refresh_token=refresh_token,
|
|
86
|
+
token_type=token_type,
|
|
87
|
+
environment=self.env,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Step 5: Set environment variables globally (platform-specific)
|
|
91
|
+
set_global_env_vars(access_token, refresh_token)
|
|
92
|
+
|
|
93
|
+
# Step 6: Automatically add to shell config (Unix-like systems)
|
|
94
|
+
shell_config_updated = auto_configure_shell(env_file_path)
|
|
95
|
+
|
|
96
|
+
print("\n" + "=" * 60)
|
|
97
|
+
print("AUTHENTICATION SUCCESSFUL")
|
|
98
|
+
print("=" * 60)
|
|
99
|
+
print("\nCredentials have been saved and configured globally.")
|
|
100
|
+
print("\nVuer CLI will automatically use these credentials.")
|
|
101
|
+
print("You can now use other Vuer CLI commands immediately!")
|
|
102
|
+
|
|
103
|
+
if shell_config_updated:
|
|
104
|
+
print(f"\nShell configuration updated: {shell_config_updated}")
|
|
105
|
+
print("Environment variables will be available in new terminal sessions.")
|
|
106
|
+
|
|
107
|
+
print("\n" + "=" * 60 + "\n")
|
|
108
|
+
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
except KeyboardInterrupt:
|
|
112
|
+
print("\n\n[INFO] Authentication cancelled by user.")
|
|
113
|
+
return 1
|
|
114
|
+
except ValueError as e:
|
|
115
|
+
print_error(str(e))
|
|
116
|
+
return 1
|
|
117
|
+
except RuntimeError as e:
|
|
118
|
+
print_error(str(e))
|
|
119
|
+
return 1
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print_error(f"Unexpected error during authentication: {e}")
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def start_device_flow(auth_url: str, device_secret_hash: str) -> Dict[
|
|
126
|
+
str, Any]:
|
|
127
|
+
"""Start the device flow and get verification details.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
auth_url: Base URL of the authentication server
|
|
131
|
+
device_secret_hash: Random secret hash for this device session
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Device flow data including user_code, verification_uri, polling_url, etc.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
RuntimeError: If the API call fails
|
|
138
|
+
"""
|
|
139
|
+
import requests
|
|
140
|
+
|
|
141
|
+
url = f"{auth_url.rstrip('/')}/api/device-flow/start"
|
|
142
|
+
payload = {
|
|
143
|
+
"client_id": "vuer-cli",
|
|
144
|
+
"device_secret_hash": device_secret_hash,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
response = requests.post(url, json=payload, timeout=30)
|
|
149
|
+
response.raise_for_status()
|
|
150
|
+
except requests.exceptions.RequestException as e:
|
|
151
|
+
raise RuntimeError(f"Failed to start device flow: {e}") from e
|
|
152
|
+
|
|
153
|
+
data = response.json()
|
|
154
|
+
|
|
155
|
+
# Validate response
|
|
156
|
+
required_fields = ["user_code", "verification_uri", "polling_url",
|
|
157
|
+
"expires_in"]
|
|
158
|
+
missing_fields = [f for f in required_fields if f not in data]
|
|
159
|
+
if missing_fields:
|
|
160
|
+
raise RuntimeError(
|
|
161
|
+
f"Invalid response from auth server: missing fields {missing_fields}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return data
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def poll_for_token(
|
|
168
|
+
polling_url: str,
|
|
169
|
+
device_secret_hash: str,
|
|
170
|
+
interval: int,
|
|
171
|
+
expires_in: int,
|
|
172
|
+
) -> Dict[str, Any]:
|
|
173
|
+
"""Poll the authentication server until user completes authentication.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
polling_url: URL to poll for token
|
|
177
|
+
device_secret_hash: Same secret hash used in start_device_flow
|
|
178
|
+
interval: Seconds to wait between polling attempts
|
|
179
|
+
expires_in: Total seconds before the code expires
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Token data including access_token, refresh_token, token_type
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
RuntimeError: If authentication fails or times out
|
|
186
|
+
"""
|
|
187
|
+
import requests
|
|
188
|
+
|
|
189
|
+
payload = {
|
|
190
|
+
"client_id": "vuer-cli",
|
|
191
|
+
"device_secret_hash": device_secret_hash,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
start_time = time.time()
|
|
195
|
+
attempt = 0
|
|
196
|
+
|
|
197
|
+
while True:
|
|
198
|
+
# Check if expired
|
|
199
|
+
elapsed = time.time() - start_time
|
|
200
|
+
if elapsed >= expires_in:
|
|
201
|
+
raise RuntimeError(
|
|
202
|
+
"Authentication timeout: verification code expired. Please try again."
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
attempt += 1
|
|
206
|
+
dots = "." * (attempt % 4)
|
|
207
|
+
remaining = int(expires_in - elapsed)
|
|
208
|
+
print(
|
|
209
|
+
f"\r[INFO] Waiting for authentication{dots:<3} ({remaining}s remaining)",
|
|
210
|
+
end="",
|
|
211
|
+
flush=True,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
response = requests.post(polling_url, json=payload, timeout=30)
|
|
216
|
+
|
|
217
|
+
# Successful authentication
|
|
218
|
+
if response.status_code == 200:
|
|
219
|
+
print("\n") # New line after polling messages
|
|
220
|
+
data = response.json()
|
|
221
|
+
|
|
222
|
+
# Validate response
|
|
223
|
+
if "access_token" not in data:
|
|
224
|
+
raise RuntimeError(
|
|
225
|
+
"Invalid response from auth server: missing access_token"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return data
|
|
229
|
+
|
|
230
|
+
# Still waiting for user to authenticate
|
|
231
|
+
elif response.status_code in [202, 428]:
|
|
232
|
+
# 202 = Accepted (still waiting)
|
|
233
|
+
# 428 = Precondition Required (authorization pending)
|
|
234
|
+
time.sleep(interval)
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
# Check for authorization_pending in error response (some backends use 400)
|
|
238
|
+
elif response.status_code == 400:
|
|
239
|
+
try:
|
|
240
|
+
error_data = response.json()
|
|
241
|
+
error_msg = error_data.get("error", "")
|
|
242
|
+
if error_msg == "authorization_pending":
|
|
243
|
+
# Still waiting for user to complete authentication
|
|
244
|
+
time.sleep(interval)
|
|
245
|
+
continue
|
|
246
|
+
else:
|
|
247
|
+
# Real error
|
|
248
|
+
print("\n")
|
|
249
|
+
raise RuntimeError(
|
|
250
|
+
f"Authentication failed ({response.status_code}): {error_msg}"
|
|
251
|
+
)
|
|
252
|
+
except ValueError:
|
|
253
|
+
# Not JSON, treat as error
|
|
254
|
+
print("\n")
|
|
255
|
+
error_msg = response.text or "Unknown error"
|
|
256
|
+
raise RuntimeError(
|
|
257
|
+
f"Authentication failed ({response.status_code}): {error_msg}"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Authentication denied or other error
|
|
261
|
+
else:
|
|
262
|
+
print("\n") # New line before error
|
|
263
|
+
try:
|
|
264
|
+
error_data = response.json()
|
|
265
|
+
error_msg = error_data.get("error", "Unknown error")
|
|
266
|
+
except Exception:
|
|
267
|
+
error_msg = response.text or "Unknown error"
|
|
268
|
+
raise RuntimeError(
|
|
269
|
+
f"Authentication failed ({response.status_code}): {error_msg}"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
except requests.exceptions.RequestException as e:
|
|
273
|
+
print("\n") # New line before error
|
|
274
|
+
raise RuntimeError(f"Failed to poll for token: {e}") from e
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def save_credentials(
|
|
278
|
+
access_token: str,
|
|
279
|
+
refresh_token: str,
|
|
280
|
+
token_type: str,
|
|
281
|
+
environment: str,
|
|
282
|
+
) -> str:
|
|
283
|
+
"""Save credentials to both JSON config file and shell script.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
access_token: JWT access token
|
|
287
|
+
refresh_token: Refresh token
|
|
288
|
+
token_type: Token type (usually "Bearer")
|
|
289
|
+
environment: Environment name (dev/production)
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Path to the generated shell environment file
|
|
293
|
+
"""
|
|
294
|
+
config_dir = Path.home() / ".vuer"
|
|
295
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
296
|
+
|
|
297
|
+
# Save to JSON file for Python code to read
|
|
298
|
+
credentials_file = config_dir / "credentials"
|
|
299
|
+
credentials = {
|
|
300
|
+
"access_token": access_token,
|
|
301
|
+
"refresh_token": refresh_token,
|
|
302
|
+
"token_type": token_type,
|
|
303
|
+
"environment": environment,
|
|
304
|
+
"updated_at": time.time(),
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
with credentials_file.open("w", encoding="utf-8") as f:
|
|
309
|
+
json.dump(credentials, f, indent=2)
|
|
310
|
+
credentials_file.chmod(0o600)
|
|
311
|
+
except Exception as e:
|
|
312
|
+
raise RuntimeError(f"Failed to save credentials to JSON: {e}") from e
|
|
313
|
+
|
|
314
|
+
# Save to shell script for users to source
|
|
315
|
+
env_file = config_dir / "env.sh"
|
|
316
|
+
env_content = f"""# Vuer CLI Authentication Configuration
|
|
317
|
+
# Generated on {time.strftime('%Y-%m-%d %H:%M:%S')}
|
|
318
|
+
# Environment: {environment}
|
|
319
|
+
|
|
320
|
+
export VUER_AUTH_TOKEN="{access_token}"
|
|
321
|
+
export REFRESH_TOKEN="{refresh_token}"
|
|
322
|
+
export TOKEN_TYPE="{token_type}"
|
|
323
|
+
|
|
324
|
+
# To use these credentials, run:
|
|
325
|
+
# source ~/.vuer/env.sh
|
|
326
|
+
# Or add this line to your ~/.bashrc or ~/.zshrc:
|
|
327
|
+
# source ~/.vuer/env.sh
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
with env_file.open("w", encoding="utf-8") as f:
|
|
332
|
+
f.write(env_content)
|
|
333
|
+
env_file.chmod(0o600)
|
|
334
|
+
return str(env_file)
|
|
335
|
+
except Exception as e:
|
|
336
|
+
raise RuntimeError(
|
|
337
|
+
f"Failed to save shell environment file: {e}") from e
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def set_global_env_vars(access_token: str, refresh_token: str) -> None:
|
|
341
|
+
"""Set environment variables globally based on platform.
|
|
342
|
+
|
|
343
|
+
On Windows: Uses setx to set user-level environment variables
|
|
344
|
+
On macOS/Linux: Sets for current process (will be in shell config too)
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
access_token: JWT access token
|
|
348
|
+
refresh_token: Refresh token
|
|
349
|
+
"""
|
|
350
|
+
import platform
|
|
351
|
+
import subprocess
|
|
352
|
+
|
|
353
|
+
system = platform.system()
|
|
354
|
+
|
|
355
|
+
if system == "Windows":
|
|
356
|
+
# Use setx to set user-level environment variables on Windows
|
|
357
|
+
try:
|
|
358
|
+
subprocess.run(
|
|
359
|
+
["setx", "VUER_AUTH_TOKEN", access_token],
|
|
360
|
+
check=False,
|
|
361
|
+
capture_output=True,
|
|
362
|
+
)
|
|
363
|
+
if refresh_token:
|
|
364
|
+
subprocess.run(
|
|
365
|
+
["setx", "REFRESH_TOKEN", refresh_token],
|
|
366
|
+
check=False,
|
|
367
|
+
capture_output=True,
|
|
368
|
+
)
|
|
369
|
+
except Exception:
|
|
370
|
+
pass # Silently fail if setx not available
|
|
371
|
+
else:
|
|
372
|
+
# On Unix-like systems, set for current process
|
|
373
|
+
# (will also be in shell config via auto_configure_shell)
|
|
374
|
+
os.environ["VUER_AUTH_TOKEN"] = access_token
|
|
375
|
+
if refresh_token:
|
|
376
|
+
os.environ["REFRESH_TOKEN"] = refresh_token
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def auto_configure_shell(env_file_path: str) -> str:
|
|
380
|
+
"""Automatically add source command to shell configuration file.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
env_file_path: Path to the env.sh file to be sourced
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Path to the updated shell config file, or empty string if not updated
|
|
387
|
+
"""
|
|
388
|
+
import platform
|
|
389
|
+
|
|
390
|
+
# Only configure shell on Unix-like systems
|
|
391
|
+
if platform.system() == "Windows":
|
|
392
|
+
return ""
|
|
393
|
+
|
|
394
|
+
# Detect shell and corresponding config file
|
|
395
|
+
shell = os.environ.get("SHELL", "")
|
|
396
|
+
config_file = None
|
|
397
|
+
|
|
398
|
+
if "zsh" in shell:
|
|
399
|
+
config_file = Path.home() / ".zshrc"
|
|
400
|
+
elif "bash" in shell:
|
|
401
|
+
config_file = Path.home() / ".bashrc"
|
|
402
|
+
# Also check for .bash_profile on macOS
|
|
403
|
+
bash_profile = Path.home() / ".bash_profile"
|
|
404
|
+
if bash_profile.exists():
|
|
405
|
+
config_file = bash_profile
|
|
406
|
+
else:
|
|
407
|
+
# Try to find config file based on what exists
|
|
408
|
+
zshrc = Path.home() / ".zshrc"
|
|
409
|
+
bashrc = Path.home() / ".bashrc"
|
|
410
|
+
bash_profile = Path.home() / ".bash_profile"
|
|
411
|
+
|
|
412
|
+
if zshrc.exists():
|
|
413
|
+
config_file = zshrc
|
|
414
|
+
elif bash_profile.exists():
|
|
415
|
+
config_file = bash_profile
|
|
416
|
+
elif bashrc.exists():
|
|
417
|
+
config_file = bashrc
|
|
418
|
+
|
|
419
|
+
if not config_file:
|
|
420
|
+
return ""
|
|
421
|
+
|
|
422
|
+
source_line = f"source {env_file_path}"
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
# Check if already configured
|
|
426
|
+
if config_file.exists():
|
|
427
|
+
content = config_file.read_text(encoding="utf-8")
|
|
428
|
+
# Check if the source line already exists
|
|
429
|
+
if source_line in content or f"source ~/.vuer/env.sh" in content:
|
|
430
|
+
return "" # Already configured
|
|
431
|
+
|
|
432
|
+
# Add source line to config file
|
|
433
|
+
with config_file.open("a", encoding="utf-8") as f:
|
|
434
|
+
f.write(f"\n# Vuer CLI authentication (auto-added by vuer login)\n")
|
|
435
|
+
f.write(f"{source_line}\n")
|
|
436
|
+
|
|
437
|
+
return str(config_file)
|
|
438
|
+
|
|
439
|
+
except Exception:
|
|
440
|
+
# If we can't modify the config file, just return empty
|
|
441
|
+
return ""
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def load_credentials() -> Dict[str, Any]:
|
|
445
|
+
"""Load credentials from local configuration file.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Credentials dictionary, or empty dict if file doesn't exist
|
|
449
|
+
"""
|
|
450
|
+
credentials_file = Path.home() / ".vuer" / "credentials"
|
|
451
|
+
|
|
452
|
+
if not credentials_file.exists():
|
|
453
|
+
return {}
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
with credentials_file.open("r", encoding="utf-8") as f:
|
|
457
|
+
return json.load(f)
|
|
458
|
+
except Exception:
|
|
459
|
+
return {}
|
vuer_cli/main.py
CHANGED
|
@@ -5,6 +5,7 @@ from params_proto import proto
|
|
|
5
5
|
from .add import Add
|
|
6
6
|
from .envs_publish import EnvsPublish
|
|
7
7
|
from .envs_pull import EnvsPull
|
|
8
|
+
from .login import Login
|
|
8
9
|
from .remove import Remove
|
|
9
10
|
from .scripts.demcap import Demcap
|
|
10
11
|
from .scripts.minimap import Minimap
|
|
@@ -21,7 +22,8 @@ def entrypoint() -> int:
|
|
|
21
22
|
|
|
22
23
|
@proto.cli(prog="vuer")
|
|
23
24
|
def _cli_entrypoint(
|
|
24
|
-
command:
|
|
25
|
+
command: Login
|
|
26
|
+
| Sync
|
|
25
27
|
| Add
|
|
26
28
|
| Remove
|
|
27
29
|
| Upgrade
|
|
@@ -35,6 +37,7 @@ def _cli_entrypoint(
|
|
|
35
37
|
"""Vuer Hub Environment Manager.
|
|
36
38
|
|
|
37
39
|
Available commands:
|
|
40
|
+
login - Authenticate with Vuer Hub using OAuth Device Flow
|
|
38
41
|
sync - Sync environments from environment.json dependencies (like npm install)
|
|
39
42
|
add - Add an environment to environment.json and run sync
|
|
40
43
|
remove - Remove an environment from environment.json and run sync
|
|
@@ -47,6 +50,8 @@ def _cli_entrypoint(
|
|
|
47
50
|
viz_ptc_proxie - Visualize GLB robot model with point cloud
|
|
48
51
|
|
|
49
52
|
Examples:
|
|
53
|
+
vuer login Authenticate with Vuer Hub (dev environment)
|
|
54
|
+
vuer login --env production Authenticate with production environment
|
|
50
55
|
vuer sync Sync all dependencies from environment.json
|
|
51
56
|
vuer add my-env/1.2.3 Add an environment and sync
|
|
52
57
|
vuer remove my-env/1.2.3 Remove an environment and sync
|
|
@@ -62,4 +67,4 @@ def _cli_entrypoint(
|
|
|
62
67
|
VUER_HUB_URL - Base URL of the Vuer Hub API
|
|
63
68
|
VUER_AUTH_TOKEN - JWT token for API authentication
|
|
64
69
|
"""
|
|
65
|
-
return command
|
|
70
|
+
return command()
|
vuer_cli/remove.py
CHANGED
|
@@ -6,92 +6,92 @@ from pathlib import Path
|
|
|
6
6
|
from params_proto import proto
|
|
7
7
|
|
|
8
8
|
from .sync import Sync, read_environments_lock
|
|
9
|
-
from .utils import
|
|
9
|
+
from .utils import normalize_env_spec, parse_env_spec, print_error
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@proto
|
|
13
13
|
class Remove:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
"""Remove an environment from environment.json and run `vuer sync`.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
vuer remove 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 remove 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
|
+
module_dir = cwd / "vuer_environments"
|
|
33
|
+
lock_path = cwd / "environments-lock.yaml"
|
|
34
|
+
|
|
35
|
+
# Step 2: Ensure vuer_environments/dependencies.toml exists
|
|
36
|
+
if not module_dir.exists() or not lock_path.exists():
|
|
37
|
+
raise FileNotFoundError(
|
|
38
|
+
"vuer_environments directory or environments-lock.yaml not found. "
|
|
39
|
+
"Please run `vuer sync` first to generate environments-lock.yaml."
|
|
40
|
+
)
|
|
41
|
+
existing_deps = read_environments_lock(lock_path)
|
|
42
|
+
if env_spec_normalized not in existing_deps:
|
|
43
|
+
print(f"[INFO] Environment {env_spec_normalized} is not present in {lock_path}")
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
# Step 3: Remove from environment.json dependencies, then run sync
|
|
47
|
+
env_json_path = cwd / "environment.json"
|
|
48
|
+
if not env_json_path.exists():
|
|
49
|
+
raise FileNotFoundError("environment.json not found. Cannot remove dependency.")
|
|
50
|
+
|
|
51
|
+
with env_json_path.open("r", encoding="utf-8") as f:
|
|
25
52
|
try:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if current_version is None:
|
|
72
|
-
print(f"[INFO] Dependency {env_spec_normalized} not found in environment.json. Skipping removal.")
|
|
73
|
-
else:
|
|
74
|
-
# Only remove if the version in environment.json matches the requested version.
|
|
75
|
-
if current_version != version:
|
|
76
|
-
print(
|
|
77
|
-
f"[INFO] Skipping removal: environment '{name}' is pinned to version "
|
|
78
|
-
f"'{current_version}' in environment.json (requested '{version}')."
|
|
79
|
-
)
|
|
80
|
-
else:
|
|
81
|
-
deps.pop(name, None)
|
|
82
|
-
print(f"[INFO] Removed {env_spec_normalized} from environment.json dependencies.")
|
|
83
|
-
|
|
84
|
-
data["dependencies"] = deps
|
|
85
|
-
with env_json_path.open("w", encoding="utf-8") as f:
|
|
86
|
-
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
87
|
-
f.write("\n")
|
|
88
|
-
|
|
89
|
-
print("[INFO] Running sync to reconcile vuer_environments/ with updated dependencies...")
|
|
90
|
-
return Sync().run()
|
|
91
|
-
|
|
92
|
-
except (FileNotFoundError, ValueError, RuntimeError) as e:
|
|
93
|
-
print_error(str(e))
|
|
94
|
-
return 1
|
|
95
|
-
except Exception as e:
|
|
96
|
-
print_error(f"Unexpected error: {e}")
|
|
97
|
-
return 1
|
|
53
|
+
data = json.load(f)
|
|
54
|
+
except json.JSONDecodeError as e:
|
|
55
|
+
raise ValueError(f"Invalid environment.json: {e}") from e
|
|
56
|
+
|
|
57
|
+
deps = data.get("dependencies")
|
|
58
|
+
if deps is None:
|
|
59
|
+
deps = {}
|
|
60
|
+
if not isinstance(deps, dict):
|
|
61
|
+
raise ValueError("environment.json 'dependencies' field must be an object")
|
|
62
|
+
|
|
63
|
+
# Remove the dependency if present and version matches exactly.
|
|
64
|
+
current_version = deps.get(name)
|
|
65
|
+
if current_version is None:
|
|
66
|
+
print(
|
|
67
|
+
f"[INFO] Dependency {env_spec_normalized} not found in environment.json. Skipping removal."
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
# Only remove if the version in environment.json matches the requested version.
|
|
71
|
+
if current_version != version:
|
|
72
|
+
print(
|
|
73
|
+
f"[INFO] Skipping removal: environment '{name}' is pinned to version "
|
|
74
|
+
f"'{current_version}' in environment.json (requested '{version}')."
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
deps.pop(name, None)
|
|
78
|
+
print(
|
|
79
|
+
f"[INFO] Removed {env_spec_normalized} from environment.json dependencies."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
data["dependencies"] = deps
|
|
83
|
+
with env_json_path.open("w", encoding="utf-8") as f:
|
|
84
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
85
|
+
f.write("\n")
|
|
86
|
+
|
|
87
|
+
print(
|
|
88
|
+
"[INFO] Running sync to reconcile vuer_environments/ with updated dependencies..."
|
|
89
|
+
)
|
|
90
|
+
return Sync().run()
|
|
91
|
+
|
|
92
|
+
except (FileNotFoundError, ValueError, RuntimeError) as e:
|
|
93
|
+
print_error(str(e))
|
|
94
|
+
return 1
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print_error(f"Unexpected error: {e}")
|
|
97
|
+
return 1
|