Commit Graph
686 Commits
Author SHA1 Message Date
bircni ea2e9aadbb fix(repo): sync a branch without switching to it
tea repo sync ran git switch on the synced branch, so syncing main while
on experiment left the user on main. A branch that is not checked out now
moves with update-ref, guarded by its old value; the checked-out branch
merges with --ff-only and needs a clean working tree; a missing branch is
created and set to track the remote. Diverged branches stay untouched.

Sync reports "✓ Synced main from origin/main (fast-forward a..b)" on a
terminal, "already up to date" when nothing changed, and the JSON result
gains the previous commit. Outside a clone, or without a remote for the
repository, the errors now name repo sync instead of checkout.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:51:43 +02:00
bircni 33f6406a1e refactor(auth): write piped auth status through writeTable
The tab-separated status rows no longer use the terminal.Row shim; the expiry is a time cell, so piped output keeps RFC3339 timestamps.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:50:47 +02:00
bircni 390e8e04f6 fix(notification): word --all confirmations and flag values clearly
notification mark-read --all asked "Marked as read all … ?"; each command now asks in its own words, such as "Pin all unread notifications in your inbox?". The status and type filters list their values in help, the api parameter flags read like gh's, and tea auth login --help aligns its method list.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:50:47 +02:00
bircni 9e74ec54ad test(notification): complete platform values and read watch output as NDJSON
A table test covers the completions of alias, config, auth and notification commands, including notification IDs from the inbox. The integration test reads tea notification watch --json as one object per line, and notification feedback, watch lines and completions name a subject the same way.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:50:46 +02:00
bircni 6c124fc998 feat(api): add --verbose to show the request and response headers
--verbose prints the method, URL without query and headers of each request, then the status and headers of its response, on stderr like curl -v. Credentials in Authorization, Proxy-Authorization and Cookie are hidden.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:50:46 +02:00
bircni 83dd4f7938 feat(version): print the version like gh version
tea version and --version printed "Version: development<TAB>golang:
1.27.2". They now print "tea version 1.0.0 (2026-10-01)", the release
notes URL for tagged releases, and "built with go1.27.2" with the build
tags. Builds from go install take the module version from the build
info, and the date comes from the commit the binary was built from.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:50:46 +02:00
bircni 85034c524f fix(notification): read the inbox by default and stream watch as NDJSON
notification commands required a repository unless --mine was given.
They now act on the account's inbox (D17); -R, TEA_REPO or the new
--this-repo limit them to one repository, and --mine is a hidden no-op.

notification list is a table with ID, TYPE, REPOSITORY, SUBJECT ("#17
Improve greeting", bold when unread), STATE and UPDATED, a "Showing N of
M" line and an empty message on a terminal; piped rows add the status
and the URL. The ID links to the subject. JSON gains the subject number.

notification view shows the subject, its state and the latest comment,
then marks an unread notification as read. mark-read, mark-unread, pin
and unpin report each change ("Pinned notification 12
(alice/project#17)"), and --all confirms on a terminal and reports the
count. notification watch --json printed one array when it exited; it
now prints each change as one JSON object per line, prints a line per
change on a terminal, and runs until Ctrl-C there instead of stopping
after a minute.

Status and type filters complete and validate their values, notification
IDs complete from the inbox, and every command has examples.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:50:46 +02:00
bircni 014dba8512 fix(doctor): run every check instead of failing at the first problem
tea doctor printed the resolved instance, account and repository and
checked nothing; it failed with exit code 4 exactly when it was needed,
such as in a clone of an instance without an account, and with a
GITEA_TOKEN of another instance it blamed GITEA_INSTANCE_URL.

It now checks the settings files, whether the instance answers (version
and latency), whether the account's credential works and its token
scopes, whether the repository is readable (default branch), Git and
whether it asks tea for credentials, and where SSH remotes connect. Each
check shows a mark on a terminal and the command that fixes a problem;
piped, each is a tab-separated line. A GITEA_TOKEN for another instance
names both instances. doctor exits with 1 when a check fails, and --json
keeps the target fields and adds checks[].

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:50:44 +02:00
bircni a2bea8da23 feat(auth): guide login, readable status and switch without --host
tea auth login without a method flag asks on a terminal for the instance
(prefilled from the repository, default_host or the only instance), how
to log in (token, browser, SSH key), the pasted token without echo with
a link to the applications page, where to keep it and whether git
should use tea for the instance. Bare hosts are accepted. Logging in to
an account tea already has replaces its credential after a
confirmation, or with --yes; a named account is confirmed before the
token is asked for. Every login ends with "Logged in to HOST as USER
(token, keyring)". OAuth requests write scopes for everything but
administration by default, and --scopes chooses others; --web is an
alias of --oauth.

auth status prints a block per instance with the active account, how
each account authenticates, its expiry, verification and token scopes,
whether git uses tea, and the GITEA_TOKEN of the environment. Piped it
prints tab-separated fields. It fails with exit code 4 when no account
or no usable credential exists instead of printing nothing.

auth switch takes the instance of the repository, --host, default_host
or the sole instance instead of requiring --host, toggles between two
accounts, asks on a terminal with more, and accepts --user. auth logout
takes the account as an argument, names it in its errors, and asks git's
other credential helpers to forget what they stored for the account.
auth refresh points token accounts to tea auth login --with-token, and
the new auth token prints the token of an account for scripts.

Every auth command reports what it did on a terminal and has a long
help text and examples.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:50:44 +02:00
bircni 60de407eca fix(api): filter raw responses with --jq and end output with a newline
tea api /version --jq .version failed with "--jq requires --json", and
--json is rejected for raw responses. api is annotated tea.rawjson now:
--jq filters each successful JSON response, page by page with
--paginate, and prints strings without quotes. A non-JSON response is
an error with --jq; an error response prints its body unfiltered.

JSON responses end with a newline, so an error no longer starts on the
line of the body; on a terminal JSON is indented and colored in the key
order of the response, and other text is sanitized as before. Piped
non-JSON bodies stay byte-exact.

api gains -i for --include and --silent, a Long help modeled on gh api
(placeholders, -f/-F typing, pagination, --jq) and examples, and its
help lists --jq.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:49:54 +02:00
bircni 5e8aa1011f fix(config): print only the value of config get and add display preferences
tea config get printed "key<TAB>value", so $(tea config get editor) was
not usable; it now prints the value alone, and --show-source appends the
source (D18). config get reads the instance's user, and instance settings
come from the selected instance (--host, TEA_HOST, default_host or the
sole instance). Unknown keys list the valid ones, and keys that need
--host or --user say so. config path no longer loads the settings, so it
works when hosts.yml is broken. config list is a table on a terminal.
config set confirms the change on a terminal and warns when default_host
has no account.

config.yml stores the hyperlinks (auto, always, never) and markdown
(auto, raw) preferences that tables and views now read, instead of the
TEA_HYPERLINKS and TEA_MARKDOWN variables. The environment table moves
out of tea config --help into tea help environment.

alias set refuses to replace an alias without --clobber, drops a leading
"tea", and explains how to quote an expansion with spaces or flags.
alias delete takes several names or --all, alias list is a table, and
an empty list says so on a terminal.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:49:52 +02:00
bircni 0dfe7def9b feat(secret,variable): read values like gh and report stored names
secret set and variable set read the value from -b/--body, -F/--body-file
or standard input when it is not a terminal (one trailing line break
removed), and otherwise prompt for it; --env-file sets every NAME=value
of a dotenv file. --value and --value-file keep working. The scope flags
are -o/--org and --account on each command, and secret list no longer
offers --account, which Gitea cannot list.

Commands report what they did, naming the scope and the name Gitea
stores: "Set Actions secret LOWER_NAME for alice/x (names are
upper-cased)"; --json returns the stored name and omits an unknown
creation time. Deleting asks "Delete secret X from alice/x?". Lists show
NAME, CREATED and DESCRIPTION for secrets and NAME, VALUE and DESCRIPTION
for variables; variable view shows a record and the new variable get
prints the raw value. A missing secret or variable is reported by name
and scope.

The global --user selects the account tea acts as, unlike gh's --user,
so secret and variable add a note on a terminal pointing to --account
when it is given, as run list does for --actor. Help explains the scopes.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:46:57 +02:00
bircni 4edb836416 feat(workflow): address workflows by name and check dispatch inputs
Workflows are found by file name, file name without extension, path or
name, case-insensitively, with the candidates listed when a name is
ambiguous. workflow list shows a NAME, FILE and STATE table, and workflow
view shows the workflow with its last runs, prints its file with --yaml
(at --ref) or opens it with --web.

workflow run reads the workflow_dispatch inputs of the workflow at the
ref and refuses unknown inputs, missing required inputs and values a
choice, boolean or number input does not accept, before dispatching. In
a terminal it asks for missing inputs, or all of them when none is given.
Without --ref it uses the checked-out branch, which must be on the
server, or the default branch when the repository does not come from the
working directory. It reports "Dispatched CI (ci.yml) on main → run 33"
with the command to watch it, prints the run's URL, and --follow follows
the logs of all jobs within --timeout.

workflow enable and disable no longer ask for confirmation, report the
change, warn when there is nothing to do, and return the workflow with
--json.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:46:26 +02:00
bircni 9f69a89537 feat(run): show runs like gh, watch them live, and download artifacts
run list prints a table with a status icon, the workflow name, title,
branch, event, elapsed time and age, and the total from the server; runs
carry workflow, workflow_path and run_number in JSON. run view shows the
run's state and trigger, its jobs with their steps and durations, the
next steps and the run's URL; --json includes the jobs, --log prints
their logs and --web opens the run.

Run commands accept #N and run URLs, and ask for a recent run in a
terminal when the argument is left out. run watch redraws the run on the
alternate screen, then prints it with a summary; an expired --timeout
names the run and suspects a missing runner when no job started.

run cancel no longer asks for confirmation, rerun refuses unfinished runs
up front, reruns a single job with --job and reports the new attempt,
and delete names the run in its prompt. run logs follows several jobs
with --follow, prefixing lines with the job's name, selects jobs by name
or with --failed, and downloads the archive with --output or --dir.
run download extracts the artifacts of a run by name, and artifact list
shows sizes and expiry.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:46:26 +02:00
bircni b09c1aab4a fix(run): show logs past jobs that have not started
run logs stopped at the first job without logs with a misleading not-found
error. Report such a job as "build (job 35): not started yet" (or that it
never started when it was canceled or skipped first) and continue with the
next job. Jobs report started_at and completed_at, null until they happen.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:46:26 +02:00
bircni 68da491b9e fix(run): filter runs by creation time and report unstarted times as null
run list --since/--until compared the start time, which queued runs report
as the Unix epoch, so every queued run was dropped. Filter on created_at,
stop scanning at the first older run, and report a run's missing start or
completion time as null instead of 1970-01-01.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:46:26 +02:00
bircni f21c05ead4 fix(ssh-key): title keys by their comment and recognize private keys
ssh-key create - required --title although the key carries a comment,
and a private key failed with "supply exactly one valid SSH public key".
The title now defaults to the key comment, then the computer's name, the
file argument is optional (stdin when piped), and a private key points to
its .pub file. Adding a registered key names the key it duplicates.

ssh-key list shows ID, TITLE, FINGERPRINT and ADDED with a header, view
shows the key instead of its list row, delete asks with the key's title
and fingerprint, and mutations print ✓ lines. create gains the gh alias
add and -t for --title, key IDs complete, and every command has examples.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:42:49 +02:00
bircni ab3e91acd1 fix(webhook): validate events and types, sort events and show delivery URLs
webhook create --events pushh created a webhook that fired on nothing,
since Gitea drops unknown events, and unknown types or a Slack webhook
without a channel failed with raw HTTP 422 errors. --events and --type
now accept only what Gitea knows, list it in help and complete it, and a
Slack webhook needs --config channel=.

Events came back in a different order on every call; they are sorted
now. Webhooks can be told apart again: list, view and JSON show the
delivery URL without user information and query, and without the path
for chat services whose paths carry tokens; view --reveal-url shows it
whole. Tables collapse complete event groups such as pull_request (8).

On a terminal, webhook edit no longer needs an authorization flag: since
Gitea drops the stored header on every edit and never returns it, it asks
for the header again and keeps it, and an empty answer suits webhooks
without one. Without a terminal the edit still requires the flags. Edit
flags lose the create defaults, the unsupported secret flags are hidden
on edit, deletions name the webhook and its scope, and mutations print ✓
lines. terminal.View gains Bodyless for resources without a body.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:42:49 +02:00
bircni 769e93f5fd fix(team): take team repositories as arguments and address teams by name
team repo add 5 org/repo was rejected, and team repo add 5 silently used
the repository of the current directory and failed with an internal
error. team repo add and remove now take the repositories as arguments,
resolve bare names inside the team's organization, refuse repositories of
other owners and --repo, and never discover a repository.

Teams can be addressed as ORG/TEAM by name or ID as well as by their
global ID. team list drops the always-empty ORG column and shows the
derived access ("write (code, issues, pulls)" instead of none), member
and repository counts; team view shows units, members and repositories.
--permission read or write now grants every repository unit on both
create and edit, and edit no longer shows the create default. team
create takes the name as ORG/NAME or a second argument. Member and
repository changes skip users and repositories already in the wanted
state with a warning, name them in confirmations and print ✓ lines. Every
command has examples, and teams, members, users, repositories and --unit
values complete.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:42:49 +02:00
bircni c18dae6685 fix(admin): offer --purge for users who own repositories and show roles
admin user delete of a user who owns repositories failed with Gitea's raw
HTTP 422. The error now says what blocks the deletion and how to proceed,
and --purge deletes the account with its repositories, packages and
organization memberships after typing the username on a terminal.

admin user list shows ROLE and STATUS columns (admin, restricted, user;
active, inactive, login prohibited) with the email and creation date
instead of admin=true/active=true cells, and --query filters by username,
name or email. admin user view shows the account instead of its list
row. create asks for a missing username and email address with readable
labels and re-asks for invalid answers, mutations print ✓ lines, usernames
complete, and every command has examples.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:42:49 +02:00
bircni ef8cfbc4b7 fix(org): check membership before member changes and polish org output
org member remove and public/private exited 0 without a word for users
who are not members, and member list --public showed concealed members,
because Gitea lists them as public to administrators. Membership changes
now check the membership first: removing a non-member warns, publicizing
one fails, and a change to the current visibility warns. Member list
checks each membership and gains a VISIBILITY column.

org list and member list get a header and context line through
writeTable, org view shows the description, website, location and member,
team and repository counts, mutations print ✓ lines, and org delete asks
to type the organization's name after refusing organizations that still
own repositories. public and private become publicize and conceal, the
old names stay as aliases, and org edit --visibility no longer shows the
create default. Every command has examples, and organizations and members
complete.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:42:49 +02:00
bircni 48a188ceac fix(app): pretty-print JSON on a terminal and reconcile the foundation streams
writeJSON uses the indented, colored JSON writer when stdout is a
terminal. Help and JSON tests now expect the -L shorthand column and
compare terminal JSON without color sequences.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:57:38 +02:00
bircni 04d5f2aed0 feat: suggest similar flags for an unknown flag
tea issue list --lable bug now fails with "unknown flag: --lable; did
you mean --label?". Suggestions are the command's visible flags that
start with the typed name or are at most two edits away from it.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:55:27 +02:00
bircni f03d0fdf54 feat(completion): complete flag values and stop offering file names
Every value completed to file names, because no command registered a
completion. Values without a completer now complete nothing, and only
path flags (--*-file, --input, --attachment, --asset, --file, --ssh-key,
--dir, --cwd) and arguments named file or directory complete paths.

--json completes the fields of the command, also after a comma. --host,
--user, --remote and -R complete from hosts.yml, Git remotes and the
instance. enumFlag and enumValues give a flag one list of values that
validation (case-insensitive) and completion share, and instance
completers for issues, pull requests, labels, milestones, users,
branches, tags, releases, organizations, teams, workflows, runs, secrets
and variables are ready for commands to attach. They give up after two
seconds, never prompt, retry or refresh an OAuth token, and drop values
with control characters.

tea completion bash now says to use eval "$(tea completion bash)",
because bash 3.2 on macOS cannot source <(...), and names the
bash-completion package; the zsh help says to run compinit first. The
README matches.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:55:27 +02:00
bircni 48c1e5b781 feat(help): group root help, show relevant flags and add help topics
The root help lists commands in gh-style groups (core, Actions,
additional and alias commands) followed by help topics, examples and
pointers. Every help page uses USAGE, ALIASES, FLAGS, INHERITED FLAGS,
ARGUMENTS, JSON FIELDS, EXAMPLES and LEARN MORE sections. Inherited
flags show --repo, --remote and --cwd only where a repository is
selected, --json and --jq where JSON is printed, --yes where the
command confirms, and one line for the remaining global flags.

tea help environment, exit-codes and formatting document global flags,
environment variables, exit codes and JSON output. tea help with an
unknown topic is a usage error with suggestions instead of printing the
root help. A test keeps Shorts, examples and Long texts complete, with
allowlists for the commands that still lack them.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:55:27 +02:00
bircni f6a0bcfa6a feat: accept ls and rm for list and delete commands
Every list command also answers to ls and every delete command to rm,
as in gh. flagAliases lets a command accept old flag names for renamed
flags without listing them in help, so singular gh names such as
--add-label can become primary while the plural names keep working.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:55:26 +02:00
bircni eb51dc8619 fix: report unknown --json fields with the fields a command outputs
tea issue list --json number,titel failed with "accepts 0 arg(s),
received 1": a field list with an unknown name stayed a positional
argument. A field-shaped word after --json now binds to it unless the
command takes the word as an argument, and an unknown field lists the
available fields. --json= is a usage error instead of printing text.

--jq alone now implies --json, except on commands annotated
tea.rawjson that filter raw JSON themselves; -q is short for --jq and
-y for --yes. Fields are listed alphabetically, and terminal.JSONOutput
can indent and color JSON for a terminal.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:55:26 +02:00
bircni 023724d4c9 refactor(app): name the feedback label helper refLabel
The reference parser uses issueRef as a reference kind.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:49:40 +02:00
bircni 71189fd15f fix: tighten error classification and pull request selection
Only Gitea's archived-repository messages mean the repository is archived,
so an archived label is no longer reported as one. Refused POSTs to the
organization, team, user and SSH key collections name the resource they would
have created, and the existing pull request is looked up with escaped branch
names.

pullSelector without an argument refuses an explicit --repo, like gh: the
checked-out branch says nothing about another repository.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:48:50 +02:00
bircni dc43272085 feat: explain refusals to delete or rerun an unfinished run
Gitea refuses to delete or rerun a run that has not finished with "this
workflow run is not done". Report "run 37 has not finished; cancel it first:
tea run cancel 37", and cover the next steps added to refusals in tests.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:48:50 +02:00
bircni 6fe72cdfd1 fix(app): publish downloads with umask permissions and add destination flags
Downloads were created like temporary files, readable by their owner only.
They now get the permissions the umask allows, as files a shell redirection
creates; API responses written with --output-file stay private. An
--output-file that is a directory is reported as such instead of "output
file already exists", and a download written to a terminal refuses binary
data and strips control sequences from text.

downloadFlags gives download commands the gh destination flags: -O/--output
(a file, a directory, or - for stdout; --output-file stays as a hidden alias),
-D/--dir, --clobber and --skip-existing, with the download's own name as the
default destination. Names that would escape the destination are refused.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:48:50 +02:00
bircni 1505df2bb1 feat(app): parse issue, pull request and run references
resolveRef accepts N, #N, [HOST/]OWNER/REPO#N and issue, pull request or run
URLs; a URL or qualified number selects its repository for the invocation and
must agree with --repo. resolveRefs does the same for several references.

pullSelector also accepts a head branch or OWNER:BRANCH and, without an
argument, finds the pull request of the checked-out branch: the one pr
checkout recorded in branch.<name>.tea-pr, else the open pull request whose
head is the branch's upstream in the upstream remote's repository, else one
whose head has the local branch's name, else N of a pr/N branch. This lets pr
commands find the pull request of a branch pr checkout created.

target.expandMe resolves @me in user-valued flags to the login tea acts as,
and git.CurrentBranchConfig reads the branch configuration involved.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:48:50 +02:00
bircni 2ef6090734 feat(app): show the usage line on argument errors and allow -L up to 1000
Positional argument errors name what is wrong from the command's usage, then
print the usage line and where to find help:

  missing issue number
  Usage: tea issue view <number> [flags]
  Run 'tea issue view --help' for more information.

instead of "accepts 1 arg(s), received 0". Resource numbers accept #N.

--limit gains the gh shorthand -L and accepts up to 1000 results; the list
services already page through the server's smaller pages to fill a window.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:48:49 +02:00
bircni 9b1d3a61e6 feat: explain failed requests and exit silently on interrupt
Failures of tea's own API requests now name what was missing or refused
instead of "resource or API endpoint unavailable":

  issue #999 not found in alice/project
  repository alice/nope not found on http://localhost:3001 (or you don't have access)
  issues are disabled for bob/project
  #1 in alice/project is an issue, not a pull request; view it with: tea issue view 1
  alice/old-stuff is archived (read-only); unarchive it with: tea repo edit ...

A 404 on a repository endpoint looks the repository up once to tell a missing
or hidden repository from a disabled feature, an empty repository or an issue
number given to a pull request command. Refusals get their next step, a
rejected GITEA_TOKEN and unreachable servers are named, and timeoutError words
an expired --timeout. Errors a command already wrapped with its own context,
and raw tea api responses, keep their wording.

Ctrl-C and SIGTERM exit with 130 without printing an error, and a declined
confirmation prints "Canceled." with exit code 3.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:48:49 +02:00
bircni 2d4d34ddfc feat: accept repository shorthands and name instances in account errors
Repository arguments and --repo accept NAME (owned by the active account, or
the --sudo user), OWNER/NAME.git and HOST/OWNER/NAME next to URLs; --host,
TEA_HOST and GITEA_INSTANCE_URL accept a bare HOST[:PORT][/PATH], which takes
the scheme of the configured instance it names, else https. --repo origin in a
clone with that remote suggests --remote origin.

Account errors name the instance, the accounts that are logged in and where
the instance came from, such as "no account for https://gitea.com (instance
from git remote \"origin\")", instead of "no selected account".

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:48:49 +02:00
bircni c8073f1b2e feat(services): classify API failures by resource and report page totals
Add NotFoundError, ConflictError and AmbiguousError so commands can report
"issue #999 not found in alice/project" or "alice/old-stuff is archived"
instead of the transport's status text. Classify wraps a failure with the
resource the caller names; ClassifyRequest infers it from the endpoint for
failures no service classified, and maps the refusals Gitea explains (archived
repository, merged or draft pull request, own review, existing resource,
unknown user, disabled workflow, completed run, reused SSH key).

Page reports the X-Total-Count that listWindow discarded, NameOrID resolves a
resource by ID or name (numeric values are IDs first, ambiguous names list
their IDs), and rawPage pages raw collection endpoints.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:48:49 +02:00
bircni 0254c0509e fix(transport): keep harmless query values visible in --debug output
--debug replaced every query value with REDACTED, so it could not show which
filters were sent. Redact only parameters that can authenticate a request
(tokens, passwords, secrets, signatures, storage credentials, OAuth codes and
the OAuth state), and log the sanitized server message of failed responses.

A 404 now names the request instead of blaming the instance, and API errors
recorded from a request keep its instance and credential so a caller can look
up why it failed. Network failures keep the server they could not reach.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:48:49 +02:00
bircni 62962b4f70 feat(terminal): add confirmation defaults and fix form input loss
Every confirmation defaulted to No, so Enter after a filled-in form
canceled it; prompts were titled with raw flag names and aborted on an
empty answer; and the shared issue and pull request form lost input:

- Multi-selects hid their last option, so a single assignee looked like
  an empty list and labels showed four of five. They now get an explicit
  height and scroll past eight options.
- A pty without a window size, such as `script` run without a terminal,
  made bubbles panic. Forms now refuse it with "terminal size unknown;
  pass the values as flags or use --no-input".
- Without a configured editor, the form ran to the end and then failed
  with "configure an editor", discarding everything typed. The editor is
  now resolved before the form; without one the body is typed into an
  inline multi-line field.
- The pull request form said "Issue title" and "Issue description". The
  shared fields read "Title (required):" and "Body (markdown):", and the
  review body label gained its missing space.

New prompt API for the area commands: confirmAction with a
terminal.Confirmation (DefaultYes for constructive actions, Typed for
type-to-confirm, NonTTY to name the action in the --yes error) and
requireInput with a human label and inline validation. Declined or
aborted prompts return terminal.ErrCanceled (exit code 3). The existing
confirm and require helpers keep working on top of them, and prompters
without the new terminal.Asker methods keep the plain behavior.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:43:11 +02:00
bircni 4fddcc6781 feat(app): report mutations on stderr and say what --web opens
Mutations printed a list row, or nothing, so a person could not tell
what changed and scripts parsed rows meant for people. Commands now get
the helpers for the gh convention:

- done and warn print "✓ Closed issue alice/x#5 (Fix login)" and
  "! … is already closed" to stderr, only when stdout is a terminal and
  --json is not set.
- printURL prints a created resource's URL to stdout unless --json is
  set, so url=$(tea issue create …) works.
- batchDone applies a mutation to several selections like batch and
  reports each success with a done line instead of a table.
- issueRef formats "alice/x#5 (title)" references.

--web now prints "Opening localhost:3001/alice/project/issues/1 in your
browser." to stderr on a terminal before the browser starts.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:43:11 +02:00
bircni 55574a9fd0 feat: fit list tables to the terminal and write TSV when piped
Lists had no header, wrapped every row once a title was longer than
the terminal, and were space-padded when piped, so cut -f and awk -F'\t'
could not split them and empty cells collapsed.

terminal.Table now takes column specs and a Display. On a terminal it
prints an uppercase header, aligns columns two spaces apart and shrinks
flexible columns, widest first, to Streams.Width (the terminal, then
$COLUMNS, then 80), cutting by display width with "…". States use the
terminal's ANSI 16 colors, hex colors fall back to the 256-color palette
unless COLORTERM advertises 24-bit, and OSC 8 links appear only on the
first column, independently of color (TEA_HYPERLINKS=auto|always|never).
Piped tables are tab-separated without header, color or links, and times
are RFC3339 UTC instead of "about 2 hours ago".

terminal.View lays out single resources (title, badges, metadata, fields,
body, footer URL) and renders markdown bodies with glamour on a terminal
only (TEA_MARKDOWN=raw keeps them raw). Shared formatters cover relative
times, timestamps, sizes, durations and status icons.

In the app, writeTable and writeView are the new list and view writers:
a "Showing N of M …" context line, an empty-state message on stderr and
the next-page hint. writeList, writeItem and batch keep working and now
render through the new table: tab-separated when piped, truncated at the
widest column on a terminal, with headers for issue list and pr list.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:43:10 +02:00
bircni 1c9cc32d28 fix(admin): keep must_change_password unless it is passed to user edit
--must-change-password defaulted to true on admin user edit as well, so
changing a password with --password silently forced the user to change
it again at the next sign-in. On edit the flag now defaults to unset and
is sent only when passed; create keeps requiring a change by default.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 22:00:29 +02:00
bircni b6b3ddc91b fix: do not ask for confirmation right after prompting for input
secret set, variable set, team create, webhook create and admin user
create asked a confirmation that defaults to No right after the user
typed the missing values, so pressing Enter discarded the input. They
now submit what the user entered; deletions still confirm.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 21:59:18 +02:00
bircni fcfc497a77 fix(webhook): explain why webhook edit needs an authorization choice
Gitea 28 drops a webhook's stored authorization header on every edit
that does not send it again, and never returns it, so tea cannot leave
it unchanged. The error offered --clear-authorization as an equal
alternative, which wiped credentials users still needed. It now says
the header is lost unless sent again, asks for it first, and reserves
--clear-authorization for hooks without one.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 21:57:52 +02:00
bircni 784423bd9d fix(team): merge team edit --unit into the current unit policy
Gitea replaces a team's whole unit policy on edit, so
`tea team edit 5 --unit repo.wiki=write` revoked code, issue and pull
request access. team edit now merges --unit into the current policy;
--unit-replace keeps the old replacing behavior.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 21:56:32 +02:00
bircni a3f5468628 fix(team): name the team in delete prompts and protect the owner team
team delete confirmed with "delete team 3 on <host>" and deleted
whatever the ID pointed at, including an organization's owner team.
It now looks the team up first and asks to delete team "devs" (ID 5)
from acme. Deleting an owner team requires --force and, interactively,
typing the team's org/name.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 21:54:57 +02:00
bircni 872ba31607 fix(auth): prompt for --with-token without echo on a terminal
With stdin on a terminal, auth login --with-token read the pasted token
from the raw terminal, which echoed it in clear text without a prompt.
It now asks with a hidden "Paste an access token" prompt, and refuses
to read the terminal when prompting is disabled. Piped stdin is read as
before.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 21:53:35 +02:00
bircni 7b5c9ad9c6 fix(auth): reset other credential helpers in auth setup-git
setup-git appended tea's helper after the helpers Git already had, such
as osxkeychain from the system configuration. Those helpers answered
first, and after a push Git asked every helper to store tea's token, so
it ended up in the macOS Keychain where auth logout never removes it.
setup-git now writes an empty helper for the instance before tea's own,
as tea already does for its per-operation Git credentials. Add
--keep-helpers for the previous behavior.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 21:52:36 +02:00
bircni ab6228e25b fix: sanitize run logs and api responses written to a terminal
run logs and api copied server bytes straight to stdout. CI logs are
attacker-influenced through pull request workflows, so they could drive
the terminal of whoever read them. On a terminal, run logs now keep only
SGR color sequences, job names are reduced to plain text, and api drops
control characters from the response. Piped output and --output-file
stay byte-exact.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 21:50:36 +02:00
bircni b86fcb2ff5 fix(pr): strip terminal control sequences from pr diff on a terminal
pr diff copied the server's patch bytes straight to stdout, so a pull
request could change the terminal title, clear the screen or move the
cursor of whoever viewed its diff. Add terminal.SafeWriter, which
filters streamed server text like terminal.SafeText when stdout is a
terminal, and use it for pr diff. Piped output stays byte-exact, so
`tea pr diff N | git apply` keeps working.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 21:49:14 +02:00
bircni 299428bf10 fix(pr): keep targeting the base repository after a fork PR checkout
Repository discovery preferred the current branch's upstream remote,
which for a fork pull request is the pulls/<owner>/<repo> remote that
pr checkout adds, so every later command targeted the fork. pr checkout
now records the pull request number and base remote in
branch.<name>.tea-pr and branch.<name>.tea-base-remote, discovery
honors the recorded base remote, and it skips pulls/* head remotes.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 21:47:06 +02:00