hac-client-cli 0.1.0__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.
- hac_client_cli/__init__.py +2 -0
- hac_client_cli/app.py +465 -0
- hac_client_cli/commands_endpoint.py +207 -0
- hac_client_cli/commands_env.py +219 -0
- hac_client_cli/commands_session.py +417 -0
- hac_client_cli/commands_update.py +582 -0
- hac_client_cli/config_loader.py +188 -0
- hac_client_cli/environment_manager.py +424 -0
- hac_client_cli-0.1.0.dist-info/METADATA +125 -0
- hac_client_cli-0.1.0.dist-info/RECORD +14 -0
- hac_client_cli-0.1.0.dist-info/WHEEL +5 -0
- hac_client_cli-0.1.0.dist-info/entry_points.txt +2 -0
- hac_client_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- hac_client_cli-0.1.0.dist-info/top_level.txt +1 -0
hac_client_cli/app.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""HAC Client CLI application.
|
|
2
|
+
|
|
3
|
+
Thin adapter over hac-client-core for basic HAC operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import typer
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from hac_client_cli.environment_manager import EnvironmentManager
|
|
15
|
+
from hac_client_cli.commands_env import env_app
|
|
16
|
+
from hac_client_cli.commands_endpoint import app as endpoint_app
|
|
17
|
+
from hac_client_cli.commands_session import session_app
|
|
18
|
+
from hac_client_cli.commands_update import update_app
|
|
19
|
+
from hac_client_core.client import HacClient, HacClientError
|
|
20
|
+
from hac_client_core.auth import BasicAuthHandler
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
help="SAP Commerce HAC client",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
add_completion=False
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Add environment, endpoint, and session management
|
|
29
|
+
app.add_typer(env_app, name="env")
|
|
30
|
+
app.add_typer(endpoint_app, name="endpoint")
|
|
31
|
+
app.add_typer(session_app, name="session")
|
|
32
|
+
app.add_typer(update_app, name="update")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_client(environment: Optional[str] = None, endpoint: Optional[str] = None, quiet: bool = False) -> HacClient:
|
|
36
|
+
"""Create HAC client from configuration.
|
|
37
|
+
|
|
38
|
+
Requires an active session. Use 'hac session start' to create one.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
environment: Environment name (uses default if None)
|
|
42
|
+
endpoint: Endpoint name (uses environment default if None)
|
|
43
|
+
quiet: Suppress informational messages
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Configured HacClient instance
|
|
47
|
+
"""
|
|
48
|
+
from hac_client_core.session import SessionManager
|
|
49
|
+
from hac_client_cli.config_loader import get_endpoint_config
|
|
50
|
+
|
|
51
|
+
# Get endpoint configuration
|
|
52
|
+
env_name, endpoint_name, ep_config = get_endpoint_config(environment, endpoint)
|
|
53
|
+
|
|
54
|
+
# Create session identifier
|
|
55
|
+
session_id = f"{env_name}/{endpoint_name}"
|
|
56
|
+
|
|
57
|
+
# Check for existing session
|
|
58
|
+
# Note: We need to find a session for this endpoint, but we don't know the username yet
|
|
59
|
+
# Sessions are keyed by (base_url, username, environment), so we need to list all sessions
|
|
60
|
+
session_manager = SessionManager()
|
|
61
|
+
all_sessions = session_manager.list_sessions()
|
|
62
|
+
|
|
63
|
+
# Find a session for this endpoint
|
|
64
|
+
# Normalize URLs for comparison (remove trailing slashes)
|
|
65
|
+
config_url_normalized = ep_config.url.rstrip('/')
|
|
66
|
+
session = None
|
|
67
|
+
for s in all_sessions:
|
|
68
|
+
session_url_normalized = s.base_url.rstrip('/')
|
|
69
|
+
if s.environment == session_id and session_url_normalized == config_url_normalized:
|
|
70
|
+
session = s
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
if not session:
|
|
74
|
+
print(f"ERROR: No active session for '{session_id}'", file=sys.stderr)
|
|
75
|
+
print(f"\nStart a session:", file=sys.stderr)
|
|
76
|
+
print(f" hac session start {env_name} --endpoint {endpoint_name} --username <user>", file=sys.stderr)
|
|
77
|
+
print(f"\nOr import existing session:", file=sys.stderr)
|
|
78
|
+
print(f" hac session import {env_name} --endpoint {endpoint_name} --username <user> --session-id <id> --csrf-token <token>", file=sys.stderr)
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
# Create client with existing session (no auto-login)
|
|
82
|
+
# We need a dummy password since BasicAuthHandler requires it, but it won't be used
|
|
83
|
+
auth = BasicAuthHandler(session.username, "dummy")
|
|
84
|
+
|
|
85
|
+
client = HacClient(
|
|
86
|
+
base_url=ep_config.url,
|
|
87
|
+
auth_handler=auth,
|
|
88
|
+
environment=session_id, # Use composite key
|
|
89
|
+
timeout=ep_config.timeout,
|
|
90
|
+
ignore_ssl=ep_config.ignore_ssl,
|
|
91
|
+
session_persistence=True,
|
|
92
|
+
quiet=quiet
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Manually set session info from loaded session
|
|
96
|
+
from hac_client_core.models import SessionInfo
|
|
97
|
+
client.session_info = SessionInfo(
|
|
98
|
+
session_id=session.session_id,
|
|
99
|
+
csrf_token=session.csrf_token,
|
|
100
|
+
route_cookie=session.route_cookie,
|
|
101
|
+
is_authenticated=session.is_authenticated
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Also set cookies in the http_session so they're sent with requests
|
|
105
|
+
# Extract domain from base URL
|
|
106
|
+
from urllib.parse import urlparse
|
|
107
|
+
parsed_url = urlparse(ep_config.url)
|
|
108
|
+
domain = parsed_url.hostname
|
|
109
|
+
|
|
110
|
+
client.http_session.cookies.set('JSESSIONID', session.session_id, domain=domain, path='/')
|
|
111
|
+
if session.route_cookie:
|
|
112
|
+
# Extract value from "ROUTE=value" format
|
|
113
|
+
route_value = session.route_cookie.split('=', 1)[1] if '=' in session.route_cookie else session.route_cookie
|
|
114
|
+
client.http_session.cookies.set('ROUTE', route_value, domain=domain, path='/')
|
|
115
|
+
|
|
116
|
+
return client
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.command("groovy")
|
|
120
|
+
def groovy_command(
|
|
121
|
+
script: str = typer.Argument(..., help="Groovy script or path to .groovy file"),
|
|
122
|
+
file: bool = typer.Option(False, "--file", "-f", help="Treat script as file path"),
|
|
123
|
+
commit: bool = typer.Option(False, "--commit", "-c", help="Enable commit mode"),
|
|
124
|
+
environment: Optional[str] = typer.Option(None, "--environment", "-e", help="Environment name"),
|
|
125
|
+
endpoint: Optional[str] = typer.Option(None, "--endpoint", "-n", help="Endpoint name"),
|
|
126
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
127
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress informational messages")
|
|
128
|
+
):
|
|
129
|
+
"""Execute Groovy script in HAC."""
|
|
130
|
+
try:
|
|
131
|
+
# Read script from file if needed
|
|
132
|
+
script_content = script
|
|
133
|
+
if file or script.endswith('.groovy'):
|
|
134
|
+
script_path = Path(script)
|
|
135
|
+
if not script_path.exists():
|
|
136
|
+
print(f"ERROR: Script file not found: {script}", file=sys.stderr)
|
|
137
|
+
raise typer.Exit(1)
|
|
138
|
+
script_content = script_path.read_text()
|
|
139
|
+
|
|
140
|
+
# Create client and execute
|
|
141
|
+
client = create_client(environment, endpoint, quiet)
|
|
142
|
+
result = client.execute_groovy(script_content, commit=commit)
|
|
143
|
+
|
|
144
|
+
if not result.success:
|
|
145
|
+
print(f"ERROR: Script execution failed\n{result.stacktrace_text}", file=sys.stderr)
|
|
146
|
+
raise typer.Exit(1)
|
|
147
|
+
|
|
148
|
+
if json_output:
|
|
149
|
+
output = {
|
|
150
|
+
"success": result.success,
|
|
151
|
+
"output_text": result.output_text,
|
|
152
|
+
"execution_result": result.execution_result,
|
|
153
|
+
"commit_mode": result.commit_mode,
|
|
154
|
+
"execution_time_ms": result.execution_time_ms
|
|
155
|
+
}
|
|
156
|
+
print(json.dumps(output, indent=2))
|
|
157
|
+
else:
|
|
158
|
+
# Print script output to stderr, result to stdout
|
|
159
|
+
if result.output_text:
|
|
160
|
+
print(result.output_text, file=sys.stderr)
|
|
161
|
+
print(result.execution_result)
|
|
162
|
+
|
|
163
|
+
except HacClientError as e:
|
|
164
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
165
|
+
raise typer.Exit(1)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.command("flexsearch")
|
|
169
|
+
def flexsearch_command(
|
|
170
|
+
query: str = typer.Argument(..., help="FlexibleSearch query"),
|
|
171
|
+
max_count: int = typer.Option(200, "--max-count", "-m", help="Maximum number of results"),
|
|
172
|
+
locale: str = typer.Option("en", "--locale", "-l", help="Locale for the query"),
|
|
173
|
+
environment: Optional[str] = typer.Option(None, "--environment", "-e", help="Environment name"),
|
|
174
|
+
endpoint: Optional[str] = typer.Option(None, "--endpoint", "-n", help="Endpoint name"),
|
|
175
|
+
csv: bool = typer.Option(False, "--csv", help="Output as CSV"),
|
|
176
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
177
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress informational messages")
|
|
178
|
+
):
|
|
179
|
+
"""Execute FlexibleSearch query in HAC."""
|
|
180
|
+
try:
|
|
181
|
+
client = create_client(environment, endpoint, quiet)
|
|
182
|
+
result = client.execute_flexiblesearch(query, max_count=max_count, locale=locale)
|
|
183
|
+
|
|
184
|
+
if not result.success:
|
|
185
|
+
print(f"ERROR: Query failed\n{result.exception}", file=sys.stderr)
|
|
186
|
+
raise typer.Exit(1)
|
|
187
|
+
|
|
188
|
+
if json_output:
|
|
189
|
+
output = {
|
|
190
|
+
"success": result.success,
|
|
191
|
+
"headers": result.headers,
|
|
192
|
+
"rows": result.rows,
|
|
193
|
+
"result_count": result.result_count,
|
|
194
|
+
"execution_time_ms": result.execution_time_ms
|
|
195
|
+
}
|
|
196
|
+
print(json.dumps(output, indent=2))
|
|
197
|
+
elif csv:
|
|
198
|
+
# Output as CSV
|
|
199
|
+
if result.headers:
|
|
200
|
+
print(",".join(result.headers))
|
|
201
|
+
for row in result.rows:
|
|
202
|
+
print(",".join(str(cell) for cell in row))
|
|
203
|
+
else:
|
|
204
|
+
# Human-readable table format
|
|
205
|
+
if result.headers:
|
|
206
|
+
print("\t".join(result.headers))
|
|
207
|
+
for row in result.rows:
|
|
208
|
+
print("\t".join(str(cell) for cell in row))
|
|
209
|
+
|
|
210
|
+
if not quiet:
|
|
211
|
+
print(f"\n{result.result_count} results", file=sys.stderr)
|
|
212
|
+
|
|
213
|
+
except HacClientError as e:
|
|
214
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
215
|
+
raise typer.Exit(1)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@app.command("impex")
|
|
219
|
+
def impex_command(
|
|
220
|
+
file: Path = typer.Option(..., "--file", "-f", help="Impex file to import"),
|
|
221
|
+
validation: str = typer.Option("import_strict", "--validation", "-v", help="Validation mode (import_strict, import_relaxed, strict, relaxed)"),
|
|
222
|
+
environment: Optional[str] = typer.Option(None, "--environment", "-e", help="Environment name"),
|
|
223
|
+
endpoint: Optional[str] = typer.Option(None, "--endpoint", "-n", help="Endpoint name"),
|
|
224
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
225
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress informational messages")
|
|
226
|
+
):
|
|
227
|
+
"""Import Impex data in HAC."""
|
|
228
|
+
try:
|
|
229
|
+
if not file.exists():
|
|
230
|
+
print(f"ERROR: Impex file not found: {file}", file=sys.stderr)
|
|
231
|
+
raise typer.Exit(1)
|
|
232
|
+
|
|
233
|
+
impex_content = file.read_text()
|
|
234
|
+
|
|
235
|
+
client = create_client(environment, endpoint, quiet)
|
|
236
|
+
result = client.import_impex(impex_content, validation_mode=validation)
|
|
237
|
+
|
|
238
|
+
if not result.success:
|
|
239
|
+
print(f"ERROR: Impex import failed\n{result.error}", file=sys.stderr)
|
|
240
|
+
raise typer.Exit(1)
|
|
241
|
+
|
|
242
|
+
if json_output:
|
|
243
|
+
output = {
|
|
244
|
+
"success": result.success,
|
|
245
|
+
"output": result.output,
|
|
246
|
+
"validation_errors": result.validation_errors
|
|
247
|
+
}
|
|
248
|
+
print(json.dumps(output, indent=2))
|
|
249
|
+
else:
|
|
250
|
+
print(result.output)
|
|
251
|
+
|
|
252
|
+
except HacClientError as e:
|
|
253
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
254
|
+
raise typer.Exit(1)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@app.command("config")
|
|
258
|
+
def config_command(
|
|
259
|
+
list_environments: bool = typer.Option(False, "--list", "-l", help="List configured environments"),
|
|
260
|
+
validate: bool = typer.Option(False, "--validate", "-v", help="Validate configuration"),
|
|
261
|
+
show_path: bool = typer.Option(False, "--path", "-p", help="Show config file path"),
|
|
262
|
+
show_example: bool = typer.Option(False, "--example", "-x", help="Show example configuration"),
|
|
263
|
+
environment: Optional[str] = typer.Option(None, "--env", "-e", help="Show specific environment"),
|
|
264
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON")
|
|
265
|
+
):
|
|
266
|
+
"""Discover, inspect, and manage HAC client configuration.
|
|
267
|
+
|
|
268
|
+
Examples:
|
|
269
|
+
hac config # Show all configuration
|
|
270
|
+
hac config -l # List environments
|
|
271
|
+
hac config -e local # Show specific environment
|
|
272
|
+
hac config -v # Validate configuration
|
|
273
|
+
hac config -p # Show config file path
|
|
274
|
+
hac config -x # Show example config
|
|
275
|
+
"""
|
|
276
|
+
from hac_client_cli.config_loader import get_config_path
|
|
277
|
+
|
|
278
|
+
config_path = get_config_path()
|
|
279
|
+
|
|
280
|
+
# Show path only
|
|
281
|
+
if show_path:
|
|
282
|
+
if json_output:
|
|
283
|
+
print(json.dumps({"config_path": str(config_path), "exists": config_path.exists()}, indent=2))
|
|
284
|
+
else:
|
|
285
|
+
print(f"Configuration file: {config_path}")
|
|
286
|
+
print(f"Status: {'exists' if config_path.exists() else 'not found'}")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
# Show example
|
|
290
|
+
if show_example:
|
|
291
|
+
print("""# HAC Client Configuration
|
|
292
|
+
# Save to: ~/.config/hac-client/config.toml
|
|
293
|
+
# Or set: export HAC_CLIENT_CONFIG_PATH=/path/to/config.toml
|
|
294
|
+
|
|
295
|
+
# Default environment to use
|
|
296
|
+
default_environment = "local"
|
|
297
|
+
|
|
298
|
+
# Environment definitions
|
|
299
|
+
[environments.local]
|
|
300
|
+
url = "https://localhost:9002"
|
|
301
|
+
username = "admin"
|
|
302
|
+
# password = "nimda" # Or use HAC_PASSWORD env var
|
|
303
|
+
ignore_ssl = true
|
|
304
|
+
timeout = 30
|
|
305
|
+
|
|
306
|
+
[environments.dev]
|
|
307
|
+
url = "https://dev.example.com"
|
|
308
|
+
username = "admin"
|
|
309
|
+
# Password from HAC_PASSWORD_DEV or HAC_PASSWORD env var
|
|
310
|
+
ignore_ssl = false
|
|
311
|
+
timeout = 60
|
|
312
|
+
|
|
313
|
+
[environments.prod]
|
|
314
|
+
url = "https://prod.example.com"
|
|
315
|
+
username = "admin"
|
|
316
|
+
# ALWAYS use env var for prod: HAC_PASSWORD_PROD
|
|
317
|
+
ignore_ssl = false
|
|
318
|
+
timeout = 120
|
|
319
|
+
""")
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
# Load and validate config
|
|
323
|
+
try:
|
|
324
|
+
config = load_config()
|
|
325
|
+
|
|
326
|
+
# Validate mode
|
|
327
|
+
if validate:
|
|
328
|
+
issues = []
|
|
329
|
+
|
|
330
|
+
# Check if config file exists
|
|
331
|
+
if not config_path.exists():
|
|
332
|
+
issues.append("Configuration file does not exist")
|
|
333
|
+
|
|
334
|
+
# Check if we have environments
|
|
335
|
+
if not config.environments:
|
|
336
|
+
issues.append("No environments configured")
|
|
337
|
+
|
|
338
|
+
# Check default environment exists
|
|
339
|
+
if config.default_environment not in config.environments:
|
|
340
|
+
issues.append(f"Default environment '{config.default_environment}' not found in configured environments")
|
|
341
|
+
|
|
342
|
+
# Check each environment
|
|
343
|
+
for name, env in config.environments.items():
|
|
344
|
+
if not env.password:
|
|
345
|
+
issues.append(f"Environment '{name}': password not configured (set in config or HAC_PASSWORD_{name.upper()} env var)")
|
|
346
|
+
if not env.url.startswith('http'):
|
|
347
|
+
issues.append(f"Environment '{name}': URL should start with http:// or https://")
|
|
348
|
+
|
|
349
|
+
if json_output:
|
|
350
|
+
print(json.dumps({"valid": len(issues) == 0, "issues": issues}, indent=2))
|
|
351
|
+
else:
|
|
352
|
+
if issues:
|
|
353
|
+
print("❌ Configuration has issues:\n")
|
|
354
|
+
for issue in issues:
|
|
355
|
+
print(f" - {issue}")
|
|
356
|
+
print(f"\nConfiguration file: {config_path}")
|
|
357
|
+
print("Run 'hac config --example' to see example configuration")
|
|
358
|
+
raise typer.Exit(1)
|
|
359
|
+
else:
|
|
360
|
+
print("✅ Configuration is valid")
|
|
361
|
+
print(f" - {len(config.environments)} environment(s) configured")
|
|
362
|
+
print(f" - Default: {config.default_environment}")
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# Show specific environment
|
|
366
|
+
if environment:
|
|
367
|
+
if environment not in config.environments:
|
|
368
|
+
print(f"ERROR: Environment '{environment}' not found", file=sys.stderr)
|
|
369
|
+
print("\nAvailable environments:", file=sys.stderr)
|
|
370
|
+
for env_name in config.environments:
|
|
371
|
+
marker = " (default)" if env_name == config.default_environment else ""
|
|
372
|
+
print(f" - {env_name}{marker}", file=sys.stderr)
|
|
373
|
+
raise typer.Exit(1)
|
|
374
|
+
|
|
375
|
+
env = config.environments[environment]
|
|
376
|
+
if json_output:
|
|
377
|
+
output = {
|
|
378
|
+
"name": environment,
|
|
379
|
+
"url": env.url,
|
|
380
|
+
"username": env.username,
|
|
381
|
+
"password_configured": env.password is not None,
|
|
382
|
+
"ignore_ssl": env.ignore_ssl,
|
|
383
|
+
"timeout": env.timeout,
|
|
384
|
+
"is_default": environment == config.default_environment
|
|
385
|
+
}
|
|
386
|
+
print(json.dumps(output, indent=2))
|
|
387
|
+
else:
|
|
388
|
+
marker = " (default)" if environment == config.default_environment else ""
|
|
389
|
+
print(f"Environment: {environment}{marker}")
|
|
390
|
+
print(f" URL: {env.url}")
|
|
391
|
+
print(f" Username: {env.username}")
|
|
392
|
+
print(f" Password: {'✓ configured' if env.password else '✗ NOT configured'}")
|
|
393
|
+
print(f" Ignore SSL: {env.ignore_ssl}")
|
|
394
|
+
print(f" Timeout: {env.timeout}s")
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
# List environments
|
|
398
|
+
if list_environments:
|
|
399
|
+
if json_output:
|
|
400
|
+
envs = list(config.environments.keys())
|
|
401
|
+
print(json.dumps({"environments": envs, "default": config.default_environment}, indent=2))
|
|
402
|
+
else:
|
|
403
|
+
print("Configured environments:")
|
|
404
|
+
for env_name in sorted(config.environments.keys()):
|
|
405
|
+
marker = " (default)" if env_name == config.default_environment else ""
|
|
406
|
+
print(f" - {env_name}{marker}")
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
# Show full config (default)
|
|
410
|
+
if json_output:
|
|
411
|
+
output = {
|
|
412
|
+
"config_path": str(config_path),
|
|
413
|
+
"default_environment": config.default_environment,
|
|
414
|
+
"environments": {
|
|
415
|
+
name: {
|
|
416
|
+
"url": env.url,
|
|
417
|
+
"username": env.username,
|
|
418
|
+
"password_configured": env.password is not None,
|
|
419
|
+
"ignore_ssl": env.ignore_ssl,
|
|
420
|
+
"timeout": env.timeout
|
|
421
|
+
}
|
|
422
|
+
for name, env in config.environments.items()
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
print(json.dumps(output, indent=2))
|
|
426
|
+
else:
|
|
427
|
+
print(f"Configuration file: {config_path}")
|
|
428
|
+
print(f"Status: {'✓ exists' if config_path.exists() else '✗ not found (using defaults)'}")
|
|
429
|
+
print(f"\nDefault environment: {config.default_environment}")
|
|
430
|
+
print(f"\nEnvironments ({len(config.environments)}):")
|
|
431
|
+
for name in sorted(config.environments.keys()):
|
|
432
|
+
env = config.environments[name]
|
|
433
|
+
marker = " ← default" if name == config.default_environment else ""
|
|
434
|
+
pwd_status = "✓" if env.password else "✗"
|
|
435
|
+
print(f" {name}{marker}")
|
|
436
|
+
print(f" URL: {env.url}")
|
|
437
|
+
print(f" Username: {env.username}")
|
|
438
|
+
print(f" Password: {pwd_status}")
|
|
439
|
+
print(f" SSL: {'ignore' if env.ignore_ssl else 'verify'}")
|
|
440
|
+
|
|
441
|
+
print("\nCommands:")
|
|
442
|
+
print(" hac config -l # List environments")
|
|
443
|
+
print(" hac config -e local # Show environment details")
|
|
444
|
+
print(" hac config -v # Validate configuration")
|
|
445
|
+
print(" hac config -x # Show example config")
|
|
446
|
+
|
|
447
|
+
except ValueError as e:
|
|
448
|
+
print(f"ERROR: Configuration validation failed", file=sys.stderr)
|
|
449
|
+
print(f" {e}", file=sys.stderr)
|
|
450
|
+
print(f"\nConfiguration file: {config_path}", file=sys.stderr)
|
|
451
|
+
print("\nRun 'hac config --example' to see correct format", file=sys.stderr)
|
|
452
|
+
raise typer.Exit(1)
|
|
453
|
+
except Exception as e:
|
|
454
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
455
|
+
raise typer.Exit(1)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def main():
|
|
459
|
+
"""Entry point for the HAC client CLI."""
|
|
460
|
+
app()
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
if __name__ == "__main__":
|
|
464
|
+
main()
|
|
465
|
+
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Endpoint management commands for HAC client CLI."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import typer
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from .environment_manager import EnvironmentManager
|
|
11
|
+
from .config_loader import get_config_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Manage HAC endpoints")
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("list")
|
|
19
|
+
def list_endpoints(
|
|
20
|
+
environment: str = typer.Argument(..., help="Environment name"),
|
|
21
|
+
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json"),
|
|
22
|
+
no_headers: bool = typer.Option(False, "--no-headers", help="Suppress column headers"),
|
|
23
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output (names only)")
|
|
24
|
+
):
|
|
25
|
+
"""List all endpoints in an environment."""
|
|
26
|
+
manager = EnvironmentManager(get_config_path())
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
endpoints = manager.list_endpoints(environment)
|
|
30
|
+
env = manager.get_environment(environment)
|
|
31
|
+
|
|
32
|
+
if not endpoints:
|
|
33
|
+
if format != "json":
|
|
34
|
+
console.print(f"[yellow]No endpoints configured for environment '{environment}'[/yellow]")
|
|
35
|
+
else:
|
|
36
|
+
print("[]")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# JSON output
|
|
40
|
+
if format == "json":
|
|
41
|
+
output = [
|
|
42
|
+
{
|
|
43
|
+
"name": ep.name,
|
|
44
|
+
"url": ep.url,
|
|
45
|
+
"ignore_ssl": ep.ignore_ssl,
|
|
46
|
+
"timeout": ep.timeout,
|
|
47
|
+
"is_default": env and ep.name == env.default_endpoint
|
|
48
|
+
}
|
|
49
|
+
for ep in endpoints
|
|
50
|
+
]
|
|
51
|
+
print(json.dumps(output, indent=2))
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# Quiet output (names only)
|
|
55
|
+
if quiet:
|
|
56
|
+
for ep in endpoints:
|
|
57
|
+
print(ep.name)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Table format (default)
|
|
61
|
+
table = Table(show_header=not no_headers)
|
|
62
|
+
table.add_column("NAME", style="cyan")
|
|
63
|
+
table.add_column("URL")
|
|
64
|
+
table.add_column("SSL")
|
|
65
|
+
table.add_column("TIMEOUT", justify="right")
|
|
66
|
+
table.add_column("DEFAULT", justify="center")
|
|
67
|
+
|
|
68
|
+
for endpoint in endpoints:
|
|
69
|
+
default_marker = "✓" if env and endpoint.name == env.default_endpoint else ""
|
|
70
|
+
ssl_status = "[yellow]ignore[/yellow]" if endpoint.ignore_ssl else "[green]verify[/green]"
|
|
71
|
+
|
|
72
|
+
table.add_row(
|
|
73
|
+
endpoint.name,
|
|
74
|
+
endpoint.url,
|
|
75
|
+
ssl_status,
|
|
76
|
+
f"{endpoint.timeout}s",
|
|
77
|
+
default_marker
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
console.print(table)
|
|
81
|
+
|
|
82
|
+
except ValueError as e:
|
|
83
|
+
console.print(f"[red]ERROR: {e}[/red]", file=sys.stderr)
|
|
84
|
+
raise typer.Exit(1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command("show")
|
|
88
|
+
def show_endpoint(
|
|
89
|
+
environment: str = typer.Argument(..., help="Environment name"),
|
|
90
|
+
endpoint: str = typer.Argument(..., help="Endpoint name")
|
|
91
|
+
):
|
|
92
|
+
"""Show details of a specific endpoint."""
|
|
93
|
+
manager = EnvironmentManager(get_config_path())
|
|
94
|
+
|
|
95
|
+
ep = manager.get_endpoint(environment, endpoint)
|
|
96
|
+
|
|
97
|
+
if not ep:
|
|
98
|
+
print(f"ERROR: Endpoint '{endpoint}' not found in environment '{environment}'")
|
|
99
|
+
raise typer.Exit(1)
|
|
100
|
+
|
|
101
|
+
env = manager.get_environment(environment)
|
|
102
|
+
is_default = env and ep.name == env.default_endpoint
|
|
103
|
+
|
|
104
|
+
print(f"Endpoint: {environment}/{endpoint}")
|
|
105
|
+
if is_default:
|
|
106
|
+
print("Status: default")
|
|
107
|
+
print()
|
|
108
|
+
print(f"URL: {ep.url}")
|
|
109
|
+
print(f"Ignore SSL: {ep.ignore_ssl}")
|
|
110
|
+
print(f"Timeout: {ep.timeout}s")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command("add")
|
|
114
|
+
def add_endpoint(
|
|
115
|
+
environment: str = typer.Argument(..., help="Environment name"),
|
|
116
|
+
endpoint: str = typer.Argument(..., help="Endpoint name"),
|
|
117
|
+
url: str = typer.Option(..., "--url", "-u", help="HAC base URL"),
|
|
118
|
+
ignore_ssl: bool = typer.Option(False, "--ignore-ssl", help="Ignore SSL certificate errors"),
|
|
119
|
+
timeout: int = typer.Option(30, "--timeout", help="HTTP timeout in seconds"),
|
|
120
|
+
set_default: bool = typer.Option(False, "--set-default", help="Set as default endpoint")
|
|
121
|
+
):
|
|
122
|
+
"""Add a new endpoint to an environment.
|
|
123
|
+
|
|
124
|
+
Note: Username is provided during session creation, not endpoint configuration.
|
|
125
|
+
An endpoint is just infrastructure (URL, connection settings).
|
|
126
|
+
"""
|
|
127
|
+
manager = EnvironmentManager(get_config_path())
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
manager.add_endpoint(
|
|
131
|
+
env_name=environment,
|
|
132
|
+
endpoint_name=endpoint,
|
|
133
|
+
url=url,
|
|
134
|
+
ignore_ssl=ignore_ssl,
|
|
135
|
+
timeout=timeout,
|
|
136
|
+
set_default=set_default
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
default_msg = " (set as default)" if set_default else ""
|
|
140
|
+
print(f"✓ Added endpoint '{endpoint}' to environment '{environment}'{default_msg}")
|
|
141
|
+
print(f"\nStart session: hac session start {environment} --endpoint {endpoint} --username <user>")
|
|
142
|
+
|
|
143
|
+
except ValueError as e:
|
|
144
|
+
print(f"ERROR: {e}")
|
|
145
|
+
raise typer.Exit(1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command("update")
|
|
149
|
+
def update_endpoint(
|
|
150
|
+
environment: str = typer.Argument(..., help="Environment name"),
|
|
151
|
+
endpoint: str = typer.Argument(..., help="Endpoint name"),
|
|
152
|
+
url: Optional[str] = typer.Option(None, "--url", "-u", help="HAC base URL"),
|
|
153
|
+
ignore_ssl: Optional[bool] = typer.Option(None, "--ignore-ssl/--no-ignore-ssl", help="Ignore SSL errors"),
|
|
154
|
+
timeout: Optional[int] = typer.Option(None, "--timeout", help="HTTP timeout in seconds")
|
|
155
|
+
):
|
|
156
|
+
"""Update an existing endpoint."""
|
|
157
|
+
manager = EnvironmentManager(get_config_path())
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
manager.update_endpoint(
|
|
161
|
+
env_name=environment,
|
|
162
|
+
endpoint_name=endpoint,
|
|
163
|
+
url=url,
|
|
164
|
+
ignore_ssl=ignore_ssl,
|
|
165
|
+
timeout=timeout
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
print(f"✓ Updated endpoint '{endpoint}' in environment '{environment}'")
|
|
169
|
+
|
|
170
|
+
except ValueError as e:
|
|
171
|
+
print(f"ERROR: {e}")
|
|
172
|
+
raise typer.Exit(1)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.command("remove")
|
|
176
|
+
def remove_endpoint(
|
|
177
|
+
environment: str = typer.Argument(..., help="Environment name"),
|
|
178
|
+
endpoint: str = typer.Argument(..., help="Endpoint name")
|
|
179
|
+
):
|
|
180
|
+
"""Remove an endpoint from an environment."""
|
|
181
|
+
manager = EnvironmentManager(get_config_path())
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
manager.remove_endpoint(environment, endpoint)
|
|
185
|
+
print(f"✓ Removed endpoint '{endpoint}' from environment '{environment}'")
|
|
186
|
+
|
|
187
|
+
except ValueError as e:
|
|
188
|
+
print(f"ERROR: {e}")
|
|
189
|
+
raise typer.Exit(1)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@app.command("set-default")
|
|
193
|
+
def set_default_endpoint(
|
|
194
|
+
environment: str = typer.Argument(..., help="Environment name"),
|
|
195
|
+
endpoint: str = typer.Argument(..., help="Endpoint name to set as default")
|
|
196
|
+
):
|
|
197
|
+
"""Set the default endpoint for an environment."""
|
|
198
|
+
manager = EnvironmentManager(get_config_path())
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
manager.set_default_endpoint(environment, endpoint)
|
|
202
|
+
print(f"✓ Set '{endpoint}' as default endpoint for environment '{environment}'")
|
|
203
|
+
|
|
204
|
+
except ValueError as e:
|
|
205
|
+
print(f"ERROR: {e}")
|
|
206
|
+
raise typer.Exit(1)
|
|
207
|
+
|