Skip to content

feat(cli): add tree command#2869

Draft
kanoru3101 wants to merge 45 commits into
mainfrom
feat/graph-command
Draft

feat(cli): add tree command#2869
kanoru3101 wants to merge 45 commits into
mainfrom
feat/graph-command

Conversation

@kanoru3101

@kanoru3101 kanoru3101 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What/Why/How?

Adds a redocly tree command that prints the structure of an API description — its
paths, operations, and the component $ref chains between them — for quick orientation
in any API (single- or multi-file) and $ref impact analysis in CI and code review.

The default mode renders the document structure (root → paths → operations → component
chains);
--affected-by <component|path|file> shows only the impacted part plus an
affected-operations summary;
--files switches to the file-level $ref graph; and
--format outputs stylish (default), json, or mermaid. Works fully with OpenAPI
2.0/3.x; AsyncAPI and Arazzo render as a flat list of top-level $ref'd components.

Reference

Testing

tree-demo.yaml
# tree-demo.yaml
openapi: 3.0.3
info:
  title: Pet Store (tree demo)
  version: 1.0.0
paths:
  /pets:
    get:
      operationId: listPets
      responses:
        '200':
          description: A list of pets
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Pet'
    post:
      operationId: createPet
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewPet'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
  /pets/{petId}:
    parameters:
      - $ref: '#/components/parameters/PetId'
    get:
      operationId: getPet
      responses:
        '200':
          description: A pet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
  /owners/{ownerId}:
    get:
      operationId: getOwner
      responses:
        '200':
          description: An owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Owner'
components:
  parameters:
    PetId:
      name: petId
      in: path
      required: true
      schema:
        type: string
  schemas:
    Pet:
      type: object
      properties:
        name:
          type: string
        home:
          $ref: '#/components/schemas/Address'
    NewPet:
      type: object
      properties:
        seed:
          $ref: '#/components/schemas/Pet'
    Owner:
      type: object
      properties:
        address:
          $ref: '#/components/schemas/Address'
        friends:
          type: array
          items:
            $ref: '#/components/schemas/Owner'
    Address:
      type: object
      properties:
        city:
          type: string
    Orphan:
      type: object
      properties:
        unused:
          type: boolean

Run tree

Screenshot 2026-06-17 at 10 28 13

Run tree --format=mermaid

Screenshot 2026-06-17 at 10 33 03
flowchart LR
  n0["/owners/{ownerId}"]
  n1["/pets"]
  n2["/pets/{petId}"]
  n3["GET /owners/{ownerId}"]
  n4["GET /pets"]
  n5["GET /pets/{petId}"]
  n6["POST /pets"]
  n7["parameters/PetId"]
  n8["schemas/Address"]
  n9["schemas/NewPet"]
  n10["schemas/Owner"]
  n11["schemas/Pet"]
  n12["tree-demo.yaml"]:::root
  n0 --> n3
  n1 --> n4
  n1 --> n6
  n2 --> n5
  n2 --> n7
  n3 --> n10
  n4 --> n11
  n5 --> n11
  n6 --> n9
  n6 --> n11
  n9 --> n11
  n10 --> n8
  n10 --> n10
  n11 --> n8
  n12 --> n0
  n12 --> n1
  n12 --> n2
  classDef root font-weight:bold
Loading

Run tree --format=json

JSON file
{
  "roots": [
    "tree-demo.yaml"
  ],
  "nodes": [
    {
      "id": "/owners/{ownerId}",
      "resolved": true,
      "kind": "path",
      "file": "tree-demo.yaml"
    },
    {
      "id": "/pets",
      "resolved": true,
      "kind": "path",
      "file": "tree-demo.yaml"
    },
    {
      "id": "/pets/{petId}",
      "resolved": true,
      "kind": "path",
      "file": "tree-demo.yaml"
    },
    {
      "id": "GET /owners/{ownerId}",
      "resolved": true,
      "kind": "operation",
      "file": "tree-demo.yaml"
    },
    {
      "id": "GET /pets",
      "resolved": true,
      "kind": "operation",
      "file": "tree-demo.yaml"
    },
    {
      "id": "GET /pets/{petId}",
      "resolved": true,
      "kind": "operation",
      "file": "tree-demo.yaml"
    },
    {
      "id": "POST /pets",
      "resolved": true,
      "kind": "operation",
      "file": "tree-demo.yaml"
    },
    {
      "id": "parameters/PetId",
      "resolved": true,
      "kind": "component",
      "file": "tree-demo.yaml"
    },
    {
      "id": "schemas/Address",
      "resolved": true,
      "kind": "component",
      "file": "tree-demo.yaml"
    },
    {
      "id": "schemas/NewPet",
      "resolved": true,
      "kind": "component",
      "file": "tree-demo.yaml"
    },
    {
      "id": "schemas/Owner",
      "resolved": true,
      "kind": "component",
      "file": "tree-demo.yaml"
    },
    {
      "id": "schemas/Pet",
      "resolved": true,
      "kind": "component",
      "file": "tree-demo.yaml"
    },
    {
      "id": "tree-demo.yaml",
      "resolved": true,
      "kind": "root",
      "file": "tree-demo.yaml",
      "root": true
    }
  ],
  "edges": [
    {
      "from": "/owners/{ownerId}",
      "to": "GET /owners/{ownerId}",
      "refs": []
    },
    {
      "from": "/pets",
      "to": "GET /pets",
      "refs": []
    },
    {
      "from": "/pets",
      "to": "POST /pets",
      "refs": []
    },
    {
      "from": "/pets/{petId}",
      "to": "GET /pets/{petId}",
      "refs": []
    },
    {
      "from": "/pets/{petId}",
      "to": "parameters/PetId",
      "refs": [
        "#/components/parameters/PetId"
      ]
    },
    {
      "from": "GET /owners/{ownerId}",
      "to": "schemas/Owner",
      "refs": [
        "#/components/schemas/Owner"
      ]
    },
    {
      "from": "GET /pets",
      "to": "schemas/Pet",
      "refs": [
        "#/components/schemas/Pet"
      ]
    },
    {
      "from": "GET /pets/{petId}",
      "to": "schemas/Pet",
      "refs": [
        "#/components/schemas/Pet"
      ]
    },
    {
      "from": "POST /pets",
      "to": "schemas/NewPet",
      "refs": [
        "#/components/schemas/NewPet"
      ]
    },
    {
      "from": "POST /pets",
      "to": "schemas/Pet",
      "refs": [
        "#/components/schemas/Pet"
      ]
    },
    {
      "from": "schemas/NewPet",
      "to": "schemas/Pet",
      "refs": [
        "#/components/schemas/Pet"
      ]
    },
    {
      "from": "schemas/Owner",
      "to": "schemas/Address",
      "refs": [
        "#/components/schemas/Address"
      ]
    },
    {
      "from": "schemas/Owner",
      "to": "schemas/Owner",
      "refs": [
        "#/components/schemas/Owner"
      ]
    },
    {
      "from": "schemas/Pet",
      "to": "schemas/Address",
      "refs": [
        "#/components/schemas/Address"
      ]
    },
    {
      "from": "tree-demo.yaml",
      "to": "/owners/{ownerId}",
      "refs": []
    },
    {
      "from": "tree-demo.yaml",
      "to": "/pets",
      "refs": []
    },
    {
      "from": "tree-demo.yaml",
      "to": "/pets/{petId}",
      "refs": []
    }
  ]
}

Run tree --affected-by <component|path|file>

Run --affected-by Pet

Screenshot 2026-06-17 at 10 38 49

Run --affected-by Pet --affected-by Address

Screenshot 2026-06-17 at 10 41 48

Run tree --files

Screenshot 2026-06-17 at 11 02 53 Screenshot 2026-06-17 at 11 02 28

Run tree for split files

Screenshot 2026-06-22 at 18 24 40

Screenshots (optional)

Check yourself

  • This PR follows the contributing guide
  • All new/updated code is covered by tests
  • Core code changed? - Tested with other Redocly products (internal contributions only)
  • New package installed? - Tested in different environments (browser/node)
  • Documentation update has been considered

Security

  • The security impact of the change has been considered
  • Code follows company security practices and guidelines

Note

Low Risk
Additive, read-only CLI analysis with heavy test coverage; the only cross-command change is skipping config-lint output for mermaid-formatted runs.

Overview
Adds redocly tree, a new CLI command that visualizes API descriptions as dependency graphs for orientation and change-impact review.

Default mode (one API) bundles the spec, walks paths/operations, and follows $ref chains into components (OpenAPI 2/3; AsyncAPI/Arazzo degrade to a flatter component list). --files builds a cross-file $ref graph and can merge multiple APIs. --affected-by narrows the graph upstream from changed pointers, names, or files, with operation/path or file summaries. --format supports stylish, pipe-safe json, and mermaid.

Wiring includes yargs registration, v2 docs/sidebar, a minor changeset, broad unit and e2e snapshot tests, and a small lint tweak so config-lint is skipped when format is mermaid (same as other single-payload formats).

Reviewed by Cursor Bugbot for commit f8f3f83. Bugbot is set up for automated code reviews on this repo. Configure here.

kanoru3101 and others added 12 commits June 11, 2026 16:38
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ck steps)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f8f3f83

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@redocly/cli Minor
@redocly/openapi-core Minor
@redocly/respect-core Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 81.3% (🎯 81%) 7647 / 9405
🔵 Statements 80.68% (🎯 80%) 7960 / 9865
🔵 Functions 84.3% (🎯 84%) 1526 / 1810
🔵 Branches 73.41% (🎯 73%) 5169 / 7041
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/cli/src/types.ts 100% 100% 100% 100%
packages/cli/src/commands/lint.ts 94.33% 86.84% 100% 94.33% 76-78, 143, 176
packages/cli/src/commands/tree/build-graph.ts 100% 94.44% 100% 100%
packages/cli/src/commands/tree/build-structure.ts 95% 85.36% 94.44% 97.33% 62, 129, 145, 154
packages/cli/src/commands/tree/filter-affected.ts 100% 100% 100% 100%
packages/cli/src/commands/tree/index.ts 0% 0% 0% 0% 46-226
packages/cli/src/commands/tree/match-affected-by.ts 100% 100% 100% 100%
packages/cli/src/commands/tree/node-id.ts 100% 100% 100% 100%
packages/cli/src/commands/tree/print/json.ts 0% 100% 0% 0% 4
packages/cli/src/commands/tree/print/mermaid.ts 100% 75% 100% 100%
packages/cli/src/commands/tree/print/stylish.ts 100% 100% 100% 100%
Generated in workflow #10419 for commit f8f3f83 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Performance Benchmark (Lower is Faster)

CLI Version Bundle Lint Check Config
cli-latest ▓ 1.01x ± 0.01 ▓ 1.00x (Fastest) ▓ 1.00x ± 0.01
cli-next ▓ 1.00x (Fastest) ▓ 1.00x ± 0.01 ▓ 1.00x (Fastest)

@RomanHotsiy RomanHotsiy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to only show details about split spec.

I think we need it not about the file strcuture but about the OpenAPI iteslef.

E.g. if there is one spec in one file I would expect affected to still work listing me the path names.

@kanoru3101 kanoru3101 self-assigned this Jun 12, 2026
kanoru3101 and others added 11 commits June 12, 2026 19:10
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…llback

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dynamic imports inside the test body made vitest transform the whole
untransformed dependency subtree within the test, exceeding the 5000ms
per-test budget when istanbul coverage is enabled. Static top-level
imports move that cost to the file-load phase, which has no timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kanoru3101 kanoru3101 changed the title feat: dependency graph command feat: tree command Jun 15, 2026
Add the `treeview` language to the example code fences in tree.md
(matching eject.md / translate.md house style) to satisfy markdownlint
MD040.

Stop tracking the internal agentic planning/spec docs under
docs/superpowers/: they were accidentally committed into the published
documentation and caused all vale and linkcheck failures plus most
markdownlint errors. Nothing references them; they remain available
locally but are no longer published.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kanoru3101 kanoru3101 marked this pull request as ready for review June 22, 2026 16:09
@kanoru3101 kanoru3101 requested review from a team as code owners June 22, 2026 16:09
Comment thread docs/@v2/commands/tree.md Outdated
Comment thread docs/@v2/commands/tree.md Outdated
Comment thread docs/@v2/commands/tree.md Outdated
Comment thread docs/@v2/commands/tree.md Outdated
Comment thread docs/@v2/commands/tree.md Outdated
Comment thread docs/@v2/commands/tree.md Outdated
Comment thread docs/@v2/commands/tree.md Outdated
Comment thread docs/@v2/commands/tree.md Outdated
Comment thread docs/@v2/commands/tree.md Outdated
Comment thread docs/@v2/commands/tree.md Outdated
Co-authored-by: Jacek Łękawa <164185257+JLekawa@users.noreply.github.com>
if (nodeIds.has(mapped.id)) {
addIds([mapped.id]);
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Root change omits dependents

Medium Severity

When --affected-by targets the document root via a JSON pointer such as #/, only the root node id is added to the changed set. filterAffected then walks reverse dependencies from that single seed, so nothing below the root is included. Structure mode treats the root file path as affecting the whole graph, but the pointer form and --files with the root file path do not, so impact output is nearly empty and inconsistent with the docs.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d6e78f8. Configure here.

if (apis.length > 1) {
return exitWithError(
'The tree command shows the structure of one API description at a time. Pass a single API, or use --files for the multi-API file-level graph.'
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default tree fails multiple config APIs

Medium Severity

Running redocly tree with no API argument loads every API from the Redocly config via getFallbackApisOrExit, then structure mode errors when more than one API is returned. Docs say the command uses the config API for default structure view, but multi-API configs fail instead of showing one tree or a clear selection path (unlike --files, which supports multiple APIs).

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fb2f569. Configure here.

Comment thread docs/@v2/commands/tree.md Outdated
config,
types: getTypes(specVersion),
externalRefResolver,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bundle errors silently ignored

Medium Severity

Default structure mode always bundles the API before walking it, but only uses the bundled document and never inspects bundleDocument’s returned problems. Bundler errors or warnings are dropped, so the command can exit successfully while printing a tree that omits or misrepresents content that failed to bundle.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7aa745b. Configure here.

Comment thread docs/@v2/commands/tree.md
roots: graph.roots.filter((root) => affected.has(root)),
nodes: graph.nodes.filter((node) => affected.has(node.id)),
edges: graph.edges.filter((edge) => affected.has(edge.from) && affected.has(edge.to)),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path affected-by omits operations

Medium Severity

Structure mode reuses filterAffected, which only walks edges backward. Path and operation nodes sit on the parent→child spine, so --affected-by on a path (for example /users) keeps the root and path but drops that path’s operations and their component chains, contradicting documented path impact analysis.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 40a8f05. Configure here.

if (nodeIds.has(input) && rel !== rootId) {
addIds([input]);
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relative affected-by inputs fail

Low Severity

After resolving an --affected-by value to a cwd-relative path, structure mode only checks nodeIds.has(input). Inputs like ./schemas/Pet normalize to schemas/Pet but never match the node id, so they warn as unknown even when the shorthand id exists.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 40a8f05. Configure here.

@kanoru3101 kanoru3101 force-pushed the feat/graph-command branch from 40a8f05 to f8f3f83 Compare June 23, 2026 11:48

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f8f3f83. Configure here.

.map((node) => node.id);
const summary =
totalOperations > 0
? `${affectedOperations} of ${totalOperations} operations affected · affected paths: ${affectedPaths.join(', ') || 'none'}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path parameter over-counts operations

Medium Severity

The affected summary counts every operation node in the filtered subgraph. Reverse impact traversal follows structural edges from path to operation, so a change to a path-level parameter component still counts that path’s operations as affected, contradicting the documented case where only the path is impacted and the summary can read 0 of N operations affected.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f8f3f83. Configure here.

@kanoru3101 kanoru3101 marked this pull request as draft June 23, 2026 12:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

snapshot Create experimental release PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants