feat: complete issue list filters, sort, and fields (task 0002)

Finish the `issue list` flag surface left minimal by the tracer slice:

- Server-side filters `--label`, `--assignee`, `--author`, `--milestone`,
  mapped to Gitea's `labels`, `assigned_by`, `created_by`, `milestones`.
- Client-side `--sort <created|updated|comments>`, always descending, over
  the fully paginated set (ADR 0005); `--limit` caps the sorted result.
- `--fields` exposing body, closedAt, labels, milestone, updatedAt, url.
- `--search` refused with a VALIDATION_ERROR redirecting to `search issues`.

Exhaustive pagination lands as a shared `paginate.ts`, since ADR 0005 makes
it a policy later slices reuse; `lookup.ts` drops its hand-rolled copy of the
same loop. The helper carries the 20-page cap that copy encoded, matching the
1000-item ceiling Principle 8 sets.
This commit is contained in:
2026-07-11 23:05:47 -04:00
parent 12a7baa378
commit 348f28a54d
7 changed files with 649 additions and 55 deletions

View File

@@ -38,6 +38,39 @@ export function flagValue(
return typeof value === "string" ? value : undefined;
}
/** ["open", "closed", "all"] → "open, closed, or all". */
function orList(values: readonly string[]): string {
if (values.length < 2) {
return values[0] ?? "";
}
return `${values.slice(0, -1).join(", ")}, or ${values[values.length - 1]}`;
}
/**
* Read a flag whose value must be one of a fixed set. Returns undefined when the
* flag was absent, leaving the default to the caller — a flag with no default
* (`--sort`) and one with a default (`--state`) then differ only in what they do
* with that undefined.
*/
export function parseEnumFlag<T extends string>(
value: string | true | undefined,
name: string,
allowed: readonly T[],
suggestions: string[],
): T | undefined {
if (value === undefined) {
return undefined;
}
if (value === true || !allowed.includes(value as T)) {
throw axiError(
`Invalid ${name} value: ${String(value)} (expected ${orList(allowed)})`,
"VALIDATION_ERROR",
suggestions,
);
}
return value as T;
}
/** Split "--flag=value" into name and inline value; "--flag" has none. */
export function splitFlag(arg: string): SplitFlag {
const equals = arg.indexOf("=");