feat: scaffold CLI and minimal issue list (task 0001)

Tracer bullet for gitea-axi: runnable npm package on axi-sdk-js with
gitea-js as the sole HTTP layer, ESM on Node 20+.

- issue list with --state/--limit, default fields, count line from
  X-Total-Count, type=issues guard, explicit empty state, and next-step
  suggestions
- repo context detection from the git origin remote (SSH/scp/HTTPS),
  tea credential discovery with the three-way login-matching split, and
  -R/--repo and --login overrides (flag > env > auto)
- token retrieval via tea login helper get: tea's login list JSON
  carries no token (ADR 0001 amended)
- full AxiError classification table with path-based 404 split, TOON
  errors on stdout, exit codes 0/1/2
- test mode (GITEA_AXI_API_URL/TOKEN/REPO) suppressing subprocesses,
  fixture server, and vitest suites driving the CLI seam (50 tests)
This commit is contained in:
2026-07-11 07:11:07 -04:00
parent 21a075f8cd
commit 38026f963d
34 changed files with 3892 additions and 0 deletions

54
src/fields.ts Normal file
View File

@@ -0,0 +1,54 @@
import { relativeTime } from "./time.js";
export interface ExtractContext {
now: Date;
}
export interface FieldDef<T> {
name: string;
extract: (raw: T, context: ExtractContext) => unknown;
}
function pluckPath(raw: unknown, path: string): unknown {
let value: unknown = raw;
for (const key of path.split(".")) {
if (typeof value !== "object" || value === null) {
return undefined;
}
value = (value as Record<string, unknown>)[key];
}
return value;
}
export function pluck<T>(name: string, path: string = name): FieldDef<T> {
return { name, extract: (raw) => pluckPath(raw, path) ?? "" };
}
export function lowercased<T>(name: string, path: string = name): FieldDef<T> {
return {
name,
extract: (raw) => String(pluckPath(raw, path) ?? "").toLowerCase(),
};
}
export function relativeTimeField<T>(name: string, path: string): FieldDef<T> {
return {
name,
extract: (raw, context) => {
const value = pluckPath(raw, path);
return relativeTime(typeof value === "string" ? value : undefined, context.now);
},
};
}
export function extractRow<T>(
raw: T,
fields: FieldDef<T>[],
context: ExtractContext,
): Record<string, unknown> {
const row: Record<string, unknown> = {};
for (const field of fields) {
row[field.name] = field.extract(raw, context);
}
return row;
}