fdai-deployment-cli 0.1.2__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.
Files changed (203) hide show
  1. fdai_deployment_cli/__about__.py +3 -0
  2. fdai_deployment_cli/__init__.py +5 -0
  3. fdai_deployment_cli/__main__.py +5 -0
  4. fdai_deployment_cli/acr_publish.py +585 -0
  5. fdai_deployment_cli/aks_catalog_review.py +137 -0
  6. fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
  7. fdai_deployment_cli/aks_job_environment.py +59 -0
  8. fdai_deployment_cli/aks_job_execution.py +794 -0
  9. fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
  10. fdai_deployment_cli/aks_preflight.py +422 -0
  11. fdai_deployment_cli/aks_readiness.py +220 -0
  12. fdai_deployment_cli/aks_service_update.py +180 -0
  13. fdai_deployment_cli/aks_workload_jobs.py +281 -0
  14. fdai_deployment_cli/application_state_adoption.py +347 -0
  15. fdai_deployment_cli/azure_actor.py +61 -0
  16. fdai_deployment_cli/azure_naming.py +252 -0
  17. fdai_deployment_cli/azure_retail_prices.py +173 -0
  18. fdai_deployment_cli/bootstrap_approval.py +99 -0
  19. fdai_deployment_cli/bootstrap_channel.py +321 -0
  20. fdai_deployment_cli/bootstrap_executor.py +311 -0
  21. fdai_deployment_cli/bootstrap_plan.py +237 -0
  22. fdai_deployment_cli/bootstrap_reconcile.py +402 -0
  23. fdai_deployment_cli/bootstrap_support.py +138 -0
  24. fdai_deployment_cli/bootstrap_worker.py +448 -0
  25. fdai_deployment_cli/bundle.py +352 -0
  26. fdai_deployment_cli/capacity.py +143 -0
  27. fdai_deployment_cli/catalog_review_profile.py +306 -0
  28. fdai_deployment_cli/cli.py +798 -0
  29. fdai_deployment_cli/cli_help.py +214 -0
  30. fdai_deployment_cli/cli_parser.py +791 -0
  31. fdai_deployment_cli/cli_plan.py +450 -0
  32. fdai_deployment_cli/compiler.py +99 -0
  33. fdai_deployment_cli/console_artifact.py +218 -0
  34. fdai_deployment_cli/console_config.py +214 -0
  35. fdai_deployment_cli/console_update.py +832 -0
  36. fdai_deployment_cli/contracts.py +307 -0
  37. fdai_deployment_cli/control_package.py +140 -0
  38. fdai_deployment_cli/deadline_transport.py +70 -0
  39. fdai_deployment_cli/deployment_cost.py +131 -0
  40. fdai_deployment_cli/deployment_deadline.py +28 -0
  41. fdai_deployment_cli/deployment_kit.py +790 -0
  42. fdai_deployment_cli/deployment_kit_cache.py +191 -0
  43. fdai_deployment_cli/deployment_progress.py +648 -0
  44. fdai_deployment_cli/development_authority_cli.py +56 -0
  45. fdai_deployment_cli/development_authority_profile.py +319 -0
  46. fdai_deployment_cli/doctor.py +136 -0
  47. fdai_deployment_cli/entitlement_preflight.py +68 -0
  48. fdai_deployment_cli/entitlement_transport.py +49 -0
  49. fdai_deployment_cli/entra_bootstrap.py +579 -0
  50. fdai_deployment_cli/entra_control_profile.py +214 -0
  51. fdai_deployment_cli/entra_graph.py +349 -0
  52. fdai_deployment_cli/entra_profiles.py +215 -0
  53. fdai_deployment_cli/entra_source.py +244 -0
  54. fdai_deployment_cli/execution_bundle.py +478 -0
  55. fdai_deployment_cli/execution_copies.py +63 -0
  56. fdai_deployment_cli/foundation_adoption.py +438 -0
  57. fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
  58. fdai_deployment_cli/foundation_adoption_host.py +157 -0
  59. fdai_deployment_cli/foundation_adoption_transport.py +51 -0
  60. fdai_deployment_cli/foundation_failure.py +72 -0
  61. fdai_deployment_cli/foundation_image.py +168 -0
  62. fdai_deployment_cli/foundation_input.py +235 -0
  63. fdai_deployment_cli/foundation_output.py +154 -0
  64. fdai_deployment_cli/foundation_plan.py +381 -0
  65. fdai_deployment_cli/foundation_process.py +64 -0
  66. fdai_deployment_cli/foundation_progress.py +231 -0
  67. fdai_deployment_cli/github_actions.py +171 -0
  68. fdai_deployment_cli/github_apply_artifact.py +259 -0
  69. fdai_deployment_cli/github_artifact_io.py +48 -0
  70. fdai_deployment_cli/github_dispatch.py +248 -0
  71. fdai_deployment_cli/github_plan_artifact.py +181 -0
  72. fdai_deployment_cli/github_provider_artifact.py +170 -0
  73. fdai_deployment_cli/github_transport.py +46 -0
  74. fdai_deployment_cli/github_workflow_values.py +312 -0
  75. fdai_deployment_cli/hub_image_deploy.py +354 -0
  76. fdai_deployment_cli/hub_image_stage.py +162 -0
  77. fdai_deployment_cli/hub_image_trust.py +281 -0
  78. fdai_deployment_cli/hub_ownership.py +161 -0
  79. fdai_deployment_cli/hub_workload_handoff.py +264 -0
  80. fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
  81. fdai_deployment_cli/identity_profile.py +227 -0
  82. fdai_deployment_cli/image_targets.py +197 -0
  83. fdai_deployment_cli/installation_scope.py +280 -0
  84. fdai_deployment_cli/license.py +311 -0
  85. fdai_deployment_cli/license_issue.py +239 -0
  86. fdai_deployment_cli/lifecycle_authorization.py +252 -0
  87. fdai_deployment_cli/lifecycle_commands.py +173 -0
  88. fdai_deployment_cli/lifecycle_configuration.py +722 -0
  89. fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
  90. fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
  91. fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
  92. fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
  93. fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
  94. fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
  95. fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
  96. fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
  97. fdai_deployment_cli/lifecycle_enrollment.py +45 -0
  98. fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
  99. fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
  100. fdai_deployment_cli/lifecycle_native.py +247 -0
  101. fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
  102. fdai_deployment_cli/lifecycle_native_transition.py +339 -0
  103. fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
  104. fdai_deployment_cli/lifecycle_plan.py +800 -0
  105. fdai_deployment_cli/lifecycle_promotion.py +185 -0
  106. fdai_deployment_cli/lifecycle_release.py +308 -0
  107. fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
  108. fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
  109. fdai_deployment_cli/notification_recipients_input.py +222 -0
  110. fdai_deployment_cli/observer_artifact.py +106 -0
  111. fdai_deployment_cli/oci_archive.py +353 -0
  112. fdai_deployment_cli/offline_kit.py +797 -0
  113. fdai_deployment_cli/offline_prepare.py +135 -0
  114. fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
  115. fdai_deployment_cli/plan_input.py +101 -0
  116. fdai_deployment_cli/private_output.py +192 -0
  117. fdai_deployment_cli/profile.py +70 -0
  118. fdai_deployment_cli/progress.py +183 -0
  119. fdai_deployment_cli/readiness.py +286 -0
  120. fdai_deployment_cli/run_command_receiver.py +418 -0
  121. fdai_deployment_cli/runtime_build.py +386 -0
  122. fdai_deployment_cli/runtime_profile.py +287 -0
  123. fdai_deployment_cli/runtime_release.py +745 -0
  124. fdai_deployment_cli/runtime_stage.py +82 -0
  125. fdai_deployment_cli/runtime_support_installation.py +215 -0
  126. fdai_deployment_cli/simulation.py +80 -0
  127. fdai_deployment_cli/source_application_inputs.py +397 -0
  128. fdai_deployment_cli/source_azure.py +586 -0
  129. fdai_deployment_cli/source_change_cli.py +107 -0
  130. fdai_deployment_cli/source_change_parser.py +75 -0
  131. fdai_deployment_cli/source_change_plan.py +181 -0
  132. fdai_deployment_cli/source_changed_update.py +196 -0
  133. fdai_deployment_cli/source_deploy.py +170 -0
  134. fdai_deployment_cli/source_foundation.py +174 -0
  135. fdai_deployment_cli/source_host_tools.py +163 -0
  136. fdai_deployment_cli/source_image_stage.py +400 -0
  137. fdai_deployment_cli/source_input.py +173 -0
  138. fdai_deployment_cli/source_projection.py +164 -0
  139. fdai_deployment_cli/source_receiver.py +76 -0
  140. fdai_deployment_cli/source_recovery.py +264 -0
  141. fdai_deployment_cli/source_runtime.py +81 -0
  142. fdai_deployment_cli/source_runtime_support.py +368 -0
  143. fdai_deployment_cli/source_service_update.py +726 -0
  144. fdai_deployment_cli/source_service_update_cli.py +47 -0
  145. fdai_deployment_cli/source_snapshot.py +178 -0
  146. fdai_deployment_cli/source_teardown.py +574 -0
  147. fdai_deployment_cli/source_terraform.py +85 -0
  148. fdai_deployment_cli/source_transport.py +399 -0
  149. fdai_deployment_cli/standalone_aks_inventory.py +558 -0
  150. fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
  151. fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
  152. fdai_deployment_cli/standalone_application.py +977 -0
  153. fdai_deployment_cli/standalone_application_completion.py +151 -0
  154. fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
  155. fdai_deployment_cli/standalone_application_recovery.py +116 -0
  156. fdai_deployment_cli/standalone_approval_input.py +24 -0
  157. fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
  158. fdai_deployment_cli/standalone_catalog_review.py +251 -0
  159. fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
  160. fdai_deployment_cli/standalone_console_publish.py +174 -0
  161. fdai_deployment_cli/standalone_database_principals.py +41 -0
  162. fdai_deployment_cli/standalone_deploy.py +638 -0
  163. fdai_deployment_cli/standalone_development_authority.py +166 -0
  164. fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
  165. fdai_deployment_cli/standalone_foundation_transition.py +767 -0
  166. fdai_deployment_cli/standalone_host.py +4608 -0
  167. fdai_deployment_cli/standalone_host_rebind.py +226 -0
  168. fdai_deployment_cli/standalone_host_residual.py +104 -0
  169. fdai_deployment_cli/standalone_host_state.py +145 -0
  170. fdai_deployment_cli/standalone_host_values.py +563 -0
  171. fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
  172. fdai_deployment_cli/standalone_license_installation.py +177 -0
  173. fdai_deployment_cli/standalone_management_egress.py +83 -0
  174. fdai_deployment_cli/standalone_migration_evidence.py +38 -0
  175. fdai_deployment_cli/standalone_operational_evidence.py +356 -0
  176. fdai_deployment_cli/standalone_planned_outputs.py +147 -0
  177. fdai_deployment_cli/standalone_private_access_probe.py +100 -0
  178. fdai_deployment_cli/standalone_product_profile.py +110 -0
  179. fdai_deployment_cli/standalone_remote_prepare.py +442 -0
  180. fdai_deployment_cli/standalone_residual_apply.py +540 -0
  181. fdai_deployment_cli/standalone_residual_successor.py +431 -0
  182. fdai_deployment_cli/standalone_review.py +266 -0
  183. fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
  184. fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
  185. fdai_deployment_cli/standalone_stage_targets.py +325 -0
  186. fdai_deployment_cli/standalone_status.py +67 -0
  187. fdai_deployment_cli/standalone_terraform_environment.py +237 -0
  188. fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
  189. fdai_deployment_cli/standalone_trial_activation.py +193 -0
  190. fdai_deployment_cli/state.py +434 -0
  191. fdai_deployment_cli/state_handoff.py +253 -0
  192. fdai_deployment_cli/status_projection.py +97 -0
  193. fdai_deployment_cli/support_install.py +257 -0
  194. fdai_deployment_cli/target.py +46 -0
  195. fdai_deployment_cli/trust/capabilities.json +62 -0
  196. fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
  197. fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
  198. fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
  199. fdai_deployment_cli/trust_roots.py +50 -0
  200. fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
  201. fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
  202. fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
  203. fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,214 @@
1
+ """Plain command discovery for fdaictl, without invoking deployment or inspection handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import shutil
7
+ import sys
8
+ import textwrap
9
+ from collections.abc import Iterable
10
+ from typing import Any, NoReturn
11
+
12
+ ROOT_DESCRIPTION = (
13
+ "Deploy FDAI to Azure and verify signed deployment artifacts.\n"
14
+ "Help never signs in, downloads a kit, or deploys resources."
15
+ )
16
+ ROOT_EPILOG = (
17
+ "Quick start:\n"
18
+ " az login\n"
19
+ " fdaictl doctor\n"
20
+ " fdaictl provision azure --source .\n\n"
21
+ "Learn more: fdaictl COMMAND --help\n"
22
+ "Deployment requires explicit approval; help grants no authority."
23
+ )
24
+ AZURE_DESCRIPTION = (
25
+ "Use the active Azure CLI human account with signed Hub images, source, or a signed kit.\n"
26
+ "Choose --hub, --source or --offline-kit. Exact plans require human approval.\n"
27
+ "Hub mode needs an independently provisioned --hub-public-key; it never builds services.\n"
28
+ "A new source install confirms scope at startup only; later stages never prompt.\n"
29
+ "Scope confirmation is not apply approval; --approval-file supplies exact approval.\n"
30
+ "Source and Hub-image bootstrap share Foundation and application acceptance checks.\n"
31
+ "Use --prepare-only for no Azure access or --preflight-only for read-only SKU/quota checks.\n"
32
+ "New installations seal Container Apps or AKS into every approved plan.\n"
33
+ "New installations can start observation-only without a capability token.\n"
34
+ "Omitting a token does not revoke one previously installed."
35
+ )
36
+ AZURE_EPILOG = (
37
+ "Examples (after az login):\n"
38
+ " fdaictl provision azure --hub . --hub-public-key /trusted/release-public.pem\n\n"
39
+ " fdaictl provision azure --source . --runtime aks --prepare-only\n\n"
40
+ " fdaictl provision azure --source . --runtime aks --preflight-only\n\n"
41
+ " fdaictl provision azure \\\n"
42
+ " --offline-kit /media/fdai-kit.tar.gz\n\n"
43
+ "Offline refers to artifact delivery; Azure connectivity is still required.\n"
44
+ "CLI source updates do not change scripts inside the signed release kit."
45
+ )
46
+
47
+ # Common meanings only; a command-specific help string always takes precedence.
48
+ _ARGUMENT_HELP = {
49
+ "--output": "Result format: text or machine-readable json (default: text)",
50
+ "--offline-kit": "Path to the complete signed deployment kit; no public artifact fallback",
51
+ "--release-root": "Path to the trusted release public key, not a private signing key",
52
+ "--bundle-public-key": "Path to the trusted deployment-bundle public key",
53
+ "--public-key": "Path to the trusted public verification key",
54
+ "--profile": "Path to a private deployment profile; see provision init --help",
55
+ "--source-commit": "Exact source revision (40-character git commit SHA)",
56
+ "--source": "Path to the clean committed FDAI source checkout",
57
+ "--service": "One supported FDAI service to build and update",
58
+ "--application-work-dir": "Retained private application work directory on the deployment host",
59
+ "--timeout-seconds": "Total bounded command lifetime in seconds",
60
+ "--work-dir": "Private local work directory; ownership and mode checks apply",
61
+ "--directory": "Existing local input directory for this operation",
62
+ "--settings": "Path to deployment-owned Console settings JSON",
63
+ "--region": "Azure region for the selected deployment profile",
64
+ "--runtime": "New-install runtime platform: container-apps or aks",
65
+ "--database": "PostgreSQL placement: postgres-flex or postgres-aks",
66
+ "--system-nodes": "AKS system node count; minimum 2",
67
+ "--system-node-sku": "AKS system node Azure VM SKU",
68
+ "--user-nodes": "AKS user node minimum; postgres-aks requires at least 4",
69
+ "--max-user-nodes": "AKS user node autoscaler maximum",
70
+ "--user-node-sku": "AKS user node Azure VM SKU",
71
+ "--database-sku": "PostgreSQL Flexible Server SKU for postgres-flex placement",
72
+ "--environment": "Deployment environment; this value grants no execution authority",
73
+ "--target-binding": "Reviewed target binding as a lowercase SHA-256 digest",
74
+ "--connectivity": "Artifact connectivity profile: online or offline",
75
+ "--host": "Execution host: %(choices)s",
76
+ "--transport": "Execution transport for the profile (manual only)",
77
+ "--access-method": "Private-host access route: %(choices)s",
78
+ "--approval-quorum": "Number of required human approvers (default: 1)",
79
+ "--monthly-cost-ceiling": "Monthly cost review input in USD (default: %(default)s); not a billing cap",
80
+ "--force": "Replace an existing local profile; does not approve Azure changes",
81
+ "--variables-file": "Path to private, target-bound Terraform input JSON",
82
+ "--stage": "Terraform root selection: platform or foundation (default: platform)",
83
+ "--ops-resource-group": "Exact operations resource group in the target subscription",
84
+ "--app-resource-group": "Exact application resource group in the target subscription",
85
+ "--state-storage-account": "Exact private Terraform state storage account name",
86
+ "--output-plan": "New private file for the expiring reconciliation plan",
87
+ "--ttl-seconds": "Reconciliation plan lifetime in seconds (default: 3600)",
88
+ "--bundle": "Path to the signed deployment bundle to verify",
89
+ "--token": "Path to the private capability-token file, never the token value",
90
+ "--image-digest": "Optional expected runtime image SHA-256 binding",
91
+ "--tenant-binding": "Optional expected deployment binding digest",
92
+ "--installation-binding": "Expected installation binding digest for an installation entitlement",
93
+ "--run-id": "Run identifier for the local simulation journal",
94
+ "--journal": "Path to the local hash-chained simulation journal",
95
+ "--simulate": "Required for guided rehearsal; never starts a live deployment",
96
+ "--interrupt-after": "Stop the simulation after this stage to exercise recovery",
97
+ "--local-state": "Path to the private local Terraform state copy",
98
+ "--remote-state": "Path to the independently obtained remote state copy",
99
+ "--plan-json": "Path to the independently obtained zero-change plan JSON",
100
+ "--output-receipt": "New private comparison receipt; does not authorize cache deletion",
101
+ "--expected-review-digest": "Expected saved-plan review SHA-256 digest",
102
+ }
103
+
104
+ _ARGUMENT_METAVARS = {
105
+ "--host": "HOST",
106
+ "--access-method": "METHOD",
107
+ "--monthly-cost-ceiling": "USD",
108
+ "--system-nodes": "COUNT",
109
+ "--user-nodes": "COUNT",
110
+ "--max-user-nodes": "COUNT",
111
+ "--ops-resource-group": "NAME",
112
+ "--app-resource-group": "NAME",
113
+ "--state-storage-account": "NAME",
114
+ "--expected-review-digest": "SHA256",
115
+ }
116
+
117
+
118
+ class PlainHelpFormatter(argparse.HelpFormatter):
119
+ """Wrap descriptions and examples without ANSI or expanded private default values."""
120
+
121
+ def __init__(self, prog: str) -> None:
122
+ width = max(40, min(88, shutil.get_terminal_size(fallback=(80, 24)).columns - 2))
123
+ super().__init__(prog, width=width, max_help_position=4 if width < 58 else 26)
124
+
125
+ def _fill_text(self, text: str, width: int, indent: str) -> str:
126
+ fill = super()._fill_text
127
+ return "\n".join(
128
+ fill(line.lstrip(), width, indent + line[: len(line) - len(line.lstrip())])
129
+ if line
130
+ else ""
131
+ for line in text.splitlines()
132
+ )
133
+
134
+ def _format_usage(
135
+ self,
136
+ usage: str | None,
137
+ actions: Iterable[argparse.Action],
138
+ groups: Iterable[argparse._MutuallyExclusiveGroup],
139
+ prefix: str | None,
140
+ ) -> str:
141
+ rendered = super()._format_usage(usage, actions, groups, prefix)
142
+ if all(len(line) <= self._width for line in rendered.splitlines()):
143
+ return rendered
144
+ # argparse indents continuations by the full command path, even when that
145
+ # leaves less space than a single option. Retain the syntax with a small indent.
146
+ return "\n".join(
147
+ textwrap.fill(
148
+ line.strip(),
149
+ width=self._width,
150
+ initial_indent="" if index == 0 else " ",
151
+ subsequent_indent=" ",
152
+ break_long_words=False,
153
+ break_on_hyphens=False,
154
+ )
155
+ if line
156
+ else ""
157
+ for index, line in enumerate(rendered.split("\n"))
158
+ )
159
+
160
+
161
+ class HelpParser(argparse.ArgumentParser):
162
+ """Keep argparse validation, with exact long options and plain explanatory help."""
163
+
164
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
165
+ kwargs.setdefault("formatter_class", PlainHelpFormatter)
166
+ kwargs.setdefault("allow_abbrev", False)
167
+ if sys.version_info >= (3, 14):
168
+ kwargs.setdefault("color", False)
169
+ super().__init__(*args, **kwargs)
170
+
171
+ def add_argument(self, *name_or_flags: str, **kwargs: Any) -> argparse.Action:
172
+ """Supply common option descriptions without overriding explicit or hidden help."""
173
+
174
+ for flag in name_or_flags:
175
+ if flag in _ARGUMENT_HELP:
176
+ kwargs.setdefault("help", _ARGUMENT_HELP[flag])
177
+ if flag in _ARGUMENT_METAVARS:
178
+ kwargs.setdefault("metavar", _ARGUMENT_METAVARS[flag])
179
+ break
180
+ return super().add_argument(*name_or_flags, **kwargs)
181
+
182
+ def error(self, message: str) -> NoReturn:
183
+ """Keep usage errors on stderr with exit 2 and an explicit next-help command."""
184
+
185
+ self.print_usage(sys.stderr)
186
+ self.exit(2, f"{self.prog}: error: {message}\nTry '{self.prog} --help' for details.\n")
187
+
188
+
189
+ def command_group(
190
+ parser: argparse.ArgumentParser,
191
+ ) -> argparse._SubParsersAction[argparse.ArgumentParser]:
192
+ """Let a group alone show help; malformed input and required leaf arguments still fail."""
193
+
194
+ def show_help(_args: argparse.Namespace) -> int:
195
+ parser.print_help()
196
+ return 0
197
+
198
+ parser.set_defaults(handler=show_help)
199
+ return parser.add_subparsers(title="Commands", metavar="COMMAND")
200
+
201
+
202
+ def command(
203
+ commands: argparse._SubParsersAction[argparse.ArgumentParser],
204
+ name: str,
205
+ summary: str,
206
+ *,
207
+ description: str | None = None,
208
+ epilog: str | None = None,
209
+ ) -> argparse.ArgumentParser:
210
+ """Describe a real registered command, keeping its handler and argument contract separate."""
211
+
212
+ return commands.add_parser(
213
+ name, help=summary, description=description or summary, epilog=epilog
214
+ )