-
Notifications
You must be signed in to change notification settings - Fork 0
fix: batch CLI issue fixes #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |
| import type { Command } from "commander"; | ||
| import { configureClient, unwrap } from "../lib/api"; | ||
| import { confirm, parseData } from "../lib/input"; | ||
| import { print, printNextCursor, toCsv } from "../lib/output"; | ||
| import { print, printNextCursor, resolveColumns, toCsv } from "../lib/output"; | ||
|
|
||
| type SdkFn = ( | ||
| opts: any, | ||
|
|
@@ -56,12 +56,17 @@ export function registerResource(program: Command, spec: ResourceSpec): void { | |
| .description(`List ${spec.name} from the active workspace`) | ||
| .option("--limit <n>", "results per page (1-100)", "20") | ||
| .option("--cursor <id>", "pagination cursor") | ||
| .option("--all", "auto-paginate: fetch every page (ignores --cursor)") | ||
| .option("--columns <list>", "comma-separated columns to display") | ||
| .option("--full", "show every field instead of the curated columns") | ||
| .option("--workspace <id>", "workspace override") | ||
| .option("--csv", "CSV output (for spreadsheets/accounting)") | ||
| .option("--json", "raw JSON output"); | ||
| .option("--json", "raw JSON output (data only)") | ||
| .option("--raw", "raw JSON output including pagination metadata (page)"); | ||
| for (const f of spec.listFlags ?? []) cmd.option(f.flag, f.describe); | ||
| cmd.action(async (opts) => { | ||
| configureClient(opts.workspace); | ||
| const columns = resolveColumns(opts, spec.columns); | ||
| const query: Record<string, unknown> = { | ||
| limit: Number(opts.limit), | ||
| cursor: opts.cursor, | ||
|
|
@@ -70,12 +75,42 @@ export function registerResource(program: Command, spec: ResourceSpec): void { | |
| const v = opts[camel(f.query)]; | ||
| if (v !== undefined) query[f.query] = v; | ||
| } | ||
|
|
||
| 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 }, | ||
| ); | ||
|
Comment on lines
+79
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Autopagination can stop early 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
|
||
| return; | ||
| } | ||
| if (opts.csv) { | ||
| process.stdout.write(`${toCsv(rows, columns)}\n`); | ||
| return; | ||
| } | ||
| print(rows, { json: opts.json, columns }); | ||
| return; | ||
|
Comment on lines
+79
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. --all lacks guardrails 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
|
||
| } | ||
|
|
||
| const body = unwrap(await list({ query })); | ||
| if (opts.raw) { | ||
| print(body, { json: true }); | ||
| return; | ||
| } | ||
| if (opts.csv) { | ||
| process.stdout.write(`${toCsv(body.data, spec.columns)}\n`); | ||
| process.stdout.write(`${toCsv(body.data, columns)}\n`); | ||
| return; | ||
| } | ||
| print(body.data, { json: opts.json, columns: spec.columns }); | ||
| print(body.data, { json: opts.json, columns }); | ||
| if (!opts.json) printNextCursor(body.page); | ||
| }); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import chalk from "chalk"; | ||
| import type { Command } from "commander"; | ||
| import { getMe } from "../client/sdk.gen"; | ||
| import { configureClient, fail, unwrap } from "../lib/api"; | ||
| import { loadConfig, saveConfig } from "../lib/config"; | ||
| import { print } from "../lib/output"; | ||
|
|
||
| /** | ||
| * `ft workspace list|use|show`: pick and persist the active workspace. | ||
| * Everything comes from GET /me (workspaces[] + activeWorkspaceId); the choice | ||
| * is stored locally and sent as the `X-Workspace-Id` header on every request. | ||
| */ | ||
| export function registerWorkspace(program: Command): void { | ||
| const root = program | ||
| .command("workspace") | ||
| .description("List, switch, and show the active workspace"); | ||
|
|
||
| root | ||
| .command("list") | ||
| .alias("ls") | ||
| .description("List the workspaces you can access (marks the active one)") | ||
| .option("--json", "raw JSON output") | ||
| .action(async (opts) => { | ||
| configureClient(); | ||
| const me = unwrap(await getMe({})).data; | ||
| const activeId = loadConfig().workspaceId ?? me.activeWorkspaceId; | ||
| const rows = me.workspaces.map((w) => ({ | ||
| active: w.id === activeId ? "*" : "", | ||
| id: w.id, | ||
| name: w.name, | ||
| slug: w.slug, | ||
| })); | ||
| print(rows, { | ||
| json: opts.json, | ||
| columns: ["active", "id", "name", "slug"], | ||
| }); | ||
| }); | ||
|
|
||
| root | ||
| .command("use <idOrSlug>") | ||
| .description( | ||
| "Set the active workspace (persisted in ~/.freeticket/config.json)", | ||
| ) | ||
| .action(async (idOrSlug) => { | ||
| configureClient(); | ||
| const me = unwrap(await getMe({})).data; | ||
| const match = me.workspaces.find( | ||
|
Comment on lines
+45
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Workspace recovery blocked 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
|
||
| (w) => w.id === idOrSlug || w.slug === idOrSlug, | ||
| ); | ||
| if (!match) { | ||
| fail( | ||
| `No workspace matches "${idOrSlug}".`, | ||
| `Available: ${me.workspaces.map((w) => w.slug).join(", ") || "(none)"}`, | ||
| ); | ||
| } | ||
| saveConfig({ workspaceId: match.id }); | ||
| console.log( | ||
| `${chalk.green("✓")} Active workspace: ${chalk.bold(match.name)} ${chalk.dim(`(${match.slug})`)}`, | ||
| ); | ||
| }); | ||
|
|
||
| root | ||
| .command("show") | ||
| .description("Show the active workspace") | ||
| .option("--json", "raw JSON output") | ||
| .action(async (opts) => { | ||
| configureClient(); | ||
| const cfg = loadConfig(); | ||
| const me = unwrap(await getMe({})).data; | ||
| const activeId = cfg.workspaceId ?? me.activeWorkspaceId; | ||
| const active = me.workspaces.find((w) => w.id === activeId); | ||
| if (!active) { | ||
| fail( | ||
| "No active workspace set.", | ||
| "Pick one with `ft workspace use <slug>`.", | ||
| ); | ||
| } | ||
| print(active, { json: opts.json }); | ||
| }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2. Readme missing minimum cli version
📎 Requirement gap⚙ MaintainabilityAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools