uipath 2.1.131__py3-none-any.whl → 2.1.132__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.

Potentially problematic release.


This version of uipath might be problematic. Click here for more details.

@@ -0,0 +1,705 @@
1
+ """Service CLI Generator for automatic CRUD command generation.
2
+
3
+ This module provides ServiceCLIGenerator, which automatically generates
4
+ standard Click CLI commands for services implementing CRUDServiceProtocol.
5
+
6
+ The generator creates commands for:
7
+ - list: List all resources
8
+ - retrieve: Get a single resource
9
+ - create: Create a new resource
10
+ - delete: Delete a resource
11
+ - exists: Check if a resource exists (if service supports it)
12
+
13
+ Example:
14
+ >>> from ._service_metadata import ServiceMetadata, CreateParameter
15
+ >>> from ._service_cli_generator import ServiceCLIGenerator
16
+ >>>
17
+ >>> metadata = ServiceMetadata(
18
+ ... service_name="buckets",
19
+ ... service_attr="buckets",
20
+ ... resource_type="Bucket",
21
+ ... create_params={
22
+ ... "description": CreateParameter(
23
+ ... type="str",
24
+ ... required=False,
25
+ ... help="Bucket description",
26
+ ... )
27
+ ... },
28
+ ... )
29
+ >>>
30
+ >>> generator = ServiceCLIGenerator(metadata)
31
+ >>> buckets_group = generator.register(parent_group)
32
+ """
33
+
34
+ from enum import Enum
35
+ from itertools import islice
36
+ from typing import Any, Callable, TypedDict
37
+
38
+ import click
39
+
40
+ from ._service_base import (
41
+ ServiceCommandBase,
42
+ common_service_options,
43
+ service_command,
44
+ )
45
+ from ._service_metadata import ServiceMetadata
46
+ from ._service_protocol import has_exists_method, validate_service_protocol
47
+ from ._type_registry import get_type
48
+
49
+
50
+ class GeneratorType(str, Enum):
51
+ """Enum for command generator types.
52
+
53
+ Attributes:
54
+ AUTO: Auto-generated by ServiceCLIGenerator
55
+ MANUAL: Manually added nested group or custom command
56
+ OVERRIDE: Manual override of auto-generated command
57
+ """
58
+
59
+ AUTO = "ServiceCLIGenerator"
60
+ MANUAL = "manual"
61
+ OVERRIDE = "manual_override"
62
+
63
+
64
+ def _optional_decorator(
65
+ condition: bool, decorator: Callable[[Any], Any]
66
+ ) -> Callable[[Any], Any]:
67
+ """Apply decorator conditionally based on a boolean flag.
68
+
69
+ This makes conditional decorator patterns more explicit and readable.
70
+
71
+ Args:
72
+ condition: Whether to apply the decorator
73
+ decorator: The decorator function to apply if condition is True
74
+
75
+ Returns:
76
+ The decorator if condition is True, otherwise an identity function
77
+
78
+ Example:
79
+ >>> @_optional_decorator(
80
+ ... condition=config.confirmation_required,
81
+ ... decorator=click.option("--confirm", is_flag=True)
82
+ ... )
83
+ ... def my_command():
84
+ ... pass
85
+ """
86
+ if condition:
87
+ return decorator
88
+ return lambda f: f
89
+
90
+
91
+ class ProvenanceDict(TypedDict, total=False):
92
+ """Type definition for command provenance tracking.
93
+
94
+ Attributes:
95
+ generator: Source of the command (ServiceCLIGenerator, manual, manual_override)
96
+ service_name: Name of the service
97
+ resource_type: Type of resource (e.g., "Bucket")
98
+ command_function: Name of the command function
99
+ type: Type of manual addition (e.g., "nested_group")
100
+ original_generator: Original generator for overridden commands
101
+ overridden_at: Context where override occurred (e.g., "user_custom")
102
+ """
103
+
104
+ generator: str
105
+ service_name: str
106
+ resource_type: str
107
+ command_function: str
108
+ type: str
109
+ original_generator: str
110
+ overridden_at: str
111
+
112
+
113
+ class ServiceCLIGenerator:
114
+ """Generates CLI commands for CRUD services.
115
+
116
+ This class automatically creates Click commands for services that implement
117
+ the CRUDServiceProtocol. It handles:
118
+ - Command generation from metadata
119
+ - Parameter type resolution
120
+ - Confirmation and dry-run support
121
+ - Output formatting integration
122
+ - Provenance tracking
123
+
124
+ Attributes:
125
+ metadata: Service metadata configuration
126
+ _command_names: Set of generated command names
127
+ _provenance: Dict tracking command generation metadata
128
+
129
+ Example:
130
+ >>> metadata = ServiceMetadata(
131
+ ... service_name="buckets",
132
+ ... service_attr="buckets",
133
+ ... resource_type="Bucket",
134
+ ... )
135
+ >>> generator = ServiceCLIGenerator(metadata)
136
+ >>> buckets_group = generator.register(parent_group)
137
+ """
138
+
139
+ def __init__(self, metadata: ServiceMetadata) -> None:
140
+ """Initialize the generator with service metadata.
141
+
142
+ Args:
143
+ metadata: Service metadata configuration
144
+
145
+ Raises:
146
+ ValueError: If metadata has invalid types
147
+ """
148
+ self.metadata = metadata
149
+ self._command_names: set[str] = set()
150
+ self._provenance: dict[str, ProvenanceDict] = {}
151
+
152
+ self.metadata.validate_types()
153
+
154
+ def validate_service(self, client: Any) -> None:
155
+ """Validate that the service implements the required protocol.
156
+
157
+ This method should be called early during CLI setup to fail fast
158
+ if the service doesn't implement the required CRUD methods.
159
+
160
+ Args:
161
+ client: Client instance with service attributes
162
+
163
+ Raises:
164
+ TypeError: If service doesn't implement CRUDServiceProtocol
165
+ AttributeError: If service attribute doesn't exist on client
166
+
167
+ Example:
168
+ >>> generator = ServiceCLIGenerator(metadata)
169
+ >>> from uipath import UiPath
170
+ >>> client = UiPath(...)
171
+ >>> generator.validate_service(client) # Fails early if invalid
172
+ >>> generator.register(cli_group) # Now safe to register
173
+ """
174
+ service = getattr(client, self.metadata.service_attr)
175
+ validate_service_protocol(service, self.metadata.service_name)
176
+
177
+ def register(self, parent_group: click.Group) -> click.Group:
178
+ """Register service commands under a parent Click group.
179
+
180
+ This creates a new Click group for the service and adds all
181
+ CRUD commands to it based on the metadata.
182
+
183
+ Args:
184
+ parent_group: Parent Click group to add service group to
185
+
186
+ Returns:
187
+ The created service Click group
188
+
189
+ Raises:
190
+ TypeError: If service doesn't implement CRUDServiceProtocol
191
+
192
+ Example:
193
+ >>> @click.group()
194
+ ... def cli():
195
+ ... pass
196
+ >>> generator = ServiceCLIGenerator(metadata)
197
+ >>> service_group = generator.register(cli)
198
+ """
199
+ service_group = click.Group(
200
+ name=self.metadata.service_name,
201
+ help=f"Manage {self.metadata.resource_plural}",
202
+ )
203
+
204
+ self._add_list_command(service_group)
205
+ self._add_retrieve_command(service_group)
206
+ self._add_create_command(service_group)
207
+ self._add_delete_command(service_group)
208
+
209
+ self._add_exists_command(service_group)
210
+
211
+ parent_group.add_command(service_group)
212
+
213
+ return service_group
214
+
215
+ def _add_list_command(self, group: click.Group) -> None:
216
+ """Add list command to the group.
217
+
218
+ Generates: uipath <service> list [--folder-path PATH] [--folder-key KEY] [--limit N] [--offset N]
219
+ """
220
+
221
+ @group.command("list")
222
+ @click.option("--limit", type=int, help="Maximum number of items to return")
223
+ @click.option("--offset", type=int, default=0, help="Number of items to skip")
224
+ @common_service_options
225
+ @service_command
226
+ def list_cmd(
227
+ ctx: click.Context,
228
+ folder_path: str | None,
229
+ folder_key: str | None,
230
+ format: str | None,
231
+ output: str | None,
232
+ limit: int | None,
233
+ offset: int,
234
+ ) -> list[Any]:
235
+ """List resources."""
236
+ client = ServiceCommandBase.get_client(ctx)
237
+ service = getattr(client, self.metadata.service_attr)
238
+
239
+ resources = service.list(folder_path=folder_path, folder_key=folder_key)
240
+
241
+ if limit is not None:
242
+ return list(islice(resources, offset, offset + limit))
243
+ elif offset > 0:
244
+ return list(islice(resources, offset, None))
245
+ else:
246
+ return list(resources)
247
+
248
+ list_cmd.help = f"""List all {self.metadata.resource_plural}.
249
+
250
+ Examples:
251
+ uipath {self.metadata.service_name} list
252
+ uipath {self.metadata.service_name} list --folder-path Shared
253
+ """
254
+
255
+ self._track_command("list", list_cmd)
256
+
257
+ def _add_retrieve_command(self, group: click.Group) -> None:
258
+ """Add retrieve command to the group.
259
+
260
+ Generates: uipath <service> retrieve <name> [--folder-path PATH]
261
+ """
262
+ identifier = self.metadata.retrieve_identifier
263
+
264
+ @group.command("retrieve")
265
+ @click.argument(identifier.upper())
266
+ @common_service_options
267
+ @service_command
268
+ def retrieve_cmd(
269
+ ctx: click.Context,
270
+ folder_path: str | None,
271
+ folder_key: str | None,
272
+ format: str | None,
273
+ output: str | None,
274
+ **kwargs: Any,
275
+ ) -> None:
276
+ """Retrieve a resource."""
277
+ client = ServiceCommandBase.get_client(ctx)
278
+ service = getattr(client, self.metadata.service_attr)
279
+
280
+ identifier_value = kwargs.get(identifier)
281
+
282
+ resource = service.retrieve(
283
+ identifier_value, folder_path=folder_path, folder_key=folder_key
284
+ )
285
+
286
+ return resource
287
+
288
+ retrieve_cmd.help = f"""Retrieve a single {self.metadata.resource_type}.
289
+
290
+ Examples:
291
+ uipath {self.metadata.service_name} retrieve my-resource
292
+ uipath {self.metadata.service_name} retrieve my-resource --folder-path Shared
293
+ """
294
+
295
+ self._track_command("retrieve", retrieve_cmd)
296
+
297
+ def _add_create_command(self, group: click.Group) -> None:
298
+ """Add create command to the group.
299
+
300
+ Generates: uipath <service> create <name> [options] [--folder-path PATH]
301
+ """
302
+
303
+ def create_command_decorator(func: Callable[..., Any]) -> Callable[..., Any]:
304
+ """Apply all create parameter decorators to the function."""
305
+ for param_name, param_config in reversed(
306
+ list(self.metadata.create_params.items())
307
+ ):
308
+ option_name = param_config.option_name or param_name
309
+ param_type = get_type(param_config.type)
310
+
311
+ # Build Click option
312
+ func = click.option(
313
+ f"--{option_name.replace('_', '-')}",
314
+ type=param_type,
315
+ required=param_config.required,
316
+ default=param_config.default,
317
+ help=param_config.help,
318
+ )(func)
319
+
320
+ return func
321
+
322
+ @group.command("create")
323
+ @click.argument("NAME")
324
+ @create_command_decorator
325
+ @common_service_options
326
+ @service_command
327
+ def create_cmd(
328
+ ctx: click.Context,
329
+ folder_path: str | None,
330
+ folder_key: str | None,
331
+ format: str | None,
332
+ output: str | None,
333
+ name: str,
334
+ **kwargs: Any,
335
+ ) -> None:
336
+ """Create a resource."""
337
+ client = ServiceCommandBase.get_client(ctx)
338
+ service = getattr(client, self.metadata.service_attr)
339
+
340
+ create_kwargs = {}
341
+ for param_name in self.metadata.create_params.keys():
342
+ if param_name in kwargs and kwargs[param_name] is not None:
343
+ create_kwargs[param_name] = kwargs[param_name]
344
+
345
+ resource = service.create(
346
+ name, folder_path=folder_path, folder_key=folder_key, **create_kwargs
347
+ )
348
+
349
+ click.echo(
350
+ f"Created {self.metadata.resource_type.lower()} '{name}'", err=True
351
+ )
352
+ return resource
353
+
354
+ create_cmd.help = f"""Create a new {self.metadata.resource_type}.
355
+
356
+ Examples:
357
+ uipath {self.metadata.service_name} create my-resource
358
+ uipath {self.metadata.service_name} create my-resource --folder-path Shared
359
+ """
360
+
361
+ self._track_command("create", create_cmd)
362
+
363
+ def _add_delete_command(self, group: click.Group) -> None:
364
+ """Add delete command to the group.
365
+
366
+ Generates: uipath <service> delete <name> [--confirm] [--dry-run] [--folder-path PATH]
367
+ """
368
+ delete_config = self.metadata.delete_cmd
369
+
370
+ @group.command("delete")
371
+ @click.argument("NAME")
372
+ @_optional_decorator(
373
+ delete_config.confirmation_required,
374
+ click.option(
375
+ "--confirm",
376
+ is_flag=True,
377
+ help="Confirm deletion without prompting",
378
+ ),
379
+ )
380
+ @_optional_decorator(
381
+ delete_config.dry_run_supported,
382
+ click.option(
383
+ "--dry-run",
384
+ is_flag=True,
385
+ help="Show what would be deleted without actually deleting",
386
+ ),
387
+ )
388
+ @common_service_options
389
+ @service_command
390
+ def delete_cmd(
391
+ ctx: click.Context,
392
+ folder_path: str | None,
393
+ folder_key: str | None,
394
+ format: str | None,
395
+ output: str | None,
396
+ name: str,
397
+ **kwargs: Any,
398
+ ) -> None:
399
+ """Delete a resource."""
400
+ client = ServiceCommandBase.get_client(ctx)
401
+ service = getattr(client, self.metadata.service_attr)
402
+
403
+ if delete_config.dry_run_supported:
404
+ dry_run = kwargs.get("dry_run", False)
405
+ if dry_run:
406
+ click.echo(
407
+ f"Would delete {self.metadata.resource_type.lower()} '{name}'",
408
+ err=True,
409
+ )
410
+ return
411
+
412
+ if delete_config.confirmation_required:
413
+ confirm_flag = kwargs.get("confirm", False)
414
+ if not confirm_flag:
415
+ if delete_config.confirmation_prompt:
416
+ prompt = delete_config.confirmation_prompt.format(
417
+ resource=self.metadata.resource_type, identifier=name
418
+ )
419
+ else:
420
+ prompt = f"Delete {self.metadata.resource_type} '{name}'?"
421
+
422
+ if not click.confirm(prompt):
423
+ click.echo("Deletion cancelled.")
424
+ return
425
+
426
+ service.delete(name=name, folder_path=folder_path, folder_key=folder_key)
427
+
428
+ click.echo(
429
+ f"Deleted {self.metadata.resource_type.lower()} '{name}'", err=True
430
+ )
431
+
432
+ delete_cmd.help = f"""Delete a {self.metadata.resource_type}.
433
+
434
+ Examples:
435
+ uipath {self.metadata.service_name} delete my-resource --confirm
436
+ uipath {self.metadata.service_name} delete my-resource --dry-run
437
+ """
438
+
439
+ self._track_command("delete", delete_cmd)
440
+
441
+ def _add_exists_command(self, group: click.Group) -> None:
442
+ """Add exists command to the group (if service supports it).
443
+
444
+ Generates: uipath <service> exists <name> [--folder-path PATH]
445
+
446
+ Note:
447
+ This command is only added to the CLI. The actual validation
448
+ of whether the service implements exists() happens at runtime.
449
+ """
450
+ exists_config = self.metadata.exists_cmd
451
+ identifier = exists_config.identifier_arg_name
452
+
453
+ @group.command("exists")
454
+ @click.argument(identifier.upper())
455
+ @common_service_options
456
+ @service_command
457
+ def exists_cmd(
458
+ ctx: click.Context,
459
+ folder_path: str | None,
460
+ folder_key: str | None,
461
+ format: str | None,
462
+ output: str | None,
463
+ **kwargs: Any,
464
+ ) -> bool | dict[str, Any] | None:
465
+ """Check if a resource exists."""
466
+ client = ServiceCommandBase.get_client(ctx)
467
+ service = getattr(client, self.metadata.service_attr)
468
+
469
+ if not has_exists_method(service):
470
+ click.echo(
471
+ f"Error: {self.metadata.resource_type} service does not support exists command",
472
+ err=True,
473
+ )
474
+ raise click.Abort()
475
+
476
+ identifier_value = kwargs.get(identifier)
477
+
478
+ exists = service.exists(
479
+ identifier_value, folder_path=folder_path, folder_key=folder_key
480
+ )
481
+
482
+ if exists_config.return_format == "bool":
483
+ return exists
484
+ elif exists_config.return_format == "dict":
485
+ return {"exists": exists, identifier: identifier_value}
486
+ else: # text
487
+ if exists:
488
+ click.echo(
489
+ f"{self.metadata.resource_type} '{identifier_value}' exists"
490
+ )
491
+ else:
492
+ click.echo(
493
+ f"{self.metadata.resource_type} '{identifier_value}' does not exist"
494
+ )
495
+ return None
496
+
497
+ exists_cmd.help = f"""Check if a {self.metadata.resource_type} exists.
498
+
499
+ Examples:
500
+ uipath {self.metadata.service_name} exists my-resource
501
+ uipath {self.metadata.service_name} exists my-resource --folder-path Shared
502
+ """
503
+
504
+ self._track_command("exists", exists_cmd)
505
+
506
+ def _track_command(self, command_name: str, command: Callable[..., Any]) -> None:
507
+ """Track generated command for provenance.
508
+
509
+ Args:
510
+ command_name: Name of the command (e.g., "list", "create")
511
+ command: The Click command function
512
+ """
513
+ self._command_names.add(command_name)
514
+
515
+ func_name = command_name
516
+ if hasattr(command, "callback") and hasattr(command.callback, "__name__"):
517
+ func_name = command.callback.__name__
518
+ elif hasattr(command, "__name__"):
519
+ func_name = command.__name__
520
+
521
+ provenance: ProvenanceDict = {
522
+ "generator": GeneratorType.AUTO.value,
523
+ "service_name": self.metadata.service_name,
524
+ "resource_type": self.metadata.resource_type,
525
+ "command_function": func_name,
526
+ }
527
+
528
+ self._provenance[command_name] = provenance
529
+ setattr(command, "__provenance__", provenance) # noqa: B010 # Dynamic attribute not in type
530
+
531
+ def get_command_names(self) -> set[str]:
532
+ """Get set of generated command names.
533
+
534
+ Returns:
535
+ Set of command names (e.g., {"list", "retrieve", "create", "delete", "exists"})
536
+ """
537
+ return self._command_names.copy()
538
+
539
+ def get_provenance(self, command_name: str) -> ProvenanceDict | None:
540
+ """Get provenance information for a generated command.
541
+
542
+ Args:
543
+ command_name: Name of the command
544
+
545
+ Returns:
546
+ Provenance dict or None if command not found
547
+ """
548
+ return self._provenance.get(command_name)
549
+
550
+ def add_nested_group(
551
+ self, service_group: click.Group, group_name: str, custom_group: click.Group
552
+ ) -> None:
553
+ """Add a custom nested command group to the service group.
554
+
555
+ This allows adding custom operations that don't fit the CRUD pattern,
556
+ such as the buckets file commands (upload, download, list-files, etc.).
557
+
558
+ Args:
559
+ service_group: The service group created by register()
560
+ group_name: Name for the nested group (e.g., "file")
561
+ custom_group: The Click group with custom commands
562
+
563
+ Raises:
564
+ ValueError: If group_name collides with existing command
565
+
566
+ Example:
567
+ >>> @click.group()
568
+ ... def file_group():
569
+ ... '''File operations'''
570
+ ... pass
571
+ >>>
572
+ >>> @file_group.command("upload")
573
+ ... def upload_cmd():
574
+ ... '''Upload a file'''
575
+ ... pass
576
+ >>>
577
+ >>> generator = ServiceCLIGenerator(metadata)
578
+ >>> service_group = generator.register(parent_group)
579
+ >>> generator.add_nested_group(service_group, "file", file_group)
580
+ """
581
+ if group_name in service_group.commands:
582
+ existing_provenance = self.get_provenance(group_name)
583
+ if existing_provenance:
584
+ raise ValueError(
585
+ f"Cannot add nested group '{group_name}': "
586
+ f"conflicts with auto-generated command '{group_name}' "
587
+ f"(generator: {existing_provenance['generator']})"
588
+ )
589
+ else:
590
+ raise ValueError(
591
+ f"Cannot add nested group '{group_name}': "
592
+ f"command name already exists"
593
+ )
594
+
595
+ service_group.add_command(custom_group, name=group_name)
596
+
597
+ self._command_names.add(group_name)
598
+ provenance: ProvenanceDict = {
599
+ "generator": GeneratorType.MANUAL.value,
600
+ "type": "nested_group",
601
+ "service_name": self.metadata.service_name,
602
+ "command_function": group_name,
603
+ }
604
+ self._provenance[group_name] = provenance
605
+ setattr(custom_group, "__provenance__", provenance) # noqa: B010 # Dynamic attribute not in type
606
+
607
+ def override_command(
608
+ self,
609
+ service_group: click.Group,
610
+ command_name: str,
611
+ custom_command: click.Command,
612
+ ) -> None:
613
+ """Override an auto-generated command with a custom implementation.
614
+
615
+ This allows replacing auto-generated CRUD commands with custom logic
616
+ while preserving the same command name and interface.
617
+
618
+ Args:
619
+ service_group: The service group created by register()
620
+ command_name: Name of the command to override (e.g., "list", "create")
621
+ custom_command: The custom Click command
622
+
623
+ Raises:
624
+ ValueError: If command_name doesn't exist or wasn't auto-generated
625
+
626
+ Example:
627
+ >>> @click.command("list")
628
+ ... def custom_list():
629
+ ... '''Custom list with extra features'''
630
+ ... pass
631
+ >>>
632
+ >>> generator = ServiceCLIGenerator(metadata)
633
+ >>> service_group = generator.register(parent_group)
634
+ >>> generator.override_command(service_group, "list", custom_list)
635
+ """
636
+ if command_name not in service_group.commands:
637
+ raise ValueError(
638
+ f"Cannot override command '{command_name}': command does not exist"
639
+ )
640
+
641
+ provenance = self.get_provenance(command_name)
642
+ if not provenance or provenance.get("generator") != GeneratorType.AUTO.value:
643
+ raise ValueError(
644
+ f"Cannot override command '{command_name}': "
645
+ f"not an auto-generated command (only auto-generated commands can be overridden)"
646
+ )
647
+
648
+ del service_group.commands[command_name]
649
+ service_group.add_command(custom_command, name=command_name)
650
+
651
+ new_provenance: ProvenanceDict = {
652
+ "generator": GeneratorType.OVERRIDE.value,
653
+ "original_generator": provenance["generator"],
654
+ "service_name": self.metadata.service_name,
655
+ "resource_type": self.metadata.resource_type,
656
+ "command_function": custom_command.callback.__name__
657
+ if hasattr(custom_command, "callback")
658
+ and custom_command.callback is not None
659
+ else command_name,
660
+ "overridden_at": "user_custom",
661
+ }
662
+ self._provenance[command_name] = new_provenance
663
+ setattr(custom_command, "__provenance__", new_provenance) # noqa: B010 # Dynamic attribute not in type
664
+
665
+ def validate_no_collisions(
666
+ self, service_group: click.Group, additional_commands: list[str]
667
+ ) -> list[str]:
668
+ """Validate that additional commands won't collide with generated ones.
669
+
670
+ This is useful for checking naming conflicts before adding custom commands.
671
+
672
+ Args:
673
+ service_group: The service group created by register()
674
+ additional_commands: List of command names to check
675
+
676
+ Returns:
677
+ List of collision errors (empty if no collisions)
678
+
679
+ Example:
680
+ >>> generator = ServiceCLIGenerator(metadata)
681
+ >>> service_group = generator.register(parent_group)
682
+ >>> errors = generator.validate_no_collisions(service_group, ["list", "custom"])
683
+ >>> if errors:
684
+ ... for error in errors:
685
+ ... print(error)
686
+ """
687
+ collisions = []
688
+
689
+ for cmd_name in additional_commands:
690
+ if cmd_name in service_group.commands:
691
+ provenance = self.get_provenance(cmd_name)
692
+ if provenance:
693
+ collisions.append(
694
+ f"Command '{cmd_name}' conflicts with auto-generated command "
695
+ f"(generator: {provenance.get('generator', 'unknown')})"
696
+ )
697
+ else:
698
+ collisions.append(
699
+ f"Command '{cmd_name}' conflicts with existing command"
700
+ )
701
+
702
+ return collisions
703
+
704
+
705
+ __all__ = ["ServiceCLIGenerator"]