glaip-sdk 0.5.2__py3-none-any.whl → 0.5.5__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/cli/commands/accounts.py +210 -23
- glaip_sdk/cli/commands/agents.py +6 -0
- glaip_sdk/cli/commands/configure.py +528 -38
- glaip_sdk/cli/slash/session.py +6 -5
- glaip_sdk/cli/update_notifier.py +5 -0
- {glaip_sdk-0.5.2.dist-info → glaip_sdk-0.5.5.dist-info}/METADATA +1 -1
- {glaip_sdk-0.5.2.dist-info → glaip_sdk-0.5.5.dist-info}/RECORD +9 -9
- {glaip_sdk-0.5.2.dist-info → glaip_sdk-0.5.5.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.5.2.dist-info → glaip_sdk-0.5.5.dist-info}/entry_points.txt +0 -0
|
@@ -6,7 +6,9 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
import getpass
|
|
8
8
|
import json
|
|
9
|
+
import os
|
|
9
10
|
import sys
|
|
11
|
+
from pathlib import Path
|
|
10
12
|
|
|
11
13
|
import click
|
|
12
14
|
from rich.console import Console
|
|
@@ -110,6 +112,153 @@ def list_accounts(output_json: bool) -> None:
|
|
|
110
112
|
console.print(f"\n[{INFO}]💡 Tip[/]: To update an account's URL or key, use: [bold]aip accounts edit <name>[/bold]")
|
|
111
113
|
|
|
112
114
|
|
|
115
|
+
def _build_account_json_payload(
|
|
116
|
+
name: str,
|
|
117
|
+
api_url: str,
|
|
118
|
+
masked_key: str,
|
|
119
|
+
config_path: str,
|
|
120
|
+
is_active: bool,
|
|
121
|
+
env_lock: bool,
|
|
122
|
+
metadata: dict[str, str | None],
|
|
123
|
+
) -> dict[str, str | bool | None]:
|
|
124
|
+
"""Build JSON payload for account display.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
name: Account name.
|
|
128
|
+
api_url: API URL.
|
|
129
|
+
masked_key: Masked API key.
|
|
130
|
+
config_path: Config file path.
|
|
131
|
+
is_active: Whether account is active.
|
|
132
|
+
env_lock: Whether env credentials are set.
|
|
133
|
+
metadata: Account metadata dict.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
JSON payload dict.
|
|
137
|
+
"""
|
|
138
|
+
payload: dict[str, str | bool | None] = {
|
|
139
|
+
"name": name,
|
|
140
|
+
"api_url": api_url,
|
|
141
|
+
"api_key_masked": masked_key,
|
|
142
|
+
"config_path": config_path,
|
|
143
|
+
"active": is_active,
|
|
144
|
+
"env_lock": env_lock,
|
|
145
|
+
}
|
|
146
|
+
for key, value in metadata.items():
|
|
147
|
+
if value:
|
|
148
|
+
payload[key] = value
|
|
149
|
+
return payload
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _format_config_path(config_path: str) -> str:
|
|
153
|
+
"""Format config path for display, shortening under home."""
|
|
154
|
+
path_obj = Path(config_path).expanduser()
|
|
155
|
+
try:
|
|
156
|
+
home = Path.home().expanduser()
|
|
157
|
+
resolved = path_obj.resolve(strict=False)
|
|
158
|
+
relative = resolved.relative_to(home).as_posix()
|
|
159
|
+
return f"~/{relative}"
|
|
160
|
+
except ValueError:
|
|
161
|
+
# Not under home; return expanded path
|
|
162
|
+
return str(path_obj)
|
|
163
|
+
except OSError:
|
|
164
|
+
# Fall back to original string on resolution errors
|
|
165
|
+
return config_path
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _build_account_display_lines(
|
|
169
|
+
name: str,
|
|
170
|
+
api_url: str,
|
|
171
|
+
masked_key: str,
|
|
172
|
+
config_path: str,
|
|
173
|
+
is_active: bool,
|
|
174
|
+
env_lock: bool,
|
|
175
|
+
metadata: dict[str, str | None],
|
|
176
|
+
) -> list[str]:
|
|
177
|
+
"""Build display lines for account information.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
name: Account name.
|
|
181
|
+
api_url: API URL.
|
|
182
|
+
masked_key: Masked API key.
|
|
183
|
+
config_path: Config file path.
|
|
184
|
+
is_active: Whether account is active.
|
|
185
|
+
env_lock: Whether env credentials are set.
|
|
186
|
+
metadata: Account metadata dict.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
List of formatted display lines.
|
|
190
|
+
"""
|
|
191
|
+
lines = [
|
|
192
|
+
f"[{SUCCESS_STYLE}]Name[/]: {name}{' (active)' if is_active else ''}",
|
|
193
|
+
f"[{SUCCESS_STYLE}]API URL[/]: {api_url or 'not set'}",
|
|
194
|
+
f"[{SUCCESS_STYLE}]Key[/]: {masked_key or 'not set'}",
|
|
195
|
+
f"[{SUCCESS_STYLE}]Config[/]: {config_path}",
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
label_map = {
|
|
199
|
+
"notes": "Notes",
|
|
200
|
+
"last_used_at": "Last used",
|
|
201
|
+
"last_validated_at": "Last validated",
|
|
202
|
+
"created_with": "Created with",
|
|
203
|
+
}
|
|
204
|
+
for key, label in label_map.items():
|
|
205
|
+
value = metadata.get(key)
|
|
206
|
+
if value:
|
|
207
|
+
lines.append(f"[{SUCCESS_STYLE}]{label}[/]: {value}")
|
|
208
|
+
|
|
209
|
+
if env_lock:
|
|
210
|
+
lines.append(
|
|
211
|
+
f"[{WARNING_STYLE}]Env credentials detected (AIP_API_URL/AIP_API_KEY); stored profile may be ignored.[/]"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return lines
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@accounts_group.command("show")
|
|
218
|
+
@click.argument("name")
|
|
219
|
+
@click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
|
|
220
|
+
def show_account(name: str, output_json: bool) -> None:
|
|
221
|
+
"""Show details for a single account profile."""
|
|
222
|
+
store = get_account_store()
|
|
223
|
+
account = store.get_account(name)
|
|
224
|
+
|
|
225
|
+
if not account:
|
|
226
|
+
console.print(f"[{ERROR_STYLE}]Error: Account '{name}' not found.[/]")
|
|
227
|
+
raise click.Abort()
|
|
228
|
+
|
|
229
|
+
api_url = account.get("api_url", "")
|
|
230
|
+
api_key = account.get("api_key")
|
|
231
|
+
masked_key = _mask_api_key(api_key or "")
|
|
232
|
+
active_account = store.get_active_account()
|
|
233
|
+
is_active = active_account == name
|
|
234
|
+
env_lock = bool(os.getenv("AIP_API_URL") or os.getenv("AIP_API_KEY"))
|
|
235
|
+
config_path_raw = str(store.config_file)
|
|
236
|
+
config_path_display = _format_config_path(config_path_raw)
|
|
237
|
+
|
|
238
|
+
metadata = {
|
|
239
|
+
"notes": account.get("notes"),
|
|
240
|
+
"last_used_at": account.get("last_used_at"),
|
|
241
|
+
"last_validated_at": account.get("last_validated_at"),
|
|
242
|
+
"created_with": account.get("created_with"),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if output_json:
|
|
246
|
+
payload = _build_account_json_payload(name, api_url, masked_key, config_path_raw, is_active, env_lock, metadata)
|
|
247
|
+
click.echo(json.dumps(payload, indent=2))
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
lines = _build_account_display_lines(name, api_url, masked_key, config_path_display, is_active, env_lock, metadata)
|
|
251
|
+
|
|
252
|
+
lock_badge = " 🔒 Env lock" if env_lock else ""
|
|
253
|
+
console.print(
|
|
254
|
+
AIPPanel(
|
|
255
|
+
"\n".join(lines),
|
|
256
|
+
title=f"AIP Account{lock_badge}",
|
|
257
|
+
border_style=ACCENT_STYLE,
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
113
262
|
def _check_account_overwrite(name: str, store: AccountStore, overwrite: bool) -> dict[str, str] | None:
|
|
114
263
|
"""Check if account exists and handle overwrite logic.
|
|
115
264
|
|
|
@@ -155,8 +304,8 @@ def _get_credentials_non_interactive(
|
|
|
155
304
|
if not sys.stdin.isatty():
|
|
156
305
|
return url, sys.stdin.read().strip()
|
|
157
306
|
console.print(
|
|
158
|
-
f"[{ERROR_STYLE}]Error: --key
|
|
159
|
-
f"Use: cat key.txt | {command_name} {name} --url {url} --key[/]",
|
|
307
|
+
f"[{ERROR_STYLE}]Error: --key expects stdin or an explicit value. "
|
|
308
|
+
f"Use '--key <value>' or pipe: cat key.txt | {command_name} {name} --url {url} --key[/]",
|
|
160
309
|
)
|
|
161
310
|
raise click.Abort()
|
|
162
311
|
# URL provided, prompt for key
|
|
@@ -179,7 +328,8 @@ def _get_credentials_interactive(read_key_from_stdin: bool, existing: dict[str,
|
|
|
179
328
|
"""
|
|
180
329
|
if read_key_from_stdin:
|
|
181
330
|
console.print(
|
|
182
|
-
f"[{ERROR_STYLE}]Error: --key requires --url.
|
|
331
|
+
f"[{ERROR_STYLE}]Error: --key requires --url. "
|
|
332
|
+
f"Provide --url with --key <value|-> for non-interactive use or omit --key to be prompted.[/]",
|
|
183
333
|
)
|
|
184
334
|
raise click.Abort()
|
|
185
335
|
# Fully interactive
|
|
@@ -237,7 +387,7 @@ def _preserve_existing_values(
|
|
|
237
387
|
|
|
238
388
|
def _collect_credentials_from_inputs(
|
|
239
389
|
url: str | None,
|
|
240
|
-
|
|
390
|
+
api_key_input: str | None,
|
|
241
391
|
name: str,
|
|
242
392
|
existing: dict[str, str] | None,
|
|
243
393
|
command_name: str,
|
|
@@ -247,7 +397,7 @@ def _collect_credentials_from_inputs(
|
|
|
247
397
|
|
|
248
398
|
Args:
|
|
249
399
|
url: Optional URL from flag.
|
|
250
|
-
|
|
400
|
+
api_key_input: API key value from flag (or "-" when stdin requested).
|
|
251
401
|
name: Account name (for error messages).
|
|
252
402
|
existing: Existing account data.
|
|
253
403
|
command_name: Command name for error messages.
|
|
@@ -256,6 +406,29 @@ def _collect_credentials_from_inputs(
|
|
|
256
406
|
Returns:
|
|
257
407
|
Tuple of (api_url, api_key).
|
|
258
408
|
"""
|
|
409
|
+
provided_key = api_key_input if api_key_input not in (None, "-") else None
|
|
410
|
+
read_key_from_stdin = api_key_input == "-"
|
|
411
|
+
|
|
412
|
+
if provided_key and url:
|
|
413
|
+
# Fully non-interactive: URL and key provided via flags
|
|
414
|
+
return url, provided_key
|
|
415
|
+
|
|
416
|
+
if provided_key:
|
|
417
|
+
# Reuse stored URL if present; otherwise require --url
|
|
418
|
+
if existing_url:
|
|
419
|
+
return existing_url, provided_key
|
|
420
|
+
if existing:
|
|
421
|
+
console.print(
|
|
422
|
+
f"[{ERROR_STYLE}]Error: Account '{name}' is missing an API URL. "
|
|
423
|
+
f"Provide --url to set it when rotating the key.[/]"
|
|
424
|
+
)
|
|
425
|
+
else:
|
|
426
|
+
console.print(
|
|
427
|
+
f"[{ERROR_STYLE}]Error: --key requires --url for new accounts. "
|
|
428
|
+
f"Run without --key for prompts or pass both flags for non-interactive setup.[/]",
|
|
429
|
+
)
|
|
430
|
+
raise click.Abort()
|
|
431
|
+
|
|
259
432
|
if url and read_key_from_stdin:
|
|
260
433
|
# Non-interactive: URL from flag, key from stdin
|
|
261
434
|
return _get_credentials_non_interactive(url, True, name, command_name)
|
|
@@ -271,15 +444,25 @@ def _collect_credentials_from_inputs(
|
|
|
271
444
|
|
|
272
445
|
def _collect_account_credentials(
|
|
273
446
|
url: str | None,
|
|
274
|
-
|
|
447
|
+
api_key_input: str | None,
|
|
275
448
|
name: str,
|
|
276
449
|
existing: dict[str, str] | None,
|
|
277
450
|
) -> tuple[str, str]:
|
|
278
451
|
"""Collect account credentials from various input methods.
|
|
279
452
|
|
|
453
|
+
Examples:
|
|
454
|
+
# Inline key
|
|
455
|
+
aip accounts add prod --url https://api.example.com --key sk-abc123
|
|
456
|
+
|
|
457
|
+
# Stdin (useful for scripts)
|
|
458
|
+
echo "sk-abc123" | aip accounts add prod --url https://api.example.com --key
|
|
459
|
+
|
|
460
|
+
# Fully interactive
|
|
461
|
+
aip accounts add prod
|
|
462
|
+
|
|
280
463
|
Args:
|
|
281
464
|
url: Optional URL from flag.
|
|
282
|
-
|
|
465
|
+
api_key_input: API key value from flag (or "-" when stdin requested).
|
|
283
466
|
name: Account name (for error messages).
|
|
284
467
|
existing: Existing account data.
|
|
285
468
|
|
|
@@ -293,9 +476,7 @@ def _collect_account_credentials(
|
|
|
293
476
|
existing_url = existing.get("api_url", "") if existing else ""
|
|
294
477
|
existing_key = existing.get("api_key", "") if existing else ""
|
|
295
478
|
|
|
296
|
-
api_url, api_key = _collect_credentials_from_inputs(
|
|
297
|
-
url, read_key_from_stdin, name, existing, command_name, existing_url
|
|
298
|
-
)
|
|
479
|
+
api_url, api_key = _collect_credentials_from_inputs(url, api_key_input, name, existing, command_name, existing_url)
|
|
299
480
|
|
|
300
481
|
# Preserve stored values when blank input is provided during edit
|
|
301
482
|
api_url, api_key = _preserve_existing_values(api_url, api_key, existing_url, existing_key)
|
|
@@ -311,9 +492,12 @@ def _collect_account_credentials(
|
|
|
311
492
|
@click.option("--url", help="API URL (required for non-interactive mode)")
|
|
312
493
|
@click.option(
|
|
313
494
|
"--key",
|
|
314
|
-
"
|
|
315
|
-
|
|
316
|
-
|
|
495
|
+
"api_key_input",
|
|
496
|
+
type=str,
|
|
497
|
+
is_flag=False,
|
|
498
|
+
flag_value="-",
|
|
499
|
+
default=None,
|
|
500
|
+
help="API key value. Pass without a value or '-' to read from stdin. Requires --url for non-interactive use.",
|
|
317
501
|
)
|
|
318
502
|
@click.option(
|
|
319
503
|
"--yes",
|
|
@@ -324,7 +508,7 @@ def _collect_account_credentials(
|
|
|
324
508
|
def add_account(
|
|
325
509
|
name: str,
|
|
326
510
|
url: str | None,
|
|
327
|
-
|
|
511
|
+
api_key_input: str | None,
|
|
328
512
|
overwrite: bool,
|
|
329
513
|
) -> None:
|
|
330
514
|
"""Add a new account profile.
|
|
@@ -332,7 +516,7 @@ def add_account(
|
|
|
332
516
|
NAME is the account name (1-32 chars, alphanumeric, dash, underscore).
|
|
333
517
|
|
|
334
518
|
By default, this command runs interactively, prompting for API URL and key.
|
|
335
|
-
For non-interactive use,
|
|
519
|
+
For non-interactive use, provide --url with --key <value> or --key - (stdin).
|
|
336
520
|
|
|
337
521
|
If the account already exists, use --yes to overwrite without prompting.
|
|
338
522
|
To update an existing account, use [bold]aip accounts edit <name>[/bold] instead.
|
|
@@ -343,7 +527,7 @@ def add_account(
|
|
|
343
527
|
existing = _check_account_overwrite(name, store, overwrite)
|
|
344
528
|
|
|
345
529
|
# Collect credentials
|
|
346
|
-
api_url, api_key = _collect_account_credentials(url,
|
|
530
|
+
api_url, api_key = _collect_account_credentials(url, api_key_input, name, existing)
|
|
347
531
|
|
|
348
532
|
# Save account
|
|
349
533
|
try:
|
|
@@ -363,14 +547,17 @@ def add_account(
|
|
|
363
547
|
@click.option("--url", help="API URL (optional, leave blank to keep current)")
|
|
364
548
|
@click.option(
|
|
365
549
|
"--key",
|
|
366
|
-
"
|
|
367
|
-
|
|
368
|
-
|
|
550
|
+
"api_key_input",
|
|
551
|
+
type=str,
|
|
552
|
+
is_flag=False,
|
|
553
|
+
flag_value="-",
|
|
554
|
+
default=None,
|
|
555
|
+
help="API key value. Pass without a value or '-' to read from stdin. Uses stored URL unless --url is provided.",
|
|
369
556
|
)
|
|
370
557
|
def edit_account(
|
|
371
558
|
name: str,
|
|
372
559
|
url: str | None,
|
|
373
|
-
|
|
560
|
+
api_key_input: str | None,
|
|
374
561
|
) -> None:
|
|
375
562
|
"""Edit an existing account profile's URL or key.
|
|
376
563
|
|
|
@@ -379,8 +566,8 @@ def edit_account(
|
|
|
379
566
|
By default, this command runs interactively, showing current values and
|
|
380
567
|
prompting for new ones. Leave fields blank to keep current values.
|
|
381
568
|
|
|
382
|
-
For non-interactive use, provide --url to change the URL, --key
|
|
383
|
-
or
|
|
569
|
+
For non-interactive use, provide --url to change the URL, --key <value> to rotate the key,
|
|
570
|
+
or --key - (stdin) for scripts. Stored values are reused for any fields not provided.
|
|
384
571
|
"""
|
|
385
572
|
store = get_account_store()
|
|
386
573
|
|
|
@@ -392,7 +579,7 @@ def edit_account(
|
|
|
392
579
|
raise click.Abort()
|
|
393
580
|
|
|
394
581
|
# Collect credentials (will pre-fill existing values in interactive mode)
|
|
395
|
-
api_url, api_key = _collect_account_credentials(url,
|
|
582
|
+
api_url, api_key = _collect_account_credentials(url, api_key_input, name, existing)
|
|
396
583
|
|
|
397
584
|
# Save account
|
|
398
585
|
try:
|
glaip_sdk/cli/commands/agents.py
CHANGED
|
@@ -1390,6 +1390,12 @@ def update(
|
|
|
1390
1390
|
|
|
1391
1391
|
if merged_data:
|
|
1392
1392
|
_handle_update_import_config(import_file, merged_data, update_data)
|
|
1393
|
+
# Ensure instruction from import file is included if not already set via CLI
|
|
1394
|
+
# This handles the case where instruction is None in CLI args but exists in import file
|
|
1395
|
+
if import_file and (instruction is None or "instruction" not in update_data):
|
|
1396
|
+
import_instruction = merged_data.get("instruction")
|
|
1397
|
+
if import_instruction is not None:
|
|
1398
|
+
update_data["instruction"] = import_instruction
|
|
1393
1399
|
|
|
1394
1400
|
if not update_data:
|
|
1395
1401
|
raise click.ClickException("No update fields specified")
|
|
@@ -5,12 +5,25 @@ 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.branding import ACCENT_STYLE, ERROR_STYLE, INFO,
|
|
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:
|
|
26
|
+
pathspec = None # type: ignore[assignment]
|
|
14
27
|
from glaip_sdk.cli.account_store import get_account_store
|
|
15
28
|
from glaip_sdk.cli.commands.common_config import check_connection, render_branding_header
|
|
16
29
|
from glaip_sdk.cli.config import CONFIG_FILE, load_config, save_config
|
|
@@ -18,27 +31,114 @@ from glaip_sdk.cli.hints import format_command_hint
|
|
|
18
31
|
from glaip_sdk.cli.masking import mask_api_key_display
|
|
19
32
|
from glaip_sdk.cli.rich_helpers import markup_text
|
|
20
33
|
from glaip_sdk.cli.utils import command_hint
|
|
21
|
-
from glaip_sdk.icons import ICON_TOOL
|
|
22
34
|
from glaip_sdk.rich_components import AIPTable
|
|
23
35
|
|
|
24
36
|
console = Console()
|
|
37
|
+
stderr_console = Console(file=sys.stderr)
|
|
38
|
+
_PATHSPEC_WARNED = False
|
|
39
|
+
_PATHSPEC_WARNED_LOCK = threading.Lock()
|
|
40
|
+
|
|
41
|
+
# Hard deprecation banner for legacy config commands (v0.6.x)
|
|
42
|
+
CONFIG_HARD_DEPRECATION_MSG = (
|
|
43
|
+
f"[{WARNING_STYLE}]⚠️ DEPRECATED: 'aip config ...' commands will be removed in v0.7.0. "
|
|
44
|
+
"Use 'aip accounts ...' (list/add/use/remove/edit) or 'aip configure' for the wizard. "
|
|
45
|
+
"Set AIP_ENABLE_LEGACY_CONFIG=1 to temporarily re-enable these commands.[/]"
|
|
46
|
+
)
|
|
25
47
|
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
f"[{WARNING_STYLE}]Deprecated: 'aip config ...' will be removed in
|
|
29
|
-
"Use 'aip accounts ...' (list/add/use/remove) or 'aip configure' for the wizard.[/]"
|
|
48
|
+
# Soft deprecation banner (for when env flag is set)
|
|
49
|
+
CONFIG_SOFT_DEPRECATION_MSG = (
|
|
50
|
+
f"[{WARNING_STYLE}]Deprecated: 'aip config ...' will be removed in v0.7.0. "
|
|
51
|
+
"Use 'aip accounts ...' (list/add/use/remove/edit) or 'aip configure' for the wizard.[/]"
|
|
30
52
|
)
|
|
31
53
|
|
|
54
|
+
# Target removal version
|
|
55
|
+
TARGET_REMOVAL_VERSION = "v0.7.0"
|
|
56
|
+
|
|
57
|
+
# Command hint constant
|
|
58
|
+
CONFIG_CONFIGURE_HINT = "config configure"
|
|
59
|
+
_DEFAULT_EXCLUDE_DIRS = {
|
|
60
|
+
".git",
|
|
61
|
+
"node_modules",
|
|
62
|
+
".venv",
|
|
63
|
+
"venv",
|
|
64
|
+
".tox",
|
|
65
|
+
"build",
|
|
66
|
+
"dist",
|
|
67
|
+
"__pycache__",
|
|
68
|
+
".mypy_cache",
|
|
69
|
+
".pytest_cache",
|
|
70
|
+
}
|
|
71
|
+
_MAX_SCAN_FILE_SIZE = 2 * 1024 * 1024 # 2MB cap for default scans
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_legacy_config_enabled() -> bool:
|
|
75
|
+
"""Check if legacy config commands are enabled via environment variable."""
|
|
76
|
+
env_value = os.environ.get("AIP_ENABLE_LEGACY_CONFIG", "").strip().lower()
|
|
77
|
+
return env_value in ("1", "true", "yes", "on")
|
|
78
|
+
|
|
32
79
|
|
|
33
80
|
def _print_config_deprecation() -> None:
|
|
34
81
|
"""Print a standardized deprecation warning for legacy config commands."""
|
|
35
|
-
|
|
82
|
+
if _is_legacy_config_enabled():
|
|
83
|
+
# Soft deprecation when env flag is set
|
|
84
|
+
stderr_console.print(CONFIG_SOFT_DEPRECATION_MSG)
|
|
85
|
+
else:
|
|
86
|
+
# Hard deprecation when env flag is not set
|
|
87
|
+
stderr_console.print(CONFIG_HARD_DEPRECATION_MSG)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _check_legacy_config_gate() -> bool:
|
|
91
|
+
"""Return True if legacy config commands are allowed; print banner otherwise."""
|
|
92
|
+
if not _is_legacy_config_enabled():
|
|
93
|
+
stderr_console.print(CONFIG_HARD_DEPRECATION_MSG)
|
|
94
|
+
return False
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _enforce_legacy_config_gate() -> None:
|
|
99
|
+
"""CLI-only gate: exit with code 0 when legacy commands are disabled."""
|
|
100
|
+
if not _check_legacy_config_gate():
|
|
101
|
+
# Spec requires non-breaking exit after banner
|
|
102
|
+
sys.exit(0)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _emit_telemetry_event(_event_name: str, properties: dict[str, Any] | None = None) -> None:
|
|
106
|
+
"""Emit telemetry event for legacy command usage tracking.
|
|
107
|
+
|
|
108
|
+
This is a stub implementation that can be connected to a real telemetry system.
|
|
109
|
+
For now, it's a no-op but structured to allow easy integration.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
_event_name: Name of the telemetry event (prefixed with _ to indicate unused for now).
|
|
113
|
+
properties: Optional event properties dictionary.
|
|
114
|
+
|
|
115
|
+
Note:
|
|
116
|
+
TODO: Connect to actual telemetry system when available.
|
|
117
|
+
"""
|
|
118
|
+
if properties is None:
|
|
119
|
+
properties = {}
|
|
120
|
+
# Mark as intentionally unused until telemetry system is integrated
|
|
121
|
+
del _event_name, properties
|
|
36
122
|
|
|
37
123
|
|
|
38
124
|
@click.group()
|
|
39
125
|
def config_group() -> None:
|
|
40
|
-
"""Configuration management operations.
|
|
126
|
+
"""Configuration management operations (deprecated).
|
|
127
|
+
|
|
128
|
+
These commands are deprecated and will be removed in v0.7.0.
|
|
129
|
+
Use 'aip accounts ...' commands instead.
|
|
130
|
+
Set AIP_ENABLE_LEGACY_CONFIG=1 to temporarily re-enable.
|
|
131
|
+
"""
|
|
132
|
+
_enforce_legacy_config_gate()
|
|
41
133
|
_print_config_deprecation()
|
|
134
|
+
# Emit telemetry for legacy command invocation
|
|
135
|
+
_emit_telemetry_event(
|
|
136
|
+
"config.command",
|
|
137
|
+
{
|
|
138
|
+
"phase": "hard_deprecation",
|
|
139
|
+
"gated_by_env": _is_legacy_config_enabled(),
|
|
140
|
+
},
|
|
141
|
+
)
|
|
42
142
|
|
|
43
143
|
|
|
44
144
|
@config_group.command("list")
|
|
@@ -49,6 +149,7 @@ def list_config(ctx: click.Context, output_json: bool) -> None:
|
|
|
49
149
|
|
|
50
150
|
Deprecated: run 'aip accounts list' for profile-aware output.
|
|
51
151
|
"""
|
|
152
|
+
_enforce_legacy_config_gate()
|
|
52
153
|
console.print(f"[{WARNING_STYLE}]Deprecated: run 'aip accounts list' for profile-aware output.[/]")
|
|
53
154
|
|
|
54
155
|
# Delegate to accounts list by invoking the command
|
|
@@ -124,7 +225,10 @@ def set_config(key: str, value: str, account_name: str | None) -> None:
|
|
|
124
225
|
|
|
125
226
|
For api_url and api_key, this operates on the specified account (or active account).
|
|
126
227
|
Other keys (timeout, history_default_limit) are global settings.
|
|
228
|
+
|
|
229
|
+
Deprecated: use 'aip accounts edit <name>' instead.
|
|
127
230
|
"""
|
|
231
|
+
_enforce_legacy_config_gate()
|
|
128
232
|
# For other keys, use legacy config
|
|
129
233
|
valid_keys = tuple(CONFIG_VALUE_TYPES.keys())
|
|
130
234
|
if key not in valid_keys:
|
|
@@ -169,7 +273,11 @@ def set_config(key: str, value: str, account_name: str | None) -> None:
|
|
|
169
273
|
@config_group.command("get")
|
|
170
274
|
@click.argument("key")
|
|
171
275
|
def get_config(key: str) -> None:
|
|
172
|
-
"""Get a configuration value.
|
|
276
|
+
"""Get a configuration value.
|
|
277
|
+
|
|
278
|
+
Deprecated: use 'aip accounts show <name>' or read ~/.aip/config.yaml directly.
|
|
279
|
+
"""
|
|
280
|
+
_enforce_legacy_config_gate()
|
|
173
281
|
config = load_config()
|
|
174
282
|
|
|
175
283
|
value = config.get(key)
|
|
@@ -194,7 +302,11 @@ def get_config(key: str) -> None:
|
|
|
194
302
|
@config_group.command("unset")
|
|
195
303
|
@click.argument("key")
|
|
196
304
|
def unset_config(key: str) -> None:
|
|
197
|
-
"""Remove a configuration value.
|
|
305
|
+
"""Remove a configuration value.
|
|
306
|
+
|
|
307
|
+
Deprecated: use 'aip accounts edit <name>' to clear specific fields.
|
|
308
|
+
"""
|
|
309
|
+
_enforce_legacy_config_gate()
|
|
198
310
|
config = load_config()
|
|
199
311
|
|
|
200
312
|
if key not in config:
|
|
@@ -210,7 +322,11 @@ def unset_config(key: str) -> None:
|
|
|
210
322
|
@config_group.command("reset")
|
|
211
323
|
@click.option("--force", is_flag=True, help="Skip confirmation prompt")
|
|
212
324
|
def reset_config(force: bool) -> None:
|
|
213
|
-
"""Reset all configuration to defaults.
|
|
325
|
+
"""Reset all configuration to defaults.
|
|
326
|
+
|
|
327
|
+
Deprecated: use 'aip accounts remove <name>' for each account or manually edit ~/.aip/config.yaml.
|
|
328
|
+
"""
|
|
329
|
+
_enforce_legacy_config_gate()
|
|
214
330
|
if not force:
|
|
215
331
|
console.print(f"[{WARNING_STYLE}]This will remove all AIP configuration.[/]")
|
|
216
332
|
confirm = input("Are you sure? (y/N): ").strip().lower()
|
|
@@ -235,7 +351,7 @@ def reset_config(force: bool) -> None:
|
|
|
235
351
|
# In-memory configuration (e.g., tests) needs explicit clearing
|
|
236
352
|
save_config({})
|
|
237
353
|
|
|
238
|
-
hint = command_hint(
|
|
354
|
+
hint = command_hint(CONFIG_CONFIGURE_HINT, slash_command="login")
|
|
239
355
|
message = Text("✅ Configuration reset.", style=SUCCESS_STYLE)
|
|
240
356
|
if hint:
|
|
241
357
|
message.append(f" Run '{hint}' to set up again.")
|
|
@@ -271,6 +387,396 @@ def _configure_interactive(account_name: str | None = None) -> None:
|
|
|
271
387
|
_print_active_account_footer(store)
|
|
272
388
|
|
|
273
389
|
|
|
390
|
+
@config_group.command("audit")
|
|
391
|
+
@click.option(
|
|
392
|
+
"--path",
|
|
393
|
+
"paths",
|
|
394
|
+
multiple=True,
|
|
395
|
+
help="Glob pattern(s) to search (repeatable). Defaults to current directory.",
|
|
396
|
+
)
|
|
397
|
+
@click.option(
|
|
398
|
+
"--stdin",
|
|
399
|
+
"read_from_stdin",
|
|
400
|
+
is_flag=True,
|
|
401
|
+
help="Read file list from stdin (one path per line).",
|
|
402
|
+
)
|
|
403
|
+
@click.option(
|
|
404
|
+
"--no-gitignore",
|
|
405
|
+
is_flag=True,
|
|
406
|
+
help="Disable .gitignore filtering (default: respects .gitignore).",
|
|
407
|
+
)
|
|
408
|
+
@click.option(
|
|
409
|
+
"--json",
|
|
410
|
+
"output_json",
|
|
411
|
+
is_flag=True,
|
|
412
|
+
help="Output results in JSON format.",
|
|
413
|
+
)
|
|
414
|
+
@click.option(
|
|
415
|
+
"--fail-on-hit/--no-fail-on-hit",
|
|
416
|
+
default=True,
|
|
417
|
+
help="Exit with code 1 if hits are found (default: fail on hit).",
|
|
418
|
+
)
|
|
419
|
+
@click.option(
|
|
420
|
+
"--silent",
|
|
421
|
+
is_flag=True,
|
|
422
|
+
help="Suppress Rich table output when --json is used.",
|
|
423
|
+
)
|
|
424
|
+
def audit_config(
|
|
425
|
+
paths: tuple[str, ...],
|
|
426
|
+
read_from_stdin: bool,
|
|
427
|
+
no_gitignore: bool,
|
|
428
|
+
output_json: bool,
|
|
429
|
+
fail_on_hit: bool,
|
|
430
|
+
silent: bool,
|
|
431
|
+
) -> None:
|
|
432
|
+
"""Scan scripts/configs for deprecated 'aip config' command usage.
|
|
433
|
+
|
|
434
|
+
Finds strings matching 'aip config' (including variations like 'aip-config',
|
|
435
|
+
'python -m glaip_sdk.cli config') in scripts, CI manifests, and docs.
|
|
436
|
+
|
|
437
|
+
Examples:
|
|
438
|
+
aip config audit
|
|
439
|
+
aip config audit --path "**/*.sh" --path "**/*.yml"
|
|
440
|
+
aip config audit --stdin < file_list.txt
|
|
441
|
+
aip config audit --json --no-fail-on-hit
|
|
442
|
+
"""
|
|
443
|
+
_enforce_legacy_config_gate()
|
|
444
|
+
# Collect files to scan
|
|
445
|
+
files_to_scan = _collect_files_to_scan(paths, read_from_stdin)
|
|
446
|
+
|
|
447
|
+
# Filter by gitignore if enabled
|
|
448
|
+
files_to_scan = _filter_by_gitignore(files_to_scan, no_gitignore)
|
|
449
|
+
|
|
450
|
+
# Scan files for matches
|
|
451
|
+
hits = _scan_files_for_matches(files_to_scan)
|
|
452
|
+
|
|
453
|
+
# Emit telemetry
|
|
454
|
+
_emit_telemetry_event(
|
|
455
|
+
"config.audit",
|
|
456
|
+
{
|
|
457
|
+
"audit_invoked": True,
|
|
458
|
+
"hits_found": len(hits),
|
|
459
|
+
"files_scanned": len(files_to_scan),
|
|
460
|
+
},
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Output results
|
|
464
|
+
_output_audit_results(hits, len(files_to_scan), output_json, silent)
|
|
465
|
+
|
|
466
|
+
# Exit with appropriate code
|
|
467
|
+
if hits and fail_on_hit:
|
|
468
|
+
sys.exit(1)
|
|
469
|
+
sys.exit(0)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# Patterns to match deprecated config command usage
|
|
473
|
+
_AUDIT_PATTERNS = [
|
|
474
|
+
r"aip\s+config",
|
|
475
|
+
r"aip-config",
|
|
476
|
+
r"python\s+-m\s+glaip_sdk\.cli\s+config",
|
|
477
|
+
r"python\s+-m\s+glaip_sdk\.cli\.main\s+config",
|
|
478
|
+
]
|
|
479
|
+
_COMPILED_AUDIT_PATTERNS = [re.compile(pattern, re.IGNORECASE) for pattern in _AUDIT_PATTERNS]
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _collect_files_from_stdin() -> list[Path]:
|
|
483
|
+
"""Collect files to scan from stdin input.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
List of file paths read from stdin.
|
|
487
|
+
"""
|
|
488
|
+
files_to_scan: list[Path] = []
|
|
489
|
+
for line in sys.stdin:
|
|
490
|
+
line = line.strip()
|
|
491
|
+
if line:
|
|
492
|
+
try:
|
|
493
|
+
file_path = Path(line).expanduser().resolve()
|
|
494
|
+
except Exception:
|
|
495
|
+
continue
|
|
496
|
+
if file_path.exists() and file_path.is_file():
|
|
497
|
+
if _should_skip_file(file_path):
|
|
498
|
+
continue
|
|
499
|
+
files_to_scan.append(file_path)
|
|
500
|
+
return files_to_scan
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _collect_files_from_patterns(paths: tuple[str, ...]) -> list[Path]:
|
|
504
|
+
"""Collect files to scan from glob patterns.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
paths: Glob patterns to search.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
List of file paths matching the patterns.
|
|
511
|
+
"""
|
|
512
|
+
files_to_scan: list[Path] = []
|
|
513
|
+
for pattern in paths:
|
|
514
|
+
for file_path in Path.cwd().rglob(pattern):
|
|
515
|
+
if file_path.is_file() and not _should_skip_file(file_path):
|
|
516
|
+
files_to_scan.append(file_path)
|
|
517
|
+
return files_to_scan
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _collect_files_default() -> list[Path]:
|
|
521
|
+
"""Collect all files from current directory recursively.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
List of all file paths in current directory.
|
|
525
|
+
"""
|
|
526
|
+
files_to_scan: list[Path] = []
|
|
527
|
+
base = Path.cwd()
|
|
528
|
+
max_files = _resolve_audit_max_files()
|
|
529
|
+
|
|
530
|
+
for root, dirs, files in os.walk(base):
|
|
531
|
+
dirs[:] = [d for d in dirs if d not in _DEFAULT_EXCLUDE_DIRS]
|
|
532
|
+
for file in files:
|
|
533
|
+
file_path = Path(root) / file
|
|
534
|
+
if _should_skip_file(file_path):
|
|
535
|
+
continue
|
|
536
|
+
files_to_scan.append(file_path)
|
|
537
|
+
if max_files and len(files_to_scan) >= max_files:
|
|
538
|
+
_warn_scan_truncated(max_files)
|
|
539
|
+
return files_to_scan
|
|
540
|
+
|
|
541
|
+
return files_to_scan
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _resolve_audit_max_files() -> int | None:
|
|
545
|
+
"""Resolve optional scan limit from env."""
|
|
546
|
+
max_files_env = os.getenv("AIP_CONFIG_AUDIT_MAX_FILES")
|
|
547
|
+
if not max_files_env:
|
|
548
|
+
return None
|
|
549
|
+
try:
|
|
550
|
+
parsed = int(max_files_env, 10)
|
|
551
|
+
except ValueError:
|
|
552
|
+
return None
|
|
553
|
+
return parsed if parsed > 0 else None
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _warn_scan_truncated(max_files: int) -> None:
|
|
557
|
+
"""Warn when scanning is truncated to avoid surprises on huge repos."""
|
|
558
|
+
console.print(
|
|
559
|
+
f"[{WARNING_STYLE}]Scanning limited to the first {max_files} files. "
|
|
560
|
+
"Use --path to narrow the search or increase AIP_CONFIG_AUDIT_MAX_FILES to scan more.[/]"
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _collect_files_to_scan(paths: tuple[str, ...], read_from_stdin: bool) -> list[Path]:
|
|
565
|
+
"""Collect files to scan based on input method.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
paths: Glob patterns to search (if not reading from stdin).
|
|
569
|
+
read_from_stdin: Whether to read file list from stdin.
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
List of file paths to scan.
|
|
573
|
+
"""
|
|
574
|
+
if read_from_stdin:
|
|
575
|
+
return _collect_files_from_stdin()
|
|
576
|
+
if paths:
|
|
577
|
+
return _collect_files_from_patterns(paths)
|
|
578
|
+
return _collect_files_default()
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _filter_by_gitignore(files_to_scan: list[Path], no_gitignore: bool) -> list[Path]:
|
|
582
|
+
"""Filter files by .gitignore patterns if enabled.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
files_to_scan: List of file paths to filter.
|
|
586
|
+
no_gitignore: If True, skip gitignore filtering.
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
Filtered list of file paths.
|
|
590
|
+
"""
|
|
591
|
+
global _PATHSPEC_WARNED
|
|
592
|
+
if no_gitignore or pathspec is None:
|
|
593
|
+
if not no_gitignore and pathspec is None and not _PATHSPEC_WARNED:
|
|
594
|
+
msg = (
|
|
595
|
+
f"[{WARNING_STYLE}]Warning:[/] pathspec is not installed; "
|
|
596
|
+
"gitignore filtering for 'aip config audit' will be skipped."
|
|
597
|
+
)
|
|
598
|
+
with _PATHSPEC_WARNED_LOCK:
|
|
599
|
+
if not _PATHSPEC_WARNED:
|
|
600
|
+
stderr_console.print(msg)
|
|
601
|
+
_PATHSPEC_WARNED = True
|
|
602
|
+
return files_to_scan
|
|
603
|
+
|
|
604
|
+
# Load .gitignore patterns
|
|
605
|
+
gitignore_path = Path.cwd() / ".gitignore"
|
|
606
|
+
if not gitignore_path.exists():
|
|
607
|
+
return files_to_scan
|
|
608
|
+
|
|
609
|
+
try:
|
|
610
|
+
with gitignore_path.open(encoding="utf-8", errors="ignore") as f:
|
|
611
|
+
spec = pathspec.PathSpec.from_lines("gitwildmatch", f)
|
|
612
|
+
|
|
613
|
+
# Guard against files outside CWD; fallback to absolute path in that case
|
|
614
|
+
def _to_git_path(path: Path) -> str:
|
|
615
|
+
try:
|
|
616
|
+
return str(path.relative_to(Path.cwd()))
|
|
617
|
+
except ValueError:
|
|
618
|
+
return str(path)
|
|
619
|
+
|
|
620
|
+
return [path for path in files_to_scan if not spec.match_file(_to_git_path(path))]
|
|
621
|
+
except Exception:
|
|
622
|
+
# If gitignore parsing fails, return all files
|
|
623
|
+
return files_to_scan
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _should_skip_file(file_path: Path) -> bool:
|
|
627
|
+
"""Check whether a file should be skipped based on size."""
|
|
628
|
+
try:
|
|
629
|
+
return file_path.stat().st_size > _MAX_SCAN_FILE_SIZE
|
|
630
|
+
except OSError:
|
|
631
|
+
return False
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _extract_match_snippet(line: str, match_obj: re.Match[str]) -> str:
|
|
635
|
+
"""Extract a snippet around a match for display.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
line: The full line containing the match.
|
|
639
|
+
match_obj: The regex match object.
|
|
640
|
+
|
|
641
|
+
Returns:
|
|
642
|
+
A snippet of text around the match.
|
|
643
|
+
"""
|
|
644
|
+
start = max(0, match_obj.start() - 20)
|
|
645
|
+
end = min(len(line), match_obj.end() + 20)
|
|
646
|
+
return line[start:end].strip()
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _process_file_for_matches(file_path: Path, compiled_patterns: list[re.Pattern[str]]) -> list[dict[str, Any]]:
|
|
650
|
+
"""Process a single file for deprecated config command matches.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
file_path: Path to the file to scan.
|
|
654
|
+
compiled_patterns: List of compiled regex patterns to match.
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
List of hit dictionaries found in this file.
|
|
658
|
+
"""
|
|
659
|
+
hits: list[dict[str, Any]] = []
|
|
660
|
+
if _should_skip_file(file_path):
|
|
661
|
+
return hits
|
|
662
|
+
try:
|
|
663
|
+
with file_path.open(encoding="utf-8", errors="ignore") as f:
|
|
664
|
+
for line_num, line in enumerate(f, start=1):
|
|
665
|
+
for pattern in compiled_patterns:
|
|
666
|
+
match_obj = pattern.search(line)
|
|
667
|
+
if match_obj:
|
|
668
|
+
snippet = _extract_match_snippet(line, match_obj)
|
|
669
|
+
replacement = _suggest_replacement(line.strip())
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
file_str = str(file_path.relative_to(Path.cwd()))
|
|
673
|
+
except ValueError:
|
|
674
|
+
file_str = str(file_path)
|
|
675
|
+
|
|
676
|
+
hits.append(
|
|
677
|
+
{
|
|
678
|
+
"file": file_str,
|
|
679
|
+
"line": line_num,
|
|
680
|
+
"match": snippet,
|
|
681
|
+
"replacement": replacement,
|
|
682
|
+
}
|
|
683
|
+
)
|
|
684
|
+
break # Only count once per line
|
|
685
|
+
except (UnicodeDecodeError, PermissionError):
|
|
686
|
+
# Skip binary files or files we can't read
|
|
687
|
+
pass
|
|
688
|
+
except OSError:
|
|
689
|
+
# Skip files with permission errors
|
|
690
|
+
pass
|
|
691
|
+
|
|
692
|
+
return hits
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _scan_files_for_matches(files_to_scan: list[Path]) -> list[dict[str, Any]]:
|
|
696
|
+
"""Scan files for deprecated config command usage.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
files_to_scan: List of file paths to scan.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
List of hit dictionaries with file, line, match, and replacement info.
|
|
703
|
+
"""
|
|
704
|
+
hits: list[dict[str, Any]] = []
|
|
705
|
+
|
|
706
|
+
for file_path in files_to_scan:
|
|
707
|
+
file_hits = _process_file_for_matches(file_path, _COMPILED_AUDIT_PATTERNS)
|
|
708
|
+
hits.extend(file_hits)
|
|
709
|
+
|
|
710
|
+
return hits
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _output_audit_results(hits: list[dict[str, Any]], files_scanned: int, output_json: bool, silent: bool) -> None:
|
|
714
|
+
"""Output audit results in the requested format.
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
hits: List of hit dictionaries.
|
|
718
|
+
files_scanned: Number of files scanned.
|
|
719
|
+
output_json: If True, output JSON format.
|
|
720
|
+
silent: If True, suppress Rich output when using JSON.
|
|
721
|
+
"""
|
|
722
|
+
if output_json:
|
|
723
|
+
result = {
|
|
724
|
+
"hits": hits,
|
|
725
|
+
"total_hits": len(hits),
|
|
726
|
+
"files_scanned": files_scanned,
|
|
727
|
+
}
|
|
728
|
+
click.echo(json.dumps(result, indent=2))
|
|
729
|
+
return
|
|
730
|
+
|
|
731
|
+
if silent:
|
|
732
|
+
return
|
|
733
|
+
|
|
734
|
+
if hits:
|
|
735
|
+
table = AIPTable(title="⚠️ Deprecated 'aip config' Usage Found")
|
|
736
|
+
table.add_column("File", style=INFO, width=30)
|
|
737
|
+
table.add_column("Line", style=NEUTRAL, width=8)
|
|
738
|
+
table.add_column("Match", style=WARNING_STYLE, width=40)
|
|
739
|
+
table.add_column("Suggested Replacement", style=SUCCESS_STYLE, width=40)
|
|
740
|
+
|
|
741
|
+
for hit in hits:
|
|
742
|
+
table.add_row(
|
|
743
|
+
hit["file"],
|
|
744
|
+
str(hit["line"]),
|
|
745
|
+
hit["match"],
|
|
746
|
+
hit["replacement"],
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
console.print(table)
|
|
750
|
+
console.print(f"\n[{WARNING_STYLE}]Found {len(hits)} deprecated usage(s).[/]")
|
|
751
|
+
else:
|
|
752
|
+
console.print(f"[{SUCCESS_STYLE}]✅ No deprecated 'aip config' usage found.[/]")
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _suggest_replacement(line: str) -> str:
|
|
756
|
+
"""Suggest a replacement command for deprecated config usage."""
|
|
757
|
+
line_lower = line.lower()
|
|
758
|
+
|
|
759
|
+
# Map common patterns to replacements
|
|
760
|
+
if "config list" in line_lower:
|
|
761
|
+
return "aip accounts list"
|
|
762
|
+
elif "config set" in line_lower:
|
|
763
|
+
if "api_url" in line_lower or "api_key" in line_lower:
|
|
764
|
+
return "aip accounts edit <name> [--url URL] [--key]"
|
|
765
|
+
return "aip accounts edit <name>"
|
|
766
|
+
elif "config get" in line_lower:
|
|
767
|
+
return "aip accounts show <name> (or read ~/.aip/config.yaml)"
|
|
768
|
+
elif "config unset" in line_lower:
|
|
769
|
+
return "aip accounts edit <name> (to clear specific fields)"
|
|
770
|
+
elif "config reset" in line_lower:
|
|
771
|
+
return "aip accounts remove <name> (for each account)"
|
|
772
|
+
# Generic "config" usage (command-like), but avoid matching any arbitrary
|
|
773
|
+
# mention of the word "config" in unrelated text.
|
|
774
|
+
elif "aip config" in line_lower or " config " in f" {line_lower} " or CONFIG_CONFIGURE_HINT in line_lower:
|
|
775
|
+
return "aip configure or aip accounts add <name>"
|
|
776
|
+
else:
|
|
777
|
+
return "Use 'aip accounts ...' or 'aip configure'"
|
|
778
|
+
|
|
779
|
+
|
|
274
780
|
@config_group.command()
|
|
275
781
|
@click.option(
|
|
276
782
|
"--account",
|
|
@@ -283,6 +789,7 @@ def configure(account_name: str | None) -> None:
|
|
|
283
789
|
This command is an alias for 'aip accounts add <name>' and will
|
|
284
790
|
configure the specified account (or active account if not specified).
|
|
285
791
|
"""
|
|
792
|
+
_enforce_legacy_config_gate()
|
|
286
793
|
_configure_interactive(account_name)
|
|
287
794
|
|
|
288
795
|
|
|
@@ -299,10 +806,15 @@ def configure_command(account_name: str | None) -> None:
|
|
|
299
806
|
This is an alias for 'aip config configure' for backward compatibility.
|
|
300
807
|
For multi-account support, use 'aip accounts add <name>' instead.
|
|
301
808
|
"""
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
809
|
+
_enforce_legacy_config_gate()
|
|
810
|
+
suppress_tip = os.environ.get("AIP_SUPPRESS_CONFIGURE_TIP", "").strip().lower() in {"1", "true", "yes", "on"}
|
|
811
|
+
if not suppress_tip:
|
|
812
|
+
tip_prefix = f"[{WARNING_STYLE}]Setup tip:[/] "
|
|
813
|
+
tip_body = (
|
|
814
|
+
"Prefer 'aip accounts add <name>' or 'aip configure' from your terminal for multi-account setup. "
|
|
815
|
+
"Launching the interactive wizard now..."
|
|
816
|
+
)
|
|
817
|
+
console.print(f"{tip_prefix}{tip_body}")
|
|
306
818
|
# Delegate to the shared function
|
|
307
819
|
_configure_interactive(account_name)
|
|
308
820
|
|
|
@@ -311,28 +823,6 @@ def configure_command(account_name: str | None) -> None:
|
|
|
311
823
|
_mask_api_key = mask_api_key_display
|
|
312
824
|
|
|
313
825
|
|
|
314
|
-
def _print_missing_config_hint() -> None:
|
|
315
|
-
"""Show guidance when no configuration file exists."""
|
|
316
|
-
hint = command_hint("config configure", slash_command="login")
|
|
317
|
-
if hint:
|
|
318
|
-
console.print(f"[{WARNING_STYLE}]No configuration found.[/] Run {format_command_hint(hint) or hint} to set up.")
|
|
319
|
-
else:
|
|
320
|
-
console.print(f"[{WARNING_STYLE}]No configuration found.[/]")
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
def _render_config_table(config: dict[str, str]) -> None:
|
|
324
|
-
"""Render the current configuration in a friendly table."""
|
|
325
|
-
table = AIPTable(title=f"{ICON_TOOL} AIP Configuration")
|
|
326
|
-
table.add_column("Setting", style=INFO, width=20)
|
|
327
|
-
table.add_column("Value", style=SUCCESS)
|
|
328
|
-
|
|
329
|
-
for key, value in config.items():
|
|
330
|
-
table.add_row(key, _mask_api_key(value) if key == "api_key" else str(value))
|
|
331
|
-
|
|
332
|
-
console.print(table)
|
|
333
|
-
console.print(Text(f"\n📁 Config file: {CONFIG_FILE}"))
|
|
334
|
-
|
|
335
|
-
|
|
336
826
|
def _render_configuration_header() -> None:
|
|
337
827
|
"""Display the interactive configuration heading/banner."""
|
|
338
828
|
render_branding_header(console, "[bold]AIP Configuration[/bold]")
|
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -279,11 +279,8 @@ class SlashSession:
|
|
|
279
279
|
def _ensure_configuration(self) -> bool:
|
|
280
280
|
"""Ensure the CLI has both API URL and credentials before continuing."""
|
|
281
281
|
while not self._configuration_ready():
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
"Slash mode cannot run 'aip accounts ...'. Run setup from your terminal (e.g., "
|
|
285
|
-
"'aip accounts add default' or 'aip configure'), or continue with the `/login` wizard here..."
|
|
286
|
-
)
|
|
282
|
+
previous_tip_env = os.environ.get("AIP_SUPPRESS_CONFIGURE_TIP")
|
|
283
|
+
os.environ["AIP_SUPPRESS_CONFIGURE_TIP"] = "1"
|
|
287
284
|
self._suppress_login_layout = True
|
|
288
285
|
try:
|
|
289
286
|
self._cmd_login([], False)
|
|
@@ -292,6 +289,10 @@ class SlashSession:
|
|
|
292
289
|
return False
|
|
293
290
|
finally:
|
|
294
291
|
self._suppress_login_layout = False
|
|
292
|
+
if previous_tip_env is None:
|
|
293
|
+
os.environ.pop("AIP_SUPPRESS_CONFIGURE_TIP", None)
|
|
294
|
+
else:
|
|
295
|
+
os.environ["AIP_SUPPRESS_CONFIGURE_TIP"] = previous_tip_env
|
|
295
296
|
|
|
296
297
|
return True
|
|
297
298
|
|
glaip_sdk/cli/update_notifier.py
CHANGED
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import importlib
|
|
10
10
|
import logging
|
|
11
|
+
import sys
|
|
11
12
|
from collections.abc import Callable, Iterable, Iterator
|
|
12
13
|
from contextlib import contextmanager
|
|
13
14
|
from typing import Any, Literal
|
|
@@ -73,6 +74,10 @@ def _fetch_latest_version(package_name: str) -> str | None:
|
|
|
73
74
|
|
|
74
75
|
def _should_check_for_updates() -> bool:
|
|
75
76
|
"""Return False when update checks are explicitly disabled."""
|
|
77
|
+
# Check module attribute first (for test overrides), then fall back to imported constant
|
|
78
|
+
module = sys.modules.get(__name__)
|
|
79
|
+
if module and hasattr(module, "UPDATE_CHECK_ENABLED"):
|
|
80
|
+
return getattr(module, "UPDATE_CHECK_ENABLED")
|
|
76
81
|
return UPDATE_CHECK_ENABLED
|
|
77
82
|
|
|
78
83
|
|
|
@@ -6,10 +6,10 @@ glaip_sdk/cli/account_store.py,sha256=NXuAVPaJS_32Aw1VTaZCNwIID-gADw4F_UMieoWmg3
|
|
|
6
6
|
glaip_sdk/cli/agent_config.py,sha256=YAbFKrTNTRqNA6b0i0Q3pH-01rhHDRi5v8dxSFwGSwM,2401
|
|
7
7
|
glaip_sdk/cli/auth.py,sha256=9hfjZyd4cx2_mImqykJ1sWQsuVTR2gy6D4hFqAQNKL4,24129
|
|
8
8
|
glaip_sdk/cli/commands/__init__.py,sha256=6Z3ASXDut0lAbUX_umBFtxPzzFyqoiZfVeTahThFu1A,219
|
|
9
|
-
glaip_sdk/cli/commands/accounts.py,sha256=
|
|
10
|
-
glaip_sdk/cli/commands/agents.py,sha256=
|
|
9
|
+
glaip_sdk/cli/commands/accounts.py,sha256=NTexdyiv9Qp3xZMxmtwOWeCkRDHegyk06O9J3UWyXHQ,24644
|
|
10
|
+
glaip_sdk/cli/commands/agents.py,sha256=WCOzllyh_Znwlju5camT4vE6OeRJbsAmjWwcyiAqWs4,48429
|
|
11
11
|
glaip_sdk/cli/commands/common_config.py,sha256=IY13gPkeifXxSdpzRFUvfRin8J7s38p6Y7TYjdGw7w4,2474
|
|
12
|
-
glaip_sdk/cli/commands/configure.py,sha256=
|
|
12
|
+
glaip_sdk/cli/commands/configure.py,sha256=95PQiJnpvsdH02v_tLVANd64qAJJnZKlhNe4tpfWIS4,30262
|
|
13
13
|
glaip_sdk/cli/commands/mcps.py,sha256=tttqQnfM89iI9Pm94u8YRhiHMQNYNouecFX0brsT4cQ,42551
|
|
14
14
|
glaip_sdk/cli/commands/models.py,sha256=vfcGprK5CHprQ0CNpNzQlNNTELvdgKC7JxTG_ijOwmE,2009
|
|
15
15
|
glaip_sdk/cli/commands/tools.py,sha256=7_RMTuTI1Guu7psClovbyt2umfk4rkp7jSW19GXKA44,18440
|
|
@@ -33,7 +33,7 @@ glaip_sdk/cli/slash/__init__.py,sha256=J9TPL2UcNTkW8eifG6nRmAEGHhyEgdYMYk4cHaaOb
|
|
|
33
33
|
glaip_sdk/cli/slash/agent_session.py,sha256=9r1xNRk5mk6rfJXV6KIf2Yo4B4hjknimd9fkxH1LO3c,11304
|
|
34
34
|
glaip_sdk/cli/slash/prompt.py,sha256=2urqR3QqN3O09lHmKKSEbhsIdlS4B7hm9O8AP_VwCSU,8034
|
|
35
35
|
glaip_sdk/cli/slash/remote_runs_controller.py,sha256=Ok6CezIeF1CPGQ8-QN3TRx5kGGEACOrgyPwH_BRRCyI,21354
|
|
36
|
-
glaip_sdk/cli/slash/session.py,sha256=
|
|
36
|
+
glaip_sdk/cli/slash/session.py,sha256=Pl6au6zwiKYeJ1JyxQ3pVfwVrE0Lzi6ejlLtEJfYG_4,57677
|
|
37
37
|
glaip_sdk/cli/slash/tui/__init__.py,sha256=ljBAeAFY2qNDkbJrZh5NgXxjwUlsv9-UxgKNIv0AF1Q,274
|
|
38
38
|
glaip_sdk/cli/slash/tui/remote_runs_app.py,sha256=YAtBtgjtzIy5y2NVOQexZX783DJpqFUkwAVYkVn1tSo,24762
|
|
39
39
|
glaip_sdk/cli/transcript/__init__.py,sha256=yiYHyNtebMCu3BXu56Xm5RBC2tDc865q8UGPnoe6QRs,920
|
|
@@ -43,7 +43,7 @@ glaip_sdk/cli/transcript/export.py,sha256=reCvrZVzli8_LzYe5ZNdaa-MwZ1ov2RjnDzKZW
|
|
|
43
43
|
glaip_sdk/cli/transcript/history.py,sha256=2FBjawxP8CX9gRPMUMP8bDjG50BGM2j2zk6IfHvAMH4,26211
|
|
44
44
|
glaip_sdk/cli/transcript/launcher.py,sha256=z5ivkPXDQJpATIqtRLUK8jH3p3WIZ72PvOPqYRDMJvw,2327
|
|
45
45
|
glaip_sdk/cli/transcript/viewer.py,sha256=ar1SzRkhKIf3_DgFz1EG1RZGDmd2w2wogAe038DLL_M,13037
|
|
46
|
-
glaip_sdk/cli/update_notifier.py,sha256=
|
|
46
|
+
glaip_sdk/cli/update_notifier.py,sha256=FnTjzS8YT94RmP6c5aU_XNIyRi7FRHvAskMy-VJikl8,10064
|
|
47
47
|
glaip_sdk/cli/utils.py,sha256=fV6PZlQ7K5zckpFWvwh3yLmETGrVylK9AXtN7zKBp-A,57374
|
|
48
48
|
glaip_sdk/cli/validators.py,sha256=d-kq4y7HWMo6Gc7wLXWUsCt8JwFvJX_roZqRm1Nko1I,5622
|
|
49
49
|
glaip_sdk/client/__init__.py,sha256=F-eE_dRSzA0cc1it06oi0tZetZBHmSUjWSHGhJMLCls,263
|
|
@@ -107,7 +107,7 @@ glaip_sdk/utils/resource_refs.py,sha256=vF34kyAtFBLnaKnQVrsr2st1JiSxVbIZ4yq0DelJ
|
|
|
107
107
|
glaip_sdk/utils/run_renderer.py,sha256=d_VMI6LbvHPUUeRmGqh5wK_lHqDEIAcym2iqpbtDad0,1365
|
|
108
108
|
glaip_sdk/utils/serialization.py,sha256=z-qpvWLSBrGK3wbUclcA1UIKLXJedTnMSwPdq-FF4lo,13308
|
|
109
109
|
glaip_sdk/utils/validation.py,sha256=Vt8oSnn7OM6ns5vjOl5FwGIMWBPb0yI6RD5XL_L5_4M,6826
|
|
110
|
-
glaip_sdk-0.5.
|
|
111
|
-
glaip_sdk-0.5.
|
|
112
|
-
glaip_sdk-0.5.
|
|
113
|
-
glaip_sdk-0.5.
|
|
110
|
+
glaip_sdk-0.5.5.dist-info/METADATA,sha256=JN-k8loq68PUth6CcKkzfbnOYkqN5Fv6jlJhxnS4AoU,7053
|
|
111
|
+
glaip_sdk-0.5.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
112
|
+
glaip_sdk-0.5.5.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
|
|
113
|
+
glaip_sdk-0.5.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|