Skip to content

Commit baad150

Browse files
MaorDavidzonclaude
andcommitted
Move generated commands under cycode platform namespace
Address review feedback: top-level registration caused namespace collisions with curated commands (scan, auth, status, report) and triggered an OpenAPI spec fetch on every invocation, leaking warnings into unrelated commands like `cycode status`. Generated commands now live under a single `platform` group that lazy-loads the spec only when the user enters it. `cycode scan`, `cycode status`, and all other commands no longer touch the spec or surface API-related warnings. Tag groups are labeled `[BETA]` to make the maturity level explicit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0249e53 commit baad150

File tree

4 files changed

+130
-35
lines changed

4 files changed

+130
-35
lines changed

README.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ This guide walks you through both installation and usage.
2121
2. [Available Options](#available-options)
2222
3. [MCP Tools](#mcp-tools)
2323
4. [Usage Examples](#usage-examples)
24-
5. [Scan Command](#scan-command)
24+
5. [Platform Command](#platform-command-beta)
25+
1. [Discovering Commands](#discovering-commands)
26+
2. [Examples](#platform-examples)
27+
3. [Notes & Limitations](#platform-notes--limitations)
28+
6. [Scan Command](#scan-command)
2529
1. [Running a Scan](#running-a-scan)
2630
1. [Options](#options)
2731
1. [Severity Threshold](#severity-option)
@@ -605,6 +609,64 @@ This information can be helpful when:
605609
- Debugging transport-specific issues
606610

607611

612+
# Platform Command \[BETA\]
613+
614+
> [!WARNING]
615+
> The `platform` command is in **beta**. Commands, arguments, and output formats are generated dynamically from the Cycode API spec and may change between releases without notice. Do not rely on them in production automation yet.
616+
617+
The `cycode platform` command exposes the Cycode platform's read APIs as CLI commands. It groups endpoints by resource (e.g. `projects`, `violations`, `workflows`) and turns each endpoint's parameters into typed CLI arguments and `--option` flags.
618+
619+
```bash
620+
cycode platform projects list --page-size 50
621+
cycode platform violations count
622+
cycode platform workflows view <workflow-id>
623+
```
624+
625+
The OpenAPI spec is fetched from the Cycode API on first use and cached at `~/.cycode/openapi-spec.json` for 24 hours. Unrelated commands (`cycode scan`, `cycode status`, etc.) do not trigger a fetch.
626+
627+
> [!NOTE]
628+
> You must be authenticated (`cycode auth` or `CYCODE_CLIENT_ID` / `CYCODE_CLIENT_SECRET` environment variables) for `cycode platform` to discover and run commands. Other Cycode CLI commands work without authentication.
629+
630+
## Discovering Commands
631+
632+
Because commands are generated from the spec, the source of truth for what's available is `--help`:
633+
634+
```bash
635+
cycode platform --help # list all resource groups
636+
cycode platform projects --help # list actions on a resource
637+
cycode platform projects list --help # list options/arguments for an action
638+
```
639+
640+
## Platform Examples
641+
642+
```bash
643+
# List projects with pagination
644+
cycode platform projects list --page-size 25
645+
646+
# View a single project by ID
647+
cycode platform projects view <project-id>
648+
649+
# Count violations across the tenant
650+
cycode platform violations count
651+
652+
# Filter using query parameters (see `--help` for what each endpoint supports)
653+
cycode platform violations list --severity CRITICAL
654+
```
655+
656+
All output is JSON by default — pipe it through `jq` for ad-hoc filtering:
657+
658+
```bash
659+
cycode platform projects list --page-size 100 | jq '.items[].name'
660+
```
661+
662+
## Platform Notes & Limitations
663+
664+
- **Read-only today.** Only `GET` endpoints are exposed in this beta.
665+
- **Spec-driven.** Adding a new endpoint to the API surfaces it automatically the next time the cache is refreshed.
666+
- **No bundled spec.** The first `cycode platform` invocation after install (or after the 24h cache expires) will perform a network fetch with a 3-second timeout; if it times out, it continues fetching in the background so the next run is fast.
667+
- **Override the cache TTL** with `CYCODE_SPEC_CACHE_TTL=<seconds>`.
668+
669+
608670
# Scan Command
609671
610672
## Running a Scan

cycode/cli/app.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from cycode import __version__
1313
from cycode.cli.apps import ai_guardrails, ai_remediation, auth, configure, ignore, report, report_import, scan, status
14-
from cycode.cli.apps.api import get_api_command_groups
14+
from cycode.cli.apps.api import get_platform_group
1515

1616
if sys.version_info >= (3, 10):
1717
from cycode.cli.apps import mcp
@@ -58,30 +58,26 @@
5858
if sys.version_info >= (3, 10):
5959
app.add_typer(mcp.app)
6060

61-
# Register API v4 command groups (dynamically built from OpenAPI spec).
62-
# Typer resolves to a Click group on invocation. We store the API groups
63-
# and inject them via a registered_callback that runs at group resolution time.
64-
_api_groups_to_register = get_api_command_groups()
65-
66-
67-
# Monkey-patch typer.main.get_group to inject API Click groups into the Typer CLI.
68-
# This is necessary because Typer doesn't support adding native Click groups directly.
69-
# Typer creates a new Click group on each get_group() call, so we intercept that call
70-
# and add our API groups to the result. The `app_typer is app` guard ensures we only
71-
# modify our own app, not other Typer apps in the process.
61+
# Register the `platform` command group (dynamically built from the OpenAPI spec).
62+
# The group itself is constructed cheaply at import time; the spec is only fetched
63+
# when the user actually invokes `cycode platform ...`. Unrelated commands like
64+
# `cycode scan` and `cycode status` never trigger a spec fetch.
65+
#
66+
# Typer doesn't support adding native Click groups directly, so we monkey-patch
67+
# typer.main.get_group to inject our `platform` group into the resolved Click group.
68+
# The `app_typer is app` guard ensures we only modify our own app.
69+
_platform_group = get_platform_group()
7270
_original_get_group = typer.main.get_group
7371

7472

75-
def _get_group_with_api(app_typer: typer.Typer) -> click.Group:
73+
def _get_group_with_platform(app_typer: typer.Typer) -> click.Group:
7674
group = _original_get_group(app_typer)
77-
if app_typer is app and _api_groups_to_register:
78-
for api_group, api_name in _api_groups_to_register:
79-
if api_name not in group.commands:
80-
group.add_command(api_group, api_name)
75+
if app_typer is app and _platform_group.name not in group.commands:
76+
group.add_command(_platform_group, _platform_group.name)
8177
return group
8278

8379

84-
typer.main.get_group = _get_group_with_api
80+
typer.main.get_group = _get_group_with_platform
8581

8682

8783
def check_latest_version_on_close(ctx: typer.Context) -> None:

cycode/cli/apps/api/__init__.py

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
"""Cycode API v4 CLI commands.
1+
"""Cycode platform API CLI commands.
22
33
Dynamically builds CLI command groups from the Cycode API v4 OpenAPI spec.
4-
The spec is fetched from the API and cached locally for 24 hours.
4+
The spec is fetched lazily — only when the user invokes `cycode platform ...` —
5+
and cached locally for 24 hours.
56
"""
67

78
from typing import Optional
@@ -10,21 +11,57 @@
1011

1112
from cycode.logger import get_logger
1213

13-
logger = get_logger('API')
14+
logger = get_logger('Platform')
1415

16+
_PLATFORM_HELP = (
17+
'[BETA] Access the Cycode platform.\n\n'
18+
'Commands are generated dynamically from the Cycode API spec and may change '
19+
'between releases. The spec is fetched on first use and cached for 24 hours.'
20+
)
1521

16-
def get_api_command_groups(
17-
client_id: Optional[str] = None,
18-
client_secret: Optional[str] = None,
19-
) -> list[tuple[click.Group, str]]:
20-
"""Get API command groups built from the OpenAPI spec.
2122

22-
Returns empty list if the spec is not available (not authenticated, no cache).
23+
class PlatformGroup(click.Group):
24+
"""Lazy-loading Click group for `cycode platform` subcommands.
25+
26+
The OpenAPI spec is only fetched when the user actually invokes
27+
`cycode platform ...` (or asks for its help). Unrelated commands like
28+
`cycode scan` or `cycode status` never trigger a spec fetch.
2329
"""
24-
try:
25-
from cycode.cli.apps.api.api_command import build_api_command_groups
2630

27-
return build_api_command_groups(client_id, client_secret)
28-
except Exception as e:
29-
logger.debug('Could not load API commands: %s', e)
30-
return []
31+
_loaded: bool = False
32+
33+
def _ensure_loaded(self, ctx: Optional[click.Context]) -> None:
34+
if self._loaded:
35+
return
36+
self._loaded = True # set first to avoid re-entrancy on errors
37+
38+
client_id = client_secret = None
39+
if ctx is not None:
40+
root = ctx.find_root()
41+
if root.obj:
42+
client_id = root.obj.get('client_id')
43+
client_secret = root.obj.get('client_secret')
44+
45+
try:
46+
from cycode.cli.apps.api.api_command import build_api_command_groups
47+
48+
for sub_group, name in build_api_command_groups(client_id, client_secret):
49+
if name not in self.commands:
50+
self.add_command(sub_group, name)
51+
except Exception as e:
52+
logger.debug('Could not load platform commands: %s', e)
53+
# Surface the error to the user only when they're inside `platform`
54+
click.echo(f'Error loading Cycode platform commands: {e}', err=True)
55+
56+
def list_commands(self, ctx: click.Context) -> list[str]:
57+
self._ensure_loaded(ctx)
58+
return super().list_commands(ctx)
59+
60+
def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]:
61+
self._ensure_loaded(ctx)
62+
return super().get_command(ctx, cmd_name)
63+
64+
65+
def get_platform_group() -> click.Group:
66+
"""Return the top-level `platform` Click group (lazy-loading)."""
67+
return PlatformGroup(name='platform', help=_PLATFORM_HELP, no_args_is_help=True)

cycode/cli/apps/api/api_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def build_api_command_groups(
134134
for tag, endpoints in groups.items():
135135
tag_name = _normalize_tag(tag)
136136

137-
group = click.Group(name=tag_name, help=f'[EXPERIMENT] Cycode API: {tag}')
137+
group = click.Group(name=tag_name, help=f'[BETA] {tag}')
138138

139139
# Compute common prefix from all GET (non-deprecated) endpoint paths in this tag
140140
get_endpoints = [ep for ep in endpoints if ep['method'] == 'get' and not ep.get('deprecated')]

0 commit comments

Comments
 (0)