mammoth-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.
- mammoth_cli/__init__.py +14 -0
- mammoth_cli/__main__.py +27 -0
- mammoth_cli/app.py +565 -0
- mammoth_cli/bundled_skill/mammoth-cli/SKILL.md +109 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/auth.md +27 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/input.md +24 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/jobs-drafts.md +35 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/machine-output.md +30 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/recovery.md +28 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/safety.md +26 -0
- mammoth_cli/commands/__init__.py +29 -0
- mammoth_cli/commands/activity.py +123 -0
- mammoth_cli/commands/addon.py +173 -0
- mammoth_cli/commands/agent.py +199 -0
- mammoth_cli/commands/ai.py +274 -0
- mammoth_cli/commands/annotation.py +166 -0
- mammoth_cli/commands/auth.py +567 -0
- mammoth_cli/commands/automation.py +245 -0
- mammoth_cli/commands/batch.py +204 -0
- mammoth_cli/commands/billing.py +462 -0
- mammoth_cli/commands/browse.py +151 -0
- mammoth_cli/commands/capability.py +43 -0
- mammoth_cli/commands/client_app.py +233 -0
- mammoth_cli/commands/completion.py +82 -0
- mammoth_cli/commands/config.py +309 -0
- mammoth_cli/commands/connector.py +464 -0
- mammoth_cli/commands/context.py +205 -0
- mammoth_cli/commands/dashboard.py +419 -0
- mammoth_cli/commands/data_app.py +239 -0
- mammoth_cli/commands/dataset.py +332 -0
- mammoth_cli/commands/doctor.py +90 -0
- mammoth_cli/commands/external_key.py +163 -0
- mammoth_cli/commands/file.py +243 -0
- mammoth_cli/commands/folder.py +224 -0
- mammoth_cli/commands/job.py +147 -0
- mammoth_cli/commands/notification.py +201 -0
- mammoth_cli/commands/parameter.py +284 -0
- mammoth_cli/commands/project.py +327 -0
- mammoth_cli/commands/registry.py +587 -0
- mammoth_cli/commands/report.py +61 -0
- mammoth_cli/commands/schedule.py +214 -0
- mammoth_cli/commands/schema.py +271 -0
- mammoth_cli/commands/skill.py +59 -0
- mammoth_cli/commands/snippet.py +200 -0
- mammoth_cli/commands/support.py +800 -0
- mammoth_cli/commands/template.py +134 -0
- mammoth_cli/commands/trash.py +115 -0
- mammoth_cli/commands/user.py +190 -0
- mammoth_cli/commands/view.py +1143 -0
- mammoth_cli/commands/view_ops.py +596 -0
- mammoth_cli/commands/webhook.py +235 -0
- mammoth_cli/commands/workflow.py +340 -0
- mammoth_cli/commands/workspace.py +359 -0
- mammoth_cli/context/__init__.py +1 -0
- mammoth_cli/context/credentials.py +223 -0
- mammoth_cli/context/endpoint.py +57 -0
- mammoth_cli/context/profiles.py +370 -0
- mammoth_cli/context/resolver.py +240 -0
- mammoth_cli/contracts/__init__.py +1 -0
- mammoth_cli/contracts/auth.py +32 -0
- mammoth_cli/errors/__init__.py +1 -0
- mammoth_cli/errors/envelope.py +144 -0
- mammoth_cli/manifest/__init__.py +1 -0
- mammoth_cli/manifest/loader.py +96 -0
- mammoth_cli/messages/__init__.py +1 -0
- mammoth_cli/output/__init__.py +1 -0
- mammoth_cli/output/envelope.py +42 -0
- mammoth_cli/output/normalize.py +84 -0
- mammoth_cli/output/policy.py +71 -0
- mammoth_cli/output/render.py +83 -0
- mammoth_cli/py.typed +0 -0
- mammoth_cli/runtime/__init__.py +0 -0
- mammoth_cli/runtime/confirm.py +124 -0
- mammoth_cli/runtime/executor.py +126 -0
- mammoth_cli/runtime/input_loader.py +140 -0
- mammoth_cli/runtime/invocation.py +87 -0
- mammoth_cli/runtime/options.py +172 -0
- mammoth_cli/runtime/session.py +82 -0
- mammoth_cli/runtime/strict.py +240 -0
- mammoth_cli/runtime/validate.py +120 -0
- mammoth_cli/services/__init__.py +1 -0
- mammoth_cli/services/argspec.py +301 -0
- mammoth_cli/services/coerce.py +172 -0
- mammoth_cli/services/conditions.py +85 -0
- mammoth_cli/services/dispatch.py +63 -0
- mammoth_cli/services/factory.py +45 -0
- mammoth_cli/services/input_fields.py +44 -0
- mammoth_cli/services/mapping.py +74 -0
- mammoth_cli/services/openapi_types.py +166 -0
- mammoth_cli/services/positionals.py +408 -0
- mammoth_cli/services/protocol.py +123 -0
- mammoth_cli/services/sdk_service.py +274 -0
- mammoth_cli/services/testing.py +153 -0
- mammoth_cli/services/type_system.py +427 -0
- mammoth_cli/skills/__init__.py +1 -0
- mammoth_cli/skills/installer.py +321 -0
- mammoth_cli/testing.py +55 -0
- mammoth_cli-1.0.0.dist-info/METADATA +92 -0
- mammoth_cli-1.0.0.dist-info/RECORD +150 -0
- mammoth_cli-1.0.0.dist-info/WHEEL +4 -0
- mammoth_cli-1.0.0.dist-info/entry_points.txt +3 -0
- mammoth_cli-1.0.0.dist-info/licenses/LICENSE +23 -0
- spec/manifests/_sdk_introspection.json +1459 -0
- spec/manifests/commands/activity.yaml +72 -0
- spec/manifests/commands/addon.yaml +239 -0
- spec/manifests/commands/agent.yaml +192 -0
- spec/manifests/commands/ai.yaml +152 -0
- spec/manifests/commands/annotation.yaml +191 -0
- spec/manifests/commands/auth.yaml +101 -0
- spec/manifests/commands/automation.yaml +273 -0
- spec/manifests/commands/batch.yaml +262 -0
- spec/manifests/commands/billing.yaml +813 -0
- spec/manifests/commands/browse.yaml +145 -0
- spec/manifests/commands/capability.yaml +74 -0
- spec/manifests/commands/client-app.yaml +193 -0
- spec/manifests/commands/completion.yaml +68 -0
- spec/manifests/commands/config.yaml +134 -0
- spec/manifests/commands/connector.yaml +944 -0
- spec/manifests/commands/context.yaml +101 -0
- spec/manifests/commands/dashboard.yaml +3595 -0
- spec/manifests/commands/data-app.yaml +485 -0
- spec/manifests/commands/dataset.yaml +580 -0
- spec/manifests/commands/doctor.yaml +35 -0
- spec/manifests/commands/external-key.yaml +151 -0
- spec/manifests/commands/file.yaml +341 -0
- spec/manifests/commands/folder.yaml +334 -0
- spec/manifests/commands/job.yaml +151 -0
- spec/manifests/commands/notification.yaml +188 -0
- spec/manifests/commands/parameter.yaml +527 -0
- spec/manifests/commands/project.yaml +669 -0
- spec/manifests/commands/report.yaml +36 -0
- spec/manifests/commands/schedule.yaml +194 -0
- spec/manifests/commands/schema.yaml +74 -0
- spec/manifests/commands/skill.yaml +167 -0
- spec/manifests/commands/snippet.yaml +311 -0
- spec/manifests/commands/support.yaml +1805 -0
- spec/manifests/commands/template.yaml +191 -0
- spec/manifests/commands/trash.yaml +105 -0
- spec/manifests/commands/user.yaml +280 -0
- spec/manifests/commands/version.yaml +35 -0
- spec/manifests/commands/view.yaml +4232 -0
- spec/manifests/commands/webhook.yaml +261 -0
- spec/manifests/commands/workflow.yaml +622 -0
- spec/manifests/commands/workspace.yaml +705 -0
- spec/manifests/openapi-operations.yaml +8853 -0
- spec/manifests/schema-v1.json +212 -0
- spec/manifests/sdk-catalog.source.yaml +4069 -0
- spec/manifests/sdk-methods.yaml +6971 -0
- spec/openapi/metadata.json +10 -0
- spec/openapi/openapi.json +1 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Handlers for the ``client-app`` command family.
|
|
2
|
+
|
|
3
|
+
Client apps are workspace-level API credentials: none of the reviewed manifest
|
|
4
|
+
signatures in this family take a ``project_id`` argument, so no handler here
|
|
5
|
+
requires an active project. The (optional) active project is still reported
|
|
6
|
+
in the envelope metadata for context, mirroring the read handlers in
|
|
7
|
+
:mod:`mammoth_cli.commands.file`. The family's resource id is ``client_key``,
|
|
8
|
+
a string identifier — never a secret — read from the first CLI positional.
|
|
9
|
+
No command in this family accepts application secret material as input at
|
|
10
|
+
all: ``create`` only takes a name and description, and ``update`` forwards an
|
|
11
|
+
opaque ``patch_request`` document from ``--input``, never a positional.
|
|
12
|
+
Handlers dispatch through the generic
|
|
13
|
+
:meth:`~mammoth_cli.services.protocol.MammothService.call` seam to the public
|
|
14
|
+
SDK method named by the command's reviewed manifest ``sdk_symbol``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from mammoth_cli.errors.envelope import (
|
|
22
|
+
CODE_MISSING_ARGUMENT,
|
|
23
|
+
CODE_MISSING_FIELD,
|
|
24
|
+
CODE_SDK_SYMBOL_UNRESOLVED,
|
|
25
|
+
EXIT_USAGE,
|
|
26
|
+
CliError,
|
|
27
|
+
)
|
|
28
|
+
from mammoth_cli.manifest.loader import command_by_id
|
|
29
|
+
from mammoth_cli.runtime.confirm import (
|
|
30
|
+
POLICY_CONFIRM_TARGET,
|
|
31
|
+
POLICY_PROMPT_OR_YES,
|
|
32
|
+
enforce_confirmation,
|
|
33
|
+
)
|
|
34
|
+
from mammoth_cli.runtime.invocation import Invocation
|
|
35
|
+
from mammoth_cli.runtime.session import open_service, resolved_project
|
|
36
|
+
|
|
37
|
+
HandlerResult = tuple[Any, dict[str, Any]]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _symbol(invocation: Invocation) -> str:
|
|
41
|
+
"""Return the reviewed backing SDK symbol for this command.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
invocation: The current command's resolved global options.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The dotted SDK symbol recorded in the command's manifest entry.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
CliError: ``sdk_symbol_unresolved`` when no symbol is recorded.
|
|
51
|
+
"""
|
|
52
|
+
record = command_by_id(invocation.command_id)
|
|
53
|
+
if record is None or not record.get("sdk_symbol"):
|
|
54
|
+
raise CliError(
|
|
55
|
+
code=CODE_SDK_SYMBOL_UNRESOLVED,
|
|
56
|
+
message=f"No SDK symbol is recorded for '{invocation.command_id}'.",
|
|
57
|
+
exit_status=EXIT_USAGE,
|
|
58
|
+
)
|
|
59
|
+
return str(record["sdk_symbol"])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _string_positional(invocation: Invocation) -> str | None:
|
|
63
|
+
"""Return the first positional argument, or None if absent.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
invocation: The current command's resolved global options.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The first extra argument, or None when there is none.
|
|
70
|
+
"""
|
|
71
|
+
return invocation.extra_args[0] if invocation.extra_args else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _require_string_positional(invocation: Invocation, name: str) -> str:
|
|
75
|
+
"""Return the required first positional argument, or raise usage.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
invocation: The current command's resolved global options.
|
|
79
|
+
name: A human-readable name for the argument, used in error text.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The first extra argument.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
CliError: ``missing_argument`` when no positional was given.
|
|
86
|
+
"""
|
|
87
|
+
value = _string_positional(invocation)
|
|
88
|
+
if value is None:
|
|
89
|
+
raise CliError(
|
|
90
|
+
code=CODE_MISSING_ARGUMENT,
|
|
91
|
+
message=f"This command requires a {name} argument.",
|
|
92
|
+
exit_status=EXIT_USAGE,
|
|
93
|
+
hint=f"Pass the {name} as a positional argument.",
|
|
94
|
+
)
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _require_field(document: dict[str, Any] | None, field: str) -> Any:
|
|
99
|
+
"""Return a required field from the ``--input`` document, or raise usage.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
document: The parsed ``--input`` request document, or None.
|
|
103
|
+
field: The required field name.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The field's value.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
CliError: ``missing_field`` when the document or field is absent.
|
|
110
|
+
"""
|
|
111
|
+
if document is None or field not in document:
|
|
112
|
+
raise CliError(
|
|
113
|
+
code=CODE_MISSING_FIELD,
|
|
114
|
+
message=f"This command requires the '{field}' input field.",
|
|
115
|
+
exit_status=EXIT_USAGE,
|
|
116
|
+
hint=f"Pass it via --input, for example: --input '{{\"{field}\": ...}}'.",
|
|
117
|
+
)
|
|
118
|
+
return document[field]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _forward_optional(
|
|
122
|
+
document: dict[str, Any], kwargs: dict[str, Any], fields: tuple[str, ...]
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Copy each of ``fields`` present in ``document`` into ``kwargs``, unchanged.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
document: The parsed ``--input`` request document.
|
|
128
|
+
kwargs: The keyword arguments accumulator to update in place.
|
|
129
|
+
fields: The optional field names to forward when present.
|
|
130
|
+
"""
|
|
131
|
+
for field in fields:
|
|
132
|
+
if field in document:
|
|
133
|
+
kwargs[field] = document[field]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _meta(invocation: Invocation, workspace_id: int, project_id: int | None) -> dict[str, Any]:
|
|
137
|
+
"""Build the common envelope metadata for a client-app command.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
invocation: The current command's resolved global options.
|
|
141
|
+
workspace_id: The authenticated client's resolved workspace id.
|
|
142
|
+
project_id: The active project id, or None when unset.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
The envelope metadata mapping.
|
|
146
|
+
"""
|
|
147
|
+
return {
|
|
148
|
+
"profile": invocation.profile,
|
|
149
|
+
"workspace_id": workspace_id,
|
|
150
|
+
"project_id": project_id,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def client_app_list(invocation: Invocation) -> HandlerResult:
|
|
155
|
+
"""List client apps in the active workspace (requires an admin role)."""
|
|
156
|
+
document = invocation.load_input() or {}
|
|
157
|
+
kwargs: dict[str, Any] = {}
|
|
158
|
+
_forward_optional(document, kwargs, ("limit", "offset", "fields", "sort"))
|
|
159
|
+
with open_service(invocation) as (service, auth):
|
|
160
|
+
data = service.call(_symbol(invocation), **kwargs)
|
|
161
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def client_app_get(invocation: Invocation) -> HandlerResult:
|
|
165
|
+
"""Get one client app by its client key."""
|
|
166
|
+
client_key = _require_string_positional(invocation, "client key")
|
|
167
|
+
document = invocation.load_input() or {}
|
|
168
|
+
kwargs: dict[str, Any] = {"client_key": client_key}
|
|
169
|
+
_forward_optional(document, kwargs, ("fields",))
|
|
170
|
+
with open_service(invocation) as (service, auth):
|
|
171
|
+
data = service.call(_symbol(invocation), **kwargs)
|
|
172
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def client_app_create(invocation: Invocation) -> HandlerResult:
|
|
176
|
+
"""Create a client app. Name comes from a positional or the ``app_name`` field.
|
|
177
|
+
|
|
178
|
+
High-impact: this mints a new set of API credentials in the workspace, so
|
|
179
|
+
it requires ``--yes --confirm APP_NAME``. No secret material is ever
|
|
180
|
+
accepted as input here; any tokens are returned by the API, not supplied.
|
|
181
|
+
"""
|
|
182
|
+
document = invocation.load_input() or {}
|
|
183
|
+
app_name = _string_positional(invocation) or document.get("app_name")
|
|
184
|
+
if not app_name:
|
|
185
|
+
raise CliError(
|
|
186
|
+
code=CODE_MISSING_ARGUMENT,
|
|
187
|
+
message="A client app name is required.",
|
|
188
|
+
exit_status=EXIT_USAGE,
|
|
189
|
+
hint="Pass the name as a positional argument or an 'app_name' input field.",
|
|
190
|
+
)
|
|
191
|
+
enforce_confirmation(
|
|
192
|
+
invocation,
|
|
193
|
+
policy=POLICY_CONFIRM_TARGET,
|
|
194
|
+
action=f"create client app '{app_name}'",
|
|
195
|
+
target=str(app_name),
|
|
196
|
+
)
|
|
197
|
+
kwargs: dict[str, Any] = {"app_name": app_name}
|
|
198
|
+
_forward_optional(document, kwargs, ("description",))
|
|
199
|
+
with open_service(invocation) as (service, auth):
|
|
200
|
+
data = service.call(_symbol(invocation), **kwargs)
|
|
201
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def client_app_update(invocation: Invocation) -> HandlerResult:
|
|
205
|
+
"""Apply a patch request to one client app. Client key is positional.
|
|
206
|
+
|
|
207
|
+
High-impact: requires ``--yes --confirm CLIENT_KEY``. The patch document
|
|
208
|
+
is a required ``--input`` field (``patch_request``); no secret material
|
|
209
|
+
is ever taken from a positional argument.
|
|
210
|
+
"""
|
|
211
|
+
client_key = _require_string_positional(invocation, "client key")
|
|
212
|
+
document = invocation.load_input()
|
|
213
|
+
patch_request = _require_field(document, "patch_request")
|
|
214
|
+
enforce_confirmation(
|
|
215
|
+
invocation,
|
|
216
|
+
policy=POLICY_CONFIRM_TARGET,
|
|
217
|
+
action=f"update client app {client_key}",
|
|
218
|
+
target=str(client_key),
|
|
219
|
+
)
|
|
220
|
+
with open_service(invocation) as (service, auth):
|
|
221
|
+
data = service.call(_symbol(invocation), client_key=client_key, patch_request=patch_request)
|
|
222
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def client_app_delete(invocation: Invocation) -> HandlerResult:
|
|
226
|
+
"""Permanently delete one client app by its client key. Prompt or ``--yes`` required."""
|
|
227
|
+
client_key = _require_string_positional(invocation, "client key")
|
|
228
|
+
enforce_confirmation(
|
|
229
|
+
invocation, policy=POLICY_PROMPT_OR_YES, action=f"delete client app {client_key}"
|
|
230
|
+
)
|
|
231
|
+
with open_service(invocation) as (service, auth):
|
|
232
|
+
data = service.call(_symbol(invocation), client_key=client_key)
|
|
233
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""The ``completion`` commands: show or install shell tab-completion.
|
|
2
|
+
|
|
3
|
+
``completion show`` prints the activation snippet for a shell (deterministic,
|
|
4
|
+
side-effect free). ``completion install`` appends that snippet to the shell's
|
|
5
|
+
startup file, idempotently, and reports the path. Both resolve the shell from a
|
|
6
|
+
positional argument, the ``--input`` ``shell`` field, or the ``$SHELL``
|
|
7
|
+
environment variable, and never prompt.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from mammoth_cli.errors.envelope import EXIT_USAGE, CliError
|
|
17
|
+
from mammoth_cli.runtime.invocation import Invocation
|
|
18
|
+
|
|
19
|
+
HandlerResult = tuple[Any, dict[str, Any]]
|
|
20
|
+
|
|
21
|
+
# click/Typer complete-var for the ``mammoth`` program.
|
|
22
|
+
_COMPLETE_VAR = "_MAMMOTH_COMPLETE"
|
|
23
|
+
_SUPPORTED = ("bash", "zsh", "fish")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _snippet(shell: str) -> str:
|
|
27
|
+
if shell == "fish":
|
|
28
|
+
return f"{_COMPLETE_VAR}=fish_source mammoth | source"
|
|
29
|
+
return f'eval "$({_COMPLETE_VAR}={shell}_source mammoth)"'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _rc_path(shell: str) -> Path:
|
|
33
|
+
home = Path.home()
|
|
34
|
+
if shell == "bash":
|
|
35
|
+
return home / ".bashrc"
|
|
36
|
+
if shell == "zsh":
|
|
37
|
+
return home / ".zshrc"
|
|
38
|
+
return home / ".config" / "fish" / "config.fish"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _resolve_shell(invocation: Invocation) -> str:
|
|
42
|
+
document = invocation.load_input() or {}
|
|
43
|
+
candidate = (
|
|
44
|
+
(invocation.extra_args[0] if invocation.extra_args else None)
|
|
45
|
+
or document.get("shell")
|
|
46
|
+
or os.path.basename(os.environ.get("SHELL", ""))
|
|
47
|
+
)
|
|
48
|
+
shell = str(candidate).strip().lower()
|
|
49
|
+
if shell not in _SUPPORTED:
|
|
50
|
+
raise CliError(
|
|
51
|
+
code="unsupported_shell",
|
|
52
|
+
message=f"Cannot generate completion for shell '{candidate or ''}'.",
|
|
53
|
+
exit_status=EXIT_USAGE,
|
|
54
|
+
hint=f"Pass one of: {', '.join(_SUPPORTED)}.",
|
|
55
|
+
)
|
|
56
|
+
return shell
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def completion_show(invocation: Invocation) -> HandlerResult:
|
|
60
|
+
"""Print the completion activation snippet for a shell."""
|
|
61
|
+
shell = _resolve_shell(invocation)
|
|
62
|
+
return {"shell": shell, "script": _snippet(shell)}, {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def completion_install(invocation: Invocation) -> HandlerResult:
|
|
66
|
+
"""Append the completion snippet to the shell startup file, idempotently."""
|
|
67
|
+
shell = _resolve_shell(invocation)
|
|
68
|
+
snippet = _snippet(shell)
|
|
69
|
+
rc_path = _rc_path(shell)
|
|
70
|
+
rc_path.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
existing = rc_path.read_text(encoding="utf-8") if rc_path.exists() else ""
|
|
72
|
+
added = snippet not in existing
|
|
73
|
+
if added:
|
|
74
|
+
prefix = "" if existing.endswith("\n") or not existing else "\n"
|
|
75
|
+
with rc_path.open("a", encoding="utf-8") as handle:
|
|
76
|
+
handle.write(f"{prefix}# Mammoth CLI completion\n{snippet}\n")
|
|
77
|
+
return {
|
|
78
|
+
"shell": shell,
|
|
79
|
+
"path": str(rc_path),
|
|
80
|
+
"added": added,
|
|
81
|
+
"detail": "installed" if added else "already present",
|
|
82
|
+
}, {}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Bespoke `config` command family: get, set, list, path.
|
|
2
|
+
|
|
3
|
+
Manages non-secret, profile-scoped configuration: the default output mode,
|
|
4
|
+
the timeout family, the one-label server prefix, and the active project id.
|
|
5
|
+
All local; no command in this family makes a network call.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from mammoth_cli.context import profiles
|
|
15
|
+
from mammoth_cli.context.endpoint import resolve_base_url
|
|
16
|
+
from mammoth_cli.errors.envelope import (
|
|
17
|
+
CODE_INVALID_CONFIG_VALUE,
|
|
18
|
+
CODE_PROFILE_NOT_FOUND,
|
|
19
|
+
EXIT_USAGE,
|
|
20
|
+
CliError,
|
|
21
|
+
)
|
|
22
|
+
from mammoth_cli.output.policy import VALID_OUTPUTS
|
|
23
|
+
from mammoth_cli.runtime import executor
|
|
24
|
+
from mammoth_cli.runtime import options as go
|
|
25
|
+
from mammoth_cli.runtime.invocation import Invocation
|
|
26
|
+
|
|
27
|
+
#: The timeout-family setting keys, shared between the settable-key registry
|
|
28
|
+
#: and the numeric-parsing branch below.
|
|
29
|
+
_TIMEOUT_KEYS = ("timeout", "job_timeout", "pipeline_timeout")
|
|
30
|
+
_PROFILE_KEYS = ("server_prefix", "project")
|
|
31
|
+
_SETTING_KEYS = ("output", *_TIMEOUT_KEYS)
|
|
32
|
+
ALL_CONFIG_KEYS = tuple(sorted(_PROFILE_KEYS + _SETTING_KEYS))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _profile_name(invocation: Invocation) -> str:
|
|
36
|
+
return invocation.profile or profiles.get_selected()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _unknown_key_error(key: str) -> CliError:
|
|
40
|
+
return CliError(
|
|
41
|
+
code="unknown_config_key",
|
|
42
|
+
message=f"'{key}' is not a recognized configuration key.",
|
|
43
|
+
exit_status=EXIT_USAGE,
|
|
44
|
+
hint=f"Use one of: {', '.join(ALL_CONFIG_KEYS)}.",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _profile_not_found_error(profile_name: str) -> CliError:
|
|
49
|
+
return CliError(
|
|
50
|
+
code=CODE_PROFILE_NOT_FOUND,
|
|
51
|
+
message=f"No profile named '{profile_name}' exists yet.",
|
|
52
|
+
exit_status=EXIT_USAGE,
|
|
53
|
+
hint="Run 'mammoth auth login' first.",
|
|
54
|
+
recovery_commands=["mammoth auth login"],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _validate_key(key: str) -> None:
|
|
59
|
+
if key not in _PROFILE_KEYS and key not in _SETTING_KEYS:
|
|
60
|
+
raise _unknown_key_error(key)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_value(profile_name: str, key: str) -> str | int | None:
|
|
64
|
+
if key in _SETTING_KEYS:
|
|
65
|
+
return profiles.get_setting(profile_name, key)
|
|
66
|
+
record = profiles.get_profile(profile_name)
|
|
67
|
+
if record is None:
|
|
68
|
+
return None
|
|
69
|
+
if key == "project":
|
|
70
|
+
return record.project_id
|
|
71
|
+
return record.server_prefix
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _require_profile(profile_name: str) -> profiles.ProfileRecord:
|
|
75
|
+
existing = profiles.get_profile(profile_name)
|
|
76
|
+
if existing is None:
|
|
77
|
+
raise _profile_not_found_error(profile_name)
|
|
78
|
+
return existing
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _set_value(profile_name: str, key: str, value: str) -> str | int | float:
|
|
82
|
+
if key == "output":
|
|
83
|
+
if value not in VALID_OUTPUTS:
|
|
84
|
+
raise CliError(
|
|
85
|
+
code=CODE_INVALID_CONFIG_VALUE,
|
|
86
|
+
message=f"'{value}' is not a valid --output mode.",
|
|
87
|
+
exit_status=EXIT_USAGE,
|
|
88
|
+
hint=f"Use one of: {', '.join(VALID_OUTPUTS)}.",
|
|
89
|
+
)
|
|
90
|
+
profiles.set_setting(profile_name, key, value)
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
if key in _TIMEOUT_KEYS:
|
|
94
|
+
try:
|
|
95
|
+
parsed = float(value)
|
|
96
|
+
except ValueError as exc:
|
|
97
|
+
raise CliError(
|
|
98
|
+
code=CODE_INVALID_CONFIG_VALUE,
|
|
99
|
+
message=f"'{value}' is not a number.",
|
|
100
|
+
exit_status=EXIT_USAGE,
|
|
101
|
+
) from exc
|
|
102
|
+
if parsed <= 0:
|
|
103
|
+
raise CliError(
|
|
104
|
+
code=CODE_INVALID_CONFIG_VALUE,
|
|
105
|
+
message="Timeouts must be positive.",
|
|
106
|
+
exit_status=EXIT_USAGE,
|
|
107
|
+
)
|
|
108
|
+
profiles.set_setting(profile_name, key, value)
|
|
109
|
+
return parsed
|
|
110
|
+
|
|
111
|
+
if key == "project":
|
|
112
|
+
try:
|
|
113
|
+
project_id = int(value)
|
|
114
|
+
except ValueError as exc:
|
|
115
|
+
raise CliError(
|
|
116
|
+
code=CODE_INVALID_CONFIG_VALUE,
|
|
117
|
+
message=f"'{value}' is not an integer project id.",
|
|
118
|
+
exit_status=EXIT_USAGE,
|
|
119
|
+
) from exc
|
|
120
|
+
if project_id <= 0:
|
|
121
|
+
raise CliError(
|
|
122
|
+
code=CODE_INVALID_CONFIG_VALUE,
|
|
123
|
+
message="project must be a positive integer.",
|
|
124
|
+
exit_status=EXIT_USAGE,
|
|
125
|
+
)
|
|
126
|
+
existing = _require_profile(profile_name)
|
|
127
|
+
profiles.save_profile(
|
|
128
|
+
profiles.ProfileRecord(
|
|
129
|
+
name=existing.name,
|
|
130
|
+
workspace_id=existing.workspace_id,
|
|
131
|
+
server_prefix=existing.server_prefix,
|
|
132
|
+
project_id=project_id,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
return project_id
|
|
136
|
+
|
|
137
|
+
if key == "server_prefix":
|
|
138
|
+
resolve_base_url(value)
|
|
139
|
+
existing = _require_profile(profile_name)
|
|
140
|
+
profiles.save_profile(
|
|
141
|
+
profiles.ProfileRecord(
|
|
142
|
+
name=existing.name,
|
|
143
|
+
workspace_id=existing.workspace_id,
|
|
144
|
+
server_prefix=value,
|
|
145
|
+
project_id=existing.project_id,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
return value
|
|
149
|
+
|
|
150
|
+
raise _unknown_key_error(key)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def config_get(
|
|
154
|
+
output: str = go.output_option(),
|
|
155
|
+
profile: str | None = go.profile_option(),
|
|
156
|
+
project: int | None = go.project_option(),
|
|
157
|
+
timeout: float | None = go.timeout_option(),
|
|
158
|
+
job_timeout: float | None = go.job_timeout_option(),
|
|
159
|
+
pipeline_timeout: float | None = go.pipeline_timeout_option(),
|
|
160
|
+
color: str = go.color_option(),
|
|
161
|
+
no_input: bool = go.no_input_option(),
|
|
162
|
+
no_progress: bool = go.no_progress_option(),
|
|
163
|
+
debug: bool = go.debug_option(),
|
|
164
|
+
input_file: str | None = go.input_file_option(),
|
|
165
|
+
input_format: str | None = go.input_format_option(),
|
|
166
|
+
key: str = typer.Argument(..., help="Configuration key."),
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Get one profile-scoped configuration value."""
|
|
169
|
+
invocation = go.make_invocation(
|
|
170
|
+
"config.get",
|
|
171
|
+
output=output,
|
|
172
|
+
profile=profile,
|
|
173
|
+
project=project,
|
|
174
|
+
timeout=timeout,
|
|
175
|
+
job_timeout=job_timeout,
|
|
176
|
+
pipeline_timeout=pipeline_timeout,
|
|
177
|
+
color=color,
|
|
178
|
+
no_input=no_input,
|
|
179
|
+
no_progress=no_progress,
|
|
180
|
+
debug=debug,
|
|
181
|
+
input_file=input_file,
|
|
182
|
+
input_format=input_format,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def producer() -> tuple[Any, dict[str, Any]]:
|
|
186
|
+
_validate_key(key)
|
|
187
|
+
profile_name = _profile_name(invocation)
|
|
188
|
+
value = _get_value(profile_name, key)
|
|
189
|
+
return {"key": key, "value": value, "profile": profile_name}, {"profile": profile_name}
|
|
190
|
+
|
|
191
|
+
executor.run(invocation.command_id, invocation.output, producer)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def config_set(
|
|
195
|
+
output: str = go.output_option(),
|
|
196
|
+
profile: str | None = go.profile_option(),
|
|
197
|
+
project: int | None = go.project_option(),
|
|
198
|
+
timeout: float | None = go.timeout_option(),
|
|
199
|
+
job_timeout: float | None = go.job_timeout_option(),
|
|
200
|
+
pipeline_timeout: float | None = go.pipeline_timeout_option(),
|
|
201
|
+
color: str = go.color_option(),
|
|
202
|
+
no_input: bool = go.no_input_option(),
|
|
203
|
+
no_progress: bool = go.no_progress_option(),
|
|
204
|
+
debug: bool = go.debug_option(),
|
|
205
|
+
input_file: str | None = go.input_file_option(),
|
|
206
|
+
input_format: str | None = go.input_format_option(),
|
|
207
|
+
key: str = typer.Argument(..., help="Configuration key."),
|
|
208
|
+
value: str = typer.Argument(..., help="New value."),
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Set one profile-scoped configuration value."""
|
|
211
|
+
invocation = go.make_invocation(
|
|
212
|
+
"config.set",
|
|
213
|
+
output=output,
|
|
214
|
+
profile=profile,
|
|
215
|
+
project=project,
|
|
216
|
+
timeout=timeout,
|
|
217
|
+
job_timeout=job_timeout,
|
|
218
|
+
pipeline_timeout=pipeline_timeout,
|
|
219
|
+
color=color,
|
|
220
|
+
no_input=no_input,
|
|
221
|
+
no_progress=no_progress,
|
|
222
|
+
debug=debug,
|
|
223
|
+
input_file=input_file,
|
|
224
|
+
input_format=input_format,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def producer() -> tuple[Any, dict[str, Any]]:
|
|
228
|
+
_validate_key(key)
|
|
229
|
+
profile_name = _profile_name(invocation)
|
|
230
|
+
stored = _set_value(profile_name, key, value)
|
|
231
|
+
return {"key": key, "value": stored, "profile": profile_name}, {"profile": profile_name}
|
|
232
|
+
|
|
233
|
+
executor.run(invocation.command_id, invocation.output, producer)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def config_list(
|
|
237
|
+
output: str = go.output_option(),
|
|
238
|
+
profile: str | None = go.profile_option(),
|
|
239
|
+
project: int | None = go.project_option(),
|
|
240
|
+
timeout: float | None = go.timeout_option(),
|
|
241
|
+
job_timeout: float | None = go.job_timeout_option(),
|
|
242
|
+
pipeline_timeout: float | None = go.pipeline_timeout_option(),
|
|
243
|
+
color: str = go.color_option(),
|
|
244
|
+
no_input: bool = go.no_input_option(),
|
|
245
|
+
no_progress: bool = go.no_progress_option(),
|
|
246
|
+
debug: bool = go.debug_option(),
|
|
247
|
+
input_file: str | None = go.input_file_option(),
|
|
248
|
+
input_format: str | None = go.input_format_option(),
|
|
249
|
+
) -> None:
|
|
250
|
+
"""List every profile-scoped configuration value."""
|
|
251
|
+
invocation = go.make_invocation(
|
|
252
|
+
"config.list",
|
|
253
|
+
output=output,
|
|
254
|
+
profile=profile,
|
|
255
|
+
project=project,
|
|
256
|
+
timeout=timeout,
|
|
257
|
+
job_timeout=job_timeout,
|
|
258
|
+
pipeline_timeout=pipeline_timeout,
|
|
259
|
+
color=color,
|
|
260
|
+
no_input=no_input,
|
|
261
|
+
no_progress=no_progress,
|
|
262
|
+
debug=debug,
|
|
263
|
+
input_file=input_file,
|
|
264
|
+
input_format=input_format,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def producer() -> tuple[Any, dict[str, Any]]:
|
|
268
|
+
profile_name = _profile_name(invocation)
|
|
269
|
+
values = {key: _get_value(profile_name, key) for key in ALL_CONFIG_KEYS}
|
|
270
|
+
return {"profile": profile_name, "values": values}, {"profile": profile_name}
|
|
271
|
+
|
|
272
|
+
executor.run(invocation.command_id, invocation.output, producer)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def config_path(
|
|
276
|
+
output: str = go.output_option(),
|
|
277
|
+
profile: str | None = go.profile_option(),
|
|
278
|
+
project: int | None = go.project_option(),
|
|
279
|
+
timeout: float | None = go.timeout_option(),
|
|
280
|
+
job_timeout: float | None = go.job_timeout_option(),
|
|
281
|
+
pipeline_timeout: float | None = go.pipeline_timeout_option(),
|
|
282
|
+
color: str = go.color_option(),
|
|
283
|
+
no_input: bool = go.no_input_option(),
|
|
284
|
+
no_progress: bool = go.no_progress_option(),
|
|
285
|
+
debug: bool = go.debug_option(),
|
|
286
|
+
input_file: str | None = go.input_file_option(),
|
|
287
|
+
input_format: str | None = go.input_format_option(),
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Print the path to the non-secret profiles file."""
|
|
290
|
+
invocation = go.make_invocation(
|
|
291
|
+
"config.path",
|
|
292
|
+
output=output,
|
|
293
|
+
profile=profile,
|
|
294
|
+
project=project,
|
|
295
|
+
timeout=timeout,
|
|
296
|
+
job_timeout=job_timeout,
|
|
297
|
+
pipeline_timeout=pipeline_timeout,
|
|
298
|
+
color=color,
|
|
299
|
+
no_input=no_input,
|
|
300
|
+
no_progress=no_progress,
|
|
301
|
+
debug=debug,
|
|
302
|
+
input_file=input_file,
|
|
303
|
+
input_format=input_format,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def producer() -> tuple[Any, dict[str, Any]]:
|
|
307
|
+
return {"profiles_path": str(profiles.profiles_path())}, {}
|
|
308
|
+
|
|
309
|
+
executor.run(invocation.command_id, invocation.output, producer)
|