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:
102
src/tea.ts
Normal file
102
src/tea.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { CliDeps } from "./deps.js";
|
||||
import { axiError } from "./errors.js";
|
||||
import { runSubprocess } from "./subprocess.js";
|
||||
|
||||
export interface TeaLogin {
|
||||
name: string;
|
||||
url: string;
|
||||
sshHost: string;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
interface RawTeaLogin {
|
||||
name?: string;
|
||||
url?: string;
|
||||
ssh_host?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
function teaNotInstalled(): never {
|
||||
throw axiError("tea is not installed", "TEA_NOT_INSTALLED", [
|
||||
"Install tea (https://gitea.com/gitea/tea) — gitea-axi discovers credentials from tea's login store",
|
||||
"Then run `tea login add` to configure a login",
|
||||
]);
|
||||
}
|
||||
|
||||
function failureDetail(result: { stderr: string; code: number | null }): string {
|
||||
return firstLine(result.stderr) || `exit code ${result.code}`;
|
||||
}
|
||||
|
||||
export async function listLogins(deps: CliDeps): Promise<TeaLogin[]> {
|
||||
const result = await runSubprocess("tea", ["login", "list", "--output", "json"], {
|
||||
env: deps.env,
|
||||
});
|
||||
if (result.enoent) {
|
||||
teaNotInstalled();
|
||||
}
|
||||
if (result.code !== 0) {
|
||||
throw axiError(`\`tea login list\` failed: ${failureDetail(result)}`, "UNKNOWN");
|
||||
}
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = JSON.parse(result.stdout);
|
||||
} catch {
|
||||
throw axiError("`tea login list --output json` returned invalid JSON", "UNKNOWN");
|
||||
}
|
||||
if (!Array.isArray(raw)) {
|
||||
throw axiError("`tea login list --output json` returned unexpected output", "UNKNOWN");
|
||||
}
|
||||
return (raw as RawTeaLogin[]).map((entry) => ({
|
||||
name: entry.name ?? "",
|
||||
url: entry.url ?? "",
|
||||
sshHost: entry.ssh_host ?? "",
|
||||
isDefault: entry.default === "true",
|
||||
}));
|
||||
}
|
||||
|
||||
// The list output carries no token (its columns are name, url, ssh_host, user,
|
||||
// default), so the token comes from tea's git-credential interface, which also
|
||||
// refreshes OAuth tokens transparently.
|
||||
export async function getToken(deps: CliDeps, login: TeaLogin, host: string): Promise<string> {
|
||||
let protocol = "https";
|
||||
try {
|
||||
protocol = new URL(login.url).protocol.replace(/:$/, "");
|
||||
} catch {
|
||||
// Fall back to https; the helper only requires the host.
|
||||
}
|
||||
const result = await runSubprocess(
|
||||
"tea",
|
||||
["login", "helper", "get", "--login", login.name],
|
||||
{
|
||||
env: deps.env,
|
||||
stdin: `protocol=${protocol}\nhost=${host}\n\n`,
|
||||
},
|
||||
);
|
||||
if (result.enoent) {
|
||||
teaNotInstalled();
|
||||
}
|
||||
if (result.code !== 0) {
|
||||
throw axiError(
|
||||
`tea could not provide a token for login "${login.name}": ${failureDetail(result)}`,
|
||||
"AUTH_REQUIRED",
|
||||
[`Run \`tea login edit ${login.name}\` to repair the login`],
|
||||
);
|
||||
}
|
||||
for (const line of result.stdout.split("\n")) {
|
||||
if (line.startsWith("password=")) {
|
||||
const token = line.slice("password=".length).trim();
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw axiError(
|
||||
`tea returned no token for login "${login.name}"`,
|
||||
"AUTH_REQUIRED",
|
||||
[`Run \`tea login edit ${login.name}\` to repair the login`],
|
||||
);
|
||||
}
|
||||
|
||||
function firstLine(text: string): string {
|
||||
return text.split("\n", 1)[0]?.trim() ?? "";
|
||||
}
|
||||
Reference in New Issue
Block a user