aidp-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aidp_cli/main.py ADDED
@@ -0,0 +1,2006 @@
1
+ # Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
2
+
3
+ from __future__ import annotations
4
+
5
+ import difflib
6
+ import inspect
7
+ import json
8
+ import os
9
+ import re
10
+ import sys
11
+ import textwrap
12
+ import uuid
13
+ import warnings
14
+ from importlib.metadata import PackageNotFoundError, version as package_version
15
+ from json import JSONDecodeError
16
+ from pathlib import Path
17
+ from types import SimpleNamespace
18
+ from typing import Any, Callable
19
+ from urllib.parse import quote
20
+
21
+ import oci
22
+ from oci.auth.signers import SecurityTokenSigner
23
+ from oci.exceptions import ConfigFileNotFound, InvalidConfig, RequestException, ServiceError
24
+ from oci.signer import Signer, load_private_key_from_file
25
+ from oci.util import to_dict
26
+ from oci._vendor.requests.exceptions import RequestException as VendorRequestException
27
+
28
+ from aidp_cli.body_security import (
29
+ body_contains_sensitive_field,
30
+ command_has_sensitive_body_fields,
31
+ json_body_argument_source,
32
+ )
33
+ from aidp_cli.discovery import discover_clients
34
+ from aidp_cli.manifest import (
35
+ BodyField,
36
+ BodyModel,
37
+ CommandDefinition,
38
+ CommandField,
39
+ CommandGroup,
40
+ CommandManifest,
41
+ find_command,
42
+ find_command_group,
43
+ load_command_manifest,
44
+ )
45
+
46
+
47
+ warnings.filterwarnings(
48
+ "ignore",
49
+ message=r"The 'strict' parameter is no longer needed on Python 3\+.",
50
+ category=FutureWarning,
51
+ )
52
+
53
+ DEFAULT_PROFILE = "DEFAULT"
54
+ DEFAULT_AUTH = "security_token"
55
+ PACKAGE_NAME = "aidp-cli"
56
+ DEFAULT_ENVIRONMENT_PREFIX = "aidp"
57
+ DEFAULT_ENVIRONMENT_DOMAIN = "oraclecloud.com"
58
+ MAX_BODY_EXAMPLE_DEPTH = 12
59
+ AUTH_CHOICES = ("api_key", "security_token", "instance_principal", "resource_principal")
60
+ SEARCH_ACTION_PREFIXES = (
61
+ "checkout",
62
+ "commit",
63
+ "create",
64
+ "delete",
65
+ "deploy",
66
+ "download",
67
+ "export",
68
+ "generate",
69
+ "head",
70
+ "list",
71
+ "manage",
72
+ "merge",
73
+ "patch",
74
+ "pull",
75
+ "push",
76
+ "rebase",
77
+ "remove",
78
+ "restart",
79
+ "restore",
80
+ "search",
81
+ "start",
82
+ "stop",
83
+ "summarize",
84
+ "update",
85
+ "upload",
86
+ "get",
87
+ )
88
+ CONFIG_ENV_OVERRIDES = {
89
+ "OCI_CLI_USER": "user",
90
+ "OCI_CLI_FINGERPRINT": "fingerprint",
91
+ "OCI_CLI_KEY_FILE": "key_file",
92
+ "OCI_CLI_KEY_CONTENT": "key_content",
93
+ "OCI_CLI_TENANCY": "tenancy",
94
+ "OCI_CLI_PASSPHRASE": "pass_phrase",
95
+ "OCI_CLI_SECURITY_TOKEN_FILE": "security_token_file",
96
+ }
97
+ GLOBAL_VALUE_OPTIONS = {
98
+ "--config-file": "config_file",
99
+ "--profile": "profile",
100
+ "-p": "profile",
101
+ "--auth": "auth",
102
+ "--region": "region",
103
+ "--endpoint": "endpoint",
104
+ "--environment-prefix": "environment_prefix",
105
+ "--environmentprefix": "environment_prefix",
106
+ "--environment-domain": "environment_domain",
107
+ "--environment-host": "environment_host",
108
+ "--timeout": "timeout",
109
+ "--instance-id": "ai_data_platform_id",
110
+ }
111
+ GLOBAL_BOOLEAN_OPTIONS = {
112
+ "--debug": "debug",
113
+ }
114
+ SKIPPED_COMMAND_FIELDS = {"opc_request_id"}
115
+ STRICT_URL_ENCODING_CLIENTS = {"NotebookClient"}
116
+ UTILITY_COMMANDS = (
117
+ ("command-groups", "List API command groups."),
118
+ ("search", "Search command groups and command names."),
119
+ ("configure", "Show AIDP CLI environment defaults."),
120
+ ("help", "Help about any command."),
121
+ ("version", "Show CLI version."),
122
+ )
123
+ CLIENTS: dict[str, type] = {}
124
+ MANIFEST: CommandManifest | None = None
125
+ AIDP_DP_MODULE: Any = None
126
+
127
+
128
+ class CliError(Exception):
129
+ pass
130
+
131
+
132
+ def main(argv: list[str] | None = None) -> int:
133
+ raw_argv = sys.argv[1:] if argv is None else argv
134
+ try:
135
+ globals_ns, args, help_requested, version_requested = parse_global_options(raw_argv)
136
+ if version_requested:
137
+ print_version()
138
+ return 0
139
+ ensure_manifest_loaded()
140
+ if not args:
141
+ print_root_help()
142
+ return 0
143
+ return dispatch(args, globals_ns, help_requested)
144
+ except CliError as exc:
145
+ print(f"aidp: error: {exc}", file=sys.stderr)
146
+ return 2
147
+ except (ConfigFileNotFound, InvalidConfig) as exc:
148
+ print(f"aidp: error: OCI config error: {exc}", file=sys.stderr)
149
+ return 2
150
+ except FileNotFoundError as exc:
151
+ print(f"aidp: error: file not found: {exc.filename}", file=sys.stderr)
152
+ return 2
153
+ except PermissionError as exc:
154
+ print(f"aidp: error: permission denied: {exc.filename}", file=sys.stderr)
155
+ return 2
156
+ except JSONDecodeError as exc:
157
+ print(f"aidp: error: invalid JSON: {exc}", file=sys.stderr)
158
+ return 2
159
+ except VendorRequestException as exc:
160
+ print(f"aidp: error: request failed: {exc}", file=sys.stderr)
161
+ return 1
162
+ except RequestException as exc:
163
+ print(f"aidp: error: request failed: {exc}", file=sys.stderr)
164
+ return 1
165
+ except OSError as exc:
166
+ print(f"aidp: error: file error: {exc}", file=sys.stderr)
167
+ return 2
168
+ except ValueError as exc:
169
+ print(f"aidp: error: {exc}", file=sys.stderr)
170
+ return 2
171
+ except ServiceError as exc:
172
+ print_error_response(
173
+ {
174
+ "status": exc.status,
175
+ "code": exc.code,
176
+ "message": exc.message,
177
+ "opc-request-id": exc.request_id,
178
+ }
179
+ )
180
+ return int(exc.status or 1)
181
+
182
+
183
+ def ensure_manifest_loaded() -> None:
184
+ global MANIFEST
185
+ if MANIFEST is not None:
186
+ return
187
+ try:
188
+ MANIFEST = load_command_manifest()
189
+ except RuntimeError as exc:
190
+ raise CliError(str(exc)) from exc
191
+
192
+
193
+ def ensure_clients_loaded() -> None:
194
+ global AIDP_DP_MODULE, CLIENTS
195
+ if AIDP_DP_MODULE is not None:
196
+ return
197
+ try:
198
+ from aidp_python_client import aidataplatform_dp as sdk_module
199
+ except ImportError as exc:
200
+ raise CliError(
201
+ f"Unable to import aidp-python-client SDK: {exc}. Install the generated SDK wheel with aidp-cli."
202
+ ) from exc
203
+ AIDP_DP_MODULE = sdk_module
204
+ CLIENTS = discover_clients(sdk_module)
205
+
206
+
207
+ def command_manifest() -> CommandManifest:
208
+ if MANIFEST is None:
209
+ raise CliError("AIDP command manifest is not loaded.")
210
+ return MANIFEST
211
+
212
+
213
+ def dispatch(args: list[str], globals_ns: SimpleNamespace, help_requested: bool) -> int:
214
+ command = args[0]
215
+ tail = args[1:]
216
+
217
+ if command == "help":
218
+ return handle_help(tail, globals_ns)
219
+ if command == "version":
220
+ print_version()
221
+ return 0
222
+ if command == "command-groups":
223
+ if help_requested:
224
+ print_command_groups_help()
225
+ else:
226
+ print_command_groups()
227
+ return 0
228
+ if command == "search":
229
+ if help_requested or not tail:
230
+ print_search_help()
231
+ else:
232
+ handle_search(tail)
233
+ return 0
234
+ if command == "configure":
235
+ if help_requested or not tail:
236
+ print_configure_help()
237
+ else:
238
+ handle_configure(tail)
239
+ return 0
240
+
241
+ group = find_command_group(command_manifest(), command)
242
+ if group is None:
243
+ raise unknown_command_group(command)
244
+ globals_ns, tail = consume_leading_global_options(tail, globals_ns)
245
+ if not tail:
246
+ print_group_help(group)
247
+ return 0
248
+
249
+ command_name = tail[0]
250
+ command_def = find_command(group, command_name)
251
+ if command_def is None:
252
+ raise unknown_command_name(group, command_name)
253
+ if help_requested:
254
+ print_command_help(group, command_def)
255
+ return 0
256
+ return handle_command(group, command_def, tail[1:], globals_ns)
257
+
258
+
259
+ def handle_help(args: list[str], globals_ns: SimpleNamespace) -> int:
260
+ if not args:
261
+ print_root_help()
262
+ return 0
263
+ group = find_command_group(command_manifest(), args[0])
264
+ if group is None:
265
+ if args[0] == "command-groups":
266
+ print_command_groups_help()
267
+ return 0
268
+ if args[0] == "search":
269
+ print_search_help()
270
+ return 0
271
+ if args[0] == "configure":
272
+ print_configure_help()
273
+ return 0
274
+ if args[0] == "version":
275
+ print_version()
276
+ return 0
277
+ raise unknown_command_group(args[0])
278
+ if len(args) == 1:
279
+ print_group_help(group)
280
+ return 0
281
+ command_def = find_command(group, args[1])
282
+ if command_def is None:
283
+ raise unknown_command_name(group, args[1])
284
+ print_command_help(group, command_def)
285
+ return 0
286
+
287
+
288
+ def parse_global_options(argv: list[str]) -> tuple[SimpleNamespace, list[str], bool, bool]:
289
+ values = global_defaults()
290
+ remaining: list[str] = []
291
+ help_requested = False
292
+ version_requested = False
293
+ index = 0
294
+ command_seen = False
295
+
296
+ while index < len(argv):
297
+ token = argv[index]
298
+ if token in {"-h", "--help"}:
299
+ help_requested = True
300
+ index += 1
301
+ continue
302
+ if token in {"-v", "--version"}:
303
+ version_requested = True
304
+ index += 1
305
+ continue
306
+ if not command_seen and token in GLOBAL_BOOLEAN_OPTIONS:
307
+ values[GLOBAL_BOOLEAN_OPTIONS[token]] = True
308
+ index += 1
309
+ continue
310
+
311
+ option_name, inline_value = split_inline_option(token)
312
+ if not command_seen and option_name in GLOBAL_VALUE_OPTIONS:
313
+ value = inline_value
314
+ if value is None:
315
+ if index + 1 >= len(argv):
316
+ raise CliError(f"{option_name} requires a value.")
317
+ value = argv[index + 1]
318
+ index += 1
319
+ apply_global_option(values, option_name, value)
320
+ index += 1
321
+ continue
322
+ if not command_seen and token.startswith("-"):
323
+ raise CliError(f"Unknown option {option_name!r}.")
324
+
325
+ remaining.append(token)
326
+ if not token.startswith("-"):
327
+ command_seen = True
328
+ index += 1
329
+
330
+ validate_global_options(values)
331
+ return SimpleNamespace(**values), remaining, help_requested, version_requested
332
+
333
+
334
+ def validate_global_options(values: dict[str, Any]) -> None:
335
+ if values["auth"] not in AUTH_CHOICES:
336
+ raise CliError(
337
+ f"argument --auth: invalid choice: {values['auth']!r} "
338
+ f"(choose from {', '.join(AUTH_CHOICES)})"
339
+ )
340
+
341
+
342
+ def consume_leading_global_options(
343
+ tokens: list[str],
344
+ globals_ns: SimpleNamespace,
345
+ ) -> tuple[SimpleNamespace, list[str]]:
346
+ values = vars(globals_ns).copy()
347
+ remaining: list[str] = []
348
+ index = 0
349
+
350
+ while index < len(tokens):
351
+ token = tokens[index]
352
+ if token in GLOBAL_BOOLEAN_OPTIONS:
353
+ values[GLOBAL_BOOLEAN_OPTIONS[token]] = True
354
+ index += 1
355
+ continue
356
+
357
+ option_name, inline_value = split_inline_option(token)
358
+ if option_name in GLOBAL_VALUE_OPTIONS:
359
+ value = inline_value
360
+ if value is None:
361
+ if index + 1 >= len(tokens):
362
+ raise CliError(f"{option_name} requires a value.")
363
+ value = tokens[index + 1]
364
+ index += 1
365
+ apply_global_option(values, option_name, value)
366
+ index += 1
367
+ continue
368
+
369
+ remaining.extend(tokens[index:])
370
+ break
371
+
372
+ validate_global_options(values)
373
+ return SimpleNamespace(**values), remaining
374
+
375
+
376
+ def global_defaults() -> dict[str, Any]:
377
+ return {
378
+ "config_file": os.getenv("OCI_CLI_CONFIG_FILE", "~/.oci/config"),
379
+ "profile": os.getenv("OCI_CLI_PROFILE", DEFAULT_PROFILE),
380
+ "auth": os.getenv("OCI_CLI_AUTH", DEFAULT_AUTH),
381
+ "region": os.getenv("OCI_CLI_REGION"),
382
+ "endpoint": os.getenv("AIDP_ENDPOINT") or os.getenv("OCI_CLI_ENDPOINT"),
383
+ "environment_prefix": DEFAULT_ENVIRONMENT_PREFIX,
384
+ "environment_domain": DEFAULT_ENVIRONMENT_DOMAIN,
385
+ "environment_host": None,
386
+ "timeout": None,
387
+ "ai_data_platform_id": configured_instance_id(),
388
+ "debug": False,
389
+ }
390
+
391
+
392
+ def apply_global_option(values: dict[str, Any], option_name: str, value: str) -> None:
393
+ key = GLOBAL_VALUE_OPTIONS[option_name]
394
+ if key == "timeout":
395
+ try:
396
+ timeout = float(value)
397
+ except ValueError as exc:
398
+ raise CliError("--timeout must be a positive number of seconds.") from exc
399
+ if timeout <= 0:
400
+ raise CliError("--timeout must be a positive number of seconds.")
401
+ values[key] = timeout
402
+ return
403
+ values[key] = value
404
+
405
+
406
+ def split_inline_option(token: str) -> tuple[str, str | None]:
407
+ if "=" not in token:
408
+ return token, None
409
+ name, value = token.split("=", 1)
410
+ return name, value
411
+
412
+
413
+ def print_root_help() -> None:
414
+ lines = [
415
+ "AIDP CLI",
416
+ "",
417
+ "Usage:",
418
+ " aidp <command-group> <command-name> [flags]",
419
+ "",
420
+ "API Command Groups:",
421
+ *format_table((group.name, group.description) for group in command_manifest().command_groups),
422
+ "",
423
+ "Utility Commands:",
424
+ *format_table(UTILITY_COMMANDS),
425
+ "",
426
+ "Flags:",
427
+ *format_flags(root_flag_rows()),
428
+ "",
429
+ 'Use "aidp <command-group> --help" for more information about a command group.',
430
+ ]
431
+ print("\n".join(lines))
432
+
433
+
434
+ def print_command_groups_help() -> None:
435
+ lines = [
436
+ "List AIDP API command groups.",
437
+ "",
438
+ "Usage:",
439
+ " aidp command-groups [flags]",
440
+ "",
441
+ "Flags:",
442
+ *format_flags([("-h, --help", "help for command-groups")]),
443
+ "",
444
+ "Global Flags:",
445
+ *format_flags(global_flag_rows()),
446
+ ]
447
+ print("\n".join(lines))
448
+
449
+
450
+ def print_command_groups() -> None:
451
+ rows = [
452
+ (group.name, group.description)
453
+ for group in command_manifest().command_groups
454
+ ]
455
+ print("Command Groups:")
456
+ print("\n".join(format_table(rows)))
457
+ print()
458
+ print('Use "aidp <command-group> --help" for command names and examples.')
459
+ example = runnable_command_groups_example()
460
+ if example:
461
+ print()
462
+ print("Example:")
463
+ print(example)
464
+
465
+
466
+ def print_group_help(group: CommandGroup) -> None:
467
+ lines = [
468
+ group.description,
469
+ "",
470
+ "Usage:",
471
+ f" aidp {group.name} [flags]",
472
+ f" aidp {group.name} <command-name> [arguments] [flags]",
473
+ "",
474
+ ]
475
+ for section, commands in grouped_commands(group).items():
476
+ lines.append(section + ":")
477
+ lines.extend(
478
+ format_table((command.name, command_summary(command)) for command in commands)
479
+ )
480
+ lines.append("")
481
+ lines.extend(
482
+ [
483
+ "Flags:",
484
+ *format_flags([("-h, --help", f"help for {group.name}")]),
485
+ "",
486
+ "Global Flags:",
487
+ *format_flags(global_flag_rows()),
488
+ "",
489
+ f'Use "aidp {group.name} <command-name> --help" for more information about a command.',
490
+ ]
491
+ )
492
+ print("\n".join(lines))
493
+
494
+
495
+ def print_command_help(group: CommandGroup, command: CommandDefinition) -> None:
496
+ description = command_description(command)
497
+ lines = []
498
+ if description:
499
+ lines.extend(wrap_paragraph(description))
500
+ lines.append("")
501
+
502
+ argument_fields = command_argument_fields(command)
503
+ usage_args = " ".join(argument_metavar(field) for field in argument_fields)
504
+ usage_args = f" {usage_args}" if usage_args else ""
505
+ lines.extend(
506
+ [
507
+ "Usage:",
508
+ f" aidp {group.name} {command.name}{usage_args} [flags]",
509
+ "",
510
+ ]
511
+ )
512
+
513
+ if argument_fields:
514
+ lines.append("Arguments:")
515
+ for field in argument_fields:
516
+ lines.extend(format_argument(field))
517
+ lines.append("")
518
+
519
+ lines.append("Flags:")
520
+ lines.extend(format_flags(command_flag_rows(command)))
521
+ lines.append("")
522
+ lines.append("Global Flags:")
523
+ lines.extend(format_flags(global_flag_rows()))
524
+ lines.append("")
525
+ lines.append("Examples:")
526
+ lines.extend(format_examples(group, command))
527
+
528
+ body_sample = example_body(command)
529
+ if body_sample is not None:
530
+ lines.extend(["", "Example JSON:", json.dumps(body_sample, indent=2)])
531
+ body_variants = root_body_variant_examples(command)
532
+ if body_variants:
533
+ lines.extend(["", "Body variants:"])
534
+ for label, sample in body_variants:
535
+ lines.extend(["", f"Example JSON - {label}:", json.dumps(sample, indent=2)])
536
+ nested_variants = nested_body_variant_examples(command)
537
+ if nested_variants:
538
+ lines.extend(["", "Nested body variants:"])
539
+ for path, label, sample in nested_variants:
540
+ lines.extend(["", f"Example JSON for {path} - {label}:", json.dumps(sample, indent=2)])
541
+ required_fields = body_required_fields(command)
542
+ if required_fields:
543
+ lines.extend(["", "Required JSON fields:"])
544
+ lines.extend(f" {field}" for field in required_fields)
545
+ enum_fields = body_enum_fields(command)
546
+ if enum_fields:
547
+ lines.extend(["", "Allowed JSON values:"])
548
+ for field, values in sorted(enum_fields.items()):
549
+ lines.append(f" {field}: {', '.join(str(value) for value in values)}")
550
+ print("\n".join(lines))
551
+
552
+
553
+ def grouped_commands(group: CommandGroup) -> dict[str, list[CommandDefinition]]:
554
+ sections: dict[str, list[CommandDefinition]] = {}
555
+ for command in group.commands:
556
+ sections.setdefault(command.section or "Available Commands", []).append(command)
557
+ if "Available Commands" in sections:
558
+ ordered = {"Available Commands": sections.pop("Available Commands")}
559
+ ordered.update(dict(sorted(sections.items())))
560
+ return ordered
561
+ return dict(sorted(sections.items()))
562
+
563
+
564
+ def command_summary(command: CommandDefinition) -> str:
565
+ summary = command.summary or command.description
566
+ return first_sentence(summary)
567
+
568
+
569
+ def command_description(command: CommandDefinition) -> str:
570
+ description = command.description or command.summary
571
+ if not description:
572
+ return ""
573
+ description = description if description.endswith((".", "!", "?")) else f"{description}."
574
+ if command.deprecated and not description.lower().startswith("deprecated"):
575
+ description = f"Deprecated. {description}"
576
+ return description
577
+
578
+
579
+ def first_sentence(value: str) -> str:
580
+ cleaned = " ".join(str(value or "").split())
581
+ if not cleaned:
582
+ return ""
583
+ for delimiter in (". ", "! ", "? "):
584
+ if delimiter in cleaned:
585
+ return cleaned.split(delimiter, 1)[0].rstrip(".!?") + "."
586
+ return cleaned if cleaned.endswith((".", "!", "?")) else f"{cleaned}."
587
+
588
+
589
+ def command_argument_fields(command: CommandDefinition) -> list[CommandField]:
590
+ return [
591
+ field
592
+ for field in command.fields
593
+ if field.location == "path" and field.required and field.name != "ai_data_platform_id"
594
+ ]
595
+
596
+
597
+ def command_option_fields(command: CommandDefinition) -> list[CommandField]:
598
+ argument_names = {field.name for field in command_argument_fields(command)}
599
+ return [
600
+ field
601
+ for field in command.fields
602
+ if field.location != "body"
603
+ and field.name not in SKIPPED_COMMAND_FIELDS
604
+ and field.name != "ai_data_platform_id"
605
+ and field.name not in argument_names
606
+ ]
607
+
608
+
609
+ def command_flag_rows(command: CommandDefinition) -> list[tuple[str, str]]:
610
+ rows = [("-h, --help", f"help for {command.name}")]
611
+ for field in command_option_fields(command):
612
+ rows.append((f"--{field.cli_name}", field_help(field)))
613
+ if command.body_field is not None:
614
+ body_help = (
615
+ "inline string, @path/to/file, file:///path, or - for stdin"
616
+ if command_uses_raw_body(command)
617
+ else (
618
+ "@path/to/file.json, file:///path/request.json, or - for stdin; inline JSON is blocked when it contains sensitive fields"
619
+ if command_has_sensitive_body_fields(command)
620
+ else "inline JSON string, @path/to/file.json, file:///path/request.json, or - for stdin"
621
+ )
622
+ )
623
+ rows.append(
624
+ (
625
+ "--body",
626
+ body_help,
627
+ )
628
+ )
629
+ rows.extend(
630
+ [
631
+ ("--opc-request-id", "request ID; generated automatically when omitted"),
632
+ ("--no-request-id", "do not add opc_request_id automatically"),
633
+ ]
634
+ )
635
+ return rows
636
+
637
+
638
+ def root_flag_rows() -> list[tuple[str, str]]:
639
+ return [
640
+ ("--debug", "enable debug logging"),
641
+ ("-h, --help", "help for aidp"),
642
+ ("-p, --profile", "OCI config profile; the default profile: DEFAULT"),
643
+ ("--auth", "OCI authentication mode; options: api_key, security_token, instance_principal, resource_principal; default: security_token"),
644
+ ("--config-file", "OCI config file path"),
645
+ ("--region", "OCI region"),
646
+ ("--endpoint", "AIDP data plane endpoint override; default endpoint points to https://aidp.<region>.oci.oraclecloud.com"),
647
+ ("--instance-id", "AIDP instance OCID"),
648
+ ("--timeout", "connection/read timeout in seconds"),
649
+ ("-v, --version", "version for aidp"),
650
+ ]
651
+
652
+
653
+ def global_flag_rows() -> list[tuple[str, str]]:
654
+ return [
655
+ ("--debug", "enable debug logging"),
656
+ ("-p, --profile", "OCI config profile; the default profile: DEFAULT"),
657
+ ("--auth", "OCI authentication mode; options: api_key, security_token, instance_principal, resource_principal; default: security_token"),
658
+ ("--config-file", "OCI config file path"),
659
+ ("--region", "OCI region"),
660
+ ("--endpoint", "AIDP data plane endpoint override; default endpoint points to https://aidp.<region>.oci.oraclecloud.com"),
661
+ ("--instance-id", "AIDP instance OCID"),
662
+ ("--timeout", "connection/read timeout in seconds"),
663
+ ]
664
+
665
+
666
+ def format_table(rows: Any, indent: int = 2, gap: int = 2) -> list[str]:
667
+ rows = [(name, description) for name, description in rows]
668
+ if not rows:
669
+ return []
670
+ width = max(len(name) for name, _description in rows)
671
+ formatted = []
672
+ for name, description in rows:
673
+ padding = " " * max(gap, width - len(name) + gap)
674
+ formatted.append(f"{' ' * indent}{name}{padding}{description}")
675
+ return formatted
676
+
677
+
678
+ def format_flags(rows: list[tuple[str, str]]) -> list[str]:
679
+ return format_table(rows, indent=2, gap=2)
680
+
681
+
682
+ def wrap_paragraph(value: str, indent: int = 0) -> list[str]:
683
+ prefix = " " * indent
684
+ return textwrap.wrap(value, width=88, initial_indent=prefix, subsequent_indent=prefix) or [prefix]
685
+
686
+
687
+ def format_argument(field: CommandField) -> list[str]:
688
+ label = f" {argument_metavar(field)}:"
689
+ description = field.description or "Required command argument."
690
+ wrapped = textwrap.wrap(description, width=78, initial_indent=label + " ", subsequent_indent=" " * len(label) + " ")
691
+ return wrapped or [label]
692
+
693
+
694
+ def field_help(field: CommandField) -> str:
695
+ parts = []
696
+ description = field.description
697
+ if field.enum_values:
698
+ description = strip_allowed_values_description(description)
699
+ if description:
700
+ parts.append(description.rstrip(".") + ".")
701
+ if field.required:
702
+ parts.append("Required.")
703
+ if field.enum_values:
704
+ parts.append("Allowed values: " + ", ".join(str(value) for value in field.enum_values) + ".")
705
+ return " ".join(parts) if parts else ("Required." if field.required else "")
706
+
707
+
708
+ def strip_allowed_values_description(value: str) -> str:
709
+ return re.sub(
710
+ r"\s*Allowed values(?:\s+are)?:\s*[^.]+\.?$",
711
+ "",
712
+ value.strip(),
713
+ flags=re.IGNORECASE,
714
+ ).strip()
715
+
716
+
717
+ def argument_metavar(field: CommandField) -> str:
718
+ return field.cli_name.replace("-", "_").upper()
719
+
720
+
721
+ def format_examples(group: CommandGroup, command: CommandDefinition) -> list[str]:
722
+ base = f" aidp {group.name} {command.name}"
723
+ for field in command_argument_fields(command):
724
+ base += f" <{field.cli_name.replace('-', '_')}>"
725
+ if command_requires_instance_id(command):
726
+ base += " --instance-id <ocid>"
727
+ if command.body_field is not None:
728
+ base += " --body @request.json"
729
+ return [base]
730
+
731
+
732
+ def runnable_command_groups_example() -> str:
733
+ candidates: list[tuple[tuple[int, int, int, str, str], CommandGroup, CommandDefinition]] = []
734
+ for group in command_manifest().command_groups:
735
+ for command in group.commands:
736
+ argument_count = len(command_argument_fields(command))
737
+ body_count = 1 if command.body_field is not None else 0
738
+ action_rank = 0 if command.name.startswith("list-") else 1
739
+ candidates.append(
740
+ (
741
+ (body_count, argument_count, action_rank, group.name, command.name),
742
+ group,
743
+ command,
744
+ )
745
+ )
746
+
747
+ if not candidates:
748
+ return ""
749
+ _rank, group, command = min(candidates, key=lambda candidate: candidate[0])
750
+ return format_examples(group, command)[0]
751
+
752
+
753
+ def command_requires_instance_id(command: CommandDefinition) -> bool:
754
+ return any(field.name == "ai_data_platform_id" and field.required for field in command.fields)
755
+
756
+
757
+ def handle_command(
758
+ group: CommandGroup,
759
+ command: CommandDefinition,
760
+ tokens: list[str],
761
+ globals_ns: SimpleNamespace,
762
+ ) -> int:
763
+ try:
764
+ invocation = parse_command_options(group, command, tokens, globals_ns)
765
+ except CliError as exc:
766
+ raise with_command_usage_hint(group, command, exc) from exc
767
+
768
+ ensure_clients_loaded()
769
+ client_cls = CLIENTS.get(group.name)
770
+ if client_cls is None:
771
+ client_cls = getattr(AIDP_DP_MODULE, group.client_class_name, None)
772
+ if client_cls is None:
773
+ raise CliError(f"Unknown command group {group.name!r}. Run 'aidp command-groups'.")
774
+ sdk_method = getattr(client_cls, command.sdk_method_name, None)
775
+ if sdk_method is None:
776
+ raise CliError(f"{group.name} has no command-name {command.name!r}.")
777
+
778
+ try:
779
+ call_args, call_kwargs = bind_command_args(
780
+ sdk_method=sdk_method,
781
+ params=invocation.params,
782
+ body=invocation.body,
783
+ body_param_name=command.body_field.name if command.body_field else None,
784
+ )
785
+ except CliError as exc:
786
+ raise with_command_usage_hint(group, command, exc) from exc
787
+ add_request_id(sdk_method, command.name, call_kwargs, invocation)
788
+ client = build_client(client_cls, invocation)
789
+ if invocation.debug:
790
+ enable_request_debug(client, group.name, command.name)
791
+ try:
792
+ response = sdk_method(client, *call_args, **call_kwargs)
793
+ print_response(response)
794
+ finally:
795
+ close_client(client)
796
+ return 0
797
+
798
+
799
+ def parse_command_options(
800
+ group: CommandGroup,
801
+ command: CommandDefinition,
802
+ tokens: list[str],
803
+ globals_ns: SimpleNamespace,
804
+ ) -> SimpleNamespace:
805
+ params: dict[str, Any] = {}
806
+ body = None
807
+ opc_request_id = None
808
+ no_request_id = False
809
+ positionals: list[str] = []
810
+ option_fields = {f"--{field.cli_name}": field for field in command_option_fields(command)}
811
+ global_values = vars(globals_ns).copy()
812
+ index = 0
813
+
814
+ while index < len(tokens):
815
+ token = tokens[index]
816
+ option_name, inline_value = split_inline_option(token)
817
+ if option_name == "--body":
818
+ if command.body_field is None:
819
+ raise command_usage_error(group, command, f"{command.name} does not accept --body.")
820
+ value, index = command_option_value(tokens, index, option_name, inline_value)
821
+ body = load_body_input(command, value)
822
+ if (
823
+ not command_uses_raw_body(command)
824
+ and json_body_argument_source(value) == "inline"
825
+ and body_contains_sensitive_field(command, body)
826
+ ):
827
+ raise command_usage_error(
828
+ group,
829
+ command,
830
+ "Inline --body JSON is blocked because this request body contains sensitive fields. "
831
+ "Use --body @request.json, --body file:///path/request.json, or --body -.",
832
+ )
833
+ continue
834
+ if option_name == "--opc-request-id":
835
+ opc_request_id, index = command_option_value(tokens, index, option_name, inline_value)
836
+ continue
837
+ if option_name == "--no-request-id":
838
+ no_request_id = True
839
+ index += 1
840
+ continue
841
+ if option_name in option_fields:
842
+ field = option_fields[option_name]
843
+ value, index = field_option_value(tokens, index, option_name, inline_value, field)
844
+ params[field.name] = value if isinstance(value, bool) else parse_value(str(value))
845
+ continue
846
+ if option_name in GLOBAL_VALUE_OPTIONS:
847
+ value, index = command_option_value(tokens, index, option_name, inline_value)
848
+ apply_global_option(global_values, option_name, value)
849
+ continue
850
+ if option_name in GLOBAL_BOOLEAN_OPTIONS:
851
+ global_values[GLOBAL_BOOLEAN_OPTIONS[option_name]] = True
852
+ index += 1
853
+ continue
854
+ if token.startswith("-"):
855
+ raise unknown_option(group, command, option_name, option_fields)
856
+ positionals.append(token)
857
+ index += 1
858
+
859
+ validate_global_options(global_values)
860
+
861
+ argument_fields = command_argument_fields(command)
862
+ if len(positionals) < len(argument_fields):
863
+ missing = argument_metavar(argument_fields[len(positionals)])
864
+ raise command_usage_error(group, command, f"Missing required argument {missing}.")
865
+ if len(positionals) > len(argument_fields):
866
+ raise command_usage_error(group, command, f"Unexpected argument {positionals[len(argument_fields)]!r}.")
867
+ for field, value in zip(argument_fields, positionals):
868
+ params[field.name] = parse_value(value)
869
+
870
+ for field in command_option_fields(command):
871
+ if field.required and field.name not in params:
872
+ raise command_usage_error(group, command, f"Missing required flag --{field.cli_name}.")
873
+ if command.body_field is not None and command.body_field.required and body is None:
874
+ raise command_usage_error(group, command, "Missing required flag --body.")
875
+ if command_requires_instance_id(command):
876
+ if not global_values["ai_data_platform_id"]:
877
+ raise command_usage_error(
878
+ group,
879
+ command,
880
+ "Missing AIDP instance OCID. Set --instance-id or AIDP_INSTANCE_ID.",
881
+ )
882
+ params["ai_data_platform_id"] = global_values["ai_data_platform_id"]
883
+
884
+ values = global_values.copy()
885
+ values.update(
886
+ {
887
+ "params": params,
888
+ "body": body,
889
+ "opc_request_id": opc_request_id,
890
+ "no_request_id": no_request_id,
891
+ }
892
+ )
893
+ return SimpleNamespace(**values)
894
+
895
+
896
+ def command_usage_error(group: CommandGroup, command: CommandDefinition, message: str) -> CliError:
897
+ return CliError(
898
+ f'{message}\n\nUse "aidp {group.name} {command.name} -h" '
899
+ f'or "aidp {group.name} {command.name} --help" for command help.'
900
+ )
901
+
902
+
903
+ def with_command_usage_hint(group: CommandGroup, command: CommandDefinition, error: CliError) -> CliError:
904
+ message = str(error)
905
+ if "\n\nUse " in message:
906
+ return error
907
+ return command_usage_error(group, command, message)
908
+
909
+
910
+ def command_option_value(
911
+ tokens: list[str],
912
+ index: int,
913
+ option_name: str,
914
+ inline_value: str | None,
915
+ ) -> tuple[str, int]:
916
+ if inline_value is not None:
917
+ return inline_value, index + 1
918
+ if index + 1 >= len(tokens):
919
+ raise CliError(f"{option_name} requires a value.")
920
+ return tokens[index + 1], index + 2
921
+
922
+
923
+ def field_option_value(
924
+ tokens: list[str],
925
+ index: int,
926
+ option_name: str,
927
+ inline_value: str | None,
928
+ field: CommandField,
929
+ ) -> tuple[Any, int]:
930
+ if inline_value is not None:
931
+ return inline_value, index + 1
932
+ if field.type_name in {"boolean", "bool"}:
933
+ return True, index + 1
934
+ return command_option_value(tokens, index, option_name, inline_value)
935
+
936
+
937
+ def bind_command_args(
938
+ sdk_method: Callable[..., Any],
939
+ params: dict[str, Any],
940
+ body: Any,
941
+ body_param_name: str | None,
942
+ ) -> tuple[list[Any], dict[str, Any]]:
943
+ signature = inspect.signature(sdk_method)
944
+ positional: list[Any] = []
945
+ kwargs: dict[str, Any] = {}
946
+ body_consumed = False
947
+
948
+ for parameter in signature.parameters.values():
949
+ if parameter.name == "self":
950
+ continue
951
+ if parameter.kind == inspect.Parameter.VAR_KEYWORD:
952
+ kwargs.update(params)
953
+ params = {}
954
+ continue
955
+
956
+ value_present = parameter.name in params
957
+ if value_present:
958
+ value = params.pop(parameter.name)
959
+ elif body is not None and not body_consumed and parameter.name == body_param_name:
960
+ value = body
961
+ body_consumed = True
962
+ elif parameter.default is inspect.Parameter.empty:
963
+ raise CliError(f"Missing required parameter {parameter_option_name(parameter.name, body_param_name)}.")
964
+ else:
965
+ continue
966
+
967
+ if parameter.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
968
+ positional.append(value)
969
+ else:
970
+ kwargs[parameter.name] = value
971
+
972
+ if body is not None and not body_consumed:
973
+ raise CliError("A request body was provided, but this command has no body parameter.")
974
+ if params:
975
+ kwargs.update(params)
976
+ return positional, kwargs
977
+
978
+
979
+ def parameter_option_name(parameter_name: str, body_param_name: str | None) -> str:
980
+ if parameter_name == body_param_name:
981
+ return "--body"
982
+ return f"--{parameter_name.replace('_', '-')}"
983
+
984
+
985
+ def add_request_id(
986
+ sdk_method: Callable[..., Any],
987
+ command_name: str,
988
+ call_kwargs: dict[str, Any],
989
+ args: SimpleNamespace,
990
+ ) -> None:
991
+ if args.opc_request_id and not sdk_method_accepts_kwarg(sdk_method, "opc_request_id"):
992
+ raise CliError(f"{command_name} does not accept opc_request_id.")
993
+ if (
994
+ not args.no_request_id
995
+ and "opc_request_id" not in call_kwargs
996
+ and sdk_method_accepts_kwarg(sdk_method, "opc_request_id")
997
+ ):
998
+ call_kwargs["opc_request_id"] = args.opc_request_id or f"aidp-cli-{uuid.uuid4()}"
999
+
1000
+
1001
+ def sdk_method_accepts_kwarg(sdk_method: Callable[..., Any], name: str) -> bool:
1002
+ try:
1003
+ source = inspect.getsource(sdk_method)
1004
+ except OSError:
1005
+ return False
1006
+ return f'"{name}"' in source or f"'{name}'" in source
1007
+
1008
+
1009
+ def parse_value(value: str) -> Any:
1010
+ stripped = value.strip()
1011
+ if stripped == "":
1012
+ return ""
1013
+ try:
1014
+ return json.loads(stripped)
1015
+ except json.JSONDecodeError:
1016
+ return value
1017
+
1018
+
1019
+ def load_body_input(command: CommandDefinition, value: str | None) -> Any:
1020
+ if command_uses_raw_body(command):
1021
+ return read_raw_body_argument(value)
1022
+ return load_json_input(value)
1023
+
1024
+
1025
+ def command_uses_raw_body(command: CommandDefinition) -> bool:
1026
+ return (
1027
+ command.body_field is not None
1028
+ and not command.body_model
1029
+ and not command.body_fields
1030
+ and not command.body_models
1031
+ )
1032
+
1033
+
1034
+ def load_json_input(value: str | None) -> Any:
1035
+ if value is None:
1036
+ return None
1037
+ raw = read_json_argument(value)
1038
+ try:
1039
+ parsed = json.loads(raw)
1040
+ except json.JSONDecodeError as exc:
1041
+ raise CliError(f"--body is not valid JSON: {exc}") from exc
1042
+ if not isinstance(parsed, (dict, list)):
1043
+ raise CliError("--body must contain a JSON object or array.")
1044
+ return parsed
1045
+
1046
+
1047
+ def read_raw_body_argument(value: str | None) -> bytes | str | None:
1048
+ if value is None:
1049
+ return None
1050
+ if value == "-":
1051
+ stdin_buffer = getattr(sys.stdin, "buffer", None)
1052
+ if stdin_buffer is not None:
1053
+ return stdin_buffer.read()
1054
+ return sys.stdin.read()
1055
+ if value.startswith("@"):
1056
+ return Path(value[1:]).expanduser().read_bytes()
1057
+ if value.startswith("file://"):
1058
+ return Path(value.removeprefix("file://")).expanduser().read_bytes()
1059
+ return value
1060
+
1061
+
1062
+ def read_json_argument(value: str) -> str:
1063
+ if value == "-":
1064
+ return sys.stdin.read()
1065
+ if value.startswith("@"):
1066
+ return Path(value[1:]).expanduser().read_text(encoding="utf-8")
1067
+ if value.startswith("file://"):
1068
+ return Path(value.removeprefix("file://")).expanduser().read_text(encoding="utf-8")
1069
+ return value
1070
+
1071
+
1072
+ def handle_search(args: list[str]) -> None:
1073
+ query = " ".join(args).strip()
1074
+ if not query:
1075
+ print_search_help()
1076
+ return
1077
+ results: list[tuple[int, str, str, str]] = []
1078
+ for group in command_manifest().command_groups:
1079
+ group_score = search_score(query, [group.name, group.description], [], group.name)
1080
+ if group_score:
1081
+ results.append((group_score, group.name, "", group.description))
1082
+ for command in group.commands:
1083
+ fields = " ".join(field.cli_name for field in command.fields)
1084
+ command_score = search_score(
1085
+ query,
1086
+ [group.name, command.name],
1087
+ [command.summary, command.description, fields],
1088
+ f"{group.name} {command.name}",
1089
+ )
1090
+ if command_score:
1091
+ results.append((command_score, group.name, command.name, command_summary(command)))
1092
+
1093
+ if not results:
1094
+ print(f"No results for {query!r}.")
1095
+ return
1096
+ results = filter_search_results(results)
1097
+ print(f"Search results for {query!r}:")
1098
+ rows = []
1099
+ for _score, group_name, command_name, description in sorted(results, key=search_result_sort_key)[:30]:
1100
+ name = f"{group_name} {command_name}".strip()
1101
+ rows.append((name, description))
1102
+ print("\n".join(format_table(rows)))
1103
+ if len(results) > 30:
1104
+ print(f"\nShowing 30 of {len(results)} results.")
1105
+
1106
+
1107
+ def filter_search_results(results: list[tuple[int, str, str, str]]) -> list[tuple[int, str, str, str]]:
1108
+ top_score = max(score for score, _group_name, _command_name, _description in results)
1109
+ if top_score < 850:
1110
+ return results
1111
+ cutoff = int(top_score * 0.5)
1112
+ return [
1113
+ result
1114
+ for result in results
1115
+ if result[0] >= cutoff
1116
+ ]
1117
+
1118
+
1119
+ def search_result_sort_key(result: tuple[int, str, str, str]) -> tuple[int, str, str]:
1120
+ score, group_name, command_name, _description = result
1121
+ return (-score, group_name, command_name)
1122
+
1123
+
1124
+ def search_score(
1125
+ query: str,
1126
+ primary_values: list[str],
1127
+ secondary_values: list[str],
1128
+ sort_value: str,
1129
+ ) -> int:
1130
+ query_tokens = search_tokens(query)
1131
+ if not query_tokens:
1132
+ return 0
1133
+
1134
+ primary_value = " ".join(item for item in primary_values if item)
1135
+ secondary_value = " ".join(item for item in secondary_values if item)
1136
+ primary_tokens = search_tokens(primary_value)
1137
+ secondary_tokens = search_tokens(secondary_value)
1138
+ if not primary_tokens and not secondary_tokens:
1139
+ return 0
1140
+
1141
+ compact_query = compact_search_text(query)
1142
+ compact_primary = compact_search_text(primary_value)
1143
+ compact_secondary = compact_search_text(secondary_value)
1144
+ compact_sort = compact_search_text(sort_value)
1145
+ compact_sort_variants = compact_sort_value_variants(sort_value)
1146
+ expanded_primary_tokens = expand_search_tokens(primary_tokens)
1147
+ expanded_secondary_tokens = expand_search_tokens(secondary_tokens)
1148
+
1149
+ if is_compact_action_query(query_tokens, compact_query):
1150
+ return max(
1151
+ compact_identity_search_score(compact_query, sort_variant)
1152
+ for sort_variant in compact_sort_variants
1153
+ )
1154
+
1155
+ score = 0
1156
+ matched = False
1157
+
1158
+ sort_identity_score = max(
1159
+ compact_identity_search_score(compact_query, sort_variant)
1160
+ for sort_variant in compact_sort_variants
1161
+ )
1162
+ if sort_identity_score >= 850:
1163
+ score += sort_identity_score
1164
+ matched = True
1165
+ elif compact_query == compact_sort:
1166
+ score += 1000
1167
+ matched = True
1168
+ elif compact_sort.startswith(compact_query):
1169
+ score += 900
1170
+ matched = True
1171
+ elif compact_query in compact_sort:
1172
+ score += 850
1173
+ matched = True
1174
+ elif compact_query in compact_primary:
1175
+ score += 700
1176
+ matched = True
1177
+ elif compact_query in compact_secondary:
1178
+ score += 350
1179
+ matched = True
1180
+
1181
+ if all(query_token_matches(token, expanded_primary_tokens) for token in query_tokens):
1182
+ score += 500
1183
+ matched = True
1184
+ if search_tokens_in_order(query_tokens, primary_tokens):
1185
+ score += 150
1186
+ elif len(query_tokens) == 1 and query_token_matches(query_tokens[0], expanded_secondary_tokens):
1187
+ score += 150
1188
+ matched = True
1189
+
1190
+ for token in query_tokens:
1191
+ if token in expanded_primary_tokens:
1192
+ score += 25
1193
+ elif len(query_tokens) == 1 and token in expanded_secondary_tokens:
1194
+ score += 10
1195
+
1196
+ fuzzy_ratio = max(
1197
+ difflib.SequenceMatcher(None, compact_query, sort_variant).ratio()
1198
+ for sort_variant in compact_sort_variants
1199
+ )
1200
+ if fuzzy_ratio >= 0.72 and compact_length_ratio(compact_query, compact_sort) >= 0.8:
1201
+ score += int(fuzzy_ratio * 250)
1202
+ matched = True
1203
+
1204
+ return score if matched else 0
1205
+
1206
+
1207
+ def is_compact_action_query(query_tokens: list[str], compact_query: str) -> bool:
1208
+ if len(query_tokens) != 1:
1209
+ return False
1210
+ return any(
1211
+ compact_query.startswith(action) and len(compact_query) > len(action) + 2
1212
+ for action in SEARCH_ACTION_PREFIXES
1213
+ )
1214
+
1215
+
1216
+ def compact_identity_search_score(compact_query: str, compact_sort: str) -> int:
1217
+ for query_variant in compact_search_variants(compact_query):
1218
+ if query_variant == compact_sort:
1219
+ return 1000
1220
+ if compact_sort.startswith(query_variant):
1221
+ return 900
1222
+ if query_variant in compact_sort:
1223
+ return 850
1224
+ return 0
1225
+
1226
+
1227
+ def compact_search_variants(value: str) -> tuple[str, ...]:
1228
+ variants = [value]
1229
+ if len(value) > 4 and value.endswith("ies"):
1230
+ variants.append(value[:-3] + "y")
1231
+ if len(value) > 3 and value.endswith("s"):
1232
+ variants.append(value[:-1])
1233
+ return tuple(dict.fromkeys(variants))
1234
+
1235
+
1236
+ def compact_sort_value_variants(value: str) -> tuple[str, ...]:
1237
+ tokens = search_tokens(value)
1238
+ variants = [compact_search_text(value)]
1239
+ if len(tokens) >= 2 and tokens[-1] in SEARCH_ACTION_PREFIXES:
1240
+ variants.append("".join([tokens[-1], *tokens[:-1]]))
1241
+ return tuple(dict.fromkeys(variants))
1242
+
1243
+
1244
+ def compact_length_ratio(left: str, right: str) -> float:
1245
+ if not left or not right:
1246
+ return 0
1247
+ return min(len(left), len(right)) / max(len(left), len(right))
1248
+
1249
+
1250
+ def search_tokens(value: str) -> list[str]:
1251
+ return re.findall(r"[a-z0-9]+", value.lower())
1252
+
1253
+
1254
+ def compact_search_text(value: str) -> str:
1255
+ return "".join(search_tokens(value))
1256
+
1257
+
1258
+ def expand_search_tokens(tokens: list[str]) -> set[str]:
1259
+ expanded: set[str] = set()
1260
+ for token in tokens:
1261
+ expanded.update(search_token_variants(token))
1262
+ return expanded
1263
+
1264
+
1265
+ def search_token_variants(token: str) -> set[str]:
1266
+ variants = {token}
1267
+ if len(token) > 4 and token.endswith("ies"):
1268
+ variants.add(token[:-3] + "y")
1269
+ if len(token) > 3 and token.endswith("s"):
1270
+ variants.add(token[:-1])
1271
+ return variants
1272
+
1273
+
1274
+ def query_token_matches(query_token: str, value_tokens: set[str]) -> bool:
1275
+ if search_token_variants(query_token) & value_tokens:
1276
+ return True
1277
+ if len(query_token) < 5:
1278
+ return False
1279
+ return bool(difflib.get_close_matches(query_token, value_tokens, n=1, cutoff=0.82))
1280
+
1281
+
1282
+ def search_tokens_in_order(query_tokens: list[str], value_tokens: list[str]) -> bool:
1283
+ value_index = 0
1284
+ expanded_values = [search_token_variants(token) for token in value_tokens]
1285
+ for query_token in query_tokens:
1286
+ query_variants = search_token_variants(query_token)
1287
+ while value_index < len(expanded_values):
1288
+ if query_variants & expanded_values[value_index]:
1289
+ value_index += 1
1290
+ break
1291
+ value_index += 1
1292
+ else:
1293
+ return False
1294
+ return True
1295
+
1296
+
1297
+ def print_search_help() -> None:
1298
+ lines = [
1299
+ "Search AIDP command groups, command names, descriptions, and flags.",
1300
+ "",
1301
+ "Usage:",
1302
+ " aidp search QUERY [flags]",
1303
+ "",
1304
+ "Examples:",
1305
+ " aidp search workspace",
1306
+ " aidp search list workspaces",
1307
+ " aidp search permission",
1308
+ "",
1309
+ "Flags:",
1310
+ *format_flags([("-h, --help", "help for search")]),
1311
+ "",
1312
+ "Global Flags:",
1313
+ *format_flags(global_flag_rows()),
1314
+ ]
1315
+ print("\n".join(lines))
1316
+
1317
+
1318
+ def handle_configure(args: list[str]) -> None:
1319
+ action = args[0]
1320
+ if action == "get":
1321
+ print("AIDP CLI configuration:")
1322
+ print(f" instance-id: {os.getenv('AIDP_INSTANCE_ID') or os.getenv('INSTANCE_ID') or '(not set)'}")
1323
+ print(f" endpoint: {os.getenv('AIDP_ENDPOINT') or os.getenv('OCI_CLI_ENDPOINT') or '(not set)'}")
1324
+ return
1325
+ if action == "set":
1326
+ raise CliError("aidp configure set is no longer supported. Use AIDP_INSTANCE_ID or --instance-id.")
1327
+ raise CliError(f"Unknown configure command {action!r}. Run 'aidp configure --help'.")
1328
+
1329
+
1330
+ def print_configure_help() -> None:
1331
+ lines = [
1332
+ "Show AIDP CLI environment defaults.",
1333
+ "",
1334
+ "Usage:",
1335
+ " aidp configure get",
1336
+ "",
1337
+ "Set defaults with AIDP_INSTANCE_ID and AIDP_ENDPOINT environment variables.",
1338
+ "",
1339
+ "Flags:",
1340
+ *format_flags([("-h, --help", "help for configure")]),
1341
+ ]
1342
+ print("\n".join(lines))
1343
+
1344
+
1345
+ def configured_instance_id() -> str | None:
1346
+ env_value = os.getenv("AIDP_INSTANCE_ID") or os.getenv("INSTANCE_ID")
1347
+ if env_value:
1348
+ return env_value
1349
+ return None
1350
+
1351
+
1352
+ def unknown_command_group(name: str) -> CliError:
1353
+ candidates = ["command-groups", "search", "configure", "help", "version"] + [
1354
+ group.name for group in command_manifest().command_groups
1355
+ ]
1356
+ return CliError(unknown_message("command group", name, candidates, "aidp command-groups"))
1357
+
1358
+
1359
+ def unknown_command_name(group: CommandGroup, name: str) -> CliError:
1360
+ candidates = [command.name for command in group.commands]
1361
+ matches = command_name_suggestions(group, name, candidates)
1362
+ return CliError(unknown_message("command name", name, candidates, f"aidp {group.name} --help", matches))
1363
+
1364
+
1365
+ def command_name_suggestions(group: CommandGroup, name: str, candidates: list[str]) -> list[str]:
1366
+ matches = difflib.get_close_matches(name, candidates, n=3)
1367
+ if matches:
1368
+ return matches
1369
+
1370
+ normalized = compact_search_text(name)
1371
+ alias_matches = []
1372
+ for command in group.commands:
1373
+ aliases = {
1374
+ command.sdk_method_name.replace("_", "-"),
1375
+ command.operation_id,
1376
+ }
1377
+ if any(normalized == compact_search_text(alias) for alias in aliases):
1378
+ alias_matches.append(command.name)
1379
+ if alias_matches:
1380
+ return list(dict.fromkeys(alias_matches))[:3]
1381
+
1382
+ scored = [
1383
+ (search_score(name, [group.name, command.name, command.sdk_method_name], [command.summary], command.name), command.name)
1384
+ for command in group.commands
1385
+ ]
1386
+ best_score = max((score for score, _command_name in scored), default=0)
1387
+ if best_score < 500:
1388
+ return []
1389
+ cutoff = max(500, int(best_score * 0.75))
1390
+ return [
1391
+ command_name
1392
+ for score, command_name in sorted(scored, key=lambda item: (-item[0], item[1]))
1393
+ if score >= cutoff
1394
+ ][:3]
1395
+
1396
+
1397
+ def unknown_option(
1398
+ group: CommandGroup,
1399
+ command: CommandDefinition,
1400
+ name: str,
1401
+ option_fields: dict[str, CommandField],
1402
+ ) -> CliError:
1403
+ candidates = (
1404
+ ["--body", "--opc-request-id", "--no-request-id"]
1405
+ + list(option_fields)
1406
+ + list(GLOBAL_VALUE_OPTIONS)
1407
+ + list(GLOBAL_BOOLEAN_OPTIONS)
1408
+ )
1409
+ return CliError(
1410
+ unknown_message("option", name, candidates, f"aidp help {group.name} {command.name}")
1411
+ )
1412
+
1413
+
1414
+ def unknown_message(
1415
+ kind: str,
1416
+ name: str,
1417
+ candidates: list[str],
1418
+ help_command: str,
1419
+ matches: list[str] | None = None,
1420
+ ) -> str:
1421
+ message = f"Unknown {kind} {name!r}."
1422
+ matches = matches if matches is not None else difflib.get_close_matches(name, candidates, n=3)
1423
+ if matches:
1424
+ message += "\n\nDid you mean this?\n"
1425
+ message += "\n".join(f" {match}" for match in matches)
1426
+ message += f'\n\nUse "{help_command}" for more information.'
1427
+ return message
1428
+
1429
+
1430
+ def print_version() -> None:
1431
+ print(f"aidp python cli version-{cli_version()}")
1432
+
1433
+
1434
+ def cli_version() -> str:
1435
+ try:
1436
+ return package_version(PACKAGE_NAME)
1437
+ except PackageNotFoundError:
1438
+ return "unknown"
1439
+
1440
+
1441
+ def example_body(command: CommandDefinition) -> dict[str, Any] | None:
1442
+ if command.body_field is None:
1443
+ return None
1444
+ model = root_body_model(command)
1445
+ fields = model.fields if model is not None else command.body_fields
1446
+ return sample_object_for_fields(fields, command, seen_models=set(), depth=0)
1447
+
1448
+
1449
+ def root_body_variant_examples(command: CommandDefinition) -> list[tuple[str, dict[str, Any]]]:
1450
+ root_model = root_body_model(command)
1451
+ if root_model is None or not root_model.variants:
1452
+ return []
1453
+ examples = []
1454
+ for variant in root_model.variants:
1455
+ if variant.model_name not in command.body_models:
1456
+ continue
1457
+ label = (
1458
+ f"{variant.model_name} "
1459
+ f"({variant.discriminator_field}={variant.discriminator_value})"
1460
+ )
1461
+ examples.append((label, variant_body_sample(command, command.body_model, variant)))
1462
+ return examples
1463
+
1464
+
1465
+ def nested_body_variant_examples(command: CommandDefinition) -> list[tuple[str, str, dict[str, Any]]]:
1466
+ root_model = root_body_model(command)
1467
+ if root_model is None:
1468
+ return []
1469
+ examples = collect_nested_body_variant_examples(
1470
+ root_model.fields,
1471
+ command,
1472
+ parent_path="",
1473
+ seen_models={command.body_model},
1474
+ depth=0,
1475
+ )
1476
+ for variant in root_model.variants:
1477
+ variant_model = command.body_models.get(variant.model_name)
1478
+ if variant_model is None:
1479
+ continue
1480
+ examples.extend(
1481
+ collect_nested_body_variant_examples(
1482
+ variant_model.fields,
1483
+ command,
1484
+ parent_path="",
1485
+ seen_models={command.body_model, variant.model_name},
1486
+ depth=0,
1487
+ )
1488
+ )
1489
+
1490
+ unique_examples = []
1491
+ seen = set()
1492
+ for path, label, sample in examples:
1493
+ key = (path, label)
1494
+ if key in seen:
1495
+ continue
1496
+ seen.add(key)
1497
+ unique_examples.append((path, label, sample))
1498
+ return unique_examples
1499
+
1500
+
1501
+ def collect_nested_body_variant_examples(
1502
+ fields: tuple[BodyField, ...],
1503
+ command: CommandDefinition,
1504
+ parent_path: str,
1505
+ seen_models: set[str],
1506
+ depth: int,
1507
+ ) -> list[tuple[str, str, dict[str, Any]]]:
1508
+ if depth >= MAX_BODY_EXAMPLE_DEPTH:
1509
+ return []
1510
+
1511
+ examples = []
1512
+ for field in fields:
1513
+ if not field.model_name:
1514
+ continue
1515
+ model = command.body_models.get(field.model_name)
1516
+ if model is None:
1517
+ continue
1518
+
1519
+ path = f"{parent_path}.{field.name}" if parent_path else field.name
1520
+ model_path = f"{path}[]" if field.type_name == "array" else path
1521
+ for variant in model.variants:
1522
+ if variant.model_name not in command.body_models:
1523
+ continue
1524
+ label = (
1525
+ f"{variant.model_name} "
1526
+ f"({variant.discriminator_field}={variant.discriminator_value})"
1527
+ )
1528
+ examples.append((model_path, label, variant_body_sample(command, field.model_name, variant)))
1529
+
1530
+ if field.model_name not in seen_models:
1531
+ next_seen_models = set(seen_models)
1532
+ next_seen_models.add(field.model_name)
1533
+ examples.extend(
1534
+ collect_nested_body_variant_examples(
1535
+ model.fields,
1536
+ command,
1537
+ parent_path=model_path,
1538
+ seen_models=next_seen_models,
1539
+ depth=depth + 1,
1540
+ )
1541
+ )
1542
+ return examples
1543
+
1544
+
1545
+ def variant_body_sample(
1546
+ command: CommandDefinition,
1547
+ base_model_name: str,
1548
+ variant: Any,
1549
+ ) -> dict[str, Any]:
1550
+ base_model = command.body_models[base_model_name]
1551
+ variant_model = command.body_models[variant.model_name]
1552
+ sample = sample_object_for_fields(base_model.fields, command, seen_models=set(), depth=0)
1553
+ variant_sample = sample_object_for_fields(
1554
+ variant_model.fields,
1555
+ command,
1556
+ seen_models={base_model_name},
1557
+ depth=0,
1558
+ )
1559
+ for name, value in variant_sample.items():
1560
+ if name not in sample:
1561
+ sample[name] = value
1562
+ sample[variant.discriminator_field] = variant.discriminator_value
1563
+ return sample
1564
+
1565
+
1566
+ def sample_object_for_fields(
1567
+ fields: tuple[BodyField, ...],
1568
+ command: CommandDefinition,
1569
+ seen_models: set[str],
1570
+ depth: int,
1571
+ ) -> dict[str, Any]:
1572
+ return {
1573
+ field.name: sample_body_value(field, command, seen_models=seen_models, depth=depth)
1574
+ for field in fields
1575
+ }
1576
+
1577
+
1578
+ def root_body_fields(command: CommandDefinition) -> tuple[BodyField, ...]:
1579
+ model = root_body_model(command)
1580
+ return model.fields if model is not None else command.body_fields
1581
+
1582
+
1583
+ def root_body_model(command: CommandDefinition) -> BodyModel | None:
1584
+ if command.body_model and command.body_model in command.body_models:
1585
+ return command.body_models[command.body_model]
1586
+ return None
1587
+
1588
+
1589
+ def sample_body_value(
1590
+ field: BodyField,
1591
+ command: CommandDefinition,
1592
+ seen_models: set[str],
1593
+ depth: int,
1594
+ ) -> Any:
1595
+ if field.type_name == "array":
1596
+ return [sample_array_item(field, command, seen_models, depth)]
1597
+ if field.model_name:
1598
+ return sample_model_value(field.model_name, command, seen_models, depth)
1599
+ return sample_scalar_value(field.type_name, field.enum_values)
1600
+
1601
+
1602
+ def sample_array_item(
1603
+ field: BodyField,
1604
+ command: CommandDefinition,
1605
+ seen_models: set[str],
1606
+ depth: int,
1607
+ ) -> Any:
1608
+ if field.model_name:
1609
+ return sample_model_value(field.model_name, command, seen_models, depth)
1610
+ return sample_scalar_value(field.item_type or "string", field.enum_values)
1611
+
1612
+
1613
+ def sample_model_value(
1614
+ model_name: str,
1615
+ command: CommandDefinition,
1616
+ seen_models: set[str],
1617
+ depth: int,
1618
+ ) -> dict[str, Any]:
1619
+ if depth >= MAX_BODY_EXAMPLE_DEPTH or model_name in seen_models:
1620
+ return {}
1621
+ model = command.body_models.get(model_name)
1622
+ if model is None:
1623
+ return {}
1624
+ next_seen_models = set(seen_models)
1625
+ next_seen_models.add(model_name)
1626
+ return {
1627
+ field.name: sample_body_value(field, command, next_seen_models, depth + 1)
1628
+ for field in model.fields
1629
+ }
1630
+
1631
+
1632
+ def sample_scalar_value(type_name: str, enum_values: tuple[Any, ...]) -> Any:
1633
+ if enum_values:
1634
+ return enum_values[0]
1635
+ if type_name in {"boolean", "bool"}:
1636
+ return False
1637
+ if type_name in {"integer", "number", "int", "float"}:
1638
+ return 0
1639
+ if type_name == "array":
1640
+ return []
1641
+ if type_name == "object":
1642
+ return {}
1643
+ return "<string>"
1644
+
1645
+
1646
+ def body_required_fields(command: CommandDefinition) -> tuple[str, ...]:
1647
+ if not command.body_models:
1648
+ return command.body_required_fields
1649
+ return tuple(
1650
+ collect_required_body_paths(
1651
+ root_body_fields(command),
1652
+ command,
1653
+ parent_path="",
1654
+ parent_required=True,
1655
+ seen_models=set(),
1656
+ depth=0,
1657
+ )
1658
+ )
1659
+
1660
+
1661
+ def collect_required_body_paths(
1662
+ fields: tuple[BodyField, ...],
1663
+ command: CommandDefinition,
1664
+ parent_path: str,
1665
+ parent_required: bool,
1666
+ seen_models: set[str],
1667
+ depth: int,
1668
+ ) -> list[str]:
1669
+ if depth >= MAX_BODY_EXAMPLE_DEPTH:
1670
+ return []
1671
+
1672
+ required_paths = []
1673
+ for field in fields:
1674
+ path = f"{parent_path}.{field.name}" if parent_path else field.name
1675
+ is_unconditionally_required = parent_required and field.required
1676
+ if is_unconditionally_required:
1677
+ required_paths.append(path)
1678
+
1679
+ nested_path = f"{path}[]" if field.type_name == "array" else path
1680
+ if field.model_name and field.model_name not in seen_models:
1681
+ model = command.body_models.get(field.model_name)
1682
+ if model is None:
1683
+ continue
1684
+ next_seen_models = set(seen_models)
1685
+ next_seen_models.add(field.model_name)
1686
+ required_paths.extend(
1687
+ collect_required_body_paths(
1688
+ model.fields,
1689
+ command,
1690
+ nested_path,
1691
+ is_unconditionally_required,
1692
+ next_seen_models,
1693
+ depth + 1,
1694
+ )
1695
+ )
1696
+ return required_paths
1697
+
1698
+
1699
+ def body_enum_fields(command: CommandDefinition) -> dict[str, tuple[Any, ...]]:
1700
+ if not command.body_models:
1701
+ return command.body_enum_fields
1702
+ return collect_body_enum_paths(
1703
+ root_body_fields(command),
1704
+ command,
1705
+ parent_path="",
1706
+ seen_models=set(),
1707
+ depth=0,
1708
+ )
1709
+
1710
+
1711
+ def collect_body_enum_paths(
1712
+ fields: tuple[BodyField, ...],
1713
+ command: CommandDefinition,
1714
+ parent_path: str,
1715
+ seen_models: set[str],
1716
+ depth: int,
1717
+ ) -> dict[str, tuple[Any, ...]]:
1718
+ if depth >= MAX_BODY_EXAMPLE_DEPTH:
1719
+ return {}
1720
+
1721
+ enum_paths: dict[str, tuple[Any, ...]] = {}
1722
+ for field in fields:
1723
+ path = f"{parent_path}.{field.name}" if parent_path else field.name
1724
+ if field.enum_values:
1725
+ enum_paths[path] = field.enum_values
1726
+
1727
+ nested_path = f"{path}[]" if field.type_name == "array" else path
1728
+ if field.model_name and field.model_name not in seen_models:
1729
+ model = command.body_models.get(field.model_name)
1730
+ if model is None:
1731
+ continue
1732
+ next_seen_models = set(seen_models)
1733
+ next_seen_models.add(field.model_name)
1734
+ enum_paths.update(
1735
+ collect_body_enum_paths(
1736
+ model.fields,
1737
+ command,
1738
+ nested_path,
1739
+ next_seen_models,
1740
+ depth + 1,
1741
+ )
1742
+ )
1743
+ return enum_paths
1744
+
1745
+
1746
+ def load_config(args: SimpleNamespace) -> dict[str, Any]:
1747
+ if args.auth in {"instance_principal", "resource_principal"}:
1748
+ return {"region": args.region} if args.region else {}
1749
+ config = oci.config.from_file(
1750
+ file_location=os.path.expanduser(args.config_file),
1751
+ profile_name=args.profile,
1752
+ )
1753
+ apply_config_env_overrides(config)
1754
+ return config
1755
+
1756
+
1757
+ def build_signer(config: dict[str, Any], args: SimpleNamespace) -> Any:
1758
+ if args.auth == "instance_principal":
1759
+ return oci.auth.signers.InstancePrincipalsSecurityTokenSigner()
1760
+ if args.auth == "resource_principal":
1761
+ return oci.auth.signers.get_resource_principals_signer()
1762
+ if args.auth == "security_token":
1763
+ token_file = expand_path(config.get("security_token_file"))
1764
+ if not token_file:
1765
+ raise CliError("security_token auth requires security_token_file in the OCI config profile.")
1766
+ key_file = expand_path(config.get("key_file"))
1767
+ if not key_file:
1768
+ raise CliError("security_token auth requires key_file in the OCI config profile.")
1769
+ token = Path(token_file).read_text(encoding="utf-8").strip()
1770
+ private_key = load_private_key_from_file(key_file, config.get("pass_phrase"))
1771
+ return SecurityTokenSigner(token=token, private_key=private_key)
1772
+ require_config_keys(config, ["tenancy", "user", "fingerprint", "key_file"], "api_key")
1773
+ return Signer(
1774
+ tenancy=config["tenancy"],
1775
+ user=config["user"],
1776
+ fingerprint=config["fingerprint"],
1777
+ private_key_file_location=expand_path(config.get("key_file")),
1778
+ pass_phrase=config.get("pass_phrase"),
1779
+ private_key_content=config.get("key_content"),
1780
+ )
1781
+
1782
+
1783
+ def require_config_keys(config: dict[str, Any], keys: list[str], auth: str) -> None:
1784
+ missing_keys = [key for key in keys if not config.get(key)]
1785
+ if not missing_keys:
1786
+ return
1787
+ hint = ""
1788
+ if auth == "api_key" and config.get("security_token_file"):
1789
+ hint = " This profile looks like a security_token profile; retry with --auth security_token."
1790
+ raise CliError(
1791
+ f"{auth} auth requires {', '.join(keys)} in the OCI config profile. "
1792
+ f"Missing: {', '.join(missing_keys)}.{hint}"
1793
+ )
1794
+
1795
+
1796
+ def apply_config_env_overrides(config: dict[str, Any]) -> None:
1797
+ for env_name, config_key in CONFIG_ENV_OVERRIDES.items():
1798
+ value = os.getenv(env_name)
1799
+ if value:
1800
+ config[config_key] = value
1801
+
1802
+
1803
+ def build_client(client_cls: type, args: SimpleNamespace) -> Any:
1804
+ config = load_config(args)
1805
+ signer = build_signer(config, args)
1806
+ region = args.region or getattr(signer, "region", None) or config.get("region")
1807
+ endpoint = resolve_endpoint(
1808
+ region=region,
1809
+ endpoint=args.endpoint,
1810
+ environment_prefix=args.environment_prefix,
1811
+ environment_domain=args.environment_domain,
1812
+ environment_host=args.environment_host,
1813
+ )
1814
+ kwargs: dict[str, Any] = {
1815
+ "signer": signer,
1816
+ "service_endpoint": endpoint,
1817
+ "skip_deserialization": True,
1818
+ }
1819
+ if args.timeout is not None:
1820
+ kwargs["timeout"] = args.timeout
1821
+ client = client_cls(config, **kwargs)
1822
+ apply_client_overrides(client)
1823
+ return client
1824
+
1825
+
1826
+ def apply_client_overrides(client: Any) -> None:
1827
+ base_client = getattr(client, "base_client", None)
1828
+ if client.__class__.__name__ not in STRICT_URL_ENCODING_CLIENTS:
1829
+ return
1830
+ if base_client is None:
1831
+ return
1832
+ if hasattr(base_client, "enable_strict_url_encoding"):
1833
+ base_client.enable_strict_url_encoding = True
1834
+ else:
1835
+ setattr(base_client, "_enable_strict_url_encoding", True)
1836
+
1837
+
1838
+ def resolve_endpoint(
1839
+ region: str | None,
1840
+ endpoint: str | None,
1841
+ environment_prefix: str,
1842
+ environment_domain: str,
1843
+ environment_host: str | None,
1844
+ ) -> str:
1845
+ if endpoint:
1846
+ return normalize_endpoint(endpoint)
1847
+ if environment_host:
1848
+ return f"https://{environment_host.strip().removeprefix('https://').removeprefix('http://').rstrip('/')}"
1849
+ if not region:
1850
+ raise CliError(
1851
+ "Set --region, --endpoint, --environment-host, or region in the OCI config profile."
1852
+ )
1853
+ if str(region).startswith(("https://", "http://")):
1854
+ raise CliError(
1855
+ "Region must be an OCI region identifier. For a full service URL, use --endpoint, "
1856
+ "AIDP_ENDPOINT, or OCI_CLI_ENDPOINT."
1857
+ )
1858
+ return f"https://{environment_prefix}.{region}.oci.{environment_domain}".rstrip("/")
1859
+
1860
+
1861
+ def normalize_endpoint(endpoint: str) -> str:
1862
+ stripped = endpoint.strip().rstrip("/")
1863
+ if not stripped:
1864
+ raise CliError("--endpoint cannot be empty.")
1865
+ if stripped.startswith(("https://", "http://")):
1866
+ return stripped
1867
+ return f"https://{stripped}"
1868
+
1869
+
1870
+ def expand_path(value: str | None) -> str | None:
1871
+ return os.path.expanduser(value) if value else value
1872
+
1873
+
1874
+ def close_client(client: Any) -> None:
1875
+ close = getattr(client, "close", None)
1876
+ if callable(close):
1877
+ close()
1878
+ return
1879
+ base_client = getattr(client, "base_client", None)
1880
+ session = getattr(base_client, "session", None)
1881
+ session_close = getattr(session, "close", None)
1882
+ if callable(session_close):
1883
+ session_close()
1884
+
1885
+
1886
+ def enable_request_debug(client: Any, command_group: str, command_name: str) -> None:
1887
+ base_client = getattr(client, "base_client", None)
1888
+ if base_client is None:
1889
+ return
1890
+
1891
+ original_call_api = base_client.call_api
1892
+
1893
+ def debug_call_api(*args: Any, **kwargs: Any) -> Any:
1894
+ debug_request(base_client, command_group, command_name, *args, **kwargs)
1895
+ return original_call_api(*args, **kwargs)
1896
+
1897
+ base_client.call_api = debug_call_api
1898
+
1899
+
1900
+ def debug_request(base_client: Any, command_group: str, command_name: str, *args: Any, **kwargs: Any) -> None:
1901
+ resource_path = get_call_value("resource_path", 0, args, kwargs) or ""
1902
+ method = get_call_value("method", 1, args, kwargs) or ""
1903
+ path_params = get_call_value("path_params", 2, args, kwargs) or {}
1904
+ query_params = get_call_value("query_params", 3, args, kwargs) or {}
1905
+ header_params = get_call_value("header_params", 4, args, kwargs) or {}
1906
+ body = get_call_value("body", 5, args, kwargs)
1907
+ base_path = getattr(base_client, "base_path", "") or ""
1908
+ endpoint = getattr(base_client, "endpoint", "") or ""
1909
+ enable_strict_url_encoding = get_call_value("enable_strict_url_encoding", 9, args, kwargs)
1910
+ rendered_path = render_resource_path(
1911
+ resource_path,
1912
+ path_params,
1913
+ strict_url_encoding=should_render_strict_url_encoding(
1914
+ base_client,
1915
+ enable_strict_url_encoding,
1916
+ ),
1917
+ )
1918
+ url = build_debug_url(endpoint, base_path, rendered_path)
1919
+
1920
+ print("AIDP CLI debug request:", file=sys.stderr)
1921
+ print(f" command_group: {command_group}", file=sys.stderr)
1922
+ print(f" command_name: {command_name}", file=sys.stderr)
1923
+ print(f" method: {method}", file=sys.stderr)
1924
+ print(f" endpoint: {endpoint}", file=sys.stderr)
1925
+ print(f" base_path: {base_path}", file=sys.stderr)
1926
+ print(f" url: {url}", file=sys.stderr)
1927
+ print(f" resource_path: {resource_path}", file=sys.stderr)
1928
+ print(f" path_params: {json.dumps(path_params, sort_keys=True, default=str)}", file=sys.stderr)
1929
+ print(f" query_params: {json.dumps(query_params, sort_keys=True, default=str)}", file=sys.stderr)
1930
+ print(f" headers: {json.dumps(safe_headers(header_params), sort_keys=True, default=str)}", file=sys.stderr)
1931
+ print(f" body: {body_debug_summary(body)}", file=sys.stderr)
1932
+
1933
+
1934
+ def get_call_value(name: str, position: int, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
1935
+ if name in kwargs:
1936
+ return kwargs[name]
1937
+ if len(args) > position:
1938
+ return args[position]
1939
+ return None
1940
+
1941
+
1942
+ def render_resource_path(
1943
+ resource_path: str,
1944
+ path_params: dict[str, Any],
1945
+ *,
1946
+ strict_url_encoding: bool = False,
1947
+ ) -> str:
1948
+ rendered = resource_path
1949
+ safe_chars = "" if strict_url_encoding else "/"
1950
+ for name, value in path_params.items():
1951
+ rendered = rendered.replace("{" + name + "}", quote(str(value), safe=safe_chars))
1952
+ return rendered
1953
+
1954
+
1955
+ def should_render_strict_url_encoding(base_client: Any, request_value: Any) -> bool:
1956
+ should_enable = getattr(base_client, "should_enable_strict_url_encoding", None)
1957
+ if callable(should_enable):
1958
+ return bool(should_enable(request_value))
1959
+ if request_value is not None:
1960
+ return bool(request_value)
1961
+ return bool(getattr(base_client, "enable_strict_url_encoding", None))
1962
+
1963
+
1964
+ def build_debug_url(endpoint: str, base_path: str, rendered_path: str) -> str:
1965
+ endpoint = endpoint.rstrip("/")
1966
+ if base_path and endpoint.endswith(base_path):
1967
+ return f"{endpoint}{rendered_path}"
1968
+ return f"{endpoint}{base_path}{rendered_path}"
1969
+
1970
+
1971
+ def safe_headers(header_params: dict[str, Any]) -> dict[str, Any]:
1972
+ sensitive = {"authorization", "x-content-sha256", "security-token"}
1973
+ return {
1974
+ name: ("<redacted>" if name.lower() in sensitive else value)
1975
+ for name, value in header_params.items()
1976
+ }
1977
+
1978
+
1979
+ def body_debug_summary(body: Any) -> str:
1980
+ if body is None:
1981
+ return "none"
1982
+ if isinstance(body, dict):
1983
+ return "json object"
1984
+ if isinstance(body, list):
1985
+ return "json array"
1986
+ return type(body).__name__
1987
+
1988
+
1989
+ def print_response(response: Any) -> None:
1990
+ data = getattr(response, "data", response)
1991
+ payload = {
1992
+ "data": to_dict(data),
1993
+ "headers": dict(getattr(response, "headers", {}) or {}),
1994
+ "status": getattr(response, "status", None),
1995
+ }
1996
+ print("Response:")
1997
+ print(json.dumps(payload, indent=2, default=str))
1998
+
1999
+
2000
+ def print_error_response(payload: dict[str, Any]) -> None:
2001
+ print("Response:", file=sys.stderr)
2002
+ print(json.dumps(payload, indent=2, default=str), file=sys.stderr)
2003
+
2004
+
2005
+ if __name__ == "__main__":
2006
+ raise SystemExit(main())