glaip-sdk 0.0.7__py3-none-any.whl → 0.6.5b6__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.
- glaip_sdk/__init__.py +6 -3
- glaip_sdk/_version.py +12 -5
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1126 -0
- glaip_sdk/branding.py +79 -15
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +699 -0
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +503 -183
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +774 -137
- glaip_sdk/cli/commands/mcps.py +1124 -181
- glaip_sdk/cli/commands/models.py +25 -10
- glaip_sdk/cli/commands/tools.py +144 -92
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +61 -0
- glaip_sdk/cli/config.py +95 -0
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +150 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +143 -53
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +24 -18
- glaip_sdk/cli/main.py +420 -145
- glaip_sdk/cli/masking.py +136 -0
- glaip_sdk/cli/mcp_validators.py +287 -0
- glaip_sdk/cli/pager.py +266 -0
- glaip_sdk/cli/parsers/__init__.py +7 -0
- glaip_sdk/cli/parsers/json_input.py +177 -0
- glaip_sdk/cli/resolution.py +28 -21
- glaip_sdk/cli/rich_helpers.py +27 -0
- glaip_sdk/cli/slash/__init__.py +15 -0
- glaip_sdk/cli/slash/accounts_controller.py +500 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +282 -0
- glaip_sdk/cli/slash/prompt.py +245 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +1679 -0
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +872 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +31 -0
- glaip_sdk/cli/transcript/cache.py +536 -0
- glaip_sdk/cli/transcript/capture.py +329 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +77 -0
- glaip_sdk/cli/transcript/viewer.py +372 -0
- glaip_sdk/cli/update_notifier.py +290 -0
- glaip_sdk/cli/utils.py +247 -1238
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +520 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +940 -574
- glaip_sdk/client/base.py +163 -48
- glaip_sdk/client/main.py +35 -12
- glaip_sdk/client/mcps.py +126 -18
- glaip_sdk/client/run_rendering.py +415 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +195 -37
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +15 -5
- glaip_sdk/exceptions.py +16 -9
- glaip_sdk/icons.py +25 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +7 -0
- glaip_sdk/payload_schemas/agent.py +85 -0
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +231 -0
- glaip_sdk/rich_components.py +98 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +597 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +158 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +177 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +59 -13
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +53 -40
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +58 -26
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +65 -32
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +20 -25
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +85 -43
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +51 -19
- glaip_sdk/utils/rendering/layout/progress.py +202 -0
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +39 -7
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
- glaip_sdk/utils/rendering/renderer/base.py +672 -759
- glaip_sdk/utils/rendering/renderer/config.py +4 -10
- glaip_sdk/utils/rendering/renderer/debug.py +75 -22
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +13 -54
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/step_tree_state.py +100 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +29 -26
- glaip_sdk/utils/runtime_config.py +422 -0
- glaip_sdk/utils/serialization.py +184 -51
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +21 -30
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/METADATA +58 -12
- glaip_sdk-0.6.5b6.dist-info/RECORD +159 -0
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -250
- glaip_sdk/utils/rendering/renderer/progress.py +0 -118
- glaip_sdk/utils/rendering/steps.py +0 -232
- glaip_sdk/utils/rich_utils.py +0 -29
- glaip_sdk-0.0.7.dist-info/RECORD +0 -55
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/utils.py
CHANGED
|
@@ -1,1254 +1,263 @@
|
|
|
1
|
-
"""CLI utilities for glaip-sdk.
|
|
1
|
+
"""CLI utilities for glaip-sdk (facade for backward compatibility).
|
|
2
|
+
|
|
3
|
+
This module is a backward-compatible facade that re-exports functions from
|
|
4
|
+
glaip_sdk.cli.core.* modules. New code should import directly from the core modules.
|
|
5
|
+
The facade is deprecated and will be removed after consumers migrate to core modules;
|
|
6
|
+
see docs/specs/refactor/cli-core-modularization.md for the migration plan.
|
|
2
7
|
|
|
3
8
|
Authors:
|
|
4
9
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
-
|
|
10
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
11
|
+
""" # pylint: disable=duplicate-code
|
|
6
12
|
|
|
7
13
|
from __future__ import annotations
|
|
8
14
|
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import os
|
|
12
|
-
import platform
|
|
13
|
-
import shlex
|
|
14
|
-
import shutil
|
|
15
|
-
import subprocess
|
|
16
|
-
import tempfile
|
|
17
|
-
from collections.abc import Callable
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
from typing import TYPE_CHECKING, Any
|
|
20
|
-
|
|
21
|
-
import click
|
|
22
|
-
from rich.console import Console, Group
|
|
23
|
-
from rich.markdown import Markdown
|
|
24
|
-
from rich.pretty import Pretty
|
|
25
|
-
from rich.text import Text
|
|
26
|
-
|
|
27
|
-
# Optional interactive deps (fuzzy palette)
|
|
28
|
-
try:
|
|
29
|
-
from prompt_toolkit.completion import Completion
|
|
30
|
-
from prompt_toolkit.shortcuts import prompt
|
|
31
|
-
|
|
32
|
-
_HAS_PTK = True
|
|
33
|
-
except Exception: # pragma: no cover - optional dependency
|
|
34
|
-
_HAS_PTK = False
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
import questionary
|
|
38
|
-
except Exception: # pragma: no cover - optional dependency
|
|
39
|
-
questionary = None
|
|
15
|
+
import threading
|
|
16
|
+
import warnings
|
|
40
17
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
18
|
+
# Re-export from core modules
|
|
19
|
+
from glaip_sdk.cli.core.context import (
|
|
20
|
+
bind_slash_session_context,
|
|
21
|
+
get_client,
|
|
22
|
+
handle_best_effort_check,
|
|
23
|
+
restore_slash_session_context,
|
|
24
|
+
)
|
|
25
|
+
from glaip_sdk.cli.core.output import (
|
|
26
|
+
coerce_to_row,
|
|
27
|
+
detect_export_format,
|
|
28
|
+
fetch_resource_for_export,
|
|
29
|
+
format_datetime_fields,
|
|
30
|
+
format_size,
|
|
31
|
+
handle_ambiguous_resource,
|
|
32
|
+
handle_resource_export,
|
|
33
|
+
output_list,
|
|
34
|
+
output_result,
|
|
35
|
+
parse_json_line,
|
|
36
|
+
resolve_resource,
|
|
37
|
+
sdk_version,
|
|
38
|
+
# Private functions for backward compatibility (used in tests)
|
|
39
|
+
_build_table_group,
|
|
40
|
+
_build_yaml_renderable,
|
|
41
|
+
_coerce_result_payload,
|
|
42
|
+
_create_table,
|
|
43
|
+
_ensure_displayable,
|
|
44
|
+
_format_yaml_text,
|
|
45
|
+
_get_interface_order,
|
|
46
|
+
_handle_empty_items,
|
|
47
|
+
_handle_fallback_numeric_ambiguity,
|
|
48
|
+
_handle_fuzzy_pick_selection,
|
|
49
|
+
_handle_json_output,
|
|
50
|
+
_handle_json_view_ambiguity,
|
|
51
|
+
_handle_markdown_output,
|
|
52
|
+
_handle_plain_output,
|
|
53
|
+
_handle_questionary_ambiguity,
|
|
54
|
+
_handle_table_output,
|
|
55
|
+
_literal_str_representer,
|
|
56
|
+
_normalise_rows,
|
|
57
|
+
_normalize_interface_preference,
|
|
58
|
+
_print_selection_tip,
|
|
59
|
+
_render_markdown_list,
|
|
60
|
+
_render_markdown_output,
|
|
61
|
+
_render_plain_list,
|
|
62
|
+
_resolve_by_id,
|
|
63
|
+
_resolve_by_name_multiple_fuzzy,
|
|
64
|
+
_resolve_by_name_multiple_questionary,
|
|
65
|
+
_resolve_by_name_multiple_with_select,
|
|
66
|
+
_resource_tip_command,
|
|
67
|
+
_should_fallback_to_numeric_prompt,
|
|
68
|
+
_should_sort_rows,
|
|
69
|
+
_should_use_fuzzy_picker,
|
|
70
|
+
_try_fuzzy_pick,
|
|
71
|
+
_try_fuzzy_selection,
|
|
72
|
+
_try_interface_selection,
|
|
73
|
+
_try_questionary_selection,
|
|
74
|
+
_LiteralYamlDumper,
|
|
75
|
+
)
|
|
76
|
+
from glaip_sdk.cli.core.prompting import (
|
|
77
|
+
_FuzzyCompleter, # Private class for backward compatibility (used in tests)
|
|
78
|
+
_fuzzy_pick_for_resources,
|
|
79
|
+
prompt_export_choice_questionary,
|
|
80
|
+
questionary_safe_ask,
|
|
81
|
+
# Private functions for backward compatibility (used in tests)
|
|
82
|
+
_asyncio_loop_running,
|
|
83
|
+
_basic_prompt,
|
|
84
|
+
_build_resource_labels,
|
|
85
|
+
_build_display_parts,
|
|
86
|
+
_build_primary_parts,
|
|
87
|
+
_build_unique_labels,
|
|
88
|
+
_calculate_consecutive_bonus,
|
|
89
|
+
_calculate_exact_match_bonus,
|
|
90
|
+
_calculate_length_bonus,
|
|
91
|
+
_check_fuzzy_pick_requirements,
|
|
92
|
+
_extract_display_fields,
|
|
93
|
+
_extract_fallback_values,
|
|
94
|
+
_extract_id_suffix,
|
|
95
|
+
_get_fallback_columns,
|
|
96
|
+
_fuzzy_pick,
|
|
97
|
+
_fuzzy_score,
|
|
98
|
+
_is_fuzzy_match,
|
|
99
|
+
_is_standard_field,
|
|
100
|
+
_load_questionary_module,
|
|
101
|
+
_make_questionary_choice,
|
|
102
|
+
_perform_fuzzy_search,
|
|
103
|
+
_prompt_with_auto_select,
|
|
104
|
+
_rank_labels,
|
|
105
|
+
_row_display,
|
|
106
|
+
_run_questionary_in_thread,
|
|
107
|
+
_strip_spaces_for_matching,
|
|
108
|
+
)
|
|
109
|
+
from glaip_sdk.cli.core.rendering import (
|
|
110
|
+
build_renderer,
|
|
111
|
+
spinner_context,
|
|
112
|
+
stop_spinner,
|
|
113
|
+
update_spinner,
|
|
114
|
+
with_client_and_spinner,
|
|
115
|
+
# Private functions for backward compatibility (used in tests)
|
|
116
|
+
_can_use_spinner,
|
|
117
|
+
_register_renderer_with_session,
|
|
118
|
+
_spinner_stop,
|
|
119
|
+
_spinner_update,
|
|
120
|
+
_stream_supports_tty,
|
|
50
121
|
)
|
|
51
122
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def get_ctx_value(ctx: Any, key: str, default: Any = None) -> Any:
|
|
59
|
-
"""Safely resolve a value from click's context object."""
|
|
60
|
-
if ctx is None:
|
|
61
|
-
return default
|
|
62
|
-
|
|
63
|
-
obj = getattr(ctx, "obj", None)
|
|
64
|
-
if obj is None:
|
|
65
|
-
return default
|
|
66
|
-
|
|
67
|
-
if isinstance(obj, dict):
|
|
68
|
-
return obj.get(key, default)
|
|
69
|
-
|
|
70
|
-
getter = getattr(obj, "get", None)
|
|
71
|
-
if callable(getter):
|
|
72
|
-
try:
|
|
73
|
-
return getter(key, default)
|
|
74
|
-
except TypeError:
|
|
75
|
-
return default
|
|
76
|
-
|
|
77
|
-
return getattr(obj, key, default) if hasattr(obj, key) else default
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# ----------------------------- Pager helpers ----------------------------- #
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _prepare_pager_env(
|
|
84
|
-
clear_on_exit: bool = True,
|
|
85
|
-
) -> None: # pragma: no cover - terminal UI setup
|
|
86
|
-
"""
|
|
87
|
-
Configure LESS flags for a predictable, high-quality UX:
|
|
88
|
-
-R : pass ANSI color escapes
|
|
89
|
-
-S : chop long lines (horizontal scroll with ←/→)
|
|
90
|
-
(No -F, no -X) so we open a full-screen pager and clear on exit.
|
|
91
|
-
Toggle wrapping with AIP_PAGER_WRAP=1 to drop -S.
|
|
92
|
-
Power users can override via AIP_LESS_FLAGS.
|
|
93
|
-
"""
|
|
94
|
-
os.environ.pop("LESSSECURE", None)
|
|
95
|
-
if os.getenv("LESS") is None:
|
|
96
|
-
want_wrap = os.getenv("AIP_PAGER_WRAP", "0") == "1"
|
|
97
|
-
base = "-R" if want_wrap else "-RS"
|
|
98
|
-
default_flags = base if clear_on_exit else (base + "FX")
|
|
99
|
-
os.environ["LESS"] = os.getenv("AIP_LESS_FLAGS", default_flags)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _render_ansi(
|
|
103
|
-
renderable: Any,
|
|
104
|
-
) -> str:
|
|
105
|
-
"""Render a Rich renderable to an ANSI string suitable for piping to 'less'."""
|
|
106
|
-
buf = io.StringIO()
|
|
107
|
-
tmp_console = Console(
|
|
108
|
-
file=buf,
|
|
109
|
-
force_terminal=True,
|
|
110
|
-
color_system=console.color_system or "auto",
|
|
111
|
-
width=console.size.width or 100,
|
|
112
|
-
legacy_windows=False,
|
|
113
|
-
soft_wrap=False,
|
|
114
|
-
record=False,
|
|
115
|
-
)
|
|
116
|
-
tmp_console.print(renderable)
|
|
117
|
-
return buf.getvalue()
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _pager_header() -> str:
|
|
121
|
-
v = (os.getenv("AIP_PAGER_HEADER", "1") or "1").strip().lower()
|
|
122
|
-
if v in {"0", "false", "off"}:
|
|
123
|
-
return ""
|
|
124
|
-
return "\n".join(
|
|
125
|
-
[
|
|
126
|
-
"TABLE VIEW — ↑/↓ PgUp/PgDn, ←/→ horiz scroll (with -S), /search, n/N next/prev, h help, q quit",
|
|
127
|
-
"───────────────────────────────────────────────────────────────────────────────────────────────",
|
|
128
|
-
"",
|
|
129
|
-
]
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _should_use_pager() -> bool:
|
|
134
|
-
"""Check if we should attempt to use a system pager."""
|
|
135
|
-
if not (console.is_terminal and os.isatty(1)):
|
|
136
|
-
return False
|
|
137
|
-
if (os.getenv("TERM") or "").lower() == "dumb":
|
|
138
|
-
return False
|
|
139
|
-
return True
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def _resolve_pager_command() -> tuple[list[str] | None, str | None]:
|
|
143
|
-
"""Resolve the pager command and path to use."""
|
|
144
|
-
pager_cmd = None
|
|
145
|
-
pager_env = os.getenv("PAGER")
|
|
146
|
-
if pager_env:
|
|
147
|
-
parts = shlex.split(pager_env)
|
|
148
|
-
if parts and os.path.basename(parts[0]).lower() == "less":
|
|
149
|
-
pager_cmd = parts
|
|
150
|
-
|
|
151
|
-
less_path = shutil.which("less")
|
|
152
|
-
return pager_cmd, less_path
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def _run_less_pager(
|
|
156
|
-
pager_cmd: list[str] | None, less_path: str | None, tmp_path: str
|
|
157
|
-
) -> None:
|
|
158
|
-
"""Run less pager with appropriate command and flags."""
|
|
159
|
-
if pager_cmd:
|
|
160
|
-
subprocess.run([*pager_cmd, tmp_path], check=False)
|
|
161
|
-
else:
|
|
162
|
-
flags = os.getenv("LESS", "-RS").split()
|
|
163
|
-
subprocess.run([less_path, *flags, tmp_path], check=False)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def _run_more_pager(tmp_path: str) -> None:
|
|
167
|
-
"""Run more pager as fallback."""
|
|
168
|
-
more_path = shutil.which("more")
|
|
169
|
-
if more_path:
|
|
170
|
-
subprocess.run([more_path, tmp_path], check=False)
|
|
171
|
-
else:
|
|
172
|
-
raise FileNotFoundError("more command not found")
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def _run_pager_with_temp_file(
|
|
176
|
-
pager_runner: Callable[[str], None], ansi_text: str
|
|
177
|
-
) -> bool:
|
|
178
|
-
"""Run a pager using a temporary file containing the content."""
|
|
179
|
-
_prepare_pager_env(clear_on_exit=True)
|
|
180
|
-
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as tmp:
|
|
181
|
-
tmp.write(_pager_header())
|
|
182
|
-
tmp.write(ansi_text)
|
|
183
|
-
tmp_path = tmp.name
|
|
184
|
-
try:
|
|
185
|
-
pager_runner(tmp_path)
|
|
186
|
-
return True
|
|
187
|
-
except Exception:
|
|
188
|
-
# If pager fails, return False to indicate paging was not successful
|
|
189
|
-
return False
|
|
190
|
-
finally:
|
|
191
|
-
try:
|
|
192
|
-
os.unlink(tmp_path)
|
|
193
|
-
except Exception:
|
|
194
|
-
pass
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def _page_with_system_pager(
|
|
198
|
-
ansi_text: str,
|
|
199
|
-
) -> bool: # pragma: no cover - spawns real pager
|
|
200
|
-
"""Prefer 'less' with a temp file so stdin remains the TTY."""
|
|
201
|
-
if not _should_use_pager():
|
|
202
|
-
return False
|
|
203
|
-
|
|
204
|
-
pager_cmd, less_path = _resolve_pager_command()
|
|
205
|
-
|
|
206
|
-
if pager_cmd or less_path:
|
|
207
|
-
return _run_pager_with_temp_file(
|
|
208
|
-
lambda tmp_path: _run_less_pager(pager_cmd, less_path, tmp_path), ansi_text
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
# Windows 'more' is poor with ANSI; let Rich fallback handle it
|
|
212
|
-
if platform.system().lower().startswith("win"):
|
|
213
|
-
return False
|
|
214
|
-
|
|
215
|
-
# POSIX 'more' fallback (may or may not honor ANSI)
|
|
216
|
-
return _run_pager_with_temp_file(_run_more_pager, ansi_text)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _get_view(ctx: Any) -> str:
|
|
220
|
-
view = get_ctx_value(ctx, "view")
|
|
221
|
-
if view:
|
|
222
|
-
return view
|
|
223
|
-
|
|
224
|
-
fallback = get_ctx_value(ctx, "format")
|
|
225
|
-
return fallback or "rich"
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
# ----------------------------- Client config ----------------------------- #
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
232
|
-
"""Get configured client from context, env, and config file (ctx > env > file)."""
|
|
233
|
-
from glaip_sdk import Client
|
|
234
|
-
|
|
235
|
-
file_config = load_config() or {}
|
|
236
|
-
context_config_obj = getattr(ctx, "obj", None)
|
|
237
|
-
context_config = context_config_obj or {}
|
|
238
|
-
|
|
239
|
-
raw_timeout = os.getenv("AIP_TIMEOUT", "0") or "0"
|
|
240
|
-
try:
|
|
241
|
-
timeout_value = float(raw_timeout)
|
|
242
|
-
except ValueError:
|
|
243
|
-
timeout_value = None
|
|
244
|
-
|
|
245
|
-
env_config = {
|
|
246
|
-
"api_url": os.getenv("AIP_API_URL"),
|
|
247
|
-
"api_key": os.getenv("AIP_API_KEY"),
|
|
248
|
-
"timeout": timeout_value if timeout_value else None,
|
|
249
|
-
}
|
|
250
|
-
env_config = {k: v for k, v in env_config.items() if v not in (None, "", 0)}
|
|
251
|
-
|
|
252
|
-
# Merge config sources: context > env > file
|
|
253
|
-
config = {
|
|
254
|
-
**file_config,
|
|
255
|
-
**env_config,
|
|
256
|
-
**{k: v for k, v in context_config.items() if v is not None},
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if not config.get("api_url") or not config.get("api_key"):
|
|
260
|
-
raise click.ClickException(
|
|
261
|
-
"Missing api_url/api_key. Run `aip configure` or set AIP_* env vars."
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
return Client(
|
|
265
|
-
api_url=config.get("api_url"),
|
|
266
|
-
api_key=config.get("api_key"),
|
|
267
|
-
timeout=float(config.get("timeout") or 30.0),
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
# ----------------------------- Secret masking ---------------------------- #
|
|
272
|
-
|
|
273
|
-
_DEFAULT_MASK_FIELDS = {
|
|
274
|
-
"api_key",
|
|
275
|
-
"apikey",
|
|
276
|
-
"token",
|
|
277
|
-
"access_token",
|
|
278
|
-
"secret",
|
|
279
|
-
"client_secret",
|
|
280
|
-
"password",
|
|
281
|
-
"private_key",
|
|
282
|
-
"bearer",
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def _mask_value(v: Any) -> str:
|
|
287
|
-
s = str(v)
|
|
288
|
-
if len(s) <= 8:
|
|
289
|
-
return "••••"
|
|
290
|
-
return f"{s[:4]}••••••••{s[-4:]}"
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def _mask_any(value: Any, mask_fields: set[str]) -> Any:
|
|
294
|
-
"""Recursively mask sensitive fields in mappings / lists."""
|
|
295
|
-
|
|
296
|
-
if isinstance(value, dict):
|
|
297
|
-
masked: dict[Any, Any] = {}
|
|
298
|
-
for key, raw in value.items():
|
|
299
|
-
if isinstance(key, str) and key.lower() in mask_fields and raw is not None:
|
|
300
|
-
masked[key] = _mask_value(raw)
|
|
301
|
-
else:
|
|
302
|
-
masked[key] = _mask_any(raw, mask_fields)
|
|
303
|
-
return masked
|
|
304
|
-
|
|
305
|
-
if isinstance(value, list):
|
|
306
|
-
return [_mask_any(item, mask_fields) for item in value]
|
|
307
|
-
|
|
308
|
-
return value
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def _maybe_mask_row(row: dict[str, Any], mask_fields: set[str]) -> dict[str, Any]:
|
|
312
|
-
"""Mask a single row (legacy function, now uses _mask_any)."""
|
|
313
|
-
if not mask_fields:
|
|
314
|
-
return row
|
|
315
|
-
return _mask_any(row, mask_fields)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def _resolve_mask_fields() -> set[str]:
|
|
319
|
-
if os.getenv("AIP_MASK_OFF", "0") in ("1", "true", "on", "yes"):
|
|
320
|
-
return set()
|
|
321
|
-
env_fields = (os.getenv("AIP_MASK_FIELDS") or "").strip()
|
|
322
|
-
if env_fields:
|
|
323
|
-
parts = [p.strip().lower() for p in env_fields.split(",") if p.strip()]
|
|
324
|
-
return set(parts)
|
|
325
|
-
return set(_DEFAULT_MASK_FIELDS)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
# ----------------------------- Fuzzy palette ----------------------------- #
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
def _extract_display_fields(row: dict[str, Any]) -> tuple[str, str, str, str]:
|
|
332
|
-
"""Extract display fields from row data."""
|
|
333
|
-
name = str(row.get("name", "")).strip()
|
|
334
|
-
_id = str(row.get("id", "")).strip()
|
|
335
|
-
type_ = str(row.get("type", "")).strip()
|
|
336
|
-
fw = str(row.get("framework", "")).strip()
|
|
337
|
-
return name, _id, type_, fw
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def _build_primary_parts(name: str, type_: str, fw: str) -> list[str]:
|
|
341
|
-
"""Build primary display parts from name, type, and framework."""
|
|
342
|
-
parts = []
|
|
343
|
-
if name:
|
|
344
|
-
parts.append(name)
|
|
345
|
-
if type_:
|
|
346
|
-
parts.append(type_)
|
|
347
|
-
if fw:
|
|
348
|
-
parts.append(fw)
|
|
349
|
-
return parts
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def _get_fallback_columns(columns: list[tuple]) -> list[tuple]:
|
|
353
|
-
"""Get first two visible columns for fallback display."""
|
|
354
|
-
return columns[:2]
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
def _is_standard_field(k: str) -> bool:
|
|
358
|
-
"""Check if field is a standard field to skip."""
|
|
359
|
-
return k in ("id", "name", "type", "framework")
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
def _extract_fallback_values(row: dict[str, Any], columns: list[tuple]) -> list[str]:
|
|
363
|
-
"""Extract fallback values from columns."""
|
|
364
|
-
fallback_parts = []
|
|
365
|
-
for k, _hdr, _style, _w in columns:
|
|
366
|
-
if _is_standard_field(k):
|
|
367
|
-
continue
|
|
368
|
-
val = str(row.get(k, "")).strip()
|
|
369
|
-
if val:
|
|
370
|
-
fallback_parts.append(val)
|
|
371
|
-
if len(fallback_parts) >= 2:
|
|
372
|
-
break
|
|
373
|
-
return fallback_parts
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
def _build_display_parts(
|
|
377
|
-
name: str, _id: str, type_: str, fw: str, row: dict[str, Any], columns: list[tuple]
|
|
378
|
-
) -> list[str]:
|
|
379
|
-
"""Build complete display parts list."""
|
|
380
|
-
parts = _build_primary_parts(name, type_, fw)
|
|
381
|
-
|
|
382
|
-
if not parts:
|
|
383
|
-
# Use fallback columns
|
|
384
|
-
fallback_columns = _get_fallback_columns(columns)
|
|
385
|
-
parts.extend(_extract_fallback_values(row, fallback_columns))
|
|
386
|
-
|
|
387
|
-
if _id:
|
|
388
|
-
parts.append(f"[{_id}]")
|
|
389
|
-
|
|
390
|
-
return parts
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
def _row_display(row: dict[str, Any], columns: list[tuple]) -> str:
|
|
394
|
-
"""
|
|
395
|
-
Build a compact text label for the palette.
|
|
396
|
-
Prefers: name • type • framework • [id] (when available)
|
|
397
|
-
Falls back to first 2 columns + [id].
|
|
398
|
-
"""
|
|
399
|
-
name, _id, type_, fw = _extract_display_fields(row)
|
|
400
|
-
parts = _build_display_parts(name, _id, type_, fw, row, columns)
|
|
401
|
-
return " • ".join(parts) if parts else (_id or "(row)")
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def _check_fuzzy_pick_requirements() -> bool:
|
|
405
|
-
"""Check if fuzzy picking requirements are met."""
|
|
406
|
-
return _HAS_PTK and console.is_terminal and os.isatty(1)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def _build_unique_labels(
|
|
410
|
-
rows: list[dict[str, Any]], columns: list[tuple]
|
|
411
|
-
) -> tuple[list[str], dict[str, dict[str, Any]]]:
|
|
412
|
-
"""Build unique display labels and reverse mapping."""
|
|
413
|
-
labels = []
|
|
414
|
-
by_label: dict[str, dict[str, Any]] = {}
|
|
415
|
-
|
|
416
|
-
for r in rows:
|
|
417
|
-
label = _row_display(r, columns)
|
|
418
|
-
# Ensure uniqueness: if duplicate, suffix with …#n
|
|
419
|
-
if label in by_label:
|
|
420
|
-
i = 2
|
|
421
|
-
base = label
|
|
422
|
-
while f"{base} #{i}" in by_label:
|
|
423
|
-
i += 1
|
|
424
|
-
label = f"{base} #{i}"
|
|
425
|
-
labels.append(label)
|
|
426
|
-
by_label[label] = r
|
|
427
|
-
|
|
428
|
-
return labels, by_label
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
class _FuzzyCompleter:
|
|
432
|
-
"""Fuzzy completer for prompt_toolkit."""
|
|
433
|
-
|
|
434
|
-
def __init__(self, words: list[str]) -> None:
|
|
435
|
-
self.words = words
|
|
436
|
-
|
|
437
|
-
def get_completions(
|
|
438
|
-
self, document: Any, _complete_event: Any
|
|
439
|
-
) -> Any: # pragma: no cover
|
|
440
|
-
word = document.get_word_before_cursor()
|
|
441
|
-
if not word:
|
|
442
|
-
return
|
|
443
|
-
|
|
444
|
-
word_lower = word.lower()
|
|
445
|
-
for label in self.words:
|
|
446
|
-
label_lower = label.lower()
|
|
447
|
-
if self._fuzzy_match(word_lower, label_lower):
|
|
448
|
-
yield Completion(label, start_position=-len(word))
|
|
449
|
-
|
|
450
|
-
def _fuzzy_match(self, search: str, target: str) -> bool: # pragma: no cover
|
|
451
|
-
"""True fuzzy matching: checks if all characters in search appear in order in target."""
|
|
452
|
-
if not search:
|
|
453
|
-
return True
|
|
454
|
-
|
|
455
|
-
search_idx = 0
|
|
456
|
-
for char in target:
|
|
457
|
-
if search_idx < len(search) and search[search_idx] == char:
|
|
458
|
-
search_idx += 1
|
|
459
|
-
if search_idx == len(search):
|
|
460
|
-
return True
|
|
461
|
-
return False
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
def _perform_fuzzy_search(
|
|
465
|
-
answer: str, labels: list[str], by_label: dict[str, dict[str, Any]]
|
|
466
|
-
) -> dict[str, Any] | None:
|
|
467
|
-
"""Perform fuzzy search fallback and return best match."""
|
|
468
|
-
# Exact label match
|
|
469
|
-
if answer in by_label:
|
|
470
|
-
return by_label[answer]
|
|
471
|
-
|
|
472
|
-
# Fuzzy search fallback
|
|
473
|
-
best_match = None
|
|
474
|
-
best_score = -1
|
|
475
|
-
|
|
476
|
-
for label in labels:
|
|
477
|
-
score = _fuzzy_score(answer.lower(), label.lower())
|
|
478
|
-
if score > best_score:
|
|
479
|
-
best_score = score
|
|
480
|
-
best_match = label
|
|
481
|
-
|
|
482
|
-
return by_label[best_match] if best_match and best_score > 0 else None
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
def _fuzzy_pick(
|
|
486
|
-
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
487
|
-
) -> dict[str, Any] | None: # pragma: no cover - requires interactive prompt toolkit
|
|
488
|
-
"""
|
|
489
|
-
Open a minimal fuzzy palette using prompt_toolkit.
|
|
490
|
-
Returns the selected row (dict) or None if cancelled/missing deps.
|
|
491
|
-
"""
|
|
492
|
-
if not _check_fuzzy_pick_requirements():
|
|
493
|
-
return None
|
|
494
|
-
|
|
495
|
-
# Build display labels and mapping
|
|
496
|
-
labels, by_label = _build_unique_labels(rows, columns)
|
|
497
|
-
|
|
498
|
-
# Create fuzzy completer
|
|
499
|
-
completer = _FuzzyCompleter(labels)
|
|
500
|
-
|
|
501
|
-
try:
|
|
502
|
-
answer = prompt(
|
|
503
|
-
message=f"Find {title.rstrip('s')}: ",
|
|
504
|
-
completer=completer,
|
|
505
|
-
complete_in_thread=True,
|
|
506
|
-
complete_while_typing=True,
|
|
507
|
-
)
|
|
508
|
-
except (KeyboardInterrupt, EOFError): # pragma: no cover - user cancelled input
|
|
509
|
-
return None
|
|
510
|
-
|
|
511
|
-
return _perform_fuzzy_search(answer, labels, by_label) if answer else None
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
def _is_fuzzy_match(search: str, target: str) -> bool:
|
|
515
|
-
"""Check if search string is a fuzzy match for target."""
|
|
516
|
-
if not search:
|
|
517
|
-
return True
|
|
518
|
-
|
|
519
|
-
search_idx = 0
|
|
520
|
-
for char in target:
|
|
521
|
-
if search_idx < len(search) and search[search_idx] == char:
|
|
522
|
-
search_idx += 1
|
|
523
|
-
if search_idx == len(search):
|
|
524
|
-
return True
|
|
525
|
-
return False
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
def _calculate_exact_match_bonus(search: str, target: str) -> int:
|
|
529
|
-
"""Calculate bonus for exact substring matches."""
|
|
530
|
-
return 100 if search.lower() in target.lower() else 0
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
def _calculate_consecutive_bonus(search: str, target: str) -> int:
|
|
534
|
-
"""Calculate bonus for consecutive character matches."""
|
|
535
|
-
consecutive = 0
|
|
536
|
-
max_consecutive = 0
|
|
537
|
-
search_idx = 0
|
|
538
|
-
|
|
539
|
-
for char in target:
|
|
540
|
-
if search_idx < len(search) and search[search_idx] == char:
|
|
541
|
-
consecutive += 1
|
|
542
|
-
max_consecutive = max(max_consecutive, consecutive)
|
|
543
|
-
search_idx += 1
|
|
544
|
-
else:
|
|
545
|
-
consecutive = 0
|
|
546
|
-
|
|
547
|
-
return max_consecutive * 10
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
def _calculate_length_bonus(search: str, target: str) -> int:
|
|
551
|
-
"""Calculate bonus for shorter search terms."""
|
|
552
|
-
return (len(target) - len(search)) * 2
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
def _fuzzy_score(search: str, target: str) -> int:
|
|
556
|
-
"""
|
|
557
|
-
Calculate fuzzy match score.
|
|
558
|
-
Higher score = better match.
|
|
559
|
-
Returns -1 if no match possible.
|
|
560
|
-
"""
|
|
561
|
-
if not search:
|
|
562
|
-
return 0
|
|
563
|
-
|
|
564
|
-
if not _is_fuzzy_match(search, target):
|
|
565
|
-
return -1 # Not a fuzzy match
|
|
566
|
-
|
|
567
|
-
# Calculate score based on different factors
|
|
568
|
-
score = 0
|
|
569
|
-
score += _calculate_exact_match_bonus(search, target)
|
|
570
|
-
score += _calculate_consecutive_bonus(search, target)
|
|
571
|
-
score += _calculate_length_bonus(search, target)
|
|
572
|
-
|
|
573
|
-
return score
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
# ----------------------------- Pretty outputs ---------------------------- #
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
def _coerce_result_payload(result: Any) -> Any:
|
|
580
|
-
try:
|
|
581
|
-
to_dict = getattr(result, "to_dict", None)
|
|
582
|
-
if callable(to_dict):
|
|
583
|
-
return to_dict()
|
|
584
|
-
except Exception:
|
|
585
|
-
return result
|
|
586
|
-
return result
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
def _apply_mask_if_configured(payload: Any) -> Any:
|
|
590
|
-
mask_fields = _resolve_mask_fields()
|
|
591
|
-
if not mask_fields:
|
|
592
|
-
return payload
|
|
593
|
-
try:
|
|
594
|
-
return _mask_any(payload, mask_fields)
|
|
595
|
-
except Exception:
|
|
596
|
-
return payload
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
def _ensure_displayable(payload: Any) -> Any:
|
|
600
|
-
if isinstance(payload, dict | list | str | int | float | bool) or payload is None:
|
|
601
|
-
return payload
|
|
602
|
-
|
|
603
|
-
if hasattr(payload, "__dict__"):
|
|
604
|
-
try:
|
|
605
|
-
return dict(payload)
|
|
606
|
-
except Exception:
|
|
607
|
-
try:
|
|
608
|
-
return dict(payload.__dict__)
|
|
609
|
-
except Exception:
|
|
610
|
-
pass
|
|
611
|
-
|
|
612
|
-
try:
|
|
613
|
-
return str(payload)
|
|
614
|
-
except Exception:
|
|
615
|
-
return repr(payload)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
def _render_markdown_output(data: Any) -> None:
|
|
619
|
-
try:
|
|
620
|
-
console.print(Markdown(str(data)))
|
|
621
|
-
except ImportError:
|
|
622
|
-
click.echo(str(data))
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
def output_result(
|
|
626
|
-
ctx: Any,
|
|
627
|
-
result: Any,
|
|
628
|
-
title: str = "Result",
|
|
629
|
-
panel_title: str | None = None,
|
|
630
|
-
success_message: str | None = None,
|
|
631
|
-
) -> None:
|
|
632
|
-
fmt = _get_view(ctx)
|
|
633
|
-
|
|
634
|
-
data = _coerce_result_payload(result)
|
|
635
|
-
data = _apply_mask_if_configured(data)
|
|
636
|
-
data = _ensure_displayable(data)
|
|
637
|
-
|
|
638
|
-
if fmt == "json":
|
|
639
|
-
click.echo(json.dumps(data, indent=2, default=str))
|
|
640
|
-
return
|
|
641
|
-
|
|
642
|
-
if fmt == "plain":
|
|
643
|
-
click.echo(str(data))
|
|
644
|
-
return
|
|
645
|
-
|
|
646
|
-
if fmt == "md":
|
|
647
|
-
_render_markdown_output(data)
|
|
648
|
-
return
|
|
649
|
-
|
|
650
|
-
if success_message:
|
|
651
|
-
console.print(Text(f"[green]✅ {success_message}[/green]"))
|
|
652
|
-
|
|
653
|
-
if panel_title:
|
|
654
|
-
console.print(
|
|
655
|
-
AIPPanel(
|
|
656
|
-
Pretty(data),
|
|
657
|
-
title=panel_title,
|
|
658
|
-
border_style="blue",
|
|
659
|
-
)
|
|
660
|
-
)
|
|
661
|
-
else:
|
|
662
|
-
console.print(Text(f"[cyan]{title}:[/cyan]"))
|
|
663
|
-
console.print(Pretty(data))
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
# ----------------------------- List rendering ---------------------------- #
|
|
667
|
-
|
|
668
|
-
# Threshold no longer used - fuzzy palette is always default for TTY
|
|
669
|
-
# _PICK_THRESHOLD = int(os.getenv("AIP_PICK_THRESHOLD", "5") or "5")
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
def _normalise_rows(
|
|
673
|
-
items: list[Any], transform_func: Callable[[Any], dict[str, Any]] | None
|
|
674
|
-
) -> list[dict[str, Any]]:
|
|
675
|
-
try:
|
|
676
|
-
rows: list[dict[str, Any]] = []
|
|
677
|
-
for item in items:
|
|
678
|
-
if transform_func:
|
|
679
|
-
rows.append(transform_func(item))
|
|
680
|
-
elif hasattr(item, "to_dict"):
|
|
681
|
-
rows.append(item.to_dict())
|
|
682
|
-
elif hasattr(item, "__dict__"):
|
|
683
|
-
rows.append(vars(item))
|
|
684
|
-
elif isinstance(item, dict):
|
|
685
|
-
rows.append(item)
|
|
686
|
-
else:
|
|
687
|
-
rows.append({"value": item})
|
|
688
|
-
return rows
|
|
689
|
-
except Exception:
|
|
690
|
-
return []
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
def _mask_rows_if_configured(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
694
|
-
mask_fields = _resolve_mask_fields()
|
|
695
|
-
if not mask_fields:
|
|
696
|
-
return rows
|
|
697
|
-
try:
|
|
698
|
-
return [_maybe_mask_row(row, mask_fields) for row in rows]
|
|
699
|
-
except Exception:
|
|
700
|
-
return rows
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
def _render_plain_list(
|
|
704
|
-
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
705
|
-
) -> None:
|
|
706
|
-
if not rows:
|
|
707
|
-
click.echo(f"No {title.lower()} found.")
|
|
708
|
-
return
|
|
709
|
-
for row in rows:
|
|
710
|
-
row_str = " | ".join(str(row.get(key, "N/A")) for key, _, _, _ in columns)
|
|
711
|
-
click.echo(row_str)
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
def _render_markdown_list(
|
|
715
|
-
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
716
|
-
) -> None:
|
|
717
|
-
if not rows:
|
|
718
|
-
click.echo(f"No {title.lower()} found.")
|
|
719
|
-
return
|
|
720
|
-
headers = [header for _, header, _, _ in columns]
|
|
721
|
-
click.echo(f"| {' | '.join(headers)} |")
|
|
722
|
-
click.echo(f"| {' | '.join('---' for _ in headers)} |")
|
|
723
|
-
for row in rows:
|
|
724
|
-
row_str = " | ".join(str(row.get(key, "N/A")) for key, _, _, _ in columns)
|
|
725
|
-
click.echo(f"| {row_str} |")
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
def _should_sort_rows(rows: list[dict[str, Any]]) -> bool:
|
|
729
|
-
return (
|
|
730
|
-
os.getenv("AIP_TABLE_NO_SORT", "0") not in ("1", "true", "on")
|
|
731
|
-
and rows
|
|
732
|
-
and isinstance(rows[0], dict)
|
|
733
|
-
and "name" in rows[0]
|
|
734
|
-
)
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
def _create_table(columns: list[tuple[str, str, str, int | None]], title: str) -> Any:
|
|
738
|
-
table = AIPTable(title=title, expand=True)
|
|
739
|
-
for _key, header, style, width in columns:
|
|
740
|
-
table.add_column(header, style=style, width=width)
|
|
741
|
-
return table
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
def _build_table_group(
|
|
745
|
-
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
746
|
-
) -> Group:
|
|
747
|
-
table = _create_table(columns, title)
|
|
748
|
-
for row in rows:
|
|
749
|
-
table.add_row(*[str(row.get(key, "N/A")) for key, _, _, _ in columns])
|
|
750
|
-
footer = Text(f"\n[dim]Total {len(rows)} items[/dim]")
|
|
751
|
-
return Group(table, footer)
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
def _should_page_output(row_count: int, is_tty: bool) -> bool:
|
|
755
|
-
pager_env = (os.getenv("AIP_PAGER", "auto") or "auto").lower()
|
|
756
|
-
if pager_env in ("0", "off", "false"):
|
|
757
|
-
return False
|
|
758
|
-
if pager_env in ("1", "on", "true"):
|
|
759
|
-
return is_tty
|
|
760
|
-
try:
|
|
761
|
-
term_h = console.size.height or 24
|
|
762
|
-
approx_lines = 5 + row_count
|
|
763
|
-
return is_tty and (approx_lines >= term_h * 0.5)
|
|
764
|
-
except Exception:
|
|
765
|
-
return is_tty
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
def _handle_json_output(items: list[Any], rows: list[dict[str, Any]]) -> None:
|
|
769
|
-
"""Handle JSON output format."""
|
|
770
|
-
data = (
|
|
771
|
-
rows
|
|
772
|
-
if rows
|
|
773
|
-
else [it.to_dict() if hasattr(it, "to_dict") else it for it in items]
|
|
774
|
-
)
|
|
775
|
-
click.echo(json.dumps(data, indent=2, default=str))
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
def _handle_plain_output(
|
|
779
|
-
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
780
|
-
) -> None:
|
|
781
|
-
"""Handle plain text output format."""
|
|
782
|
-
_render_plain_list(rows, title, columns)
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
def _handle_markdown_output(
|
|
786
|
-
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
787
|
-
) -> None:
|
|
788
|
-
"""Handle markdown output format."""
|
|
789
|
-
_render_markdown_list(rows, title, columns)
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
def _handle_empty_items(title: str) -> None:
|
|
793
|
-
"""Handle case when no items are found."""
|
|
794
|
-
console.print(Text(f"[yellow]No {title.lower()} found.[/yellow]"))
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
def _handle_fuzzy_pick_selection(
|
|
798
|
-
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
799
|
-
) -> bool:
|
|
800
|
-
"""Handle fuzzy picker selection, returns True if selection was made."""
|
|
801
|
-
picked = (
|
|
802
|
-
_fuzzy_pick(rows, columns, title)
|
|
803
|
-
if console.is_terminal and os.isatty(1)
|
|
804
|
-
else None
|
|
805
|
-
)
|
|
806
|
-
if picked:
|
|
807
|
-
table = _create_table(columns, title)
|
|
808
|
-
table.add_row(*[str(picked.get(key, "N/A")) for key, _, _, _ in columns])
|
|
809
|
-
console.print(table)
|
|
810
|
-
console.print(Text("\n[dim]Tip: use `aip agents get <ID>` for details[/dim]"))
|
|
811
|
-
return True
|
|
812
|
-
return False
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
def _handle_table_output(
|
|
816
|
-
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
817
|
-
) -> None:
|
|
818
|
-
"""Handle table output with paging."""
|
|
819
|
-
content = _build_table_group(rows, columns, title)
|
|
820
|
-
if _should_page_output(len(rows), console.is_terminal and os.isatty(1)):
|
|
821
|
-
ansi = _render_ansi(content)
|
|
822
|
-
if not _page_with_system_pager(ansi):
|
|
823
|
-
with console.pager(styles=True):
|
|
824
|
-
console.print(content)
|
|
825
|
-
else:
|
|
826
|
-
console.print(content)
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
def output_list(
|
|
830
|
-
ctx: Any,
|
|
831
|
-
items: list[Any],
|
|
832
|
-
title: str,
|
|
833
|
-
columns: list[tuple[str, str, str, int | None]],
|
|
834
|
-
transform_func: Callable | None = None,
|
|
835
|
-
) -> None:
|
|
836
|
-
"""Display a list with fuzzy palette by default on TTY, Rich table as fallback."""
|
|
837
|
-
fmt = _get_view(ctx)
|
|
838
|
-
rows = _normalise_rows(items, transform_func)
|
|
839
|
-
rows = _mask_rows_if_configured(rows)
|
|
840
|
-
|
|
841
|
-
if fmt == "json":
|
|
842
|
-
_handle_json_output(items, rows)
|
|
843
|
-
return
|
|
844
|
-
|
|
845
|
-
if fmt == "plain":
|
|
846
|
-
_handle_plain_output(rows, title, columns)
|
|
847
|
-
return
|
|
848
|
-
|
|
849
|
-
if fmt == "md":
|
|
850
|
-
_handle_markdown_output(rows, title, columns)
|
|
851
|
-
return
|
|
852
|
-
|
|
853
|
-
if not items:
|
|
854
|
-
_handle_empty_items(title)
|
|
855
|
-
return
|
|
856
|
-
|
|
857
|
-
if _should_sort_rows(rows):
|
|
858
|
-
try:
|
|
859
|
-
rows = sorted(rows, key=lambda r: str(r.get("name", "")).lower())
|
|
860
|
-
except Exception:
|
|
861
|
-
pass
|
|
862
|
-
|
|
863
|
-
if _handle_fuzzy_pick_selection(rows, columns, title):
|
|
864
|
-
return
|
|
865
|
-
|
|
866
|
-
_handle_table_output(rows, columns, title)
|
|
867
|
-
|
|
123
|
+
# Re-export from other modules for backward compatibility
|
|
124
|
+
from glaip_sdk.cli.context import get_ctx_value
|
|
125
|
+
from glaip_sdk.cli.hints import command_hint
|
|
126
|
+
from glaip_sdk.utils import is_uuid
|
|
868
127
|
|
|
869
|
-
#
|
|
128
|
+
# Re-export module-level variables for backward compatibility
|
|
129
|
+
# Note: console is re-exported from output.py since that's where _handle_table_output uses it
|
|
130
|
+
from glaip_sdk.cli.core.output import console
|
|
131
|
+
import logging
|
|
870
132
|
|
|
133
|
+
logger = logging.getLogger("glaip_sdk.cli.utils")
|
|
134
|
+
questionary = None # type: ignore[assignment]
|
|
871
135
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
return
|
|
875
|
-
ctx.ensure_object(dict)
|
|
876
|
-
ctx.obj["view"] = value
|
|
136
|
+
_warn_lock = threading.Lock()
|
|
137
|
+
_warned = False
|
|
877
138
|
|
|
878
139
|
|
|
879
|
-
def
|
|
880
|
-
|
|
140
|
+
def _warn_once() -> None:
|
|
141
|
+
"""Emit the deprecation warning once in a thread-safe way."""
|
|
142
|
+
global _warned
|
|
143
|
+
if _warned:
|
|
881
144
|
return
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
890
|
-
f = click.option(
|
|
891
|
-
"--json",
|
|
892
|
-
"json_mode",
|
|
893
|
-
is_flag=True,
|
|
894
|
-
expose_value=False,
|
|
895
|
-
help="Shortcut for --view json",
|
|
896
|
-
callback=_set_json,
|
|
897
|
-
)(f)
|
|
898
|
-
f = click.option(
|
|
899
|
-
"-o",
|
|
900
|
-
"--output",
|
|
901
|
-
"--view",
|
|
902
|
-
"view_opt",
|
|
903
|
-
type=click.Choice(["rich", "plain", "json", "md"]),
|
|
904
|
-
expose_value=False,
|
|
905
|
-
help="Output format",
|
|
906
|
-
callback=_set_view,
|
|
907
|
-
)(f)
|
|
908
|
-
return f
|
|
909
|
-
|
|
910
|
-
return decorator
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
# ------------------------- Ambiguity handling --------------------------- #
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
def coerce_to_row(item: Any, keys: list[str]) -> dict[str, Any]:
|
|
917
|
-
"""Coerce an item (dict or object) to a row dict with specified keys.
|
|
918
|
-
|
|
919
|
-
Args:
|
|
920
|
-
item: The item to coerce (dict or object with attributes)
|
|
921
|
-
keys: List of keys/attribute names to extract
|
|
922
|
-
|
|
923
|
-
Returns:
|
|
924
|
-
Dict with the extracted values, "N/A" for missing values
|
|
925
|
-
"""
|
|
926
|
-
result = {}
|
|
927
|
-
for key in keys:
|
|
928
|
-
if isinstance(item, dict):
|
|
929
|
-
value = item.get(key, "N/A")
|
|
930
|
-
else:
|
|
931
|
-
value = getattr(item, key, "N/A")
|
|
932
|
-
result[key] = str(value) if value is not None else "N/A"
|
|
933
|
-
return result
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
def build_renderer(
|
|
937
|
-
_ctx: Any,
|
|
938
|
-
*,
|
|
939
|
-
save_path: str | os.PathLike[str] | None,
|
|
940
|
-
theme: str = "dark",
|
|
941
|
-
verbose: bool = False,
|
|
942
|
-
_tty_enabled: bool = True,
|
|
943
|
-
live: bool | None = None,
|
|
944
|
-
snapshots: bool | None = None,
|
|
945
|
-
) -> tuple[RichStreamRenderer, Console | CapturingConsole]:
|
|
946
|
-
"""Build renderer and capturing console for CLI commands.
|
|
947
|
-
|
|
948
|
-
Args:
|
|
949
|
-
ctx: Click context
|
|
950
|
-
save_path: Path to save output to (enables capturing)
|
|
951
|
-
theme: Color theme ("dark" or "light")
|
|
952
|
-
verbose: Whether to enable verbose mode
|
|
953
|
-
tty_enabled: Whether TTY is available
|
|
954
|
-
|
|
955
|
-
Returns:
|
|
956
|
-
Tuple of (renderer, capturing_console)
|
|
957
|
-
"""
|
|
958
|
-
# Use capturing console if saving output
|
|
959
|
-
working_console = console
|
|
960
|
-
if save_path:
|
|
961
|
-
working_console = CapturingConsole(console, capture=True)
|
|
962
|
-
|
|
963
|
-
# Configure renderer based on verbose mode and explicit overrides
|
|
964
|
-
if live is None:
|
|
965
|
-
live_enabled = not verbose # Disable live mode in verbose (unless overridden)
|
|
966
|
-
else:
|
|
967
|
-
live_enabled = bool(live)
|
|
968
|
-
|
|
969
|
-
renderer_cfg = RendererConfig(
|
|
970
|
-
theme=theme,
|
|
971
|
-
style="debug" if verbose else "pretty",
|
|
972
|
-
live=live_enabled,
|
|
973
|
-
show_delegate_tool_panels=True,
|
|
974
|
-
append_finished_snapshots=bool(snapshots)
|
|
975
|
-
if snapshots is not None
|
|
976
|
-
else RendererConfig.append_finished_snapshots,
|
|
977
|
-
)
|
|
978
|
-
|
|
979
|
-
# Create the renderer instance
|
|
980
|
-
renderer = RichStreamRenderer(
|
|
981
|
-
working_console.original_console
|
|
982
|
-
if isinstance(working_console, CapturingConsole)
|
|
983
|
-
else working_console,
|
|
984
|
-
cfg=renderer_cfg,
|
|
985
|
-
verbose=verbose,
|
|
986
|
-
)
|
|
987
|
-
|
|
988
|
-
return renderer, working_console
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
def _build_resource_labels(resources: list[Any]) -> tuple[list[str], dict[str, Any]]:
|
|
992
|
-
"""Build unique display labels for resources."""
|
|
993
|
-
labels = []
|
|
994
|
-
by_label: dict[str, Any] = {}
|
|
995
|
-
|
|
996
|
-
for resource in resources:
|
|
997
|
-
name = getattr(resource, "name", "Unknown")
|
|
998
|
-
_id = getattr(resource, "id", "Unknown")
|
|
999
|
-
|
|
1000
|
-
# Create display label
|
|
1001
|
-
label_parts = []
|
|
1002
|
-
if name and name != "Unknown":
|
|
1003
|
-
label_parts.append(name)
|
|
1004
|
-
label_parts.append(f"[{_id[:8]}...]") # Show first 8 chars of ID
|
|
1005
|
-
label = " • ".join(label_parts)
|
|
1006
|
-
|
|
1007
|
-
# Ensure uniqueness
|
|
1008
|
-
if label in by_label:
|
|
1009
|
-
i = 2
|
|
1010
|
-
base = label
|
|
1011
|
-
while f"{base} #{i}" in by_label:
|
|
1012
|
-
i += 1
|
|
1013
|
-
label = f"{base} #{i}"
|
|
1014
|
-
|
|
1015
|
-
labels.append(label)
|
|
1016
|
-
by_label[label] = resource
|
|
1017
|
-
|
|
1018
|
-
return labels, by_label
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
def _fuzzy_pick_for_resources(
|
|
1022
|
-
resources: list[Any], resource_type: str, _search_term: str
|
|
1023
|
-
) -> Any | None: # pragma: no cover - interactive selection helper
|
|
1024
|
-
"""
|
|
1025
|
-
Fuzzy picker for resource objects, similar to _fuzzy_pick but without column dependencies.
|
|
1026
|
-
|
|
1027
|
-
Args:
|
|
1028
|
-
resources: List of resource objects to choose from
|
|
1029
|
-
resource_type: Type of resource (e.g., "agent", "tool")
|
|
1030
|
-
search_term: The search term that led to multiple matches
|
|
1031
|
-
|
|
1032
|
-
Returns:
|
|
1033
|
-
Selected resource object or None if cancelled/no selection
|
|
1034
|
-
"""
|
|
1035
|
-
if not _check_fuzzy_pick_requirements():
|
|
1036
|
-
return None
|
|
1037
|
-
|
|
1038
|
-
# Build labels and mapping
|
|
1039
|
-
labels, by_label = _build_resource_labels(resources)
|
|
1040
|
-
|
|
1041
|
-
# Create fuzzy completer
|
|
1042
|
-
completer = _FuzzyCompleter(labels)
|
|
1043
|
-
|
|
1044
|
-
try:
|
|
1045
|
-
answer = prompt(
|
|
1046
|
-
message=f"Find 🤖 {resource_type.title()}: ",
|
|
1047
|
-
completer=completer,
|
|
1048
|
-
complete_in_thread=True,
|
|
1049
|
-
complete_while_typing=True,
|
|
1050
|
-
)
|
|
1051
|
-
except (KeyboardInterrupt, EOFError):
|
|
1052
|
-
return None
|
|
1053
|
-
|
|
1054
|
-
return _perform_fuzzy_search(answer, labels, by_label) if answer else None
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
def _resolve_by_id(ref: str, get_by_id: Callable) -> Any | None:
|
|
1058
|
-
"""Resolve resource by UUID if ref is a valid UUID."""
|
|
1059
|
-
if is_uuid(ref):
|
|
1060
|
-
return get_by_id(ref)
|
|
1061
|
-
return None
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
def _resolve_by_name_multiple_with_select(matches: list[Any], select: int) -> Any:
|
|
1065
|
-
"""Resolve multiple matches using select parameter."""
|
|
1066
|
-
idx = int(select) - 1
|
|
1067
|
-
if not (0 <= idx < len(matches)):
|
|
1068
|
-
raise click.ClickException(f"--select must be 1..{len(matches)}")
|
|
1069
|
-
return matches[idx]
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
def _resolve_by_name_multiple_fuzzy(
|
|
1073
|
-
ctx: Any, ref: str, matches: list[Any], label: str
|
|
1074
|
-
) -> Any:
|
|
1075
|
-
"""Resolve multiple matches using fuzzy picker interface."""
|
|
1076
|
-
picked = _fuzzy_pick_for_resources(matches, label.lower(), ref)
|
|
1077
|
-
if picked:
|
|
1078
|
-
return picked
|
|
1079
|
-
# Fallback to original ambiguity handler if fuzzy picker fails
|
|
1080
|
-
return handle_ambiguous_resource(ctx, label.lower(), ref, matches)
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
def _resolve_by_name_multiple_questionary(
|
|
1084
|
-
ctx: Any, ref: str, matches: list[Any], label: str
|
|
1085
|
-
) -> Any:
|
|
1086
|
-
"""Resolve multiple matches using questionary interface."""
|
|
1087
|
-
return handle_ambiguous_resource(ctx, label.lower(), ref, matches)
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
def resolve_resource(
|
|
1091
|
-
ctx: Any,
|
|
1092
|
-
ref: str,
|
|
1093
|
-
*,
|
|
1094
|
-
get_by_id: Callable,
|
|
1095
|
-
find_by_name: Callable,
|
|
1096
|
-
label: str,
|
|
1097
|
-
select: int | None = None,
|
|
1098
|
-
interface_preference: str = "fuzzy",
|
|
1099
|
-
) -> Any | None:
|
|
1100
|
-
"""Resolve resource reference (ID or name) with ambiguity handling.
|
|
1101
|
-
|
|
1102
|
-
Args:
|
|
1103
|
-
ctx: Click context
|
|
1104
|
-
ref: Resource reference (ID or name)
|
|
1105
|
-
get_by_id: Function to get resource by ID
|
|
1106
|
-
find_by_name: Function to find resources by name
|
|
1107
|
-
label: Resource type label for error messages
|
|
1108
|
-
select: Optional selection index for ambiguity resolution
|
|
1109
|
-
interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list
|
|
1110
|
-
|
|
1111
|
-
Returns:
|
|
1112
|
-
Resolved resource object
|
|
1113
|
-
"""
|
|
1114
|
-
# Try to resolve by ID first
|
|
1115
|
-
result = _resolve_by_id(ref, get_by_id)
|
|
1116
|
-
if result is not None:
|
|
1117
|
-
return result
|
|
1118
|
-
|
|
1119
|
-
# If get_by_id returned None, the resource doesn't exist
|
|
1120
|
-
if is_uuid(ref):
|
|
1121
|
-
raise click.ClickException(f"{label} '{ref}' not found")
|
|
1122
|
-
|
|
1123
|
-
# Find resources by name
|
|
1124
|
-
matches = find_by_name(name=ref)
|
|
1125
|
-
if not matches:
|
|
1126
|
-
raise click.ClickException(f"{label} '{ref}' not found")
|
|
1127
|
-
|
|
1128
|
-
if len(matches) == 1:
|
|
1129
|
-
return matches[0]
|
|
1130
|
-
|
|
1131
|
-
# Multiple matches found, handle ambiguity
|
|
1132
|
-
if select:
|
|
1133
|
-
return _resolve_by_name_multiple_with_select(matches, select)
|
|
1134
|
-
|
|
1135
|
-
# Choose interface based on preference
|
|
1136
|
-
if interface_preference == "fuzzy":
|
|
1137
|
-
return _resolve_by_name_multiple_fuzzy(ctx, ref, matches, label)
|
|
1138
|
-
else:
|
|
1139
|
-
return _resolve_by_name_multiple_questionary(ctx, ref, matches, label)
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
def _handle_json_view_ambiguity(matches: list[Any]) -> Any:
|
|
1143
|
-
"""Handle ambiguity in JSON view by returning first match."""
|
|
1144
|
-
return matches[0]
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
def _handle_questionary_ambiguity(
|
|
1148
|
-
resource_type: str, ref: str, matches: list[Any]
|
|
1149
|
-
) -> Any:
|
|
1150
|
-
"""Handle ambiguity using questionary interactive interface."""
|
|
1151
|
-
if not (questionary and os.getenv("TERM") and os.isatty(0) and os.isatty(1)):
|
|
1152
|
-
raise click.ClickException("Interactive selection not available")
|
|
1153
|
-
|
|
1154
|
-
# Escape special characters for questionary
|
|
1155
|
-
safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
|
|
1156
|
-
safe_ref = ref.replace("{", "{{").replace("}", "}}")
|
|
1157
|
-
|
|
1158
|
-
picked_idx = questionary.select(
|
|
1159
|
-
f"Multiple {safe_resource_type}s match '{safe_ref}'. Pick one:",
|
|
1160
|
-
choices=[
|
|
1161
|
-
questionary.Choice(
|
|
1162
|
-
title=f"{getattr(m, 'name', '—').replace('{', '{{').replace('}', '}}')} — {getattr(m, 'id', '').replace('{', '{{').replace('}', '}}')}",
|
|
1163
|
-
value=i,
|
|
1164
|
-
)
|
|
1165
|
-
for i, m in enumerate(matches)
|
|
1166
|
-
],
|
|
1167
|
-
use_indicator=True,
|
|
1168
|
-
qmark="🧭",
|
|
1169
|
-
instruction="↑/↓ to select • Enter to confirm",
|
|
1170
|
-
).ask()
|
|
1171
|
-
if picked_idx is None:
|
|
1172
|
-
raise click.ClickException("Selection cancelled")
|
|
1173
|
-
return matches[picked_idx]
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
def _handle_fallback_numeric_ambiguity(
|
|
1177
|
-
resource_type: str, ref: str, matches: list[Any]
|
|
1178
|
-
) -> Any:
|
|
1179
|
-
"""Handle ambiguity using numeric prompt fallback."""
|
|
1180
|
-
# Escape special characters for display
|
|
1181
|
-
safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
|
|
1182
|
-
safe_ref = ref.replace("{", "{{").replace("}", "}}")
|
|
1183
|
-
|
|
1184
|
-
console.print(
|
|
1185
|
-
Text(
|
|
1186
|
-
f"[yellow]Multiple {safe_resource_type}s found matching '{safe_ref}':[/yellow]"
|
|
145
|
+
with _warn_lock:
|
|
146
|
+
if _warned:
|
|
147
|
+
return
|
|
148
|
+
warnings.warn(
|
|
149
|
+
"Importing from glaip_sdk.cli.utils is deprecated. Use glaip_sdk.cli.core.* modules instead.",
|
|
150
|
+
DeprecationWarning,
|
|
151
|
+
stacklevel=3,
|
|
1187
152
|
)
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
""
|
|
1212
|
-
#
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
#
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
""
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
""
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
""
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
153
|
+
_warned = True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_warn_once()
|
|
157
|
+
|
|
158
|
+
# Re-export everything for backward compatibility
|
|
159
|
+
__all__ = [
|
|
160
|
+
# Context
|
|
161
|
+
"bind_slash_session_context",
|
|
162
|
+
"get_client",
|
|
163
|
+
"get_ctx_value", # Re-exported from context module
|
|
164
|
+
"handle_best_effort_check",
|
|
165
|
+
"restore_slash_session_context",
|
|
166
|
+
# Prompting
|
|
167
|
+
"_FuzzyCompleter", # Private class for backward compatibility (used in tests)
|
|
168
|
+
"_asyncio_loop_running", # Private function for backward compatibility (used in tests)
|
|
169
|
+
"_basic_prompt", # Private function for backward compatibility (used in tests)
|
|
170
|
+
"_build_display_parts", # Private function for backward compatibility (used in tests)
|
|
171
|
+
"_build_primary_parts", # Private function for backward compatibility (used in tests)
|
|
172
|
+
"_build_resource_labels", # Private function for backward compatibility (used in tests)
|
|
173
|
+
"_build_unique_labels", # Private function for backward compatibility (used in tests)
|
|
174
|
+
"_calculate_consecutive_bonus", # Private function for backward compatibility (used in tests)
|
|
175
|
+
"_calculate_exact_match_bonus", # Private function for backward compatibility (used in tests)
|
|
176
|
+
"_calculate_length_bonus", # Private function for backward compatibility (used in tests)
|
|
177
|
+
"_check_fuzzy_pick_requirements", # Private function for backward compatibility (used in tests)
|
|
178
|
+
"_extract_display_fields", # Private function for backward compatibility (used in tests)
|
|
179
|
+
"_extract_fallback_values", # Private function for backward compatibility (used in tests)
|
|
180
|
+
"_extract_id_suffix", # Private function for backward compatibility (used in tests)
|
|
181
|
+
"_fuzzy_pick", # Private function for backward compatibility (used in tests)
|
|
182
|
+
"_fuzzy_pick_for_resources",
|
|
183
|
+
"_fuzzy_score", # Private function for backward compatibility (used in tests)
|
|
184
|
+
"_get_fallback_columns", # Private function for backward compatibility (used in tests)
|
|
185
|
+
"_is_fuzzy_match", # Private function for backward compatibility (used in tests)
|
|
186
|
+
"_is_standard_field", # Private function for backward compatibility (used in tests)
|
|
187
|
+
"_load_questionary_module", # Private function for backward compatibility (used in tests)
|
|
188
|
+
"_make_questionary_choice", # Private function for backward compatibility (used in tests)
|
|
189
|
+
"_perform_fuzzy_search", # Private function for backward compatibility (used in tests)
|
|
190
|
+
"_prompt_with_auto_select", # Private function for backward compatibility (used in tests)
|
|
191
|
+
"_rank_labels", # Private function for backward compatibility (used in tests)
|
|
192
|
+
"_row_display", # Private function for backward compatibility (used in tests)
|
|
193
|
+
"_run_questionary_in_thread", # Private function for backward compatibility (used in tests)
|
|
194
|
+
"_strip_spaces_for_matching", # Private function for backward compatibility (used in tests)
|
|
195
|
+
"prompt_export_choice_questionary",
|
|
196
|
+
"questionary_safe_ask",
|
|
197
|
+
# Rendering
|
|
198
|
+
"_can_use_spinner", # Private function for backward compatibility (used in tests)
|
|
199
|
+
"_register_renderer_with_session", # Private function for backward compatibility (used in tests)
|
|
200
|
+
"_spinner_stop", # Private function for backward compatibility (used in tests)
|
|
201
|
+
"_spinner_update", # Private function for backward compatibility (used in tests)
|
|
202
|
+
"_stream_supports_tty", # Private function for backward compatibility (used in tests)
|
|
203
|
+
"build_renderer",
|
|
204
|
+
"console", # Module-level variable for backward compatibility
|
|
205
|
+
"logger", # Module-level variable for backward compatibility
|
|
206
|
+
"questionary", # Module-level variable for backward compatibility
|
|
207
|
+
"spinner_context",
|
|
208
|
+
"stop_spinner",
|
|
209
|
+
"update_spinner",
|
|
210
|
+
"with_client_and_spinner",
|
|
211
|
+
# Output
|
|
212
|
+
"_LiteralYamlDumper", # Private class for backward compatibility (used in tests)
|
|
213
|
+
"_build_table_group", # Private function for backward compatibility (used in tests)
|
|
214
|
+
"_build_yaml_renderable", # Private function for backward compatibility (used in tests)
|
|
215
|
+
"_coerce_result_payload", # Private function for backward compatibility (used in tests)
|
|
216
|
+
"_create_table", # Private function for backward compatibility (used in tests)
|
|
217
|
+
"_ensure_displayable", # Private function for backward compatibility (used in tests)
|
|
218
|
+
"_format_yaml_text", # Private function for backward compatibility (used in tests)
|
|
219
|
+
"_get_interface_order", # Private function for backward compatibility (used in tests)
|
|
220
|
+
"_handle_empty_items", # Private function for backward compatibility (used in tests)
|
|
221
|
+
"_handle_fallback_numeric_ambiguity", # Private function for backward compatibility (used in tests)
|
|
222
|
+
"_handle_fuzzy_pick_selection", # Private function for backward compatibility (used in tests)
|
|
223
|
+
"_handle_json_output", # Private function for backward compatibility (used in tests)
|
|
224
|
+
"_handle_json_view_ambiguity", # Private function for backward compatibility (used in tests)
|
|
225
|
+
"_handle_markdown_output", # Private function for backward compatibility (used in tests)
|
|
226
|
+
"_handle_plain_output", # Private function for backward compatibility (used in tests)
|
|
227
|
+
"_handle_questionary_ambiguity", # Private function for backward compatibility (used in tests)
|
|
228
|
+
"_handle_table_output", # Private function for backward compatibility (used in tests)
|
|
229
|
+
"_literal_str_representer", # Private function for backward compatibility (used in tests)
|
|
230
|
+
"_normalise_rows", # Private function for backward compatibility (used in tests)
|
|
231
|
+
"_normalize_interface_preference", # Private function for backward compatibility (used in tests)
|
|
232
|
+
"_print_selection_tip", # Private function for backward compatibility (used in tests)
|
|
233
|
+
"_render_markdown_list", # Private function for backward compatibility (used in tests)
|
|
234
|
+
"_render_markdown_output", # Private function for backward compatibility (used in tests)
|
|
235
|
+
"_render_plain_list", # Private function for backward compatibility (used in tests)
|
|
236
|
+
"_resolve_by_id", # Private function for backward compatibility (used in tests)
|
|
237
|
+
"_resolve_by_name_multiple_fuzzy", # Private function for backward compatibility (used in tests)
|
|
238
|
+
"_resolve_by_name_multiple_questionary", # Private function for backward compatibility (used in tests)
|
|
239
|
+
"_resolve_by_name_multiple_with_select", # Private function for backward compatibility (used in tests)
|
|
240
|
+
"_resource_tip_command", # Private function for backward compatibility (used in tests)
|
|
241
|
+
"_should_fallback_to_numeric_prompt", # Private function for backward compatibility (used in tests)
|
|
242
|
+
"_should_sort_rows", # Private function for backward compatibility (used in tests)
|
|
243
|
+
"_should_use_fuzzy_picker", # Private function for backward compatibility (used in tests)
|
|
244
|
+
"_try_fuzzy_pick", # Private function for backward compatibility (used in tests)
|
|
245
|
+
"_try_fuzzy_selection", # Private function for backward compatibility (used in tests)
|
|
246
|
+
"_try_interface_selection", # Private function for backward compatibility (used in tests)
|
|
247
|
+
"_try_questionary_selection", # Private function for backward compatibility (used in tests)
|
|
248
|
+
"coerce_to_row",
|
|
249
|
+
"command_hint", # Re-exported from hints module
|
|
250
|
+
"detect_export_format",
|
|
251
|
+
"fetch_resource_for_export",
|
|
252
|
+
"format_datetime_fields",
|
|
253
|
+
"format_size",
|
|
254
|
+
"handle_ambiguous_resource",
|
|
255
|
+
"handle_resource_export",
|
|
256
|
+
"output_list",
|
|
257
|
+
"output_result",
|
|
258
|
+
"parse_json_line",
|
|
259
|
+
"resolve_resource",
|
|
260
|
+
"sdk_version",
|
|
261
|
+
# Utils
|
|
262
|
+
"is_uuid", # Re-exported from glaip_sdk.utils for backward compatibility
|
|
263
|
+
]
|