ocmo-cli 0.8.19__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 (81) hide show
  1. LICENSE +201 -0
  2. NOTICE +16 -0
  3. ocmo_cli/__init__.py +3 -0
  4. ocmo_cli/__main__.py +4 -0
  5. ocmo_cli/_address.py +182 -0
  6. ocmo_cli/_audit_item.py +33 -0
  7. ocmo_cli/_auth_resolve.py +66 -0
  8. ocmo_cli/_body_payload.py +203 -0
  9. ocmo_cli/_click_groups.py +227 -0
  10. ocmo_cli/_client.py +351 -0
  11. ocmo_cli/_client_commands.py +35 -0
  12. ocmo_cli/_command_registry.py +29 -0
  13. ocmo_cli/_commands_map.py +65 -0
  14. ocmo_cli/_config.py +183 -0
  15. ocmo_cli/_config_content.py +43 -0
  16. ocmo_cli/_delete_item_output.py +129 -0
  17. ocmo_cli/_diff_input.py +144 -0
  18. ocmo_cli/_dry_run.py +292 -0
  19. ocmo_cli/_errors.py +151 -0
  20. ocmo_cli/_exit.py +13 -0
  21. ocmo_cli/_export_metadata.py +47 -0
  22. ocmo_cli/_generated_registry.py +45 -0
  23. ocmo_cli/_get_cast_output.py +52 -0
  24. ocmo_cli/_get_item_list.py +23 -0
  25. ocmo_cli/_globalpermission_output.py +98 -0
  26. ocmo_cli/_import_metadata.py +140 -0
  27. ocmo_cli/_import_plan.py +221 -0
  28. ocmo_cli/_item_output.py +223 -0
  29. ocmo_cli/_lazy_mixin.py +29 -0
  30. ocmo_cli/_ls_wide.py +286 -0
  31. ocmo_cli/_mutating.py +39 -0
  32. ocmo_cli/_notice.py +17 -0
  33. ocmo_cli/_options.py +77 -0
  34. ocmo_cli/_output.py +424 -0
  35. ocmo_cli/_output_manifest.py +332 -0
  36. ocmo_cli/_resolve_draft.py +50 -0
  37. ocmo_cli/_resolve_options.py +143 -0
  38. ocmo_cli/_resolve_output.py +228 -0
  39. ocmo_cli/_resolve_parameters_output.py +72 -0
  40. ocmo_cli/_resolve_series.py +89 -0
  41. ocmo_cli/_resolve_series_chart.py +409 -0
  42. ocmo_cli/_resolve_write.py +185 -0
  43. ocmo_cli/_resolver_output.py +34 -0
  44. ocmo_cli/_resource_aliases.py +22 -0
  45. ocmo_cli/_sdk_dispatch.py +310 -0
  46. ocmo_cli/_typing.py +27 -0
  47. ocmo_cli/_version_output.py +39 -0
  48. ocmo_cli/_whoami_output.py +43 -0
  49. ocmo_cli/commands/__init__.py +1 -0
  50. ocmo_cli/commands/api.py +125 -0
  51. ocmo_cli/commands/api_health.py +12 -0
  52. ocmo_cli/commands/apply.py +153 -0
  53. ocmo_cli/commands/auth.py +786 -0
  54. ocmo_cli/commands/can_i.py +51 -0
  55. ocmo_cli/commands/completion.py +59 -0
  56. ocmo_cli/commands/config_cmd.py +235 -0
  57. ocmo_cli/commands/describe.py +109 -0
  58. ocmo_cli/commands/diff.py +86 -0
  59. ocmo_cli/commands/edit.py +200 -0
  60. ocmo_cli/commands/export.py +242 -0
  61. ocmo_cli/commands/generated.py +907 -0
  62. ocmo_cli/commands/gp_move.py +108 -0
  63. ocmo_cli/commands/import_.py +595 -0
  64. ocmo_cli/commands/item_relocate.py +187 -0
  65. ocmo_cli/commands/ls.py +481 -0
  66. ocmo_cli/commands/resolve.py +658 -0
  67. ocmo_cli/commands/resolve_draft.py +102 -0
  68. ocmo_cli/commands/resolve_parameters.py +65 -0
  69. ocmo_cli/commands/resolve_series.py +109 -0
  70. ocmo_cli/commands/schema.py +112 -0
  71. ocmo_cli/commands/timeline.py +135 -0
  72. ocmo_cli/commands/version_cmd.py +102 -0
  73. ocmo_cli/commands/whoami.py +12 -0
  74. ocmo_cli/main.py +241 -0
  75. ocmo_cli/output_formats.yaml +295 -0
  76. ocmo_cli-0.8.19.dist-info/METADATA +169 -0
  77. ocmo_cli-0.8.19.dist-info/RECORD +81 -0
  78. ocmo_cli-0.8.19.dist-info/WHEEL +4 -0
  79. ocmo_cli-0.8.19.dist-info/entry_points.txt +2 -0
  80. ocmo_cli-0.8.19.dist-info/licenses/LICENSE +201 -0
  81. ocmo_cli-0.8.19.dist-info/licenses/NOTICE +16 -0
ocmo_cli/main.py ADDED
@@ -0,0 +1,241 @@
1
+ """Root CLI entry point.
2
+
3
+ All heavy imports (SDK, yaml, httpx) are deferred into command functions so
4
+ that `ocmo --help` returns within the 200 ms budget on a warm cache.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from typing import Any
11
+
12
+ import click
13
+
14
+ from ._click_groups import LazyGroup
15
+ from ._client import OcmoCtx
16
+ from ._command_registry import HAND_WRITTEN_SHORT_HELP
17
+ from ._lazy_mixin import ClickLazyMixin
18
+ from ._output import _OUTPUT_FORMATS
19
+
20
+
21
+ def _validate_output_global(ctx: click.Context, param: click.Parameter, value: str | None) -> str | None:
22
+ if value is None:
23
+ return value
24
+ if value in _OUTPUT_FORMATS or value.startswith("jsonpath="):
25
+ return value
26
+ valid = ", ".join(_OUTPUT_FORMATS)
27
+ raise click.BadParameter(
28
+ f"{value!r} is not a valid format. Valid values: {valid}, or jsonpath=<expr>.",
29
+ ctx=ctx,
30
+ param=param,
31
+ )
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Global flags — attached to every invocation via callback
36
+ # ---------------------------------------------------------------------------
37
+
38
+ CONTEXT_SETTINGS = dict(help_option_names=["--help", "-h"], max_content_width=120)
39
+
40
+
41
+ @click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
42
+ @click.version_option(package_name="ocmo-cli", prog_name="ocmo")
43
+ @click.option(
44
+ "-n",
45
+ "--namespace",
46
+ envvar="OCMO_NAMESPACE",
47
+ default=None,
48
+ metavar="NS",
49
+ help="Namespace (overrides config and OCMO_NAMESPACE).",
50
+ )
51
+ @click.option(
52
+ "-o",
53
+ "--output",
54
+ "output_fmt",
55
+ default=None,
56
+ metavar="FORMAT",
57
+ callback=lambda ctx, p, v: _validate_output_global(ctx, p, v),
58
+ is_eager=False,
59
+ help=(
60
+ "Output format: table, json, yaml, name, path, raw, or jsonpath=<expr>. "
61
+ "Default: table on TTY, yaml otherwise."
62
+ ),
63
+ )
64
+ @click.option(
65
+ "--dry-run", is_flag=True, default=False, help="Print what would be done without contacting mutating endpoints."
66
+ )
67
+ @click.option("-v", "--verbose", is_flag=True, default=False, help="Raise log level.")
68
+ @click.option("-q", "--quiet", is_flag=True, default=False, help="Suppress non-error stderr output.")
69
+ @click.option("--yes", "-y", is_flag=True, default=False, help="Skip confirmation prompts.")
70
+ @click.option("--no-color", is_flag=True, default=False, envvar="OCMO_NO_COLOR", help="Disable ANSI colour.")
71
+ @click.option(
72
+ "--skip-version-check",
73
+ is_flag=True,
74
+ default=False,
75
+ envvar="OCMO_SKIP_VERSION_CHECK",
76
+ help="Skip CLI/SDK/server version compatibility check.",
77
+ )
78
+ @click.pass_context
79
+ def cli(
80
+ ctx: click.Context,
81
+ namespace: str | None,
82
+ output_fmt: str | None,
83
+ dry_run: bool,
84
+ verbose: bool,
85
+ quiet: bool,
86
+ yes: bool,
87
+ no_color: bool,
88
+ skip_version_check: bool,
89
+ ) -> None:
90
+ """ocmo — configuration management CLI.
91
+
92
+ \b
93
+ Examples:
94
+ ocmo config set server https://ocmo.example.com
95
+ ocmo auth login
96
+ ocmo -n prod ls
97
+ ocmo -n prod get item app/web
98
+ ocmo -n prod resolve app/web --cast json
99
+ ocmo -n prod apply -f config.yaml app/web/main
100
+ """
101
+ if no_color:
102
+ os.environ["OCMO_NO_COLOR"] = "1"
103
+
104
+ ctx.ensure_object(dict)
105
+ ctx.obj = OcmoCtx(
106
+ namespace=namespace,
107
+ output=output_fmt,
108
+ dry_run=dry_run,
109
+ verbose=verbose,
110
+ quiet=quiet,
111
+ yes=yes,
112
+ skip_version_check=skip_version_check,
113
+ no_color=no_color,
114
+ )
115
+
116
+ if ctx.invoked_subcommand is None:
117
+ click.echo(ctx.get_help())
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Hand-written command modules
122
+ # ---------------------------------------------------------------------------
123
+
124
+
125
+ def _lazy_cmd(
126
+ name: str,
127
+ module: str,
128
+ attr: str,
129
+ short_help: str,
130
+ *,
131
+ hidden: bool = False,
132
+ ) -> click.Command:
133
+ """Return a placeholder that imports and delegates on first invocation."""
134
+
135
+ class LazyCommand(ClickLazyMixin, click.Command):
136
+ """A stub that replaces itself with the real command on first use."""
137
+
138
+ def __init__(self) -> None:
139
+ super().__init__(name)
140
+ self.short_help = short_help
141
+ self.hidden = hidden
142
+
143
+ def _lazy_target(self) -> click.Command:
144
+ return self._real()
145
+
146
+ def _real(self) -> click.Command:
147
+ import importlib
148
+
149
+ mod = importlib.import_module(f".commands.{module}", package="ocmo_cli")
150
+ cmd = getattr(mod, attr)
151
+ assert isinstance(cmd, click.Command)
152
+ return cmd
153
+
154
+ def get_params(self, ctx: click.Context) -> list[click.Parameter]:
155
+ return self._real().get_params(ctx)
156
+
157
+ def get_help(self, ctx: click.Context) -> str:
158
+ return self._real().get_help(ctx)
159
+
160
+ def get_short_help_str(self, limit: int = 150) -> str:
161
+ text = self.short_help or ""
162
+ if limit and len(text) > limit:
163
+ return text[: limit - 3] + "..."
164
+ return text
165
+
166
+ def main( # type: ignore[override]
167
+ self,
168
+ args: list[str] | None = None,
169
+ prog_name: str | None = None,
170
+ complete_var: str | None = None,
171
+ standalone_mode: bool = True,
172
+ **extra: Any,
173
+ ) -> Any:
174
+ return self._real().main(
175
+ args,
176
+ prog_name=prog_name,
177
+ complete_var=complete_var,
178
+ standalone_mode=standalone_mode,
179
+ **extra,
180
+ )
181
+
182
+ return LazyCommand()
183
+
184
+
185
+ def _register_hand_written() -> None:
186
+ # Register lazy stubs — actual imports happen only when the command runs.
187
+ for cli_name, (module, attr, short_help) in HAND_WRITTEN_SHORT_HELP.items():
188
+ cli.add_command(_lazy_cmd(cli_name, module, attr, short_help), name=cli_name)
189
+
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # Generated command groups (built from commands.yaml on first use)
193
+ # ---------------------------------------------------------------------------
194
+
195
+
196
+ def _build_generated_action(action: str) -> click.Group:
197
+ from .commands.generated import build_action_group
198
+
199
+ return build_action_group(action)
200
+
201
+
202
+ def _register_generated() -> None:
203
+ from ._generated_registry import action_help, generated_action_names
204
+
205
+ # Hand-written commands take precedence — skip generated groups that share a name
206
+ existing_names = set(cli.commands or {})
207
+ for action in generated_action_names():
208
+ if action not in existing_names:
209
+
210
+ def _build(action_name: str = action) -> click.Group:
211
+ return _build_generated_action(action_name)
212
+
213
+ cli.add_command(
214
+ LazyGroup(
215
+ action,
216
+ help=action_help(action),
217
+ build_fn=_build,
218
+ ),
219
+ name=action,
220
+ )
221
+
222
+ cli.add_command(
223
+ _lazy_cmd(
224
+ "api",
225
+ "api",
226
+ "api_cmd",
227
+ "Invoke any API operation directly by operation_id.",
228
+ hidden=True,
229
+ ),
230
+ name="api",
231
+ )
232
+
233
+ from .commands.gp_move import register_gp_move_command
234
+ from .commands.item_relocate import register_item_relocate_commands
235
+
236
+ register_item_relocate_commands(cli)
237
+ register_gp_move_command(cli)
238
+
239
+
240
+ _register_hand_written()
241
+ _register_generated()
@@ -0,0 +1,295 @@
1
+ # Output format manifest — source of truth for OCMO CLI -o/--output behavior.
2
+ #
3
+ # Command keys use space-separated paths: "get namespace", "ls", "resolve".
4
+ # Generated commands use "{action} {resource}" (canonical resource name).
5
+ #
6
+ # Profile ``supported_formats`` and defaults merge into each command entry.
7
+ # Per-command ``table`` / ``wide`` / ``raw`` blocks configure column layout.
8
+
9
+ profiles:
10
+ list_wide:
11
+ supported_formats: [table, wide, json, yaml, name]
12
+ default_tty: table
13
+ default_non_tty: yaml
14
+ name_field: name
15
+
16
+ list:
17
+ supported_formats: [table, json, yaml, name]
18
+ default_tty: table
19
+ default_non_tty: yaml
20
+ name_field: name
21
+
22
+ list_versions:
23
+ supported_formats: [table, wide, json, yaml, name]
24
+ default_tty: table
25
+ default_non_tty: yaml
26
+ name_field: version
27
+
28
+ document:
29
+ supported_formats: [raw, json, yaml, name]
30
+ default: raw
31
+ name_field: path
32
+
33
+ ls:
34
+ supported_formats: [table, wide, json, yaml, name, path]
35
+ default_tty: table
36
+ default_non_tty: yaml
37
+ name_field: path
38
+
39
+ resolve:
40
+ supported_formats: [raw, json, yaml, name]
41
+ default: raw
42
+ name_field: name
43
+
44
+ can_i:
45
+ supported_formats: [table, json, yaml]
46
+ default_tty: table
47
+ default_non_tty: yaml
48
+
49
+ schema:
50
+ supported_formats: [table, json, yaml]
51
+ default_tty: yaml
52
+ default_non_tty: yaml
53
+
54
+ commands:
55
+ # ── Hand-written ──────────────────────────────────────────────────────────
56
+
57
+ ls:
58
+ profile: ls
59
+ table:
60
+ fields: [type, path]
61
+ wide:
62
+ fields: [permissions, versions, author, created, updated, type, path]
63
+
64
+ resolve:
65
+ profile: resolve
66
+ raw:
67
+ mode: metadata_and_data
68
+ hide_null_values: true
69
+
70
+ can-i:
71
+ profile: can_i
72
+
73
+ whoami:
74
+ profile: can_i
75
+ table:
76
+ fields: "*"
77
+
78
+ api-health:
79
+ profile: list
80
+ name_field: status
81
+
82
+ schema:
83
+ supported_formats: [json, yaml]
84
+ default_tty: yaml
85
+ default_non_tty: yaml
86
+
87
+ auth status:
88
+ profile: schema
89
+
90
+ edit config:
91
+ profile: document
92
+
93
+ edit template:
94
+ profile: document
95
+
96
+ edit secret:
97
+ profile: document
98
+
99
+ edit resolver:
100
+ profile: document
101
+
102
+ # ── Generated: get ────────────────────────────────────────────────────────
103
+
104
+ get namespace:
105
+ profile: list_wide
106
+ table:
107
+ fields: [name, description]
108
+ wide:
109
+ fields: [name, description, permissions_tag, webhooks_tag, git_sync_tag, created_at, updated_at]
110
+
111
+ get globalpermission:
112
+ profile: list_wide
113
+ name_field: id
114
+ table:
115
+ fields: [id, namespace, position]
116
+ wide:
117
+ fields: [id, namespace, position, read, write, delete, audit]
118
+
119
+ get lock:
120
+ profile: list_wide
121
+ name_field: path
122
+ table:
123
+ fields: [path, reason, locked_by]
124
+ wide:
125
+ fields: "*"
126
+
127
+ get audit:
128
+ profile: list_wide
129
+ name_field: id
130
+ table:
131
+ fields: [occurred_at, id, operation, object_id]
132
+ wide:
133
+ fields: [occurred_at, id, operation, object_id, auth_email, object_type, object_version, error]
134
+
135
+ get cast:
136
+ profile: schema
137
+
138
+ get cast list:
139
+ profile: list
140
+ name_field: format
141
+ table:
142
+ fields: [format]
143
+
144
+ get version:
145
+ profile: list_versions
146
+ table:
147
+ fields: [version, tags, updated_at]
148
+ wide:
149
+ fields: "*"
150
+
151
+ get item:
152
+ profile: document
153
+
154
+ get item list:
155
+ profile: list
156
+ name_field: path
157
+ table:
158
+ fields: [node_type, path, name]
159
+
160
+ # ── Generated: create ─────────────────────────────────────────────────────
161
+
162
+ create namespace:
163
+ profile: list
164
+
165
+ create globalpermission:
166
+ profile: document
167
+ name_field: id
168
+
169
+ create config:
170
+ profile: document
171
+
172
+ create template:
173
+ profile: document
174
+
175
+ create secret:
176
+ profile: document
177
+
178
+ create resolver:
179
+ profile: document
180
+
181
+ create lock:
182
+ profile: list
183
+ name_field: path
184
+
185
+ # ── Generated: update / delete / move / copy / tag ─────────────────────────
186
+
187
+ update namespace:
188
+ profile: list
189
+
190
+ update globalpermission:
191
+ profile: document
192
+ name_field: id
193
+
194
+ update config:
195
+ profile: document
196
+
197
+ update template:
198
+ profile: document
199
+
200
+ update secret:
201
+ profile: document
202
+
203
+ update resolver:
204
+ profile: document
205
+
206
+ update lock:
207
+ profile: list
208
+ name_field: path
209
+
210
+ delete namespace:
211
+ profile: list
212
+ name_field: namespace
213
+
214
+ delete item:
215
+ profile: list
216
+ name_field: path
217
+ supported_formats: [table, json, yaml, name, path]
218
+ table:
219
+ fields: [type, version, path]
220
+
221
+ delete globalpermission:
222
+ profile: list
223
+ name_field: id
224
+
225
+ delete lock:
226
+ profile: list
227
+ name_field: path
228
+
229
+ tag item:
230
+ profile: list_versions
231
+ table:
232
+ fields: [version, tags, updated_at]
233
+ wide:
234
+ fields: "*"
235
+
236
+ untag item:
237
+ profile: list_versions
238
+ table:
239
+ fields: [version, tags, updated_at]
240
+ wide:
241
+ fields: "*"
242
+
243
+ propagate config:
244
+ profile: list
245
+ name_field: path
246
+
247
+ search tree:
248
+ profile: list
249
+ name_field: path
250
+
251
+ resolve-series audit:
252
+ supported_formats: [chart, json, yaml]
253
+ default_tty: chart
254
+ default_non_tty: yaml
255
+
256
+ resolve parameters:
257
+ supported_formats: [table, json, yaml]
258
+ default_tty: table
259
+ default_non_tty: yaml
260
+ table:
261
+ fields: [name, type, value, resolved_value, description, transformers]
262
+
263
+ resolve draft:
264
+ profile: resolve
265
+
266
+ # Fallback for generated commands not listed explicitly.
267
+ _generated_defaults:
268
+ get:
269
+ profile: list_wide
270
+ create:
271
+ profile: list
272
+ update:
273
+ profile: list
274
+ delete:
275
+ profile: list
276
+ move:
277
+ profile: list
278
+ copy:
279
+ profile: list
280
+ tag:
281
+ profile: list
282
+ untag:
283
+ profile: list
284
+ rotate:
285
+ profile: list
286
+ propagate:
287
+ profile: list
288
+ search:
289
+ profile: list_wide
290
+ timeline:
291
+ profile: list_wide
292
+ resolve-series:
293
+ profile: list_wide
294
+ resolve:
295
+ profile: resolve
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.5
2
+ Name: ocmo-cli
3
+ Version: 0.8.19
4
+ Summary: Command-line interface for the OCMO configuration service
5
+ Project-URL: Homepage, https://github.com/ocmo-oss/ocmo
6
+ Project-URL: Repository, https://github.com/ocmo-oss/ocmo
7
+ Project-URL: Documentation, https://github.com/ocmo-oss/ocmo/tree/main/cli
8
+ Author: OCMO OSS Contributors
9
+ License: Apache-2.0
10
+ License-File: LICENSE
11
+ License-File: NOTICE
12
+ Keywords: cli,configuration,devops,yaml
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: License :: OSI Approved :: Apache Software License
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: System :: Systems Administration
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: click<9,>=8.1
25
+ Requires-Dist: ocmo-sdk<0.9,>=0.8.19
26
+ Requires-Dist: pyyaml>=6.0
27
+ Requires-Dist: rich>=13.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ocmo-cli
31
+
32
+ Command-line interface for the OCMO configuration service. A thin shell over
33
+ [`ocmo-sdk`](https://pypi.org/project/ocmo-sdk/) with a `kubectl`-shaped verb/noun grammar. The CLI
34
+ contains no API protocol logic of its own.
35
+
36
+ **Distribution:** PyPI name `ocmo-cli` · **Python:** 3.11+ · **License:** Apache-2.0
37
+
38
+ **Pre-1.0.0:** Pin `ocmo-cli` and `ocmo-sdk` to the same release as your OCMO API server.
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ Published package:
45
+
46
+ ```bash
47
+ uv tool install ocmo-cli
48
+ # or
49
+ pipx install ocmo-cli
50
+ ```
51
+
52
+ Monorepo development (from repository root):
53
+
54
+ ```bash
55
+ uv sync --all-packages
56
+ uv run ocmo --help
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Quick start
62
+
63
+ ```bash
64
+ # Point at your server (saved to ~/.config/ocmo/config.yaml)
65
+ ocmo config set server https://ocmo.example.com
66
+ ocmo config set namespace prod
67
+
68
+ # Authenticate
69
+ ocmo auth login
70
+
71
+ # Use it
72
+ ocmo -n prod get config app/web
73
+ ocmo -n prod resolve app/web --cast json
74
+ ocmo -n prod ls tree app/
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Configuration
80
+
81
+ Precedence (same model as `kubectl`):
82
+
83
+ ```text
84
+ command flag > OCMO_* env var > current context in ~/.config/ocmo/config.yaml > built-in default
85
+ ```
86
+
87
+ Manage contexts:
88
+
89
+ ```bash
90
+ ocmo config get-contexts
91
+ ocmo config use-context prod
92
+ ocmo config set server https://ocmo.example.com
93
+ ocmo config view
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Output formats
99
+
100
+ `-o, --output` accepts: `table` (default on TTY), `wide`, `json`, `yaml`, `name`, `raw`,
101
+ `jsonpath=<expr>`.
102
+
103
+ ---
104
+
105
+ ## Exit codes
106
+
107
+ | Code | Meaning |
108
+ |------|---------|
109
+ | 0 | Success |
110
+ | 1 | Generic runtime failure |
111
+ | 2 | Usage error |
112
+ | 3 | Not found (404) |
113
+ | 4 | Auth / permission failure (401, 403) |
114
+ | 5 | Conflict (409) |
115
+ | 6 | Path locked (423) |
116
+ | 7 | Validation failure (422, 413) |
117
+ | 8 | Hook execution failure |
118
+ | 9 | Import/export verification failure |
119
+ | 130 | Interrupted |
120
+
121
+ ---
122
+
123
+ ## Development
124
+
125
+ From repository root (workspace includes CLI and SDK):
126
+
127
+ ```bash
128
+ uv sync --all-packages
129
+ uv run pytest cli/tests -q
130
+ uv run ruff check cli/
131
+ uv run mypy cli/ocmo_cli
132
+ ```
133
+
134
+ Regenerate the command tree from the SDK operation map:
135
+
136
+ ```bash
137
+ cd cli
138
+ uv run python scripts/generate_commands.py --write
139
+ ```
140
+
141
+ Integration smoke tests (live API):
142
+
143
+ ```bash
144
+ OCMO_RUN_INTEGRATION=1 uv run pytest cli/tests/test_smoke_integration.py -v
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Architecture notes
150
+
151
+ ### Command surface
152
+
153
+ Generated operations are registered at import time from committed `commands.yaml` (produced from
154
+ `sdk/operations.yaml`). CI fails if the file drifts from the spec. Hand-written commands live in
155
+ `ocmo_cli/commands/`.
156
+
157
+ ### Framework
158
+
159
+ Built on **click** (not typer) for fast cold start, dynamic command registration, and programmatic
160
+ `ocmo api` escape hatch.
161
+
162
+ ---
163
+
164
+ ## Related components
165
+
166
+ - SDK: [../sdk/README.md](../sdk/README.md)
167
+ - API: [../api/README.md](../api/README.md)
168
+ - Integration guides: [../docs/how-to/README.md](../docs/how-to/README.md) · [../docs/reference/cli.md](../docs/reference/cli.md)
169
+ - Monorepo overview: [../README.md](../README.md)