Skip to content

Latest commit

 

History

History
105 lines (94 loc) · 42 KB

File metadata and controls

105 lines (94 loc) · 42 KB

Codacy Cloud CLI — Specs

This is the single source of truth for all project tasks and specs.

Agents: read this file at the start of every session. Pick up the next pending task, then read the relevant spec file for full details.

Pending Tasks

Task Spec Notes
Resolve the per-image tag budget images.md The cap is org-wide and counts image × tag rows; --keep-latest is per image, so the safe ceiling is cap ÷ images. Four options, none chosen: _main_/projects/container-tagging-guidance/research/per-image-tag-budget.md. The CLI warns today; it does not solve it

Command Inventory

Command Alias Status Spec
info inf ✅ Done info.md
repositories repos ✅ Done (coverage status added) repositories.md
repository repo ✅ Done (actions + coverage status added) repository.md
ls N/A ✅ Done ls.md
directories dirs ✅ Done directories.md
pull-request pr ✅ Done (--diff + Diff Coverage Summary added) pull-request.md
pull-requests prs ✅ Done pull-requests.md
issues is ✅ Done issues.md
issue iss ✅ Done issue.md
findings fins ✅ Done findings.md
finding fin ✅ Done (CVE enrichment included) finding.md
tools tls ✅ Done tools-and-patterns.md
tool tl ✅ Done tools-and-patterns.md
patterns pats ✅ Done (--matches-stack added) tools-and-patterns.md
pattern pat ✅ Done (info mode + guards added) tools-and-patterns.md
images imgs ✅ Done images.md
image img ✅ Done (list/show tags + upload + delete incl. --keep-latest) images.md
analysis N/A ✅ Done analysis.md
json-output N/A ✅ Done json-output.md
login N/A ✅ Done —
logout N/A ✅ Done —

Other Specs

  • setup.md — test framework, build, CI/CD setup
  • deployment.md — npm publishing, brew formula
  • repository-tokens.md — read before touching auth or adding a command: the two token kinds, precedence, the 13-operation backend whitelist, and the per-command support matrix
  • missing-endpoints.md — API v3 operations that don't accept repository tokens yet, ranked; candidate Linear tasks

Changelog

Date What was done
2026-09-23 (OD-748) images shows a Tags column (per-image tagCount) and an Image tags: X of Y used line under the header, read from listOrganizationImages' new usage object (OD-724, API 57.6.4 — fetch-api bumped from 57.4.17). Still one request per page. The line turns red at the cap, where new tags are rejected. Exact figures, not formatCount. --output json stays an array and gains tagCount only; usage is not in the JSON, since adding it would change the top-level shape The image --delete --keep-latest budget warning now reads that same usage.limit from the image-count request it already made, in place of the hardcoded DEFAULT_ORG_TAG_CAP (1,000), and drops its "this CLI cannot read the value in force" disclaimer Both commands tolerate a response without usage/tagCount (an API behind the client), and the exact-figure formatter is now shared as formatExactCount (5 new tests, 784 total)
2026-09-22 (OD-710) Fix: path parameters are now escaped per segment. Every image subcommand 404'd against a namespaced image name — codacy/codacy-website, the shape of all seven images in gh/codacy — because the generated client falls back to encodeURI when OpenAPI.ENCODE_PATH is unset, and encodeURI leaves / intact by design: it encodes whole URLs, not the segments they are built from. The value expanded into two segments and hit a route that does not exist; verified against the API, where the raw slash returns 404 and %2F returns 200. src/utils/api-path.ts exports encodePathSegment (encodeURIComponent) and src/index.ts installs it beside OpenAPI.BASE — the generated client is untouched, so npm run update-api cannot undo it. The encoder is global, which is correct rather than incidental: {branchName} and {filePath} carry slashes for the same reason, though no shipped command sends either as a path parameter today, so nothing else changes shape. Confirmed end to end against gh/codacy after the fix: images lists 7, image codacy/codacy-website lists 84 tags, --delete --keep-latest 10 --dry-run reports 74 of 84 (5 new tests, 767 total)
2026-02-17 Project setup: Vitest, --output json, src/index.ts cleaned up
2026-02-17 info command + tests (4 tests)
2026-02-17 repositories command + tests (5 tests)
2026-02-17 Utility tests: auth, providers (6 tests)
2026-02-17 src/commands/CLAUDE.md created with design decisions
2026-02-18 repository command + tests (5 tests)
2026-02-18 Shared formatting helpers extracted to utils/formatting.ts
2026-02-18 pull-request command + tests (11 tests)
2026-02-18 npm package ready (bin, files, prepublishOnly, tsconfig.build.json, engines)
2026-02-18 CI pipelines: build+test on Node 18/20/22, publish to npm on release
2026-02-18 CLI help examples added to all commands
2026-02-19 issues command + tests (11 tests)
2026-02-20 findings command + tests (13 tests)
2026-02-23 issue command + tests (8 tests); issues cards now show resultDataId
2026-02-23 pull-request --issue <id> option added (4 new tests)
2026-02-24 finding command + tests (9 tests); findings cards now show finding id
2026-02-24 CVE enrichment for finding: fetches cveawg.mitre.org in parallel, shows CVSS/description/references (5 new tests, 102 total)
2026-02-24 SPECS folder created — TODO.md split into SPECS/README.md + per-command specs + setup/deployment
2026-02-25 pull-request --diff option + Diff Coverage Summary section (6 new tests, 108 total)
2026-02-25 repository actions: --add, --remove, --follow, --unfollow (4 new tests, 112 total)
2026-02-25 tools, tool, patterns, pattern commands + tests (35 new tests, 147 total); findToolByName helper added to utils/formatting.ts
2026-03-02 issue --ignore, pull-request --ignore-issue / --ignore-all-false-positives, finding --ignore + tests (17 new tests, 164 total); all use -R/--ignore-reason and -m/--ignore-comment options
2026-03-05 Analysis status in repository and pull-request About sections using formatAnalysisStatus(); --reanalyze option for both commands (13 new tests, 185 total)
2026-03-05 JSON output filtering with pickDeep across all commands: info, repositories, repository, pull-request, issues, issue, findings, finding, tools, patterns; documented pattern in src/commands/CLAUDE.md
2026-03-12 patterns --enable-all / --disable-all bulk update with filter support (6 new tests, 196 total)
2026-03-12 login and logout commands: encrypted token storage in ~/.codacy/credentials, masked interactive prompt, --token flag for non-interactive use, token resolution chain (env var → stored credentials); checkApiToken() updated to set OpenAPI.HEADERS dynamically (9 new tests, 219 total)
2026-06-02 --reanalyze-and-wait (-w) blocking variant for repository and pull-request: triggers reanalysis, polls to completion (10s interval, 20min cap), then prints issue deltas by pattern/severity/category. New src/utils/reanalyze-wait.ts + formatDuration/isBeingAnalyzed helpers (26 new tests, 356 total)
2026-06-02 issues --overview improvements: relabel False Positives buckets (belowThreshold/equalOrAboveThreshold → "Not a False Positive"/"Potential False Positive"), and a "Suggested actions to reduce noise" section that flags noisy patterns (≥10% of issues or ≥3× the average) with a runnable codacy pattern … --disable command, resolving the tool via its prefix (3 new tests, 360 total)
2026-06-02 Pattern config-file & coding-standard awareness: new pattern <tool> <id> info mode (same card as patterns); pattern/patterns skip listing and refuse updates when a tool uses a local config file; pattern refuses to modify coding-standard-enforced patterns; issues --overview noise suggestions now render a manual "update your config file / coding standard" step instead of a command when a pattern can't be disabled via CLI. printPatternCard/PATTERN_JSON_FIELDS moved to utils/formatting.ts (11 new tests, 371 total)
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)
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)
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)
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)
2026-07-16 issues --ignored (-i): new read-only mode listing issues marked as ignored on Codacy, via the dedicated searchRepositoryIgnoredIssues endpoint. Boolean flag modeled on --false-positives for consistency (not a --state <value> selector). Reuses buildFilterBody (all existing filters + --false-positives pass through) and the paginate-to---limit loop; errors when combined with --overview/--ignore. New printIgnoredIssueCard in utils/formatting.ts renders the ignore metadata line (Ignored as <reason> by <name> · <date> + optional comment) and the string issueId (ignored issues have no numeric resultDataId). Omitting the flag keeps existing behavior unchanged (10 new tests, 475 total)
2026-07-08 issues --overview noise suggestions tuned to stop firing on low-volume repos: added a NOISE_MIN_TOTAL (200) floor on the repo's total issues that suppresses the whole "reduce noise" section below it, and a NOISE_MIN_PATTERN (100) absolute floor on each pattern's own count (AND-gated with the relative rules) so a long tail of tiny patterns can't drag the median down and make a ~9-issue pattern look noisy — the total floor is kept above the per-pattern floor so it isn't dead code; the ≥10% share rule now only applies with ≥11 distinct patterns (NOISE_MIN_PATTERNS_FOR_SHARE — an even split only drops below 10% once N > 10, so 8–10 balanced patterns would otherwise all be flagged); and the ≥3× multiple rule (NOISE_MEDIAN_MULTIPLE) now measures against the median (via new medianOf()) instead of the mean, so a single huge pattern can no longer inflate the baseline and mask smaller disproportionate patterns (5 new tests, 465 total)
2026-07-17 issues --ignore now confirms before bulk-ignoring: executeBulkIgnore prints the match count then prompts via shared confirmAction (utils/prompt.ts), proceeding only on y. New --skip-confirmation (-y) bypasses the prompt for CI/scripts (same short flag as tools --import --skip-approval); non-TTY without the flag aborts rather than ignoring by accident. Confirmation runs after the fetch (count is shown) but before any bulkIgnoreIssues call (3 new tests, 478 total)
2026-07-24 Security (CWE-150, HackerOne): neutralize terminal control characters in human-readable output. New src/utils/sanitize.ts (sanitizeText) strips C0 (0x00–0x1F except TAB/LF), DEL (0x7F) and C1 (0x80–0x9F) — CR included — replacing each with visible caret/\xNN notation so a crafted PR can't inject ANSI/OSC sequences to repaint or hide findings, spoof gate status, or drive terminal side effects (OSC 52 clipboard, OSC 8 hyperlinks). Applied before the CLI's own ansis styling (can't sanitize at the console boundary — that would strip the CLI's legitimate colours, and allow-listing SGR would still pass attacker SGR through). Covers every render path: shared helpers in utils/formatting.ts (issue cards/detail, code context, CVE block, dependency chains, version segments) plus pull-request (About table, Files, diff-coverage, annotated diff line/hunk/path), findings/finding, issues (overview tables, noise suggestions), issue, repository (About, PRs, overview), ls/directories. JSON output left intact (JSON encoding already escapes control bytes). New src/utils/sanitize.test.ts + pull-request table/diff regression tests (12 new tests, 490 total)
2026-07-24 (OD-296) Vulnerable/affected functions for SCA issues: CommitIssue.advisoryInformation (added server-side in API 57.3.0, bumped from pinned 56.2.9) is now read and rendered. issues/issue/pull-request default cards show a compact "Vulnerable functions: fn1, fn2 (+N more)" line (printIssueCard); issue/pull-request --issue detail views show the full block with advisory ID + published date (printAdvisoryBlock, wired into printIssueCodeContext). Not shown for ignored issues (IgnoredIssue has no advisoryInformation). Gated purely on advisoryInformation presence. Added to all three commands' JSON pickDeep whitelists (10 new tests, 488 total). Originally scoped to the findings command, but SrmItem has no equivalent field anywhere server-side — redirected to issues, where the data already existed in the public API.
2026-07-28 (OD-296, findings side) SrmItem gained its own advisoryInformation field server-side (bumped pinned API 57.3.0 → 57.3.9), closing the gap noted on 2026-07-24. findings (list) now shows the same compact "Vulnerable functions: fn1, fn2 (+N more)" line as issues, via the newly-exported summarizeFunctions. finding (detail) shows the full printAdvisoryBlock — but only when there's no linked Codacy issue, since printIssueCodeContext already renders the equivalent block from issue.advisoryInformation in that case; this is what makes vulnerable functions visible for SCA/dependency findings (and any other non-Codacy-source finding) that have no linked issue to borrow it from at all. Added to both commands' JSON pickDeep whitelists (6 new tests, 494 total)
2026-07-28 (OD-378) New pull-requests (prs) command — the plural counterpart to pull-request, listing PRs for a repository with the same analysis-gated table columns as repository's "Open Pull Requests" section (reuses buildGateStatus/formatStandards/formatPrIssues/formatPrCoverage/formatDelta). --search-text/-q and --branch/-b map to the API's textQuery/targetBranch params added in OD-376; the classification param (search, Merged vs. last-updated) is deliberately not exposed — different axis, out of scope. [provider] [org] [repo] auto-detect via resolveRepoArgs, paginate-to---limit loop matching findings. Registered in src/index.ts (10 new tests, 516 total)
2026-07-30 (OD-378, review follow-up) pull-requests table polish + a real data bug. Bug: Complexity rendered as "no data" on every PR because the API omits the flat top-level deltaComplexity and only returns quality.deltaComplexity (while still sending a top-level deltaClonesCount) — new shared prQualityMetric(pr, key) in utils/formatting.ts reads the nested quality value first and falls back to the flat field; also applied to repository's Open PR table and pull-request's Analysis section, which had the same bug. Layout: ✓ moved to the first column; metric order now matches repositories (issues → complexity → duplication → coverage); the Coverage column is dropped entirely when no listed PR has a coverage value (new hasAnyPrCoverage() — repos without coverage return diffCoverage.cause and no numbers on any PR); missing metric values now render as a dim - instead of N/A in formatDelta/formatPrCoverage/formatPrIssues, matching formatStandards/formatCountCell/formatCoverageCell; and a zero issue count renders as a bare 0 rather than +0/-0 (-0 read as a negative), matching what pull-request's Files table and formatDelta already did. JSON: added quality.resultReasons/coverage.resultReasons (Codacy review suggestion — they drive the per-metric gate coloring, so consumers need them to see which gates passed/failed) plus the quality.* metric mirrors the table actually renders (23 new tests, 544 total)
2026-08-11 (OD-489) Repository (project) token support. New --repository-token <token> on every command (plus CODACY_PROJECT_TOKEN), sent as the project-token header; account tokens keep api-token. src/utils/auth.ts rewritten around a RemoteAuth discriminated union carrying both kind and source, replacing checkApiToken() with resolveAuth(this) / resolveAccountAuth(this, why) / requireAccountToken(...) / fetchIfAccountToken(...). Precedence matches codacy-analysis exactly — flag > CODACY_PROJECT_TOKEN > CODACY_API_TOKEN > stored login — so vitest.config.mts now blanks CODACY_PROJECT_TOKEN (it outranks the account token and is exported job-wide by the coverage reporter, so tests would otherwise depend on the developer's shell). Codacy whitelists only 13 operations for repository tokens, so tool/patterns/pattern work unchanged, issues (incl. --overview) and tools --import work, and the 9 account-only commands plus repository's 6 management flags, issues --ignore/--ignored, and tools --import --force (only when standards exist) fail fast before any request with a message naming the operation, the reason, and where the token came from. repository's dashboard skips the two non-whitelisted calls: the table keeps the "Open Pull Requests" header with an explanatory line, and JSON keeps pullRequests: [] (so jq '.pullRequests[]' still works) plus an additive unavailable: ["pullRequests"] — under an account token the payload is byte-identical. Also added the long-missing .catch() on the PR call so an account token lacking PR access degrades instead of losing the whole dashboard, and fixed login's 401 message, which told repository-token users their token was "invalid" when it is rejected by /user by design. New SPECS/repository-tokens.md (whitelist + matrix, re-verify on every npm run update-api) and SPECS/missing-endpoints.md (ranked gaps for follow-up Linear tasks) (40 new tests, 606 total)
2026-09-07 HTTP/HTTPS proxy + TLS support (issue #40). Node's global fetch — used by the generated client and the MITRE CVE lookup in commands/finding.ts — ignores HTTP_PROXY/HTTPS_PROXY/NO_PROXY, so the CLI was unusable behind a corporate proxy. Rather than reimplement it, this delegates to configureProxy() from @codacy/tooling (pinned 0.1.0 → 0.23.0, the same function analysis-cli calls), which installs a global undici dispatcher doing per-request protocol + NO_PROXY routing, bare host:port normalization, and SSL_CERT_FILE/NODE_EXTRA_CA_CERTS CA loading. New src/utils/proxy.ts is a ~4-line seam — configureProxyFromEnv() calls it and routes its deliberate fail-loud throw (unreadable/non-PEM CA bundle) into handleError(), giving red Error: <message> and exit 1 like every other failure here; analysis-cli exits 2 because it has a documented exit-code scheme, which this CLI does not. Called at the top of src/index.ts, above OpenAPI.BASE (ordering is only constrained to precede program.parse, since the dispatcher is resolved per request). Kept top-level rather than in the preAction hook so a typo'd SSL_CERT_FILE fails even on --version. Deliberately zero-argument: env is the sole input, which is what keeps parity exact. Superseded external PR #39, which hand-rolled the same feature with undici@8.10.1 — that requires Node ≥ 22.19.0 against this package's engines: ">=20", so require("undici") threw at module load and the CLI would not start at all on any Node 20.x; tooling's undici@^6.21.0 supports Node ≥ 18.17. That regression passed CI, so ci.yml gained a smoke step running the built entry point (plain, with HTTPS_PROXY, and with a bad SSL_CERT_FILE expected to fail) — previously nothing executed src/index.ts, since every command test builds a bare new Command(). Upstream owns the proxy semantics and their 24 tests, so only the seam is tested here. Pinned exactly rather than with a caret: for a pre-1.0 package ^0.22.0 spans patches only (>=0.22.0 <0.23.0-0), so a caret would have bought silent patch drift against a dependency this repo has no proxy coverage for, without ever picking up a minor. Two findings from this work were fixed upstream and taken here via 0.23.0 — undici now loads lazily behind configureProxy's early-out (an unproxied --version went from +27 ms to +0 ms against a main build), and a malformed proxy URL now fails with Invalid HTTPS_PROXY value "...": <reason>, naming the setting and redacting any credentials instead of surfacing a bare Invalid URL (4 new tests, 614 total)
2026-09-09 New -k, --matches-stack [value] filter on patterns, surfacing the API's matchesStack query param (filter a tool's code patterns by whether they match the repository's detected stack). Tri-state, matching the existing issues --false-positives: the bare flag or true sends matchesStack=true, false sends matchesStack=false, omitting it sends nothing — read explicitly rather than by truthiness so an explicit false stays distinct from "not requested". Applies in both list mode (listRepositoryToolPatterns) and bulk mode (updateRepositoryToolPatterns), like every other filter; the post-update toolPatternsOverview call deliberately stays unfiltered, since its counts describe the whole tool rather than the updated subset. The shared tri-state coercion parseBooleanOption moved out of issues.ts into a new utils/options.ts (+ tests) and is now imported by both commands. Required an API bump: pinned 57.3.9 → 57.4.17 (matchesStack first ships in 57.4.14; 57.4.17 is the latest published build). The spec delta is purely additive — 2 unused new operations, 4 new schemas, stackTagsFilterParam on listOrganizationRepositories (unused; the CLI calls ...WithAnalysis) — but matchesStack is inserted mid-signature on listRepositoryToolPatterns (arg 12, before sort), so every full-positional-arg assertion in patterns.test.ts gained a trailing undefined; pattern.ts/issues.ts stop at search (arg 9) and were unaffected. SPECS/repository-tokens.md re-verified: 57.4.x now declares the ProjectTokenAuth scheme in the spec (it was absent in 57.3.9), making the whitelist machine-checkable, and it is 14 operations, not 13 — the addition is searchAiInventoryCategories, unused here. patterns stays fully whitelisted, so no new token guard (11 new tests, 625 total)
2026-09-10 Coverage status surfaced in repositories and repository, from the API's new Coverage.status (CoverageStatus: None/UpToDate/Waiting/Stopped), mirroring codacy-spa#3110. No API bump — pinned 57.4.17 already ships the field. It rides on Coverage, embedded only in RepositoryWithAnalysis, so these two commands are the only places it can appear; ls/directories (flat coverageWithDecimals) and pull-request/pull-requests (PullRequestCoverage/DiffCoverage) carry no status. The payload shapes differ in more than status, which is what drove the rendering: Waiting returns a stale percentage (from lastCommitWithCoverage, valueUpdatedAt older than statusUpdatedAt), Stopped returns no percentage at all, None returns nothing but the status, and status is undefined on a large share of repositories. repositories: a dim ⋯ after a Waiting value, a dim ⊘ instead of a Stopped value, and a legend under the table carrying only the statuses actually present (coverageStatusLegend). Glyphs follow the existing vocabulary — ⋯ is already formatStandards's "not final yet" marker, ⊘ shares the Mathematical Operators block with the ⊙ public-repo marker — no emojis. repository: the Metrics row spells the state out (Not reported yet for the latest commit — value from 11h ago (5474cbf) / Stopped receiving reports 2026-08-26 — last report 8752dbd, plus — coverage gate no longer enforced when goals.minCoveragePercentage is set / dim Not set up for None, which a bare N/A could never distinguish from an uncomputed metric), colored on formatAnalysisStatus's existing scale (blueBright = in-flight, yellow = attention, dim = nothing there). Analysis row rewritten: formatAnalysisStatus gained an authoritative coverageStatus that wins over its expectsCoverage/hasCoverageData heuristic, extracted into coverageAnalysisSuffix. The heuristic was wrong for Waiting — a waiting repo still reports a (stale) percentage, so hasCoverageData was true and the row said nothing in exactly the case worth surfacing — and vague for Stopped ("Missing coverage reports"). That let repository drop its listCoverageReports call entirely (5 parallel requests → 4), which in turn means repository-token users get the coverage state for the first time (getRepositoryWithAnalysis is whitelisted, listCoverageReports is not) and unavailable is now ["pullRequests"] alone. pull-request keeps the heuristic — its coverage models have no status field — and is untouched. Accepted trade-off: with status undefined, repository's Analysis row now shows no coverage hint where the heuristic might have said "Missing coverage reports". Also de-duplicated repositories.ts's local formatMetric (a stale copy of the shared colorMetric returning a bare "N/A"), which would otherwise have let coverage and complexity/duplication drift inside the same table. Five new helpers in utils/formatting.ts (coverageStatusGlyph/formatRepoCoverageCell/coverageStatusLegend/coverageStatusNote/formatRepoCoverageDetail); formatCoverageCell deliberately untouched — it renders file/folder coverage, which has no status. JSON gains coverage.status/lastCommitWithCoverage/statusUpdatedAt/valueUpdatedAt on both commands (valueUpdatedAt is what tells a consumer the Waiting value is stale — the job the glyph does in the table); pickDeep drops undefined, so a None repo emits {"status":"None"} and a status-less one gains no keys (39 new tests, 664 total)
2026-09-21 (OD-710) New images (imgs) and image (img) commands — container images with SBOMs uploaded to an organization, and the tags under them. Built entirely on operations already present in the generated client (SbomService): no npm run update-api. images <provider> <org> lists images (Image / Latest Tag / Last Upload / Last Generated) in a single request. image <provider> <org> <image> lists that image's tags (Tag / Environment / Repository / Generated / Uploaded / Last Analysed), shows one with -t, --tag <tag>, and deletes with -D, --delete. --delete is the action and --tag is the scope, the same split issues --ignore makes with its filters — the flag that narrows what is acted on is the flag that narrows what is shown — so --delete alone removes the image and every SBOM under it, --tag X --delete removes one tag, and there is no second delete verb needing a mutual-exclusion guard. --tag without an action pages the listing and matches exactly (the tags endpoint has no per-tag filter — the same shape pull-request --issue <id> uses) and errors naming the tag when absent; with --delete it skips the lookup, since the API 404s on a missing tag at lower cost. No tag count on the images table: it is the number an org at the 1000-tag cap actually wants, but ImageSummary doesn't carry one and deriving it costs one request per image, so it is being added server-side instead (pending task above) rather than fanned out from the client. A whole-image --delete still reads a limit: 1 pagination.total so its prompt can name how many tags are about to go; a failed lookup drops the count rather than blocking, and -y skips it. Both delete scopes go through the shared confirmAction (non-TTY without -y aborts, as in issues --ignore) and print a yellow notice first: a single SBOM delete currently zero-fills Container Scanning metrics for the whole organization until the next nightly scan. That defect is also why this is the first of three stacked PRs — bulk cleanup (--keep-latest) would fire it once per tag and waits on the fix; --upload is PR 2. Both commands are account-token only — no image operation is on the 14-operation repository-token whitelist — and are covered by the cross-cutting repository-token-refusals.test.ts rather than their own token tests. Image/tag/environment/repository names arrive with the SBOM upload, so all four go through sanitizeText() (26 new tests, 699 total)
2026-09-21 (OD-710, PR 2) image --upload <file>: push an SBOM (SPDX or CycloneDX) for one image tag, via the already-generated uploadImageSbom. Fits the command's existing action/scope split — --upload is a verb, --tag scopes it — and requires --tag, since the API keys the upload on image and tag with no untagged fallback; that is refused by name before the file is read. The file is validated locally first (unreadable path, empty file), so the common mistakes fail with something actionable instead of a 400 from the other side of the network. Sent as a File rather than a bare Blob so the multipart part carries the real filename — a Blob goes out as filename="blob" — with the media type inferred from the extension (.json/.xml, else application/octet-stream, letting the API decide rather than guessing wrong in the request); the generated isBlob accepts both shapes. Optional -e, --environment and -r, --repository map to the API's environment/repositoryName and are omitted from the form rather than sent as undefined. --upload and --delete are refused together: unlike --delete's two scopes these are two different verbs, and asking for both says nothing coherent about what should happen to the SBOM. Still account-token only, which is the awkward part — uploading from a pipeline is exactly where a project token would be natural, so the gap is logged in SPECS/missing-endpoints.md (9 new tests, 708 total)
2026-09-21 (OD-710, PR 3) image --delete --keep-latest <n> + --dry-run: the release-pipeline cleanup step, keeping the n most recently uploaded tags of an image and deleting every older one. Shape is a deliberate departure from the design proposal. container-tagging-guidance/AGENTS.md §2 (CLI-1) proposed image delete-tags <image> --keep-latest N and marked it "Builder's call"; --delete is already the verb here and --tag already the scope, so --keep-latest is simply one more scope ("all but the newest n") rather than a second command doing almost the same thing, and this CLI avoids its first nested subcommand. The proposal's goal ("verb first, names what it deletes") is met, and the Figma snippet needed changing regardless — research/setup-copy-and-snippet.md §7 lists its verbless codacy image ${IMAGE_NAME} --keep-latest 19 as an outstanding defect. Cleanup runs before the upload (forced by upsertImageTag raising in-transaction at the cap, so upload-then-delete strands an org there), which sets three behaviours: n is literal — it counts what exists when it runs, not after the upload, so cleanup-first with 10 leaves 11 and making n secretly mean n-1 would print a number the user didn't type; "nothing to delete" exits 0, since an org under n is the common case on every release; and a failed delete does not stop the loop — giving up at tag 3 of 80 leaves the org no better off — though the exit code is still 1, because a partial cleanup is a real failure for the step behind it. Ordered by uploadedAt, not generatedAt: "latest" means when Codacy received the SBOM, which is what accumulates against the cap. Deletes are sequential — one request at a time is what makes "deleted 77 of 80, here are the 3 that failed" reportable; the org-wide metrics wipe that originally forced it is fixed. Org-budget warning: the cap is org-wide and counts image × tag rows while this flag is per image, so --keep-latest 10 puts a 212-image org at 2,120 against a 1,000 cap while appearing to follow instructions (research/per-image-tag-budget.md). That file's rule — never quote a constant n without the image count beside it — is honoured by reading the image count (one request, limit: 1) and warning when n × images exceeds the cap, naming the count and what the cap allows per image; it warns rather than refuses and deliberately does not pick between that file's four unowned options. DEFAULT_ORG_TAG_CAP is 1,000 but the cap is configuration (sbom.image.max-image-tags-per-org) that no endpoint exposes, so the copy says "default" and admits the CLI cannot read it, with exact figures rather than formatCount's "1k"/"2.1k". --dry-run is long-only, a documented exception to the short-flag rule: every free letter sits one shift-key from -D, --delete and that typo is the destructive one. Shipped once Fix org-wide metrics wipe on image tag deletion landed, which is what had held a delete loop back while PR 1's single-tag and whole-image deletes went ahead; METRICS_WIPE_NOTICE was removed with it. Verified end to end against gh/claudiacodacy with five Trivy-generated CycloneDX SBOMs: upload x5, list, dry-run, apply, idempotent re-run, single-tag delete, whole-image delete (13 new tests, 721 total)
2026-09-21 Surface the API's own error message instead of the generated client's status name. catchErrorCodes builds ApiError.message from a static {400: 'Bad Request', 404: 'Not Found', …} table, so handleError — which printed err.message and ignored err.body — was discarding the explanation the server had already sent, on every command. Error: Not Found is now Error: Could not find repository gh/claudiacodacy/no-such-repo (HTTP 404); Error: Unauthorized is Error: Bad credentials (HTTP 401); and the case that prompted this, a mistyped SBOM tag, went from Error: Bad Request to Error: SBOM tag mismatch: expected 9.9.9, found 3.20 (HTTP 400). The status is kept as a trailing (HTTP nnn) — it is what gets quoted in a bug report, and "Unauthorized" carries meaning a terse body might not. Extraction is apiErrorDetails(body), moved from import-config.ts (where it was parseApiErrorBody, feeding tools --import's failure table) into utils/error.ts so there is one copy: it reads the spec's message, an out-of-spec errors array, a plain string body, else the serialized body. A body carrying nothing, or only echoing the status name, leaves the old output untouched — so commands whose failures the API doesn't explain are byte-identical. The message is passed through sanitizeText first: error bodies quote back image names, tags and branches, which a crafted repository controls, and this is the one render path that had no sanitizer because it never rendered server text before. Verified before/after against the live API across repository, repositories, issues, issue, finding, pull-request, pull-requests, ls, directories, images and info (13 new tests, 734 total)
2026-09-22 (OD-710, review follow-up) confirmAction now prompts on stderr, not stdout. process.stdin.isTTY is true whenever stdin is a terminal — including when stdout is a pipe — so `image … --delete --output json
2026-09-22 (OD-710, review follow-up) Two --keep-latest defects that compounded into unconfirmed data loss. executeKeepLatest dispatched into keepLatestAsJson before it ever reached confirmKeepLatest, so --output json — the path a release pipeline runs — deleted without prompting; the gate now sits inside keepLatestAsJson next to the deletion it guards, and a decline emits {deleted: [], aborted: true} (an array, not the single-tag delete's false, since deleted names tags in this mode). And Number("") is 0, so --keep-latest "$KEEP_COUNT" with the variable unset doomed every tag — now rejected on value.trim() === "", the same shape auth.ts uses for an empty --repository-token (5 new tests, 738 total)
2026-09-22 (Review follow-up) Two gaps in the error-surfacing work above. ApiError.body is response.text() for any content type the generated client does not treat as JSON, so a proxy's HTML 502 page was printed in full behind Error: — a regression in the environments the HTTPS_PROXY support was added for. A string body is now used only when it plausibly is a message (first line, at most MAX_DETAIL_LENGTH 200 characters, not starting with <), falling back to the status name; the same cap guards the serializedBody fallback. Separately, --keep-latest's per-tag failure list formats at its own call site and so never reached formatError, reading "Bad Request" for every failure — new exported errorReason(err) shares the extraction with formatError and returns the bare reason, without the prefix, the (HTTP n) suffix or sanitization (10 new tests, 762 total)
2026-09-23 (OD-449) dependencyChains surfaced on issue/issues/pull-request --issue, now that CommitIssue carries it (codacy-website OD-444, deployed to prod). API bump: pinned 57.4.17 → 57.5.12 — purely additive per a diff of both bundled specs (CommitIssue.dependencyChains added, DependencyChains pulled into a shared $ref, an unused 403 on uploadImageSbom, one billing-field description change); no operation signatures shifted, so SPECS/repository-tokens.md's whitelist needed no changes. Rendering reuses finding/findings' existing formatDependencyChainsLine/formatDependencyChainsBlock helpers via printIssueCard (list) and the shared printIssueCodeContext (detail, also used by pull-request --issue and by finding's linked-issue block) — no per-command variant. CommitIssue has no affectedVersion/fixedVersion, so from issue/issues directly a single-package (direct) chain renders as a bare Direct - Update <pkg> with no target version. Same-PR fix: the first pass duplicated the chain block on finding's detail view for a Codacy-source SCA finding whose linked issue also carries chains, since printIssueCodeContext started rendering issue.dependencyChains unconditionally while finding.ts still unconditionally rendered item.dependencyChains too. Fixed by gating finding.ts's item-level block on hasChains && !issue, matching the CVE/advisory blocks' existing convention, and adding a dependencyChainsFixedVersion param to printIssueCodeContext so finding.ts can pass item.fixedVersion through — the merged block still shows the fixed version even though CommitIssue itself has none. Also added a leading blank line before the chain line in printIssueCard (it previously ran flush against a preceding false-positive warning, reading as part of it) (issue.test.ts 20→24, issues.test.ts 72→76, finding.test.ts 27→28, pull-request.test.ts +2)