feat: add issue create and comment (task 0004)
All checks were successful
CI / test (pull_request) Successful in 24s
CI / test (push) Successful in 28s

Introduce the first mutations, along with the shared machinery the later
issue and PR mutation slices reuse.

- `issue create` with --title/--body/--body-file/--assignee/--label/
  --milestone/--fields, emitting `issue: { number, title, state, url }`
- `issue comment <n>`, echoing the created comment from the POST response
  with the body cleaned and truncated at 800 chars
- body-source resolution (--body vs --body-file), label and milestone
  name->ID lookup, repeatable flags, and the `joined`/`selectExtraFields`
  field extractors

Label lookup pages until exhausted, since a repo with more labels than one
page would otherwise fail to resolve a valid name. The end-to-end tier seeds
a mixed-case label and milestone and passes both in a different case, so the
case-insensitive lookup is verified against live Gitea rather than only
against fixtures.
This commit was merged in pull request #3.
This commit is contained in:
2026-07-11 20:39:01 -04:00
parent 5e66b4d746
commit f82414a933
13 changed files with 1328 additions and 41 deletions

View File

@@ -1,12 +1,22 @@
import { axiError } from "./errors.js";
export interface FlagSpec {
/** Flag names (e.g. "--state") mapped to whether they take a value. */
[name: string]: { takesValue: boolean };
/** Flag names (e.g. "--state") mapped to how they are parsed. */
[name: string]: {
takesValue: boolean;
/** Accumulate every occurrence into `lists` instead of `flags` (e.g. `--label`). */
repeatable?: boolean;
};
}
export interface ParsedFlags {
/** Single-valued flags: the value, or `true` for a bare switch. */
flags: Record<string, string | true>;
/**
* Values of each repeatable flag in argv order. Every repeatable flag in the
* spec is present, holding an empty array when it was not passed.
*/
lists: Record<string, string[]>;
positionals: string[];
}
@@ -15,6 +25,19 @@ export interface SplitFlag {
inlineValue?: string;
}
/**
* Read a value-taking flag. `parseFlags` rejects such a flag without a value,
* so anything present here is a string; the `true` case only arises for bare
* switches, which callers never read through this helper.
*/
export function flagValue(
flags: Record<string, string | true>,
name: string,
): string | undefined {
const value = flags[name];
return typeof value === "string" ? value : undefined;
}
/** Split "--flag=value" into name and inline value; "--flag" has none. */
export function splitFlag(arg: string): SplitFlag {
const equals = arg.indexOf("=");
@@ -53,6 +76,12 @@ export function parseFlags(
helpCommand: string,
): ParsedFlags {
const flags: Record<string, string | true> = {};
const lists: Record<string, string[]> = {};
for (const [name, entry] of Object.entries(spec)) {
if (entry.repeatable) {
lists[name] = [];
}
}
const positionals: string[] = [];
const helpSuggestion = [`Run \`gitea-axi ${helpCommand} --help\` to see available flags`];
for (let i = 0; i < args.length; i++) {
@@ -74,8 +103,12 @@ export function parseFlags(
continue;
}
const consumed = consumeFlagValue(args, i, flag, helpSuggestion);
flags[flag.name] = consumed.value;
if (entry.repeatable) {
lists[flag.name]!.push(consumed.value);
} else {
flags[flag.name] = consumed.value;
}
i = consumed.lastIndex;
}
return { flags, positionals };
return { flags, lists, positionals };
}