Skip to content

Commit d1951f8

Browse files
MaorDavidzonclaude
andcommitted
Path-based command naming instead of summary parsing
Derive CLI command names from URL path structure instead of parsing English summaries. Rules: - Strip common prefix shared by endpoints in the tag - Remove path param segments ({id}) - Nothing left: 'list' (collection) or 'view' (single resource) - Otherwise: use remaining path segments as the name - Fix redundancy: 'cycode groups groups' -> 'cycode groups list' - Duplicates (deprecated endpoints) get -v2 suffix Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ffb3b45 commit d1951f8

File tree

1 file changed

+56
-32
lines changed

1 file changed

+56
-32
lines changed

cycode/cli/apps/api/api_command.py

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,33 +29,48 @@ def _normalize_tag(tag: str) -> str:
2929
return re.sub(r'[^a-z0-9]+', '-', tag.lower()).strip('-')
3030

3131

32-
def _normalize_command_name(summary: str) -> str:
33-
"""Derive a CLI subcommand name from an endpoint summary."""
34-
s = summary.strip()
35-
lower = s.lower()
36-
37-
if lower.startswith(('get all ', 'fetch all ', 'list all ', 'retrieve a paginated list of ')):
38-
return 'list'
39-
if lower.startswith('list '):
40-
return 'list'
41-
if lower.startswith(('fetch ', 'retrieve ', 'get all')):
42-
return 'list'
43-
44-
if re.match(r'^get\s+\w+\s+by\s+id$', lower):
45-
return 'get'
46-
47-
if re.match(r'^(get|retrieve)\s+(a\s+)?\w+(\s+details)?$', lower):
48-
return 'get'
32+
def _find_common_prefix(paths: list[str]) -> str:
33+
"""Find the longest common path prefix shared by all paths."""
34+
if not paths:
35+
return ''
36+
if len(paths) == 1:
37+
# For single-path tags, use the parent directory as prefix
38+
return '/'.join(paths[0].split('/')[:-1])
39+
40+
common = paths[0]
41+
for p in paths[1:]:
42+
while not p.startswith(common + '/') and common != p:
43+
common = '/'.join(common.split('/')[:-1])
44+
return common
45+
46+
47+
def _path_to_command_name(path: str, common_prefix: str, has_path_params: bool) -> str:
48+
"""Derive a CLI command name from an API path relative to the tag's common prefix.
49+
50+
Rules:
51+
1. Strip the common prefix shared by all endpoints in the tag
52+
2. Remove path parameter segments ({id})
53+
3. If nothing remains: 'list' (no path params) or 'view' (has path params)
54+
4. Otherwise: use remaining segments joined with hyphens
55+
56+
Examples:
57+
/v4/projects (prefix=/v4/projects) -> list
58+
/v4/projects/{id} (prefix=/v4/projects) -> view
59+
/v4/projects/assets (prefix=/v4/projects) -> assets
60+
/v4/violations/count (prefix=/v4/violations) -> count
61+
"""
62+
# Strip common prefix
63+
relative = path[len(common_prefix) :] if path.startswith(common_prefix) else path
64+
relative = relative.strip('/')
4965

50-
# Take meaningful words after the verb
51-
words = re.sub(r'[^a-z0-9\s]', '', lower).split()
52-
skip_verbs = {'get', 'fetch', 'retrieve', 'list', 'find', 'search', 'a', 'an', 'the', 'all', 'by'}
53-
meaningful = [w for w in words if w not in skip_verbs]
66+
# Remove path parameter segments and empty parts
67+
parts = [p for p in relative.split('/') if p and not p.startswith('{')]
5468

55-
if not meaningful:
56-
return re.sub(r'[^a-z0-9]+', '-', lower).strip('-')
69+
if not parts:
70+
return 'view' if has_path_params else 'list'
5771

58-
return '-'.join(meaningful[:3])
72+
# Join remaining segments with hyphens, normalize to kebab-case
73+
return re.sub(r'[^a-z0-9]+', '-', '-'.join(parts).lower()).strip('-')
5974

6075

6176
def _param_to_option_name(name: str) -> str:
@@ -121,20 +136,29 @@ def build_api_command_groups(
121136

122137
group = click.Group(name=tag_name, help=f'[EXPERIMENT] Cycode API: {tag}')
123138

139+
# Compute common prefix from all GET endpoint paths in this tag
140+
get_endpoints = [ep for ep in endpoints if ep['method'] == 'get']
141+
if not get_endpoints:
142+
continue
143+
144+
clean_paths = [re.sub(r'/\{[^}]+\}', '', ep['path']) for ep in get_endpoints]
145+
common_prefix = _find_common_prefix(clean_paths)
146+
124147
used_names: dict[str, int] = {}
125148

126-
for endpoint in endpoints:
127-
if endpoint['method'] != 'get':
128-
continue
149+
for endpoint in get_endpoints:
150+
has_path_params = bool(endpoint['path_params'])
151+
cmd_name = _path_to_command_name(endpoint['path'], common_prefix, has_path_params)
129152

130-
cmd_name = _normalize_command_name(endpoint['summary'])
153+
# Fix redundancy: if command name matches the tag name, use list/view
154+
# e.g. "cycode groups groups" -> "cycode groups list"
155+
if cmd_name == tag_name or cmd_name == tag_name.replace('-', '_'):
156+
cmd_name = 'view' if has_path_params else 'list'
131157

158+
# Handle duplicate names (e.g. deprecated + new endpoint for same resource)
132159
if cmd_name in used_names:
133160
used_names[cmd_name] += 1
134-
path_parts = endpoint['path'].strip('/').split('/')
135-
suffix = path_parts[-1] if path_parts else str(used_names[cmd_name])
136-
suffix = re.sub(r'[{}]', '', suffix)
137-
cmd_name = f'{cmd_name}-{suffix}'
161+
cmd_name = f'{cmd_name}-v{used_names[cmd_name]}'
138162
else:
139163
used_names[cmd_name] = 1
140164

0 commit comments

Comments
 (0)