This change fixes vercel-labs/specli#7. specli exec crashes with the error cannot add command 'list-priorities' when an OpenAPI specification (spec) defines more than one collection GET operation under a single tag. The Atlassian Jira Cloud REST API v3 spec triggers this crash, which makes it unusable with specli.
After this change, you can run specli exec against specs that define multiple collection GET operations under one tag.
What causes the crash
planOperations renames operations when their resource:action pair repeats, but it doesn't check whether the renamed actions are themselves unique. In the reproduction case from the issue, two operationId values reduce to the same disambiguator:
getPrioritiesloses itsget-prefix, becomespriorities, and produceslist-priorities.searchPrioritiesloses itssearch-prefix, becomespriorities, and produceslist-priorities.
Commander throws an error when it registers the second command. The code that resolves collisions creates a collision of its own.
How the fix works
Disambiguation now generates a list of candidate names and selects the first name that no other command claims:
disambiguationCandidatesreplacesderiveDisambiguatedAction. It returns the candidate names, which are the name derived from theoperationIdfollowed by a path segment, instead of selecting one name without checking it.pickUnclaimedActionchecks each candidate against the set of claimedresource:actionkeys. When every candidate is taken, it appends a numeric suffix.planOperationsadds the already-unique names to the claimed set before disambiguation begins, so a renamed command can't take a name that another command already uses. It adds each new name to the set as it assigns it.
Uniqueness is now a property of the algorithm rather than an assumption. The numeric suffix replaces the per-collision-group counter.
The change affects one file and about 40 lines. The path-segment loop changes only from return to push and break, because the function now collects candidates instead of returning them one at a time.
Validation against the Jira Cloud v3 spec
The live spec from developer.atlassian.com contains 617 operations across 2.4 MB. Before this change, specli exec crashes in addGeneratedCommands. After this change, the spec loads and every command registers.
| Check | Result |
|---|---|
| Operations in the spec | 617 |
| Commands in the model | 617, so the fix drops no operations |
| Distinct operation keys | 617, so no command shadows another |
Duplicate resource action names | 0, down from four names across eight operations |
Of the 414 operations that require disambiguation, 412 receive a name derived from the operationId or the path, and two fall back to a numeric suffix.
Naming changes on this spec
Four names collided before this change. The fix renames those four operations and no others. In each pair, the first operation keeps its name and the second operation receives a new name.
| Operation | operationId | Name before | Name after |
|---|---|---|---|
GET /priority | getPriorities | issue-priorities list-priorities | Unchanged |
GET /priority/search | searchPriorities | issue-priorities list-priorities, a duplicate | issue-priorities list-search |
GET /resolution | getResolutions | issue-resolutions list-resolutions | Unchanged |
GET /resolution/search | searchResolutions | issue-resolutions list-resolutions, a duplicate | issue-resolutions list-search |
GET /issue/{issueIdOrKey}/worklog | getIssueWorklog | issue-worklogs get-worklog | Unchanged |
GET /issue/{issueIdOrKey}/worklog/{id} | getWorklog | issue-worklogs get-worklog, a duplicate | issue-worklogs get-1 |
GET /project/{projectIdOrKey}/role | getProjectRoles | project-roles get-role | Unchanged |
GET /project/{projectIdOrKey}/role/{id} | getProjectRole | project-roles get-role, a duplicate | project-roles get-1 |
The two /search operations receive descriptive names. The other two operations receive get-1, because their paths follow the /{parent}/{child}/{id} pattern, in which every segment that isn't a parameter already matches the resource name, and their operationId values contain no additional information. These two names are unique and correct, but they don't describe the operation. For a possible improvement, see Follow-up work.
Specs without collisions
The same comparison against other published specs produces no naming changes:
| Spec | Operations | Names changed |
|---|---|---|
| Box | 296 | 0 |
| Swagger Petstore v3 | 19 | 0 |
The Jira comparison also shows no renumbering. Wherever the previous counter produced a valid name, the numeric suffix produces the same name.
Two other specs remain unevaluated. The DigitalOcean spec spans multiple files with relative $ref values, so it doesn't resolve on its own. The Stripe spec, at 8 MB, didn't finish dereferencing within nine minutes in the test environment.
Behavior
The eight rows in the preceding table describe every naming change. The existing test expectations, which are get-1, create-v13, get-events, and get-profile, remain unchanged.
The reproduction case from the issue now produces two working commands:
$ specli exec ./repro.json issue-priorities list-priorities --curl
curl -sS -X GET 'https://example.com/priority'
$ specli exec ./repro.json issue-priorities list-search --curl
curl -sS -X GET 'https://example.com/priority/search'Test results
naming.test.ts gains two regression tests: one for two operations whose operationId values reduce to the same disambiguator, and one for a renamed command that collides with an unrelated command.
bun test: 135 tests pass, and none fail.bun run lint: no errors.bun run typecheck: no errors.
Follow-up work
The following items fall outside the scope of this change:
- Version segments produce unhelpful names. The path fallback selects the last segment that doesn't match the resource, so a path such as
/rest/api/3/dashboard/{id}produces a name such asdashboards get-3. This behavior predates this change. Skipping segments that contain only a version number would improve about 15 names in the Jira spec. - The
typecheckscript writes build output. The script runstsgowithout--noEmit, so it createsdist/. As a result,biome cichecks 224 files instead of 82, andbun testruns each test twice: once fromsrcand once from the compiled copy. - Seven tests depend on network access. These tests call
petstore3.swagger.ioandhttpbin.org, so they time out in environments that restrict outbound requests.