Skip to content

Commit bf903e4

Browse files
alerizzoclaude
andauthored
feat: add ls and directories commands for browsing repo trees (#26)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bf527ad commit bf903e4

21 files changed

Lines changed: 1948 additions & 20 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@codacy/codacy-cloud-cli": minor
3+
---
4+
5+
Add `ls` and `directories` commands to browse a repository's tree with quality
6+
metrics. `ls` lists the directories and files at a path — showing Grade, Issues,
7+
Complexity, Duplication, and Coverage per row — and `directories` (alias `dirs`)
8+
lists folders only, with `--plus-children` to also show one level of
9+
sub-directories as a `└─` tree. Both auto-detect the provider/organization/repository
10+
from the git remote and the path from your current directory (relative to the
11+
repo root); override with positional args, `--path`, and `--branch`. Sort with
12+
`--sort <field>` (`name`, `issues`, `grade`, `duplication`, `complexity`,
13+
`coverage`) and `--direction asc|desc`. `codacy ls --search <term>` finds files
14+
at any depth under the path. Folders and files are marked with `▸` and `·` (no
15+
emojis). Both commands fetch every page of results, so nothing is truncated.

‎.gemini/styleguide.md‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@ Commander.js CLI). Please weigh these before flagging style or correctness
55
concerns.
66

77
## Testing
8-
- Tests mutate `process.env` directly: assign the variable in the test and
9-
`delete` it in `beforeEach`/`afterEach` for isolation. This is the repo-wide
10-
convention (see `src/utils/auth.test.ts` and ~10 other test files). Do **not**
11-
suggest `vi.stubEnv` / `vi.unstubAllEnvs` — the codebase deliberately does not
12-
use them, and consistency across the suite is preferred.
8+
- Tests mutate `process.env` directly: set the variable in `beforeEach` (which
9+
re-assigns it before every test, so leakage between tests is a non-issue) and,
10+
when a specific test needs it unset, `delete` it inside that test. An
11+
`afterEach` cleanup block is **not** required and is **not** the convention —
12+
~13 of the 14 test files (`repositories.test.ts`, `issue.test.ts`,
13+
`findings.test.ts`, …) use `beforeEach` only. Do **not** suggest adding
14+
`afterEach`, and do **not** suggest `vi.stubEnv` / `vi.unstubAllEnvs` — the
15+
codebase deliberately does not use them, and consistency across the suite is
16+
preferred.
1317
- API service calls are mocked with `vi.mock(...)`; tests are co-located as
1418
`<module>.test.ts` next to the source.
1519

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ Supported providers: GitHub (`gh`), GitLab (`gl`), Bitbucket (`bb`).
7878
| `info` | Show authenticated user info and their organizations |
7979
| `repositories <provider> <org>` | List repositories for an organization |
8080
| `repository [provider] [org] [repo]` | Show metrics for a repository, or add/remove/follow/unfollow/reanalyze it (optionally waiting for results) |
81+
| `ls [provider] [org] [repo]` | List (or search) directories and files at a path in a repository, with quality metrics and sorting |
82+
| `directories [provider] [org] [repo]` | List directories at a path in a repository (optionally one level of sub-directories), with quality metrics |
8183
| `issues [provider] [org] [repo]` | Search issues in a repository with filters |
8284
| `issue [provider] [org] [repo] <id>` | Show details for a single issue, or ignore/unignore it |
8385
| `findings [provider] [org] [repo]` | Show security findings for a repository or organization |

‎SPECS/README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ _No pending tasks._ All commands implemented.
1515
| `info` | `inf` | ✅ Done | [info.md](commands/info.md) |
1616
| `repositories` | `repos` | ✅ Done | [repositories.md](commands/repositories.md) |
1717
| `repository` | `repo` | ✅ Done (actions added) | [repository.md](commands/repository.md) |
18+
| `ls` | N/A | ✅ Done | [ls.md](commands/ls.md) |
19+
| `directories` | `dirs` | ✅ Done | [directories.md](commands/directories.md) |
1820
| `pull-request` | `pr` | ✅ Done (--diff + Diff Coverage Summary added) | [pull-request.md](commands/pull-request.md) |
1921
| `issues` | `is` | ✅ Done | [issues.md](commands/issues.md) |
2022
| `issue` | `iss` | ✅ Done | [issue.md](commands/issue.md) |
@@ -71,3 +73,4 @@ _No pending tasks._ All commands implemented.
7173
| 2026-06-18 | `repo --output json` now includes `repository.fileCount`, plucked from `coverage.numberTotalFiles` on the existing `getRepositoryWithAnalysis` response (present even without coverage data — no extra API call). Unlocks repo-size visibility for downstream consumers like the `configure-codacy-cloud` skill (1 new test, 373 total) |
7274
| 2026-06-24 | `findings` and `finding` now surface the vulnerable dependency's import chain from the new `dependencyChains` field: Direct (`Update <pkg> to <fixed>`) vs Transitive (`<chain> (Fixed in <fixed>)`), with the middle collapsed to `... N more ...` for 4+ packages. List shows the first chain + `... and X more`; detail shows all chains aligned under a single label. New helpers in `utils/formatting.ts` (`formatDependencyChain`, `formatDependencyChainsLine`, `formatDependencyChainsBlock`); `dependencyChains` added to both JSON projections (17 new tests, 390 total) |
7375
| 2026-06-30 | npm-style "update available" notice via `update-notifier@5`: one-time stderr hint when a newer version is published, gated to `--output table` (suppressed for `json`, when piped, in CI, under `npx`). Non-blocking daily background check; never auto-updates. New `src/version.ts` (single source of name/version) + `src/utils/update-check.ts` (`maybeNotifyUpdate`); `preAction` hook + `--no-update-notifier` flag wired in `index.ts`. Opt-outs: `CODACY_DISABLE_UPDATE_CHECK`, `NO_UPDATE_NOTIFIER`, `--no-update-notifier`. `package.json` `overrides` pin transitive `got@^11.8.6`/`package-json@^7` (CVE-2022-33987) (7 new tests, 409 total) |
76+
| 2026-07-07 | `ls` and `directories` commands: browse a repository's folders/files with quality metrics (Grade/Issues/Complexity/Duplication/Coverage). Auto-detect provider/org/repo and the cwd-relative path; `--path`/`--branch` options; `directories --plus-children` shows one extra level as a `└─` tree (header adds `, M subdirectories`). `--sort`/`--direction` (server-side; in `ls`, directories and files sorted independently), and `ls --search <term>` (files only; folds the path into the search as `<path>/%<term>` and shows full paths). Duplication uses `numberOfClones`; Complexity uses `complexity` (hotspots). Both fetch **all** pages (no pagination warning) via new `src/utils/repo-tree.ts` (path resolution + `resolveSort`/`resolveDirection` + `fetchAllDirectories`/`fetchAllFiles`). Row markers `▸` folder / dim `·` file (no emojis). Promoted `formatGrade` to `utils/formatting.ts` (now also colors E red) and added `formatCountCell`/`formatCoverageCell` (48 new tests, 457 total) |

‎SPECS/commands/directories.md‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# `directories` Command Spec
2+
3+
**Status:** ✅ Done (2026-07-07)
4+
5+
## Purpose
6+
7+
List the immediate directories at a path in a repository, each with quality metrics — like `ls` but folders only. With `--plus-children`, also lists each directory's immediate sub-directories (one extra level) so you can see a bigger picture of the folder structure and its metrics.
8+
9+
## Usage
10+
11+
```
12+
codacy directories # current directory's folders (auto-detected)
13+
codacy directories --plus-children # also one more level of sub-folders
14+
codacy directories --path src/website
15+
codacy directories --sort issues --direction desc
16+
codacy directories gh my-org my-repo --plus-children
17+
codacy dirs --output json
18+
```
19+
20+
## API Endpoints
21+
22+
- [`listDirectories`](https://api.codacy.com/api/api-docs#listdirectories) — `RepositoryService.listDirectories(provider, org, repo, branch, path, sort, direction, cursor, limit)`
23+
24+
Cursor-paginated; **all pages are fetched** (`fetchAllDirectories` in `utils/repo-tree.ts`) with no pagination warning. `--plus-children` issues one additional (fully paginated) `listDirectories` call per directory, concurrently, listing that directory's `path`.
25+
26+
## Options
27+
28+
| Option | Short | Description |
29+
|---|---|---|
30+
| `--path <path>` | `-p` | Repository-relative folder to list (defaults to the cwd relative to the repo root) |
31+
| `--branch <branch>` | `-b` | Branch name (defaults to the main branch) |
32+
| `--plus-children` | `-c` | Also list each directory's immediate sub-directories |
33+
| `--sort <field>` | `-S` | Sort by `name`, `issues`, `grade`, `duplication`, `complexity`, or `coverage` |
34+
| `--direction <direction>` | `-d` | `asc`/`ascending` or `desc`/`descending` |
35+
36+
`--sort`/`--direction`, when given, are applied server-side to both the top-level listing and each `--plus-children` listing; otherwise directories are ordered by name.
37+
38+
## Output
39+
40+
Bold header `/<repo>/<path> — N directories` (with `--plus-children`, also `, M subdirectories` — the total children across all directories), then a columnar table (same columns as `ls`). Parent rows are prefixed `▸ <name>`; with `--plus-children`, each child is rendered on its own indented row ` └─ <name>` under its parent.
41+
42+
| Column | Source | Notes |
43+
|---|---|---|
44+
| Name | `▸ <name>` / ` └─ <name>` | `▸` parent folder, `└─` child connector |
45+
| Grade | `gradeLetter` | A/B green, C yellow, D/E/F red |
46+
| Issues | `totalIssues` | Abbreviated (`formatCount`) |
47+
| Complexity | `complexity` | Highest file complexity under the path (hotspots) |
48+
| Duplication | `numberOfClones` | Number of cloned code blocks (matches Codacy's UI) |
49+
| Coverage | `coverageWithDecimals` | e.g. `76.3%` |
50+
51+
Missing metrics render as a dim `-`. Only the Grade letter is colored. No pagination warning. Empty result prints "No directories found at …".
52+
53+
JSON output: `{ path, directories: [ { …, children?: [...] } ] }`, each item projected via `pickDeep`; `children` present only with `--plus-children`.
54+
55+
## Tests
56+
57+
File: `src/commands/directories.test.ts` — 8 tests. Shared helpers covered in `src/utils/repo-tree.test.ts`.

‎SPECS/commands/ls.md‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# `ls` Command Spec
2+
3+
**Status:** ✅ Done (2026-07-07)
4+
5+
## Purpose
6+
7+
List the immediate directories **and** files at a path in a repository (like Unix `ls`), each with quality metrics. Repository is auto-detected from the git remote; the path is auto-detected from the current working directory relative to the repo root, or given via `--path`, or defaults to the repo root.
8+
9+
## Usage
10+
11+
```
12+
codacy ls # current directory (auto-detected)
13+
codacy ls --path src/website # a specific folder
14+
codacy ls gh my-org my-repo # root of an explicit repository
15+
codacy ls gh my-org my-repo --path src/website
16+
codacy ls --sort issues --direction desc # worst files/folders first
17+
codacy ls --path app/client --search config # search files under a path
18+
codacy ls --output json
19+
```
20+
21+
## API Endpoints
22+
23+
- [`listDirectories`](https://api.codacy.com/api/api-docs#listdirectories) — `RepositoryService.listDirectories(provider, org, repo, branch, path, sort, direction, cursor, limit)`
24+
- [`listFiles`](https://api.codacy.com/api/api-docs#listfiles) — `RepositoryService.listFiles(provider, org, repo, branch, path, search, sort, direction, cursor, limit)`
25+
26+
Both endpoints are cursor-paginated. This command **fetches every page** (via `fetchAllDirectories` / `fetchAllFiles` in `utils/repo-tree.ts`) — it does **not** show a pagination warning. The `path` argument is always passed as an explicit string (`""` = repo root, non-recursive); it is never `undefined`, which would make `listFiles` recursive.
27+
28+
## Options
29+
30+
| Option | Short | Description |
31+
|---|---|---|
32+
| `--path <path>` | `-p` | Repository-relative folder to list (defaults to the cwd relative to the repo root) |
33+
| `--branch <branch>` | `-b` | Branch name (defaults to the main branch) |
34+
| `--search <term>` | `-s` | Search files (any depth) under the path instead of listing immediate children |
35+
| `--sort <field>` | `-S` | Sort by `name`, `issues`, `grade`, `duplication`, `complexity`, or `coverage` |
36+
| `--direction <direction>` | `-d` | `asc`/`ascending` or `desc`/`descending` |
37+
38+
**Sorting.** With no `--sort`/`--direction`, results are ordered by name client-side. When either is given, ordering is delegated to the API (`sort`/`direction` params) — directories and files are sorted **independently** (each fully paginated, then concatenated), never merged. The CLI `name` field maps to the API's `filename` for files.
39+
40+
**Search.** `--search` lists **files only** (the directories endpoint has no search). The folder scope is folded into the search term (`<path>/%<term>`, or just `<term>` at the root) and `path` is **not** sent, so matches are found at any depth under the path. Results show each file's full repository-relative path.
41+
42+
## Output
43+
44+
Bold header `/<repo>/<path> — N directories, M files`, then a columnar table with directories first (each sorted by name), then files.
45+
46+
| Column | Source (directory / file) | Notes |
47+
|---|---|---|
48+
| Name | `▸ <dir.name>` / `· <basename(file.path)>` | `▸` folder, dim `·` file (no emojis); in search mode the file's full path is shown |
49+
| Grade | `gradeLetter` | A/B green, C yellow, D/E/F red |
50+
| Issues | `totalIssues` | Abbreviated (`formatCount`, e.g. `1.2k`) |
51+
| Complexity | `complexity` | Highest file complexity under the path (hotspots); files show the file value |
52+
| Duplication | `numberOfClones` | Number of cloned code blocks (matches Codacy's UI, not duplicated lines) |
53+
| Coverage | `coverageWithDecimals` | e.g. `76.3%` |
54+
55+
Missing metrics render as a dim `-`. Only the Grade letter is colored (directory/file items carry no goal thresholds). No pagination warning (all pages fetched). Empty result prints "Nothing found at …".
56+
57+
JSON output: `{ path, directories: [...], files: [...] }`, each item projected via `pickDeep`.
58+
59+
## Tests
60+
61+
File: `src/commands/ls.test.ts` — 14 tests. Shared helpers (path/sort/direction resolution, fetch-all) covered in `src/utils/repo-tree.test.ts`.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"prepublishOnly": "npm run update-api && npm run build",
2727
"start": "npx ts-node src/index.ts",
2828
"start:dist": "node dist/index.js",
29-
"fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/56.1.1/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs",
29+
"fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/56.2.9/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs",
3030
"generate-api": "rm -rf ./src/api/client && openapi --input ./api-v3/api-swagger.yaml --output ./src/api/client --useUnionTypes --indent 2 --client fetch",
3131
"update-api": "npm run fetch-api && npm run generate-api",
3232
"check-types": "tsc --noEmit"

‎src/commands/AGENTS.md‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,65 @@ Instead of a dedicated "Visibility" column (wastes horizontal space), public rep
109109
- **`--reanalyze` mode** (`-R`): fetches HEAD commit SHA, calls `RepositoryService.reanalyzeCommitById`; early return
110110
- **`--reanalyze-and-wait` mode** (`-w`): blocking variant — see "Reanalyze and wait" below. Baseline comes from `issuesOverview`; polling reads the repo's first commit via `listRepositoryCommits(limit=1)` analysis timestamps
111111

112+
## ls command (`ls.ts`) and directories command (`directories.ts`)
113+
114+
Repository-level "file browser" commands built on `RepositoryService.listFiles`
115+
and `listDirectories`. `ls` lists directories **and** files at a path;
116+
`directories` (alias `dirs`) lists folders only, with `-c, --plus-children` to
117+
also pull each folder's immediate sub-folders (one extra level).
118+
119+
- **Repo + path resolution**: optional `[provider] [organization] [repository]`
120+
positionals via `resolveRepoArgs(..., 0, ...)` (auto-detected from the git
121+
remote when omitted). The listed path comes from `resolveListingPath(options.path, autoDetected)`
122+
in `utils/repo-tree.ts`: an explicit `--path` wins; otherwise, when the repo
123+
was auto-detected, the current working directory relative to the git root
124+
(`getCwdRepoRelativePath` → `git rev-parse --show-toplevel` + `path.relative`);
125+
otherwise the repo root. `autoDetected = !(provider && organization && repository)`.
126+
- **Fetch-all, no pagination warning (deliberate deviation)**: unlike every other
127+
paginated command, these two fetch *all* pages (`fetchAllDirectories` /
128+
`fetchAllFiles` loop over `pagination.cursor`) and do **not** call
129+
`printPaginationWarning` — the whole listing must be shown. Do not "fix" this
130+
by adding a warning.
131+
- **Explicit empty-string path**: the repo root is listed by passing `path = ""`
132+
(not `undefined`). The generated client serializes `""` as `path=`, which the
133+
API treats as "root, non-recursive"; `undefined` would omit the param and make
134+
`listFiles` recursive across the whole repo. `fetchAll*` always take a `string`.
135+
- **Row markers (no emojis)**: `▸` (U+25B8) for folders, dim `·` for files.
136+
`directories --plus-children` renders each child on its own indented row with a
137+
`└─` connector under its parent (`DirectoryNode.children`).
138+
- **Metric columns**: Name, Grade, Issues, Complexity, Duplication, Coverage.
139+
Grade via `formatGrade` (A/B green, C yellow, D/E/F red — Codacy folder/file
140+
grades can be E). Issues/Complexity/Duplication via `formatCountCell`
141+
(abbreviated, dim `-` when absent). Complexity is `complexity` (for a folder,
142+
the *highest* file complexity under it — surfaces hotspots — not the
143+
`complexitySum` total). Duplication is `numberOfClones` (number of cloned
144+
code blocks, matching Codacy's UI — not `duplication`, which is duplicated
145+
*lines*). Coverage via `formatCoverageCell` (`coverageWithDecimals`). Only the
146+
Grade is colored — directory/file items carry no `goals` thresholds, so
147+
metrics are not threshold-colored.
148+
- **Ordering / `--sort` + `--direction`**: with no sort flags, results are
149+
sorted client-side by name (directories) / basename (files), ascending. When
150+
`--sort` or `--direction` is given, ordering is delegated to the API (`sort`/
151+
`direction` params, order preserved across pages) and the client sort is
152+
skipped. `resolveSort`/`resolveDirection` (in `repo-tree.ts`) validate the
153+
values and map them: CLI `name` → API `filename` for the files endpoint;
154+
`ascending`/`descending` (and `asc`/`desc`) → `asc`/`desc`. In `ls`,
155+
directories and files are always sorted **independently** and never merged
156+
(dirs fully paginated + sorted, then files) so the type grouping holds. In
157+
`directories`, the same sort is applied to the top-level and each
158+
`--plus-children` listing.
159+
- **`ls --search <term>` (files only)**: `listDirectories` has no `search`, so
160+
search mode lists files only (directories skipped). The folder scope is folded
161+
into the search string as `<path>/%<term>` (just `<term>` at the repo root) and
162+
`path` is **not** sent — otherwise the API would restrict to immediate
163+
children. Results show each file's full repository-relative path (not just the
164+
basename), since matches span folders.
165+
- **`directories --plus-children` header**: appends `, M subdirectories` where M
166+
is the summed count of children across all listed directories.
167+
- **Aliases**: `directories` → `dirs`; `ls` has no alias (already the minimal
168+
form).
169+
- JSON: `ls` → `{ path, directories, files }`; `directories` → `{ path, directories: [{…, children?}] }`, each item projected via `pickDeep`.
170+
112171
## Shared Formatting Utilities (`utils/formatting.ts`)
113172

114173
Several helpers are shared between `repository.ts` and `pull-request.ts` via `utils/formatting.ts`:

0 commit comments

Comments
 (0)