glaip-sdk 0.0.20__py3-none-any.whl ā 0.7.7__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 +44 -4
- glaip_sdk/_version.py +10 -3
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1250 -0
- glaip_sdk/branding.py +15 -6
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +271 -45
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents/__init__.py +119 -0
- glaip_sdk/cli/commands/agents/_common.py +561 -0
- glaip_sdk/cli/commands/agents/create.py +151 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +734 -143
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +14 -12
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/transcripts_original.py +756 -0
- glaip_sdk/cli/commands/update.py +164 -23
- glaip_sdk/cli/config.py +49 -7
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +45 -32
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +14 -17
- glaip_sdk/cli/main.py +344 -167
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +15 -22
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +5 -10
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +580 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +65 -29
- glaip_sdk/cli/slash/prompt.py +24 -10
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +827 -232
- glaip_sdk/cli/slash/tui/__init__.py +34 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/clipboard.py +147 -0
- glaip_sdk/cli/slash/tui/context.py +59 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/slash/tui/terminal.py +402 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +123 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +258 -60
- glaip_sdk/cli/transcript/capture.py +72 -21
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +79 -329
- glaip_sdk/cli/update_notifier.py +385 -24
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +3 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +370 -100
- glaip_sdk/client/base.py +78 -35
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +25 -10
- glaip_sdk/client/mcps.py +166 -27
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py ā payloads/agent/requests.py} +65 -74
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +583 -79
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +214 -56
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/icons.py +9 -3
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +107 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/payload_schemas/agent.py +1 -3
- 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 +445 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +76 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +112 -0
- glaip_sdk/runner/langgraph.py +872 -0
- glaip_sdk/runner/logging_config.py +77 -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 +257 -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 +242 -0
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +468 -0
- glaip_sdk/utils/__init__.py +59 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +4 -14
- glaip_sdk/utils/bundler.py +403 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +46 -28
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +25 -21
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +15 -16
- glaip_sdk/utils/import_resolver.py +524 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +38 -23
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer ā layout}/panels.py +10 -3
- glaip_sdk/utils/rendering/{renderer ā layout}/progress.py +73 -12
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +18 -8
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
- glaip_sdk/utils/rendering/renderer/base.py +534 -882
- glaip_sdk/utils/rendering/renderer/config.py +4 -10
- glaip_sdk/utils/rendering/renderer/debug.py +30 -34
- 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.py ā steps/manager.py} +122 -26
- 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 +425 -0
- glaip_sdk/utils/serialization.py +32 -46
- glaip_sdk/utils/sync.py +162 -0
- glaip_sdk/utils/tool_detection.py +301 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- glaip_sdk/utils/validation.py +20 -28
- {glaip_sdk-0.0.20.dist-info ā glaip_sdk-0.7.7.dist-info}/METADATA +78 -23
- glaip_sdk-0.7.7.dist-info/RECORD +213 -0
- {glaip_sdk-0.0.20.dist-info ā glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1412
- glaip_sdk/cli/commands/mcps.py +0 -1225
- glaip_sdk/cli/commands/tools.py +0 -597
- glaip_sdk/cli/utils.py +0 -1330
- glaip_sdk/models.py +0 -259
- glaip_sdk-0.0.20.dist-info/RECORD +0 -80
- glaip_sdk-0.0.20.dist-info/entry_points.txt +0 -3
|
@@ -5,88 +5,292 @@ Authors:
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import getpass
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
8
15
|
|
|
9
16
|
import click
|
|
10
17
|
from rich.console import Console
|
|
11
18
|
from rich.text import Text
|
|
12
19
|
|
|
13
|
-
from glaip_sdk import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
WARNING_STYLE,
|
|
23
|
-
AIPBranding,
|
|
24
|
-
)
|
|
20
|
+
from glaip_sdk.branding import ACCENT_STYLE, ERROR_STYLE, INFO, NEUTRAL, SUCCESS_STYLE, WARNING_STYLE
|
|
21
|
+
|
|
22
|
+
# Optional import for gitignore support; warn when missing to avoid silent expansion
|
|
23
|
+
try:
|
|
24
|
+
import pathspec # type: ignore[import-untyped] # noqa: PLC0415
|
|
25
|
+
except ImportError: # pragma: no cover - optional dependency
|
|
26
|
+
pathspec = None # type: ignore[assignment]
|
|
27
|
+
from glaip_sdk.cli.account_store import get_account_store
|
|
28
|
+
from glaip_sdk.cli.commands.common_config import check_connection, render_branding_header
|
|
25
29
|
from glaip_sdk.cli.config import CONFIG_FILE, load_config, save_config
|
|
30
|
+
from glaip_sdk.cli.hints import command_hint, format_command_hint
|
|
31
|
+
from glaip_sdk.cli.masking import mask_api_key_display
|
|
26
32
|
from glaip_sdk.cli.rich_helpers import markup_text
|
|
27
|
-
from glaip_sdk.cli.utils import command_hint, format_command_hint
|
|
28
|
-
from glaip_sdk.icons import ICON_TOOL
|
|
29
33
|
from glaip_sdk.rich_components import AIPTable
|
|
30
34
|
|
|
31
35
|
console = Console()
|
|
36
|
+
stderr_console = Console(file=sys.stderr)
|
|
37
|
+
_PATHSPEC_WARNED = False
|
|
38
|
+
_PATHSPEC_WARNED_LOCK = threading.Lock()
|
|
39
|
+
|
|
40
|
+
# Hard deprecation banner for legacy config commands (v0.6.x)
|
|
41
|
+
CONFIG_HARD_DEPRECATION_MSG = (
|
|
42
|
+
f"[{WARNING_STYLE}]ā ļø DEPRECATED: 'aip config ...' commands will be removed in v0.7.0. "
|
|
43
|
+
"Use 'aip accounts ...' (list/add/use/remove/edit) or 'aip configure' for the wizard. "
|
|
44
|
+
"Set AIP_ENABLE_LEGACY_CONFIG=1 to temporarily re-enable these commands.[/]"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Soft deprecation banner (for when env flag is set)
|
|
48
|
+
CONFIG_SOFT_DEPRECATION_MSG = (
|
|
49
|
+
f"[{WARNING_STYLE}]Deprecated: 'aip config ...' will be removed in v0.7.0. "
|
|
50
|
+
"Use 'aip accounts ...' (list/add/use/remove/edit) or 'aip configure' for the wizard.[/]"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Target removal version
|
|
54
|
+
TARGET_REMOVAL_VERSION = "v0.7.0"
|
|
55
|
+
|
|
56
|
+
# Command hint constant
|
|
57
|
+
CONFIG_CONFIGURE_HINT = "config configure"
|
|
58
|
+
_DEFAULT_EXCLUDE_DIRS = {
|
|
59
|
+
".git",
|
|
60
|
+
"node_modules",
|
|
61
|
+
".venv",
|
|
62
|
+
"venv",
|
|
63
|
+
".tox",
|
|
64
|
+
"build",
|
|
65
|
+
"dist",
|
|
66
|
+
"__pycache__",
|
|
67
|
+
".mypy_cache",
|
|
68
|
+
".pytest_cache",
|
|
69
|
+
}
|
|
70
|
+
_MAX_SCAN_FILE_SIZE = 2 * 1024 * 1024 # 2MB cap for default scans
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_legacy_config_enabled() -> bool:
|
|
74
|
+
"""Check if legacy config commands are enabled via environment variable."""
|
|
75
|
+
env_value = os.environ.get("AIP_ENABLE_LEGACY_CONFIG", "").strip().lower()
|
|
76
|
+
return env_value in ("1", "true", "yes", "on")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _print_config_deprecation() -> None:
|
|
80
|
+
"""Print a standardized deprecation warning for legacy config commands."""
|
|
81
|
+
if _is_legacy_config_enabled():
|
|
82
|
+
# Soft deprecation when env flag is set
|
|
83
|
+
stderr_console.print(CONFIG_SOFT_DEPRECATION_MSG)
|
|
84
|
+
else:
|
|
85
|
+
# Hard deprecation when env flag is not set
|
|
86
|
+
stderr_console.print(CONFIG_HARD_DEPRECATION_MSG)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _check_legacy_config_gate() -> bool:
|
|
90
|
+
"""Return True if legacy config commands are allowed; print banner otherwise."""
|
|
91
|
+
if not _is_legacy_config_enabled():
|
|
92
|
+
stderr_console.print(CONFIG_HARD_DEPRECATION_MSG)
|
|
93
|
+
return False
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _enforce_legacy_config_gate() -> None:
|
|
98
|
+
"""CLI-only gate: exit with code 0 when legacy commands are disabled."""
|
|
99
|
+
if not _check_legacy_config_gate():
|
|
100
|
+
# Spec requires non-breaking exit after banner
|
|
101
|
+
sys.exit(0)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _emit_telemetry_event(_event_name: str, properties: dict[str, Any] | None = None) -> None:
|
|
105
|
+
"""Emit telemetry event for legacy command usage tracking.
|
|
106
|
+
|
|
107
|
+
This is a stub implementation that can be connected to a real telemetry system.
|
|
108
|
+
For now, it's a no-op but structured to allow easy integration.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
_event_name: Name of the telemetry event (prefixed with _ to indicate unused for now).
|
|
112
|
+
properties: Optional event properties dictionary.
|
|
113
|
+
|
|
114
|
+
Note:
|
|
115
|
+
TODO: Connect to actual telemetry system when available.
|
|
116
|
+
"""
|
|
117
|
+
if properties is None:
|
|
118
|
+
properties = {}
|
|
119
|
+
# Mark as intentionally unused until telemetry system is integrated
|
|
120
|
+
del _event_name, properties
|
|
32
121
|
|
|
33
122
|
|
|
34
123
|
@click.group()
|
|
35
124
|
def config_group() -> None:
|
|
36
|
-
"""Configuration management operations.
|
|
37
|
-
|
|
125
|
+
"""Configuration management operations (deprecated).
|
|
126
|
+
|
|
127
|
+
These commands are deprecated and will be removed in v0.7.0.
|
|
128
|
+
Use 'aip accounts ...' commands instead.
|
|
129
|
+
Set AIP_ENABLE_LEGACY_CONFIG=1 to temporarily re-enable.
|
|
130
|
+
"""
|
|
131
|
+
_enforce_legacy_config_gate()
|
|
132
|
+
_print_config_deprecation()
|
|
133
|
+
# Emit telemetry for legacy command invocation
|
|
134
|
+
_emit_telemetry_event(
|
|
135
|
+
"config.command",
|
|
136
|
+
{
|
|
137
|
+
"phase": "hard_deprecation",
|
|
138
|
+
"gated_by_env": _is_legacy_config_enabled(),
|
|
139
|
+
},
|
|
140
|
+
)
|
|
38
141
|
|
|
39
142
|
|
|
40
143
|
@config_group.command("list")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
144
|
+
@click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
|
|
145
|
+
@click.pass_context
|
|
146
|
+
def list_config(ctx: click.Context, output_json: bool) -> None:
|
|
147
|
+
"""List current configuration.
|
|
148
|
+
|
|
149
|
+
Deprecated: run 'aip accounts list' for profile-aware output.
|
|
150
|
+
"""
|
|
151
|
+
_enforce_legacy_config_gate()
|
|
152
|
+
console.print(f"[{WARNING_STYLE}]Deprecated: run 'aip accounts list' for profile-aware output.[/]")
|
|
153
|
+
|
|
154
|
+
# Delegate to accounts list by invoking the command
|
|
155
|
+
from glaip_sdk.cli.commands.accounts import accounts_group # noqa: PLC0415
|
|
156
|
+
|
|
157
|
+
list_cmd = accounts_group.get_command(ctx, "list")
|
|
158
|
+
if list_cmd:
|
|
159
|
+
ctx.invoke(list_cmd, output_json=output_json)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
CONFIG_VALUE_TYPES: dict[str, str] = {
|
|
163
|
+
"api_url": "string",
|
|
164
|
+
"api_key": "string",
|
|
165
|
+
"timeout": "float",
|
|
166
|
+
"history_default_limit": "int",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _parse_bool_config(value: str) -> bool:
|
|
171
|
+
"""Parse boolean-like CLI input."""
|
|
172
|
+
normalized = value.strip().lower()
|
|
173
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
174
|
+
return True
|
|
175
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
176
|
+
return False
|
|
177
|
+
raise click.ClickException("Invalid boolean value. Use one of: true, false, yes, no, 1, 0.")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _parse_int_config(value: str) -> int:
|
|
181
|
+
"""Parse integer CLI input with non-negative enforcement."""
|
|
182
|
+
try:
|
|
183
|
+
parsed = int(value, 10)
|
|
184
|
+
except ValueError as exc:
|
|
185
|
+
raise click.ClickException("Invalid integer value.") from exc
|
|
186
|
+
if parsed < 0:
|
|
187
|
+
raise click.ClickException("Value must be greater than or equal to 0.")
|
|
188
|
+
return parsed
|
|
44
189
|
|
|
45
|
-
if not config:
|
|
46
|
-
_print_missing_config_hint()
|
|
47
|
-
return
|
|
48
190
|
|
|
49
|
-
|
|
191
|
+
def _parse_float_config(value: str) -> float:
|
|
192
|
+
"""Parse float CLI input with non-negative enforcement."""
|
|
193
|
+
try:
|
|
194
|
+
parsed = float(value)
|
|
195
|
+
except ValueError as exc:
|
|
196
|
+
raise click.ClickException("Invalid float value.") from exc
|
|
197
|
+
if parsed < 0:
|
|
198
|
+
raise click.ClickException("Value must be greater than or equal to 0.")
|
|
199
|
+
return parsed
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _coerce_config_value(key: str, raw_value: str) -> str | bool | int | float:
|
|
203
|
+
"""Convert CLI string values to their target config types."""
|
|
204
|
+
kind = CONFIG_VALUE_TYPES.get(key, "string")
|
|
205
|
+
if kind == "bool":
|
|
206
|
+
return _parse_bool_config(raw_value)
|
|
207
|
+
if kind == "int":
|
|
208
|
+
return _parse_int_config(raw_value)
|
|
209
|
+
if kind == "float":
|
|
210
|
+
return _parse_float_config(raw_value)
|
|
211
|
+
return raw_value
|
|
50
212
|
|
|
51
213
|
|
|
52
214
|
@config_group.command("set")
|
|
53
215
|
@click.argument("key")
|
|
54
216
|
@click.argument("value")
|
|
55
|
-
|
|
56
|
-
""
|
|
57
|
-
|
|
217
|
+
@click.option(
|
|
218
|
+
"--account",
|
|
219
|
+
"account_name",
|
|
220
|
+
help="Account name to set value for (defaults to active account)",
|
|
221
|
+
)
|
|
222
|
+
def set_config(key: str, value: str, account_name: str | None) -> None:
|
|
223
|
+
"""Set a configuration value.
|
|
224
|
+
|
|
225
|
+
For api_url and api_key, this operates on the specified account (or active account).
|
|
226
|
+
Other keys (timeout, history_default_limit) are global settings.
|
|
58
227
|
|
|
228
|
+
Deprecated: use 'aip accounts edit <name>' instead.
|
|
229
|
+
"""
|
|
230
|
+
_enforce_legacy_config_gate()
|
|
231
|
+
# For other keys, use legacy config
|
|
232
|
+
valid_keys = tuple(CONFIG_VALUE_TYPES.keys())
|
|
59
233
|
if key not in valid_keys:
|
|
60
|
-
console.print(
|
|
61
|
-
f"[{ERROR_STYLE}]Error: Invalid key '{key}'. Valid keys are: {', '.join(valid_keys)}[/]"
|
|
62
|
-
)
|
|
234
|
+
console.print(f"[{ERROR_STYLE}]Error: Invalid key '{key}'. Valid keys are: {', '.join(valid_keys)}[/]")
|
|
63
235
|
raise click.ClickException(f"Invalid configuration key: {key}")
|
|
64
236
|
|
|
237
|
+
store = get_account_store()
|
|
238
|
+
# For api_url and api_key, update account profile but also mirror to legacy config
|
|
239
|
+
if key in ("api_url", "api_key"):
|
|
240
|
+
target_account = account_name or store.get_active_account() or "default"
|
|
241
|
+
try:
|
|
242
|
+
account = store.get_account(target_account) or {}
|
|
243
|
+
account[key] = value
|
|
244
|
+
store.add_account(
|
|
245
|
+
target_account,
|
|
246
|
+
account.get("api_url", ""),
|
|
247
|
+
account.get("api_key", ""),
|
|
248
|
+
overwrite=True,
|
|
249
|
+
)
|
|
250
|
+
except Exception:
|
|
251
|
+
# If account store persistence fails (e.g., mocked I/O), continue with legacy config
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
# Always update legacy config for backward compatibility and test isolation
|
|
255
|
+
legacy_config = load_config()
|
|
256
|
+
legacy_config[key] = value
|
|
257
|
+
save_config(legacy_config)
|
|
258
|
+
|
|
259
|
+
display_value = _mask_api_key(value) if key == "api_key" else value
|
|
260
|
+
console.print(Text(f"ā
Set {key} = {display_value} for account '{target_account}'", style=SUCCESS_STYLE))
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
coerced_value = _coerce_config_value(key, value)
|
|
65
264
|
config = load_config()
|
|
66
|
-
config[key] =
|
|
265
|
+
config[key] = coerced_value
|
|
67
266
|
save_config(config)
|
|
68
267
|
|
|
69
|
-
if key == "api_key"
|
|
70
|
-
|
|
71
|
-
Text(f"ā
Set {key} = {_mask_api_key(value)}", style=SUCCESS_STYLE)
|
|
72
|
-
)
|
|
73
|
-
else:
|
|
74
|
-
console.print(Text(f"ā
Set {key} = {value}", style=SUCCESS_STYLE))
|
|
268
|
+
display_value = _mask_api_key(coerced_value) if key == "api_key" else str(coerced_value)
|
|
269
|
+
console.print(Text(f"ā
Set {key} = {display_value}", style=SUCCESS_STYLE))
|
|
75
270
|
|
|
76
271
|
|
|
77
272
|
@config_group.command("get")
|
|
78
273
|
@click.argument("key")
|
|
79
274
|
def get_config(key: str) -> None:
|
|
80
|
-
"""Get a configuration value.
|
|
275
|
+
"""Get a configuration value.
|
|
276
|
+
|
|
277
|
+
Deprecated: use 'aip accounts show <name>' or read ~/.aip/config.yaml directly.
|
|
278
|
+
"""
|
|
279
|
+
_enforce_legacy_config_gate()
|
|
81
280
|
config = load_config()
|
|
82
281
|
|
|
83
|
-
|
|
84
|
-
console.print(
|
|
85
|
-
markup_text(f"[{WARNING_STYLE}]Configuration key '{key}' not found.[/]")
|
|
86
|
-
)
|
|
87
|
-
raise click.ClickException(f"Configuration key not found: {key}")
|
|
282
|
+
value = config.get(key)
|
|
88
283
|
|
|
89
|
-
|
|
284
|
+
# Fallback to account store for api_url/api_key when legacy config lacks the key
|
|
285
|
+
if value is None and key in {"api_url", "api_key"}:
|
|
286
|
+
store = get_account_store()
|
|
287
|
+
active = store.get_active_account() or "default"
|
|
288
|
+
account = store.get_account(active) or {}
|
|
289
|
+
value = account.get(key)
|
|
290
|
+
|
|
291
|
+
if value is None:
|
|
292
|
+
console.print(markup_text(f"[{WARNING_STYLE}]Configuration key '{key}' not found.[/]"))
|
|
293
|
+
raise click.ClickException(f"Configuration key not found: {key}")
|
|
90
294
|
|
|
91
295
|
if key == "api_key":
|
|
92
296
|
console.print(_mask_api_key(value))
|
|
@@ -97,13 +301,15 @@ def get_config(key: str) -> None:
|
|
|
97
301
|
@config_group.command("unset")
|
|
98
302
|
@click.argument("key")
|
|
99
303
|
def unset_config(key: str) -> None:
|
|
100
|
-
"""Remove a configuration value.
|
|
304
|
+
"""Remove a configuration value.
|
|
305
|
+
|
|
306
|
+
Deprecated: use 'aip accounts edit <name>' to clear specific fields.
|
|
307
|
+
"""
|
|
308
|
+
_enforce_legacy_config_gate()
|
|
101
309
|
config = load_config()
|
|
102
310
|
|
|
103
311
|
if key not in config:
|
|
104
|
-
console.print(
|
|
105
|
-
markup_text(f"[{WARNING_STYLE}]Configuration key '{key}' not found.[/]")
|
|
106
|
-
)
|
|
312
|
+
console.print(markup_text(f"[{WARNING_STYLE}]Configuration key '{key}' not found.[/]"))
|
|
107
313
|
return
|
|
108
314
|
|
|
109
315
|
del config[key]
|
|
@@ -115,7 +321,11 @@ def unset_config(key: str) -> None:
|
|
|
115
321
|
@config_group.command("reset")
|
|
116
322
|
@click.option("--force", is_flag=True, help="Skip confirmation prompt")
|
|
117
323
|
def reset_config(force: bool) -> None:
|
|
118
|
-
"""Reset all configuration to defaults.
|
|
324
|
+
"""Reset all configuration to defaults.
|
|
325
|
+
|
|
326
|
+
Deprecated: use 'aip accounts remove <name>' for each account or manually edit ~/.aip/config.yaml.
|
|
327
|
+
"""
|
|
328
|
+
_enforce_legacy_config_gate()
|
|
119
329
|
if not force:
|
|
120
330
|
console.print(f"[{WARNING_STYLE}]This will remove all AIP configuration.[/]")
|
|
121
331
|
confirm = input("Are you sure? (y/N): ").strip().lower()
|
|
@@ -128,9 +338,7 @@ def reset_config(force: bool) -> None:
|
|
|
128
338
|
|
|
129
339
|
if not file_exists and not config_data:
|
|
130
340
|
console.print(f"[{WARNING_STYLE}]No configuration found to reset.[/]")
|
|
131
|
-
console.print(
|
|
132
|
-
Text("ā
Configuration reset (nothing to remove).", style=SUCCESS_STYLE)
|
|
133
|
-
)
|
|
341
|
+
console.print(Text("ā
Configuration reset (nothing to remove).", style=SUCCESS_STYLE))
|
|
134
342
|
return
|
|
135
343
|
|
|
136
344
|
if file_exists:
|
|
@@ -142,92 +350,500 @@ def reset_config(force: bool) -> None:
|
|
|
142
350
|
# In-memory configuration (e.g., tests) needs explicit clearing
|
|
143
351
|
save_config({})
|
|
144
352
|
|
|
145
|
-
hint = command_hint(
|
|
353
|
+
hint = command_hint(CONFIG_CONFIGURE_HINT, slash_command="login")
|
|
146
354
|
message = Text("ā
Configuration reset.", style=SUCCESS_STYLE)
|
|
147
355
|
if hint:
|
|
148
356
|
message.append(f" Run '{hint}' to set up again.")
|
|
149
357
|
console.print(message)
|
|
150
358
|
|
|
151
359
|
|
|
152
|
-
def _configure_interactive() -> None:
|
|
360
|
+
def _configure_interactive(account_name: str | None = None) -> None:
|
|
153
361
|
"""Shared configuration logic for both configure commands."""
|
|
362
|
+
store = get_account_store()
|
|
363
|
+
|
|
364
|
+
# Determine account name (use provided, active, or default)
|
|
365
|
+
if not account_name:
|
|
366
|
+
account_name = store.get_active_account() or "default"
|
|
367
|
+
|
|
368
|
+
# Get existing account if it exists
|
|
369
|
+
existing = store.get_account(account_name)
|
|
370
|
+
|
|
154
371
|
_render_configuration_header()
|
|
155
|
-
config =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
372
|
+
config = _prompt_configuration_inputs_for_account(existing)
|
|
373
|
+
|
|
374
|
+
# Save to account store
|
|
375
|
+
api_url = config.get("api_url", "")
|
|
376
|
+
api_key = config.get("api_key", "")
|
|
377
|
+
if api_url and api_key:
|
|
378
|
+
store.add_account(account_name, api_url, api_key, overwrite=True)
|
|
379
|
+
console.print(Text(f"\nā
Configuration saved to account '{account_name}'", style=SUCCESS_STYLE))
|
|
380
|
+
|
|
381
|
+
_test_and_report_connection_for_account(account_name)
|
|
159
382
|
_print_post_configuration_hints()
|
|
383
|
+
# Show active account footer
|
|
384
|
+
from glaip_sdk.cli.commands.accounts import _print_active_account_footer # noqa: PLC0415
|
|
160
385
|
|
|
386
|
+
_print_active_account_footer(store)
|
|
161
387
|
|
|
162
|
-
@config_group.command()
|
|
163
|
-
def configure() -> None:
|
|
164
|
-
"""Configure AIP CLI credentials and settings interactively."""
|
|
165
|
-
_configure_interactive()
|
|
166
388
|
|
|
389
|
+
@config_group.command("audit")
|
|
390
|
+
@click.option(
|
|
391
|
+
"--path",
|
|
392
|
+
"paths",
|
|
393
|
+
multiple=True,
|
|
394
|
+
help="Glob pattern(s) to search (repeatable). Defaults to current directory.",
|
|
395
|
+
)
|
|
396
|
+
@click.option(
|
|
397
|
+
"--stdin",
|
|
398
|
+
"read_from_stdin",
|
|
399
|
+
is_flag=True,
|
|
400
|
+
help="Read file list from stdin (one path per line).",
|
|
401
|
+
)
|
|
402
|
+
@click.option(
|
|
403
|
+
"--no-gitignore",
|
|
404
|
+
is_flag=True,
|
|
405
|
+
help="Disable .gitignore filtering (default: respects .gitignore).",
|
|
406
|
+
)
|
|
407
|
+
@click.option(
|
|
408
|
+
"--json",
|
|
409
|
+
"output_json",
|
|
410
|
+
is_flag=True,
|
|
411
|
+
help="Output results in JSON format.",
|
|
412
|
+
)
|
|
413
|
+
@click.option(
|
|
414
|
+
"--fail-on-hit/--no-fail-on-hit",
|
|
415
|
+
default=True,
|
|
416
|
+
help="Exit with code 1 if hits are found (default: fail on hit).",
|
|
417
|
+
)
|
|
418
|
+
@click.option(
|
|
419
|
+
"--silent",
|
|
420
|
+
is_flag=True,
|
|
421
|
+
help="Suppress Rich table output when --json is used.",
|
|
422
|
+
)
|
|
423
|
+
def audit_config(
|
|
424
|
+
paths: tuple[str, ...],
|
|
425
|
+
read_from_stdin: bool,
|
|
426
|
+
no_gitignore: bool,
|
|
427
|
+
output_json: bool,
|
|
428
|
+
fail_on_hit: bool,
|
|
429
|
+
silent: bool,
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Scan scripts/configs for deprecated 'aip config' command usage.
|
|
432
|
+
|
|
433
|
+
Finds strings matching 'aip config' (including variations like 'aip-config',
|
|
434
|
+
'python -m glaip_sdk.cli config') in scripts, CI manifests, and docs.
|
|
435
|
+
|
|
436
|
+
Examples:
|
|
437
|
+
aip config audit
|
|
438
|
+
aip config audit --path "**/*.sh" --path "**/*.yml"
|
|
439
|
+
aip config audit --stdin < file_list.txt
|
|
440
|
+
aip config audit --json --no-fail-on-hit
|
|
441
|
+
"""
|
|
442
|
+
_enforce_legacy_config_gate()
|
|
443
|
+
# Collect files to scan
|
|
444
|
+
files_to_scan = _collect_files_to_scan(paths, read_from_stdin)
|
|
445
|
+
|
|
446
|
+
# Filter by gitignore if enabled
|
|
447
|
+
files_to_scan = _filter_by_gitignore(files_to_scan, no_gitignore)
|
|
448
|
+
|
|
449
|
+
# Scan files for matches
|
|
450
|
+
hits = _scan_files_for_matches(files_to_scan)
|
|
451
|
+
|
|
452
|
+
# Emit telemetry
|
|
453
|
+
_emit_telemetry_event(
|
|
454
|
+
"config.audit",
|
|
455
|
+
{
|
|
456
|
+
"audit_invoked": True,
|
|
457
|
+
"hits_found": len(hits),
|
|
458
|
+
"files_scanned": len(files_to_scan),
|
|
459
|
+
},
|
|
460
|
+
)
|
|
167
461
|
|
|
168
|
-
#
|
|
169
|
-
|
|
170
|
-
def configure_command() -> None:
|
|
171
|
-
"""Configure AIP CLI credentials and settings interactively.
|
|
462
|
+
# Output results
|
|
463
|
+
_output_audit_results(hits, len(files_to_scan), output_json, silent)
|
|
172
464
|
|
|
173
|
-
|
|
465
|
+
# Exit with appropriate code
|
|
466
|
+
if hits and fail_on_hit:
|
|
467
|
+
sys.exit(1)
|
|
468
|
+
sys.exit(0)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# Patterns to match deprecated config command usage
|
|
472
|
+
_AUDIT_PATTERNS = [
|
|
473
|
+
r"aip\s+config",
|
|
474
|
+
r"aip-config",
|
|
475
|
+
r"python\s+-m\s+glaip_sdk\.cli\s+config",
|
|
476
|
+
r"python\s+-m\s+glaip_sdk\.cli\.main\s+config",
|
|
477
|
+
]
|
|
478
|
+
_COMPILED_AUDIT_PATTERNS = [re.compile(pattern, re.IGNORECASE) for pattern in _AUDIT_PATTERNS]
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _collect_files_from_stdin() -> list[Path]:
|
|
482
|
+
"""Collect files to scan from stdin input.
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
List of file paths read from stdin.
|
|
174
486
|
"""
|
|
175
|
-
|
|
176
|
-
|
|
487
|
+
files_to_scan: list[Path] = []
|
|
488
|
+
for line in sys.stdin:
|
|
489
|
+
line = line.strip()
|
|
490
|
+
if line:
|
|
491
|
+
try:
|
|
492
|
+
file_path = Path(line).expanduser().resolve()
|
|
493
|
+
except Exception:
|
|
494
|
+
continue
|
|
495
|
+
if file_path.exists() and file_path.is_file():
|
|
496
|
+
if _should_skip_file(file_path):
|
|
497
|
+
continue
|
|
498
|
+
files_to_scan.append(file_path)
|
|
499
|
+
return files_to_scan
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _collect_files_from_patterns(paths: tuple[str, ...]) -> list[Path]:
|
|
503
|
+
"""Collect files to scan from glob patterns.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
paths: Glob patterns to search.
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
List of file paths matching the patterns.
|
|
510
|
+
"""
|
|
511
|
+
files_to_scan: list[Path] = []
|
|
512
|
+
for pattern in paths:
|
|
513
|
+
for file_path in Path.cwd().rglob(pattern):
|
|
514
|
+
if file_path.is_file() and not _should_skip_file(file_path):
|
|
515
|
+
files_to_scan.append(file_path)
|
|
516
|
+
return files_to_scan
|
|
177
517
|
|
|
178
518
|
|
|
179
|
-
|
|
519
|
+
def _collect_files_default() -> list[Path]:
|
|
520
|
+
"""Collect all files from current directory recursively.
|
|
180
521
|
|
|
522
|
+
Returns:
|
|
523
|
+
List of all file paths in current directory.
|
|
524
|
+
"""
|
|
525
|
+
files_to_scan: list[Path] = []
|
|
526
|
+
base = Path.cwd()
|
|
527
|
+
max_files = _resolve_audit_max_files()
|
|
528
|
+
|
|
529
|
+
for root, dirs, files in os.walk(base):
|
|
530
|
+
dirs[:] = [d for d in dirs if d not in _DEFAULT_EXCLUDE_DIRS]
|
|
531
|
+
for file in files:
|
|
532
|
+
file_path = Path(root) / file
|
|
533
|
+
if _should_skip_file(file_path):
|
|
534
|
+
continue
|
|
535
|
+
files_to_scan.append(file_path)
|
|
536
|
+
if max_files and len(files_to_scan) >= max_files:
|
|
537
|
+
_warn_scan_truncated(max_files)
|
|
538
|
+
return files_to_scan
|
|
539
|
+
|
|
540
|
+
return files_to_scan
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _resolve_audit_max_files() -> int | None:
|
|
544
|
+
"""Resolve optional scan limit from env."""
|
|
545
|
+
max_files_env = os.getenv("AIP_CONFIG_AUDIT_MAX_FILES")
|
|
546
|
+
if not max_files_env:
|
|
547
|
+
return None
|
|
548
|
+
try:
|
|
549
|
+
parsed = int(max_files_env, 10)
|
|
550
|
+
except ValueError:
|
|
551
|
+
return None
|
|
552
|
+
return parsed if parsed > 0 else None
|
|
181
553
|
|
|
182
|
-
def _mask_api_key(value: str | None) -> str:
|
|
183
|
-
if not value:
|
|
184
|
-
return ""
|
|
185
|
-
return "***" + value[-4:] if len(value) > 4 else "***"
|
|
186
554
|
|
|
555
|
+
def _warn_scan_truncated(max_files: int) -> None:
|
|
556
|
+
"""Warn when scanning is truncated to avoid surprises on huge repos."""
|
|
557
|
+
console.print(
|
|
558
|
+
f"[{WARNING_STYLE}]Scanning limited to the first {max_files} files. "
|
|
559
|
+
"Use --path to narrow the search or increase AIP_CONFIG_AUDIT_MAX_FILES to scan more.[/]"
|
|
560
|
+
)
|
|
187
561
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
)
|
|
562
|
+
|
|
563
|
+
def _collect_files_to_scan(paths: tuple[str, ...], read_from_stdin: bool) -> list[Path]:
|
|
564
|
+
"""Collect files to scan based on input method.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
paths: Glob patterns to search (if not reading from stdin).
|
|
568
|
+
read_from_stdin: Whether to read file list from stdin.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
List of file paths to scan.
|
|
572
|
+
"""
|
|
573
|
+
if read_from_stdin:
|
|
574
|
+
return _collect_files_from_stdin()
|
|
575
|
+
if paths:
|
|
576
|
+
return _collect_files_from_patterns(paths)
|
|
577
|
+
return _collect_files_default()
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _filter_by_gitignore(files_to_scan: list[Path], no_gitignore: bool) -> list[Path]:
|
|
581
|
+
"""Filter files by .gitignore patterns if enabled.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
files_to_scan: List of file paths to filter.
|
|
585
|
+
no_gitignore: If True, skip gitignore filtering.
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
Filtered list of file paths.
|
|
589
|
+
"""
|
|
590
|
+
global _PATHSPEC_WARNED
|
|
591
|
+
if no_gitignore or pathspec is None:
|
|
592
|
+
if not no_gitignore and pathspec is None and not _PATHSPEC_WARNED:
|
|
593
|
+
msg = (
|
|
594
|
+
f"[{WARNING_STYLE}]Warning:[/] pathspec is not installed; "
|
|
595
|
+
"gitignore filtering for 'aip config audit' will be skipped."
|
|
596
|
+
)
|
|
597
|
+
with _PATHSPEC_WARNED_LOCK:
|
|
598
|
+
if not _PATHSPEC_WARNED:
|
|
599
|
+
stderr_console.print(msg)
|
|
600
|
+
_PATHSPEC_WARNED = True
|
|
601
|
+
return files_to_scan
|
|
602
|
+
|
|
603
|
+
# Load .gitignore patterns
|
|
604
|
+
gitignore_path = Path.cwd() / ".gitignore"
|
|
605
|
+
if not gitignore_path.exists():
|
|
606
|
+
return files_to_scan
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
with gitignore_path.open(encoding="utf-8", errors="ignore") as f:
|
|
610
|
+
spec = pathspec.PathSpec.from_lines("gitwildmatch", f)
|
|
611
|
+
|
|
612
|
+
# Guard against files outside CWD; fallback to absolute path in that case
|
|
613
|
+
def _to_git_path(path: Path) -> str:
|
|
614
|
+
try:
|
|
615
|
+
return str(path.relative_to(Path.cwd()))
|
|
616
|
+
except ValueError:
|
|
617
|
+
return str(path)
|
|
618
|
+
|
|
619
|
+
return [path for path in files_to_scan if not spec.match_file(_to_git_path(path))]
|
|
620
|
+
except Exception:
|
|
621
|
+
# If gitignore parsing fails, return all files
|
|
622
|
+
return files_to_scan
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _should_skip_file(file_path: Path) -> bool:
|
|
626
|
+
"""Check whether a file should be skipped based on size."""
|
|
627
|
+
try:
|
|
628
|
+
return file_path.stat().st_size > _MAX_SCAN_FILE_SIZE
|
|
629
|
+
except OSError:
|
|
630
|
+
return False
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _extract_match_snippet(line: str, match_obj: re.Match[str]) -> str:
|
|
634
|
+
"""Extract a snippet around a match for display.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
line: The full line containing the match.
|
|
638
|
+
match_obj: The regex match object.
|
|
639
|
+
|
|
640
|
+
Returns:
|
|
641
|
+
A snippet of text around the match.
|
|
642
|
+
"""
|
|
643
|
+
start = max(0, match_obj.start() - 20)
|
|
644
|
+
end = min(len(line), match_obj.end() + 20)
|
|
645
|
+
return line[start:end].strip()
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _process_file_for_matches(file_path: Path, compiled_patterns: list[re.Pattern[str]]) -> list[dict[str, Any]]:
|
|
649
|
+
"""Process a single file for deprecated config command matches.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
file_path: Path to the file to scan.
|
|
653
|
+
compiled_patterns: List of compiled regex patterns to match.
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
List of hit dictionaries found in this file.
|
|
657
|
+
"""
|
|
658
|
+
hits: list[dict[str, Any]] = []
|
|
659
|
+
if _should_skip_file(file_path):
|
|
660
|
+
return hits
|
|
661
|
+
try:
|
|
662
|
+
with file_path.open(encoding="utf-8", errors="ignore") as f:
|
|
663
|
+
for line_num, line in enumerate(f, start=1):
|
|
664
|
+
for pattern in compiled_patterns:
|
|
665
|
+
match_obj = pattern.search(line)
|
|
666
|
+
if match_obj:
|
|
667
|
+
snippet = _extract_match_snippet(line, match_obj)
|
|
668
|
+
replacement = _suggest_replacement(line.strip())
|
|
669
|
+
|
|
670
|
+
try:
|
|
671
|
+
file_str = str(file_path.relative_to(Path.cwd()))
|
|
672
|
+
except ValueError:
|
|
673
|
+
file_str = str(file_path)
|
|
674
|
+
|
|
675
|
+
hits.append(
|
|
676
|
+
{
|
|
677
|
+
"file": file_str,
|
|
678
|
+
"line": line_num,
|
|
679
|
+
"match": snippet,
|
|
680
|
+
"replacement": replacement,
|
|
681
|
+
}
|
|
682
|
+
)
|
|
683
|
+
break # Only count once per line
|
|
684
|
+
except (UnicodeDecodeError, PermissionError):
|
|
685
|
+
# Skip binary files or files we can't read
|
|
686
|
+
pass
|
|
687
|
+
except OSError:
|
|
688
|
+
# Skip files with permission errors
|
|
689
|
+
pass
|
|
690
|
+
|
|
691
|
+
return hits
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _scan_files_for_matches(files_to_scan: list[Path]) -> list[dict[str, Any]]:
|
|
695
|
+
"""Scan files for deprecated config command usage.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
files_to_scan: List of file paths to scan.
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
List of hit dictionaries with file, line, match, and replacement info.
|
|
702
|
+
"""
|
|
703
|
+
hits: list[dict[str, Any]] = []
|
|
704
|
+
|
|
705
|
+
for file_path in files_to_scan:
|
|
706
|
+
file_hits = _process_file_for_matches(file_path, _COMPILED_AUDIT_PATTERNS)
|
|
707
|
+
hits.extend(file_hits)
|
|
708
|
+
|
|
709
|
+
return hits
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _output_audit_results(hits: list[dict[str, Any]], files_scanned: int, output_json: bool, silent: bool) -> None:
|
|
713
|
+
"""Output audit results in the requested format.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
hits: List of hit dictionaries.
|
|
717
|
+
files_scanned: Number of files scanned.
|
|
718
|
+
output_json: If True, output JSON format.
|
|
719
|
+
silent: If True, suppress Rich output when using JSON.
|
|
720
|
+
"""
|
|
721
|
+
if output_json:
|
|
722
|
+
result = {
|
|
723
|
+
"hits": hits,
|
|
724
|
+
"total_hits": len(hits),
|
|
725
|
+
"files_scanned": files_scanned,
|
|
726
|
+
}
|
|
727
|
+
click.echo(json.dumps(result, indent=2))
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
if silent:
|
|
731
|
+
return
|
|
732
|
+
|
|
733
|
+
if hits:
|
|
734
|
+
table = AIPTable(title="ā ļø Deprecated 'aip config' Usage Found")
|
|
735
|
+
table.add_column("File", style=INFO, width=30)
|
|
736
|
+
table.add_column("Line", style=NEUTRAL, width=8)
|
|
737
|
+
table.add_column("Match", style=WARNING_STYLE, width=40)
|
|
738
|
+
table.add_column("Suggested Replacement", style=SUCCESS_STYLE, width=40)
|
|
739
|
+
|
|
740
|
+
for hit in hits:
|
|
741
|
+
table.add_row(
|
|
742
|
+
hit["file"],
|
|
743
|
+
str(hit["line"]),
|
|
744
|
+
hit["match"],
|
|
745
|
+
hit["replacement"],
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
console.print(table)
|
|
749
|
+
console.print(f"\n[{WARNING_STYLE}]Found {len(hits)} deprecated usage(s).[/]")
|
|
194
750
|
else:
|
|
195
|
-
console.print(f"[{
|
|
751
|
+
console.print(f"[{SUCCESS_STYLE}]ā
No deprecated 'aip config' usage found.[/]")
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _suggest_replacement(line: str) -> str:
|
|
755
|
+
"""Suggest a replacement command for deprecated config usage."""
|
|
756
|
+
line_lower = line.lower()
|
|
757
|
+
|
|
758
|
+
# Map common patterns to replacements
|
|
759
|
+
if "config list" in line_lower:
|
|
760
|
+
return "aip accounts list"
|
|
761
|
+
elif "config set" in line_lower:
|
|
762
|
+
if "api_url" in line_lower or "api_key" in line_lower:
|
|
763
|
+
return "aip accounts edit <name> [--url URL] [--key]"
|
|
764
|
+
return "aip accounts edit <name>"
|
|
765
|
+
elif "config get" in line_lower:
|
|
766
|
+
return "aip accounts show <name> (or read ~/.aip/config.yaml)"
|
|
767
|
+
elif "config unset" in line_lower:
|
|
768
|
+
return "aip accounts edit <name> (to clear specific fields)"
|
|
769
|
+
elif "config reset" in line_lower:
|
|
770
|
+
return "aip accounts remove <name> (for each account)"
|
|
771
|
+
# Generic "config" usage (command-like), but avoid matching any arbitrary
|
|
772
|
+
# mention of the word "config" in unrelated text.
|
|
773
|
+
elif "aip config" in line_lower or " config " in f" {line_lower} " or CONFIG_CONFIGURE_HINT in line_lower:
|
|
774
|
+
return "aip configure or aip accounts add <name>"
|
|
775
|
+
else:
|
|
776
|
+
return "Use 'aip accounts ...' or 'aip configure'"
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
@config_group.command()
|
|
780
|
+
@click.option(
|
|
781
|
+
"--account",
|
|
782
|
+
"account_name",
|
|
783
|
+
help="Account name to configure (defaults to active account)",
|
|
784
|
+
)
|
|
785
|
+
def configure(account_name: str | None) -> None:
|
|
786
|
+
"""Configure AIP CLI credentials and settings interactively.
|
|
196
787
|
|
|
788
|
+
This command is an alias for 'aip accounts add <name>' and will
|
|
789
|
+
configure the specified account (or active account if not specified).
|
|
790
|
+
"""
|
|
791
|
+
_enforce_legacy_config_gate()
|
|
792
|
+
_configure_interactive(account_name)
|
|
197
793
|
|
|
198
|
-
def _render_config_table(config: dict[str, str]) -> None:
|
|
199
|
-
table = AIPTable(title=f"{ICON_TOOL} AIP Configuration")
|
|
200
|
-
table.add_column("Setting", style=INFO, width=20)
|
|
201
|
-
table.add_column("Value", style=SUCCESS)
|
|
202
794
|
|
|
203
|
-
|
|
204
|
-
|
|
795
|
+
# Alias command for backward compatibility
|
|
796
|
+
@click.command()
|
|
797
|
+
@click.option(
|
|
798
|
+
"--account",
|
|
799
|
+
"account_name",
|
|
800
|
+
help="Account name to configure (defaults to active account)",
|
|
801
|
+
)
|
|
802
|
+
def configure_command(account_name: str | None) -> None:
|
|
803
|
+
"""Configure AIP CLI credentials and settings interactively.
|
|
205
804
|
|
|
206
|
-
|
|
207
|
-
|
|
805
|
+
This is an alias for 'aip config configure' for backward compatibility.
|
|
806
|
+
For multi-account support, use 'aip accounts add <name>' instead.
|
|
807
|
+
"""
|
|
808
|
+
_enforce_legacy_config_gate()
|
|
809
|
+
suppress_tip = os.environ.get("AIP_SUPPRESS_CONFIGURE_TIP", "").strip().lower() in {"1", "true", "yes", "on"}
|
|
810
|
+
if not suppress_tip:
|
|
811
|
+
tip_prefix = f"[{WARNING_STYLE}]Setup tip:[/] "
|
|
812
|
+
tip_body = (
|
|
813
|
+
"Prefer 'aip accounts add <name>' or 'aip configure' from your terminal for multi-account setup. "
|
|
814
|
+
"Launching the interactive wizard now..."
|
|
815
|
+
)
|
|
816
|
+
console.print(f"{tip_prefix}{tip_body}")
|
|
817
|
+
# Delegate to the shared function
|
|
818
|
+
_configure_interactive(account_name)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
# Note: The config command group should be registered in main.py
|
|
822
|
+
_mask_api_key = mask_api_key_display
|
|
208
823
|
|
|
209
824
|
|
|
210
825
|
def _render_configuration_header() -> None:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
)
|
|
214
|
-
heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
|
|
215
|
-
console.print(heading)
|
|
216
|
-
console.print()
|
|
217
|
-
console.print(branding.get_welcome_banner())
|
|
218
|
-
console.rule("[bold]AIP Configuration[/bold]", style=PRIMARY)
|
|
826
|
+
"""Display the interactive configuration heading/banner."""
|
|
827
|
+
render_branding_header(console, "[bold]AIP Configuration[/bold]")
|
|
219
828
|
|
|
220
829
|
|
|
221
|
-
def
|
|
830
|
+
def _prompt_configuration_inputs_for_account(existing: dict[str, str] | None) -> dict[str, str]:
|
|
831
|
+
"""Interactively prompt for account configuration values."""
|
|
222
832
|
console.print("\n[bold]Enter your AIP configuration:[/bold]")
|
|
223
|
-
|
|
833
|
+
if existing:
|
|
834
|
+
console.print("(Leave blank to keep current values)")
|
|
224
835
|
console.print("ā" * 50)
|
|
225
836
|
|
|
837
|
+
config = existing.copy() if existing else {}
|
|
838
|
+
|
|
226
839
|
_prompt_api_url(config)
|
|
227
840
|
_prompt_api_key(config)
|
|
228
841
|
|
|
842
|
+
return config
|
|
843
|
+
|
|
229
844
|
|
|
230
845
|
def _prompt_api_url(config: dict[str, str]) -> None:
|
|
846
|
+
"""Ask the user for the API URL, preserving existing values by default."""
|
|
231
847
|
current_url = config.get("api_url", "")
|
|
232
848
|
suffix = f"(current: {current_url})" if current_url else ""
|
|
233
849
|
console.print(f"\n[{ACCENT_STYLE}]AIP API URL[/] {suffix}:")
|
|
@@ -239,6 +855,7 @@ def _prompt_api_url(config: dict[str, str]) -> None:
|
|
|
239
855
|
|
|
240
856
|
|
|
241
857
|
def _prompt_api_key(config: dict[str, str]) -> None:
|
|
858
|
+
"""Prompt the user for the API key while masking previous input."""
|
|
242
859
|
current_key_masked = _mask_api_key(config.get("api_key"))
|
|
243
860
|
suffix = f"(current: {current_key_masked})" if current_key_masked else ""
|
|
244
861
|
console.print(f"\n[{ACCENT_STYLE}]AIP API Key[/] {suffix}:")
|
|
@@ -247,58 +864,32 @@ def _prompt_api_key(config: dict[str, str]) -> None:
|
|
|
247
864
|
config["api_key"] = new_key
|
|
248
865
|
|
|
249
866
|
|
|
250
|
-
def
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
867
|
+
def _test_and_report_connection_for_account(account_name: str) -> None:
|
|
868
|
+
"""Sanity-check the provided credentials against the backend."""
|
|
869
|
+
store = get_account_store()
|
|
870
|
+
account = store.get_account(account_name)
|
|
871
|
+
if not account:
|
|
872
|
+
return
|
|
255
873
|
|
|
874
|
+
api_url = account.get("api_url", "")
|
|
875
|
+
api_key = account.get("api_key", "")
|
|
876
|
+
if not api_url or not api_key:
|
|
877
|
+
return
|
|
256
878
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
agents = client.list_agents()
|
|
264
|
-
console.print(
|
|
265
|
-
Text(
|
|
266
|
-
f"ā
Connection successful! Found {len(agents)} agents",
|
|
267
|
-
style=SUCCESS_STYLE,
|
|
268
|
-
)
|
|
269
|
-
)
|
|
270
|
-
except Exception as exc: # pragma: no cover - API failures depend on network
|
|
271
|
-
console.print(
|
|
272
|
-
Text(
|
|
273
|
-
f"ā ļø Connection established but API call failed: {exc}",
|
|
274
|
-
style=WARNING_STYLE,
|
|
275
|
-
)
|
|
276
|
-
)
|
|
277
|
-
console.print(
|
|
278
|
-
" You may need to check your API permissions or network access"
|
|
279
|
-
)
|
|
280
|
-
except Exception as exc:
|
|
281
|
-
console.print(Text(f"ā Connection failed: {exc}"))
|
|
282
|
-
console.print(" Please check your API URL and key")
|
|
283
|
-
hint_status = command_hint("status", slash_command="status")
|
|
284
|
-
if hint_status:
|
|
285
|
-
console.print(
|
|
286
|
-
f" You can run {format_command_hint(hint_status) or hint_status} later to test again"
|
|
287
|
-
)
|
|
288
|
-
finally:
|
|
289
|
-
if client is not None:
|
|
290
|
-
client.close()
|
|
879
|
+
hint_status = command_hint("status", slash_command="status")
|
|
880
|
+
extra_hint = None
|
|
881
|
+
if hint_status:
|
|
882
|
+
extra_hint = f" You can run {format_command_hint(hint_status) or hint_status} later to test again"
|
|
883
|
+
|
|
884
|
+
check_connection(api_url, api_key, console, abort_on_error=False, extra_hint=extra_hint)
|
|
291
885
|
|
|
292
886
|
|
|
293
887
|
def _print_post_configuration_hints() -> None:
|
|
888
|
+
"""Offer next-step guidance after configuration completes."""
|
|
294
889
|
console.print("\nš” You can now use AIP CLI commands!")
|
|
295
890
|
hint_status = command_hint("status", slash_command="status")
|
|
296
891
|
if hint_status:
|
|
297
|
-
console.print(
|
|
298
|
-
f" ⢠Run {format_command_hint(hint_status) or hint_status} to check connection"
|
|
299
|
-
)
|
|
892
|
+
console.print(f" ⢠Run {format_command_hint(hint_status) or hint_status} to check connection")
|
|
300
893
|
hint_agents = command_hint("agents list", slash_command="agents")
|
|
301
894
|
if hint_agents:
|
|
302
|
-
console.print(
|
|
303
|
-
f" ⢠Run {format_command_hint(hint_agents) or hint_agents} to see your agents"
|
|
304
|
-
)
|
|
895
|
+
console.print(f" ⢠Run {format_command_hint(hint_agents) or hint_agents} to see your agents")
|