Skip to content

fix: batch CLI issue fixes#14

Merged
lu-ochenty merged 1 commit into
mainfrom
fix/cli-issues-batch
Jul 1, 2026
Merged

fix: batch CLI issue fixes#14
lu-ochenty merged 1 commit into
mainfrom
fix/cli-issues-batch

Conversation

@LucasLeguizamo

Copy link
Copy Markdown
Contributor

Batch of client-side CLI fixes (no contract changes). Build passed; lint passed.

Fixed

  • Actualizar README/skill para evitar drift con el binario publicado #13: Rewrote README.md: removed the stale "writes return 501 / phase 2" claim, documented every write command (create/update/delete/publish/cancel/refund/set-role) and the new workspace group in the Commands table, added the new list/write flags, and updated Quickstart + Pagination sections. Also replaced the stale 501 hint text in src/lib/api.ts. Left docs/PRD.md and docs/BACKLOG.md untouched as historical roadmap artifacts.
  • Mejorar UX para seleccionar y persistir workspace activo #12: Added src/commands/workspace.ts with ft workspace list|use|show, registered in index.ts. Reads getMe (workspaces[] + activeWorkspaceId), persists the selection through the existing saveConfig({workspaceId}) which api.ts already sends as the X-Workspace-Id header. Pure local config, no contract change.
  • Preservar metadata de paginación en salidas JSON #10: Added a --raw flag to list commands in resource.ts that prints the full { data, page } envelope as JSON, surfacing the existing page.nextCursor/hasMore metadata that --json (data-only) omits.
  • Agregar --all para autopaginación en comandos list #8: Added --all to list commands: a client-side loop over the existing cursor query param that fetches every page (while page.hasMore) and concatenates the results before output; works with --json/--raw/--csv/table.
  • Corregir columnas de listados: capacity/startsAt/CSV #6: Fixed column-map bugs in index.ts against the DTOs: ticket-types stock->capacity, events startsAt->createdAt (startsAt lives on EventDate), and plans interval->billingCycle (same-class latent bug). Added --columns/--full via a new resolveColumns() helper in output.ts; CSV now honors the resolved columns too.

Closes #13
Closes #12
Closes #10
Closes #8
Closes #6

Contract-blocked (not in this PR)

These issues require endpoints/params the OpenAPI contract does not yet expose, so they are out of scope here. They are tracked in CONTRACT-GAPS.md and need endpoint-requester to open the backend requests in free-admin:

🤖 Generated with Claude Code

Rewrite README write-command docs (#13), add ft workspace group (#12),
--raw envelope output (#10), --all cursor pagination (#8), and column-map
fixes plus --columns/--full (#6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lu-ochenty lu-ochenty merged commit ab2e999 into main Jul 1, 2026
1 check passed
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix CLI list output, pagination, and workspace selection; refresh docs

🐞 Bug fix ✨ Enhancement 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add ft workspace commands to list, select, and show the active workspace.
• Improve list UX with --all auto-pagination, --raw envelope output, and column selection flags.
• Fix DTO column mappings and update README/error hints to match current behavior.
Diagram

graph TD
  CLI["ft CLI"] --> RES["resource commands"] --> OUT["output helpers"]
  CLI --> WS["workspace commands"] --> CFG["config lib"] --> FILE[("config.json")]
  RES --> API["api client"] --> BACK{{"Backend API"}}
  WS --> API --> CFG

  subgraph Legend
    direction LR
    _cmd["Command module"] ~~~ _db[("Local config file")] ~~~ _ext{{"External service"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Stream `--all` pagination to output
  • ➕ Avoids holding all rows in memory for large datasets
  • ➕ Can start producing output immediately (faster perceived performance)
  • ➖ Harder to support table output (needs buffering for column inference and formatting)
  • --csv and --raw modes need careful streaming/flush semantics
2. Make `--json` accept a mode (`data|raw`) instead of adding `--raw`
  • ➕ Reduces flag surface area (single output flag)
  • ➕ Groups JSON output concerns under one option
  • ➖ Less discoverable than a dedicated --raw flag
  • ➖ More complex option parsing/validation in commander

Recommendation: Current approach is solid for a CLI: --raw is explicit and script-friendly, and buffering in --all is acceptable for typical list sizes while keeping table/CSV behavior consistent. If very large lists become common, consider adding a streaming path (at least for --csv/--json) while keeping table mode buffered.

Files changed (6) +171 / -14

Enhancement (3) +138 / -4
resource.tsAdd '--all', '--raw', and column selection to list commands +39/-4

Add '--all', '--raw', and column selection to list commands

• Extends resource list commands with auto-pagination ('--all') and envelope output ('--raw'). Adds '--columns'/'--full' support (via 'resolveColumns') and ensures CSV/table output honors the resolved columns.

src/commands/resource.ts

workspace.tsIntroduce 'ft workspace list|use|show' command group +80/-0

Introduce 'ft workspace list|use|show' command group

• Adds workspace listing, selection (persisted to config), and display commands. Uses 'GET /me' to discover workspaces and relies on existing 'X-Workspace-Id' header behavior (via stored workspaceId).

src/commands/workspace.ts

output.tsAdd 'resolveColumns()' helper for list output/CSV +19/-0

Add 'resolveColumns()' helper for list output/CSV

• Introduces a helper to choose columns based on '--full', '--columns', or resource defaults, enabling consistent behavior across table and CSV outputs.

src/lib/output.ts

Bug fix (2) +7 / -4
index.tsRegister workspace commands and fix default list column mappings +6/-3

Register workspace commands and fix default list column mappings

• Registers the new workspace command group in the CLI entrypoint. Fixes several resource list column defaults to match DTO fields (events use 'createdAt', ticket-types use 'capacity', plans use 'billingCycle').

src/index.ts

api.tsUpdate 501 error hint to reflect backend status +1/-1

Update 501 error hint to reflect backend status

• Replaces the stale phase-2/write-ops hint for HTTP 501 with a generic backend-not-implemented message.

src/lib/api.ts

Documentation (1) +26 / -6
README.mdRewrite CLI docs for workspace + write/list flags +26/-6

Rewrite CLI docs for workspace + write/list flags

• Updates Quickstart and Commands table to include 'ft workspace' and all write/action commands. Documents new list flags ('--all', '--raw', '--columns', '--full', '--csv') and clarifies pagination metadata behavior.

README.md

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (2) 📜 Skill insights (0)

Context used

Grey Divider


Action required

1. Workspace recovery blocked 🐞 Bug ☼ Reliability
Description
ft workspace list|use|show calls configureClient() which automatically applies the persisted
workspaceId as X-Workspace-Id; if that ID is stale/unauthorized, getMe() can fail and the user
cannot use these commands to recover. workspace show also prefers the persisted workspaceId even
when it doesn’t exist in me.workspaces, causing an unnecessary hard failure.
Code

src/commands/workspace.ts[R45-47]

+      configureClient();
+      const me = unwrap(await getMe({})).data;
+      const match = me.workspaces.find(
Evidence
Workspace commands call configureClient() before getMe(), and configureClient() injects
X-Workspace-Id from the persisted config by default, so a stale workspaceId can break the recovery
commands themselves. Additionally, workspace show prefers the persisted workspaceId even if it
isn’t in me.workspaces, leading to an avoidable failure path.

src/commands/workspace.ts[23-37]
src/commands/workspace.ts[44-60]
src/commands/workspace.ts[66-79]
src/lib/api.ts[9-21]
src/lib/config.ts[30-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new `workspace` commands are intended to help users switch workspaces, but they currently depend on the existing configured workspace header being valid. Because `configureClient()` injects `X-Workspace-Id` from config by default, a stale/unauthorized `workspaceId` can prevent `getMe()` from succeeding, leaving users unable to run `ft workspace list/use/show` to fix the configuration.

### Issue Context
- `configureClient()` chooses `workspaceOverride ?? cfg.workspaceId` and sends it as `X-Workspace-Id`.
- Workspace commands call `configureClient()` with no override before calling `getMe()`.
- `workspace show` uses `cfg.workspaceId ?? me.activeWorkspaceId` and fails if that ID isn’t found in `me.workspaces`, instead of falling back.

### Fix Focus Areas
- src/lib/api.ts[9-21]
- src/commands/workspace.ts[23-79]

### Suggested fix
1. Add a supported way to configure the client **without** the workspace header (e.g., `configureClient({ workspaceOverride?: string, disableWorkspaceHeader?: boolean })` or accept `null` as “do not send”).
2. In `workspace list/use/show`, call the “no workspace header” configuration so `GET /me` works even when the saved workspace is broken.
3. In `workspace show` (and optionally `list`), if a configured workspaceId is not present in `me.workspaces`, fall back to `me.activeWorkspaceId` and/or print a warning that the local config is stale and needs `ft workspace use <slug>`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. --all lacks guardrails 📎 Requirement gap ☼ Reliability
Description
The new --all auto-pagination loop can fetch an unbounded number of pages with no limits or
safeguards, risking very large downloads and rate-limit/time issues. This does not meet the
requirement that --all be safe/guardrailed for large lists/exports.
Code

src/commands/resource.ts[R79-101]

+      if (opts.all) {
+        const rows: unknown[] = [];
+        let cursor: string | undefined;
+        let page: { nextCursor?: string | null; hasMore?: boolean } | undefined;
+        do {
+          const body = unwrap(await list({ query: { ...query, cursor } }));
+          rows.push(...(body.data ?? []));
+          page = body.page;
+          cursor = page?.nextCursor ?? undefined;
+        } while (page?.hasMore && cursor);
+        if (opts.raw) {
+          print(
+            { data: rows, page: { nextCursor: null, hasMore: false } },
+            { json: true },
+          );
+          return;
+        }
+        if (opts.csv) {
+          process.stdout.write(`${toCsv(rows, columns)}\n`);
+          return;
+        }
+        print(rows, { json: opts.json, columns });
+        return;
Evidence
Compliance ID 3 requires --all autopagination to be safe/guardrailed. The added loop fetches pages
until completion with no maximum pages/items or other safety controls, making the behavior
potentially unbounded.

Autopagination via --all is available and consistent for large lists/exports
src/commands/resource.ts[79-101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `--all` implementation auto-paginates without any guardrails (max pages/items, timeout/abort, or confirmation), which can lead to unbounded requests and very large outputs.

## Issue Context
Compliance requires `--all` to be safe and consistent for large lists/exports. Currently, the loop continues while `page.hasMore` and `nextCursor` are truthy, with no upper bound.

## Fix Focus Areas
- src/commands/resource.ts[79-101]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. README missing minimum CLI version 📎 Requirement gap ⚙ Maintainability
Description
The updated README documents new/updated CLI behaviors (writes, --all, --raw, workspace
switching) but does not specify a minimum CLI version or add troubleshooting/update steps to prevent
version drift. Users running an older cached/global version may not have these behaviors, causing
operational confusion and support churn.
Code

README.md[R98-103]

+> **Write operations** (`create`/`update`/`delete` and actions like `publish`,
+> `cancel`, `refund`) send a JSON body via `--data <inline-json>` or
+> `--data @file.json`. `delete` prompts for confirmation unless you pass `--yes`.

### Common flags
Evidence
Compliance ID 8 requires minimum-version guidance and troubleshooting steps to avoid documentation
drift. The modified README section documents write operations and list flags (--all, --raw,
etc.) but provides no minimum version requirement or update/troubleshooting guidance in the updated
content.

README/skill documentation matches the published CLI behavior (avoid version drift)
README.md[98-113]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
README describes behaviors that may not exist in older installed/cached CLI versions, but it does not state a minimum required CLI version nor provide upgrade/troubleshooting steps.

## Issue Context
Compliance requires documentation to avoid version drift, including minimum version guidance and troubleshooting (e.g., checking version and using `@latest`). The README was updated for writes/pagination/workspace behavior but still lacks those guardrails.

## Fix Focus Areas
- README.md[98-113]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Autopagination can stop early 🐞 Bug ≡ Correctness
Description
In --all mode, pagination stops when page.hasMore is true but page.nextCursor is
missing/empty, returning partial results without any error. The --raw output in --all mode also
hardcodes { hasMore: false, nextCursor: null }, which can mask that early termination.
Code

src/commands/resource.ts[R79-93]

+      if (opts.all) {
+        const rows: unknown[] = [];
+        let cursor: string | undefined;
+        let page: { nextCursor?: string | null; hasMore?: boolean } | undefined;
+        do {
+          const body = unwrap(await list({ query: { ...query, cursor } }));
+          rows.push(...(body.data ?? []));
+          page = body.page;
+          cursor = page?.nextCursor ?? undefined;
+        } while (page?.hasMore && cursor);
+        if (opts.raw) {
+          print(
+            { data: rows, page: { nextCursor: null, hasMore: false } },
+            { json: true },
+          );
Evidence
The loop condition requires both hasMore and a truthy cursor, so hasMore=true with a
missing/empty cursor terminates without signaling an error. In --raw mode the code then overwrites
pagination metadata with a fixed {hasMore:false,nextCursor:null} regardless of what the server
returned.

src/commands/resource.ts[67-115]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The `--all` autopagination loop assumes that `page.hasMore === true` always comes with a usable `page.nextCursor`. If that assumption is violated (or if `nextCursor` is accidentally empty), the loop stops early and returns incomplete results with no warning.

### Issue Context
Current loop:
- sets `cursor = page?.nextCursor ?? undefined`
- continues while `(page?.hasMore && cursor)`
So `hasMore=true` + missing cursor will terminate.

Also, `--raw` in `--all` mode emits a fabricated `page` object with `hasMore:false`, which can hide that termination condition.

### Fix Focus Areas
- src/commands/resource.ts[79-101]

### Suggested fix
1. After each page, if `page?.hasMore` is true but `page?.nextCursor` is falsy, abort with `fail()` explaining the pagination metadata is inconsistent.
2. Consider tracking seen cursors and aborting if a cursor repeats to prevent potential infinite loops.
3. For `--raw --all`, either:
  - return the last real `page` from the server plus a flag like `aggregated: true`, or
  - include a warning field indicating the page metadata is no longer meaningful after aggregation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/commands/resource.ts
Comment on lines +79 to +101
if (opts.all) {
const rows: unknown[] = [];
let cursor: string | undefined;
let page: { nextCursor?: string | null; hasMore?: boolean } | undefined;
do {
const body = unwrap(await list({ query: { ...query, cursor } }));
rows.push(...(body.data ?? []));
page = body.page;
cursor = page?.nextCursor ?? undefined;
} while (page?.hasMore && cursor);
if (opts.raw) {
print(
{ data: rows, page: { nextCursor: null, hasMore: false } },
{ json: true },
);
return;
}
if (opts.csv) {
process.stdout.write(`${toCsv(rows, columns)}\n`);
return;
}
print(rows, { json: opts.json, columns });
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. --all lacks guardrails 📎 Requirement gap ☼ Reliability

The new --all auto-pagination loop can fetch an unbounded number of pages with no limits or
safeguards, risking very large downloads and rate-limit/time issues. This does not meet the
requirement that --all be safe/guardrailed for large lists/exports.
Agent Prompt
## Issue description
The new `--all` implementation auto-paginates without any guardrails (max pages/items, timeout/abort, or confirmation), which can lead to unbounded requests and very large outputs.

## Issue Context
Compliance requires `--all` to be safe and consistent for large lists/exports. Currently, the loop continues while `page.hasMore` and `nextCursor` are truthy, with no upper bound.

## Fix Focus Areas
- src/commands/resource.ts[79-101]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread README.md
Comment on lines +98 to 103
> **Write operations** (`create`/`update`/`delete` and actions like `publish`,
> `cancel`, `refund`) send a JSON body via `--data <inline-json>` or
> `--data @file.json`. `delete` prompts for confirmation unless you pass `--yes`.

### Common flags

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Readme missing minimum cli version 📎 Requirement gap ⚙ Maintainability

The updated README documents new/updated CLI behaviors (writes, --all, --raw, workspace
switching) but does not specify a minimum CLI version or add troubleshooting/update steps to prevent
version drift. Users running an older cached/global version may not have these behaviors, causing
operational confusion and support churn.
Agent Prompt
## Issue description
README describes behaviors that may not exist in older installed/cached CLI versions, but it does not state a minimum required CLI version nor provide upgrade/troubleshooting steps.

## Issue Context
Compliance requires documentation to avoid version drift, including minimum version guidance and troubleshooting (e.g., checking version and using `@latest`). The README was updated for writes/pagination/workspace behavior but still lacks those guardrails.

## Fix Focus Areas
- README.md[98-113]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/commands/workspace.ts
Comment on lines +45 to +47
configureClient();
const me = unwrap(await getMe({})).data;
const match = me.workspaces.find(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Workspace recovery blocked 🐞 Bug ☼ Reliability

ft workspace list|use|show calls configureClient() which automatically applies the persisted
workspaceId as X-Workspace-Id; if that ID is stale/unauthorized, getMe() can fail and the user
cannot use these commands to recover. workspace show also prefers the persisted workspaceId even
when it doesn’t exist in me.workspaces, causing an unnecessary hard failure.
Agent Prompt
### Issue description
The new `workspace` commands are intended to help users switch workspaces, but they currently depend on the existing configured workspace header being valid. Because `configureClient()` injects `X-Workspace-Id` from config by default, a stale/unauthorized `workspaceId` can prevent `getMe()` from succeeding, leaving users unable to run `ft workspace list/use/show` to fix the configuration.

### Issue Context
- `configureClient()` chooses `workspaceOverride ?? cfg.workspaceId` and sends it as `X-Workspace-Id`.
- Workspace commands call `configureClient()` with no override before calling `getMe()`.
- `workspace show` uses `cfg.workspaceId ?? me.activeWorkspaceId` and fails if that ID isn’t found in `me.workspaces`, instead of falling back.

### Fix Focus Areas
- src/lib/api.ts[9-21]
- src/commands/workspace.ts[23-79]

### Suggested fix
1. Add a supported way to configure the client **without** the workspace header (e.g., `configureClient({ workspaceOverride?: string, disableWorkspaceHeader?: boolean })` or accept `null` as “do not send”).
2. In `workspace list/use/show`, call the “no workspace header” configuration so `GET /me` works even when the saved workspace is broken.
3. In `workspace show` (and optionally `list`), if a configured workspaceId is not present in `me.workspaces`, fall back to `me.activeWorkspaceId` and/or print a warning that the local config is stale and needs `ft workspace use <slug>`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/commands/resource.ts
Comment on lines +79 to +93
if (opts.all) {
const rows: unknown[] = [];
let cursor: string | undefined;
let page: { nextCursor?: string | null; hasMore?: boolean } | undefined;
do {
const body = unwrap(await list({ query: { ...query, cursor } }));
rows.push(...(body.data ?? []));
page = body.page;
cursor = page?.nextCursor ?? undefined;
} while (page?.hasMore && cursor);
if (opts.raw) {
print(
{ data: rows, page: { nextCursor: null, hasMore: false } },
{ json: true },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Autopagination can stop early 🐞 Bug ≡ Correctness

In --all mode, pagination stops when page.hasMore is true but page.nextCursor is
missing/empty, returning partial results without any error. The --raw output in --all mode also
hardcodes { hasMore: false, nextCursor: null }, which can mask that early termination.
Agent Prompt
### Issue description
The `--all` autopagination loop assumes that `page.hasMore === true` always comes with a usable `page.nextCursor`. If that assumption is violated (or if `nextCursor` is accidentally empty), the loop stops early and returns incomplete results with no warning.

### Issue Context
Current loop:
- sets `cursor = page?.nextCursor ?? undefined`
- continues while `(page?.hasMore && cursor)`
So `hasMore=true` + missing cursor will terminate.

Also, `--raw` in `--all` mode emits a fabricated `page` object with `hasMore:false`, which can hide that termination condition.

### Fix Focus Areas
- src/commands/resource.ts[79-101]

### Suggested fix
1. After each page, if `page?.hasMore` is true but `page?.nextCursor` is falsy, abort with `fail()` explaining the pagination metadata is inconsistent.
2. Consider tracking seen cursors and aborting if a cursor repeats to prevent potential infinite loops.
3. For `--raw --all`, either:
   - return the last real `page` from the server plus a flag like `aggregated: true`, or
   - include a warning field indicating the page metadata is no longer meaningful after aggregation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants