feat: add issue view with body cleaning and truncation (task 0003)
All checks were successful
CI / test (pull_request) Successful in 26s
CI / test (push) Successful in 23s

Add `issue view <n>` as the first detail command, introducing the
content-cleaning and truncation machinery (src/body.ts) that later issue
and PR slices reuse.

- Default output: number, title, state, author, created, body (truncated
  at 500), plus comment_count; --comments expands every comment (bodies
  truncated at 800); --full suppresses all truncation.
- cleanBody runs only when a body exceeds its limit: normalizes Gitea
  issue/PR URLs on the detected host to Issue#N/PR#N, strips image embeds
  and long URLs, and collapses quoted blocks.
- Type guard: a PR number fails with VALIDATION_ERROR and a `pr view <n>`
  hint, detected via the fetched object's pull_request field.
- renderDetail joins the detail entity, optional sub-blocks, and help.
This commit was merged in pull request #2.
This commit is contained in:
2026-07-11 19:32:31 -04:00
parent ae0f4c2675
commit 5e66b4d746
8 changed files with 680 additions and 16 deletions

View File

@@ -28,6 +28,11 @@ export function formatCountLine(
return `count: ${shown} of ${total} total`;
}
/** Encode a named list block, with an explicit empty-state line when there are no rows. */
function encodeRows(noun: string, rows: Record<string, unknown>[]): string {
return rows.length > 0 ? encode({ [noun]: rows }) : `${noun}[0]: (none)`;
}
export interface RenderListOptions {
noun: string;
rows: Record<string, unknown>[];
@@ -36,9 +41,28 @@ export interface RenderListOptions {
}
export function renderList(options: RenderListOptions): string {
const body =
options.rows.length > 0
? encode({ [options.noun]: options.rows })
: `${options.noun}[0]: (none)`;
const body = encodeRows(options.noun, options.rows);
return [options.countLine, body, encode({ help: options.help })].join("\n");
}
/** A secondary list block appended below a detail entity (e.g. comments). */
export interface DetailBlock {
noun: string;
rows: Record<string, unknown>[];
}
export interface RenderDetailOptions {
noun: string;
item: Record<string, unknown>;
blocks?: DetailBlock[];
help: string[];
}
export function renderDetail(options: RenderDetailOptions): string {
const parts = [encode({ [options.noun]: options.item })];
for (const block of options.blocks ?? []) {
parts.push(encodeRows(block.noun, block.rows));
}
parts.push(encode({ help: options.help }));
return parts.join("\n");
}