Files
gitea-axi/src/cli.ts
alexion be7226b321
All checks were successful
CI / test (pull_request) Successful in 52s
CI / test (push) Successful in 51s
feat: add setup skill/hooks and update shadow (task 0018)
Distribute gitea-axi's ambient context via explicit user actions (ADRs
0009, 0013), with no postinstall script:

- Bundle the Agent Skill markdown at skills/gitea-axi/SKILL.md (a
  minimal pointer, not a command reference) and ship it via package.json
  files.
- Add `setup`, which installs the skill into ~/.claude/skills/
  idempotently (installed/updated/unchanged).
- Add `setup hooks`, which registers a SessionStart hook running the
  bare dashboard for Claude Code, Codex, and OpenCode via the SDK's
  installSessionStartHooks(), updating managed entries in place.
- Shadow the SDK's built-in `update` so it fails with VALIDATION_ERROR
  and points at the npm update command, keeping the ten-code error list
  intact.

Integration tests drive all three at the CLI seam against a temporary
HOME; these commands make no Gitea API calls, so there is no live-Gitea
e2e case.
2026-07-14 11:26:56 -04:00

134 lines
4.6 KiB
TypeScript

import { readFileSync } from "node:fs";
import { exitCodeForError, runAxiCli, AxiError } from "axi-sdk-js";
import { dashboardCommand } from "./commands/dashboard.js";
import { issueCommand } from "./commands/issue.js";
import { labelCommand } from "./commands/label.js";
import { prCommand } from "./commands/pr.js";
import { searchCommand } from "./commands/search.js";
import { setupCommand } from "./commands/setup.js";
import { updateCommand } from "./commands/update.js";
import type { CliDeps, GlobalFlags } from "./deps.js";
import { consumeFlagValue, splitFlag } from "./flags.js";
import { renderErrorOutput } from "./render.js";
const DESCRIPTION = "Agent-ergonomic CLI for Gitea issues and pull requests";
const TOP_LEVEL_HELP = `usage: gitea-axi <command> [flags]
Run with no command to see the repository dashboard (open issues and pull
requests); add --full for the rich view (the open-PR table and issue counts
by label).
commands:
issue list List issues in the current repository
issue view Show a single issue's details
issue create Create an issue
issue comment Post a comment on an issue or pull request
pr create Create a pull request
pr comment Post a comment on a pull request
label list List labels in the current repository
label create Create a label
search issues Full-text search for issues in the current repository
search prs Full-text search for pull requests in the current repository
setup Install the bundled Agent Skill (setup hooks adds the session-start hook)
global flags:
-R, --repo <OWNER/NAME> Override the repository detected from the git origin remote
--login <name> Select a tea login profile by name
--help Show help (also available on every command)
-v, --version Show version
environment:
GITEA_AXI_REPO Repository override, as OWNER/NAME
GITEA_AXI_LOGIN Login profile override
`;
const GLOBAL_FLAG_NAMES: Record<string, keyof GlobalFlags> = {
"-R": "repo",
"--repo": "repo",
"--login": "login",
};
interface ExtractedArgv {
argv: string[];
globals: GlobalFlags;
}
/**
* Pull the context override flags out of argv before the SDK sees it: they are
* accepted anywhere on the command line, while the SDK rejects any flag placed
* before the command.
*/
export function extractGlobalFlags(argv: string[]): ExtractedArgv {
const globals: GlobalFlags = {};
const rest: string[] = [];
for (let i = 0; i < argv.length; i++) {
const flag = splitFlag(argv[i]!);
const target = GLOBAL_FLAG_NAMES[flag.name];
if (!target) {
rest.push(argv[i]!);
continue;
}
const consumed = consumeFlagValue(argv, i, flag);
globals[target] = consumed.value;
i = consumed.lastIndex;
}
return { argv: rest, globals };
}
function readVersion(): string {
const packageJson = JSON.parse(
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
) as { version: string };
return packageJson.version;
}
export interface RunCliOptions {
argv: string[];
env: Record<string, string | undefined>;
cwd: string;
stdout: { write: (chunk: string) => unknown };
}
export async function runCli(options: RunCliOptions): Promise<number> {
process.exitCode = 0;
let extracted: ExtractedArgv;
try {
extracted = extractGlobalFlags(options.argv);
} catch (error) {
const axi = error as AxiError;
options.stdout.write(`${renderErrorOutput(axi.message, axi.code, axi.suggestions)}\n`);
process.exitCode = exitCodeForError(error);
return process.exitCode;
}
const deps: CliDeps = {
env: options.env,
cwd: options.cwd,
globals: extracted.globals,
};
// The dashboard's full tier is `gitea-axi --full`. The SDK rejects any flag
// before a command, so `--full` never reaches the home handler on its own; it
// is pulled out here when it is the sole remaining argument, leaving an empty
// argv for the SDK to dispatch to the home handler in full-tier mode.
const full = extracted.argv.length === 1 && extracted.argv[0] === "--full";
const argv = full ? [] : extracted.argv;
await runAxiCli({
description: DESCRIPTION,
version: readVersion(),
argv,
topLevelHelp: TOP_LEVEL_HELP,
commands: {
issue: issueCommand(deps),
pr: prCommand(deps),
label: labelCommand(deps),
search: searchCommand(deps),
setup: setupCommand(deps),
// Shadow the SDK's built-in `update` self-update command (ADR 0013).
update: updateCommand(deps),
},
home: dashboardCommand(deps, full),
stdout: options.stdout,
});
return typeof process.exitCode === "number" ? process.exitCode : 0;
}