diff --git a/.claude/tasks/0015-label-commands.md b/.claude/tasks/0015-label-commands.md index 6bd15b8..d83d65b 100644 --- a/.claude/tasks/0015-label-commands.md +++ b/.claude/tasks/0015-label-commands.md @@ -13,10 +13,28 @@ The label command group: `label list`, `label create`, `label edit`, `label dele ## Acceptance criteria -- [ ] `label list` renders the count line and `labels:` block; empty repos get the explicit empty state -- [ ] `label create --name --color` creates the label, prepending `#` to the color, and outputs `created: ok` + `label: ` -- [ ] Creating an existing label (case-insensitive) outputs `create: already_exists` + the existing name, exit 0 -- [ ] `label edit ` applies `--name`/`--color`/`--description` and outputs `edit: ok` + the resulting name -- [ ] `label edit`/`label delete` on an unknown name yield `VALIDATION_ERROR` (exit 2) -- [ ] `label delete ` outputs `delete: ok` + `label: ` -- [ ] Fixture-server tests cover create, idempotent re-create, edit, delete, and the unknown-name refusals +- [x] `label list` renders the count line and `labels:` block; empty repos get the explicit empty state +- [x] `label create --name --color` creates the label, prepending `#` to the color, and outputs `created: ok` + `label: ` +- [x] Creating an existing label (case-insensitive) outputs `create: already_exists` + the existing name, exit 0 +- [x] `label edit ` applies `--name`/`--color`/`--description` and outputs `edit: ok` + the resulting name +- [x] `label edit`/`label delete` on an unknown name yield `VALIDATION_ERROR` (exit 2) +- [x] `label delete ` outputs `delete: ok` + `label: ` +- [x] Fixture-server tests cover create, idempotent re-create, edit, delete, and the unknown-name refusals + +## Implementation Notes + +Built the `label` group in `src/commands/label.ts`, wired into `src/cli.ts` (dispatcher plus the two most-common entries in the top-level help), following the sibling `issue`/`pr` command patterns. + +Deviations and decisions: + +- **`label edit` requires at least one change.** +The spec/gh-axi reference leaves all edit flags optional, but sending an empty `PATCH` is a pointless call, so an edit with none of `--name`/`--color`/`--description` is refused with `VALIDATION_ERROR` — mirroring `issue edit`'s "requires at least one change" guard for consistency within gitea-axi. +- **Resulting name for `edit`/`create`/`delete` comes from the API response** (`edited.name`, `label.name`), not the input, so the reported name is the server's canonical echo (correct casing, and the unchanged original when `--name` was omitted). +- **`create` vs `created` output keys.** +The success key is `created: ok` and the idempotent-hit key is `create: already_exists` — two different top-level keys. +This is spec-mandated (spec lines 342–343) rather than the `already: true` shape the dependency no-ops use; kept verbatim to match the fixed contract. +- **Flat `renderObject` output shape.** +Added `renderObject(item, help)` to `src/render.ts` because the label create/edit/delete outputs are flat top-level fields (`created: ok` / `label: `), which the sibling `renderDetail` (nests under a `noun:` block) cannot produce. This is the spec's output shape, not a new convention chosen freely. +- **Shared lookup + positional helpers (cleanups from review).** +Extracted `findLabel`/`resolveLabel` and a shared `labelNotFound` message into `src/lookup.ts`, reused by the existing `resolveLabelIds`; and extracted `parseSinglePositional` in `src/flags.ts`, now shared by `parsePositionalNumber` and the label name positionals, removing the duplicated count-check/error scaffolding. +- **`label list` uses a single-page fetch with `--limit` (default 500)**, reading `X-Total-Count` for the count line, rather than the exhaustive pagination `resolveLabel`/`resolveLabelIds` use; the spec asks only for `--limit`, and a repo with >500 labels is signalled by the `count: N of T total` line. diff --git a/src/cli.ts b/src/cli.ts index 200912b..0223f70 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,7 @@ import { readFileSync } from "node:fs"; import { exitCodeForError, runAxiCli, AxiError } from "axi-sdk-js"; import { issueCommand } from "./commands/issue.js"; +import { labelCommand } from "./commands/label.js"; import { prCommand } from "./commands/pr.js"; import { resolveRepoContext } from "./context.js"; import type { CliDeps, GlobalFlags } from "./deps.js"; @@ -19,6 +20,8 @@ commands: 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 global flags: -R, --repo Override the repository detected from the git origin remote @@ -115,6 +118,7 @@ export async function runCli(options: RunCliOptions): Promise { commands: { issue: issueCommand(deps), pr: prCommand(deps), + label: labelCommand(deps), }, home: homeCommand(deps), stdout: options.stdout, diff --git a/src/commands/label.ts b/src/commands/label.ts new file mode 100644 index 0000000..6615254 --- /dev/null +++ b/src/commands/label.ts @@ -0,0 +1,316 @@ +import type { CreateLabelOption, EditLabelOption, Label } from "gitea-js"; +import { createClient } from "../client.js"; +import { resolveRepoContext } from "../context.js"; +import type { CliDeps } from "../deps.js"; +import { axiError, classifyHttpError } from "../errors.js"; +import { extractRow, pluck, type FieldDef } from "../fields.js"; +import { flagValue, parseFlags, parsePositiveInt, parseSinglePositional } from "../flags.js"; +import { findLabel, listAllLabels, resolveLabel } from "../lookup.js"; +import { readTotalCount } from "../paginate.js"; +import { formatCountLine, renderList, renderObject } from "../render.js"; +import { suggestCommand } from "../suggestions.js"; + +export const LABEL_HELP = `usage: gitea-axi label [flags] + +commands: + list List labels in the current repository + create Create a label + edit Edit a label's name, color, or description + delete Delete a label + +Run \`gitea-axi label --help\` for the flags of a command. +`; + +export const LABEL_LIST_HELP = `usage: gitea-axi label list [flags] + +List labels in the current repository. + +flags: + --limit Maximum number of labels to return (default: 500) + --help Show this help + +global flags: + -R, --repo Override the repository detected from the git origin remote + --login Select a tea login profile by name +`; + +export const LABEL_CREATE_HELP = `usage: gitea-axi label create --name --color [flags] + +Create a label in the current repository. Idempotent: a label whose name already +exists (case-insensitive) is reported rather than duplicated. + +flags: + --name Label name (required) + --color Label color as a hex code without \`#\` (required) + --description Label description + --help Show this help + +global flags: + -R, --repo Override the repository detected from the git origin remote + --login Select a tea login profile by name +`; + +export const LABEL_EDIT_HELP = `usage: gitea-axi label edit [flags] + +Edit a label in the current repository. The positional name is resolved +case-insensitively. At least one change is required. + +flags: + --name New label name + --color New color as a hex code without \`#\` + --description New description + --help Show this help + +global flags: + -R, --repo Override the repository detected from the git origin remote + --login Select a tea login profile by name +`; + +export const LABEL_DELETE_HELP = `usage: gitea-axi label delete + +Delete a label in the current repository. The positional name is resolved +case-insensitively. Deleting a nonexistent label is an error, not a silent +success (see ADR 0010). + +flags: + --help Show this help + +global flags: + -R, --repo Override the repository detected from the git origin remote + --login Select a tea login profile by name +`; + +const DEFAULT_LIMIT = 500; + +const LABEL_LIST_HELP_SUGGESTION = ["Run `gitea-axi label list --help` to see available flags"]; + +// The list block carries only each label's name, per the spec. +const LABEL_LIST_FIELDS: FieldDef