10 Commits

Author SHA1 Message Date
Peter Schilling d341afdbd5 Add explicit clearing flags to issue update and project update
issue update could set a due date, estimate, parent, project, or
milestone but never remove one, and project update had the same gap for
lead, start date, and target date. Only --unassign and --clear-cycle
existed. cliffy rejects an empty string as a missing option value, so
--due-date "" is not a workaround, and an agent driving the CLI had to
fall back to a hand-written projectUpdate/issueUpdate mutation through
linear api.

Add one boolean clear flag per field, each placed after its set flag and
modelled on --clear-cycle: it conflicts with its set flag (a
ValidationError before any request, with a null check so --estimate 0
counts as a value), skips the lookup the set flag would run, and puts an
explicit null in the mutation input. --clear-project also rejects
--milestone, because a milestone belongs to the project being removed;
--project with --clear-milestone is allowed so a move can detach a stale
milestone in one update. The project update guard treats each clear flag
as an update and its suggestion lists them.

Linear honours null for every field, including startDate, verified on a
scratch project and issue.

Claude-Session: https://claude.ai/code/session_01A9qEGri4p2HZMQSuYsBmub
2026-09-05 07:25:32 -07:00
Peter Schilling d7bba4a670 Add document, project, and initiative comments with threaded replies
The CLI could only comment on issues. Documents, projects, and initiatives
all take comments in Linear (GitHub issue #230 asked for document comments),
so this adds `document comment list|add`, `project comment list|add`, and
`initiative comment list|add`, mirroring `issue comment` with the same
--body / --body-file conventions and the shared Markdown hint. Every comment
`add`, including the issue one, now takes `--reply-to <commentId>`; -p and
--parent stay as aliases so existing scripts keep working.

The entity-agnostic parts live in src/utils/comments.ts: a typed comment
target union feeding one AddComment mutation, strict body handling (an
explicitly blank --body or body file is an error, not a fall-through to the
prompt), a CommentListFields fragment so the four --json shapes cannot drift,
a page collector, and the threaded renderer. Comment lists now fetch every
page instead of stopping silently at 50, and their JSON nodes, plus the
comments in `issue view --json`, carry quotedText (the passage an inline
comment quotes) alongside parent.id. Replies whose root is missing from the
result are rendered as replies naming their parent instead of being dropped.

API findings, verified live against scratch objects on 2026-09-04:

- A reply must carry its entity id as well as parentId; parentId alone is
  rejected, so every add sends both.
- Project comments attach via projectId, but the schema's Project.comments
  connection does not return them; only the root `comments` query filtered
  by project does. Initiative has no comments connection at all. Both list
  commands therefore use the root query and select the entity in the same
  operation so an unknown UUID is reported as not found rather than as an
  empty list.
- Document comments attach via the document's documentContentId, which is
  looked up first; `document(id:)` accepts a UUID or slug directly.
- Linear rejects a reply to a reply and a cross-entity parent with a
  user-presentable message, which is surfaced verbatim.

Linear's not-found error carries the user-presentable message "Could not
find referenced <Type>.", which isNotFoundError never matched, so the
existing not-found branches were dead. Matching that wording exposed a
`document view` catch block that re-threw instead of reporting; it now goes
through handleError like everything else.

Claude-Session: https://claude.ai/code/session_01A9qEGri4p2HZMQSuYsBmub
2026-09-05 07:22:36 -07:00
Peter Schilling 3a00cc7604 Accept team names and IDs, and state names, wherever the CLI takes them
Every command that takes a team now accepts its key, name, or UUID through
one shared resolver (findTeam / resolveTeam / resolveTeams), replacing the
key-only getTeamIdByKey and the ad-hoc uppercasing spread across commands.
One aliased ResolveTeam operation looks up key, name, and (for UUID-shaped
input) id in a single round trip; precedence is key, then id, then name,
applied client-side so a reference that equals one team's key and another
team's name always means the key. Keys stay the canonical downstream form:
filters that matched on team.key still do, with the server's uppercase key,
and callers that need a UUID take it from the same resolved object. An
unknown team now errors with the list of valid keys instead of an empty
result or a raw "Entity not found" from the API.

Only explicit input goes through the resolver. The configured default team
is already a normalized key, and resolving it would add a round trip to
every default-team invocation of the most-used commands for no gain. In
issue create, the interactive substring picker survives only for that
default; an explicit --team that matches nothing errors like everywhere
else.

issue query --state and issue mine --state take a workflow state name or
ID as well as the six type tokens. Names and IDs are resolved within the
queried scope (the team, the teams, or the whole workspace under
--all-teams, where a name matches every team's same-named state), so a
state from another team errors instead of silently matching nothing, and
the error lists the scope's states. Type-only input still sends the same
{ type: { in } } filter with no extra request; a mix of types and names
becomes an or-filter.

The MCP server already describes these parameters as "key, name, or ID"
and "type, name, or ID"; this brings the CLI to parity so an agent does
not need a preliminary team list to translate a name into a key.

Claude-Session: https://claude.ai/code/session_01A9qEGri4p2HZMQSuYsBmub
2026-09-05 07:19:38 -07:00
Peter Schilling 7e0577fa59 Add --content and --content-file to project update
A project's long-form overview body could be set at creation via
project create --content / --content-file, but never changed afterwards:
project update only exposed --description, which is Linear's separate
255-character summary field. Updating the body meant hand-writing a
projectUpdate mutation through linear api and reading the markdown from a
file yourself.

project update now takes the same two flags as create, spelled and worded
identically, and resolves them through create's existing helper so the
mutual-exclusion and file-read behavior cannot drift between the two
commands. Content and description are independent API fields and may be
set together. The no-options guard uses null checks so an empty content
file still counts as an explicit value to forward; Linear currently keeps
the existing body when sent an empty string, so this is not a way to clear
it, and cliffy rejects --content "" outright.

No short aliases: -f already means --description-file on this command and
create has none for content either.

Claude-Session: https://claude.ai/code/session_01A9qEGri4p2HZMQSuYsBmub
2026-09-03 20:11:16 -07:00
Peter Schilling 6684ffe2e3 Add --json to team list, cycle list/view, milestone list/view, project view
Scripts need to map a team name to its key and id, and team list was the
only way to see both, so they had to scrape its table. It was also the last
list command without JSON. The maintainer asked to sweep the other commands
in the same state; the survey found cycle list, cycle view, milestone list,
milestone view, and project view, so all six get -j, --json here. issue mine
stays human-only on purpose (issue query is its JSON surface).

The reporter proposed a five-field subset for team list. The JSON instead
carries every field the query already selects, per the repository rule to
preserve GraphQL names and nesting rather than invent CLI shapes. Lists emit
{ nodes, pageInfo } after the same filtering and ordering as the table, so
archived teams stay hidden and cycles stay newest-first. Views emit the
object as fetched, including every issue rather than the ten-item preview,
and milestone view --all --json includes every page.

Two of these queries took Linear's default page with no cursor: cycle list
and milestone list silently dropped everything past fifty. Adding JSON would
have made that easier to consume without making it safer, so both now
paginate and fail loudly if Linear advertises a page without a cursor. The
view queries gain pageInfo on their issues connection so callers can see
when a page was partial. A 2.0.0 changelog entry claimed cycle list --json;
that merge only touched SVG files, so this is the first time it ships.

Github-Issue: Fixes #276
Github-Issue-Url: https://github.com/schpet/linear-cli/issues/276

Claude-Session: https://claude.ai/code/session_01A9qEGri4p2HZMQSuYsBmub
2026-09-03 16:28:35 -07:00
Peter Schilling 5214ca9b80 Add --add-label and --remove-label to issue update
The reporter asked for a way to detach a label from one issue without
deleting it team-wide, believing --label was additive. It actually
replaces the issue's entire label set (IssueUpdateInput.labelIds), so
the gap was wider than reported: adding one label clobbered the rest.
Rather than only the suggested --remove-label, this maps both
--add-label and --remove-label onto the API's addedLabelIds/
removedLabelIds (one atomic mutation, no read-modify-write). --label
keeps its documented replace semantics for existing scripts, with help
text that now says so. Flag names match gh issue edit, the surface
users and agents reach for first.

A --clear-labels flag was considered (an empty label set is currently
inexpressible in one command) and deliberately deferred: adding a flag
later is backwards compatible, removing one is breaking, and nothing
has asked for clear-all yet.

Invalid combinations error before any network call: --label with
incremental flags, the same resolved label ID in both add and remove,
and --team moves combined with incremental flags (label names resolve
against the destination team, which would make source-team labels
silently unresolvable). Live QA confirmed removing an unattached label
is rejected by Linear's API ("Label <id> is not on issue <id>"), not a
silent no-op — surfaced as-is, consistent with the repo's
explicit-input-errors philosophy.

The reporter's alternative ask (label rename) is deferred: it is
team-wide and would not solve the per-issue detach workflow.

Github-Issue: Fixes #258
Github-Issue-Url: https://github.com/schpet/linear-cli/issues/258
2026-08-05 14:59:10 -07:00
Bryan ad630c5d69 feat(project): support overview content on create (#216)
## Why

Linear projects have two text fields: a short description and a longer
project overview. The CLI only supported the short description, so
projects created from the terminal still needed a manual edit in Linear
to add goals, plans, specs, or launch notes.

This PR lets users create a project with its overview already filled in,
either from inline markdown or a markdown file. That makes `linear
project create` more useful for scripted project setup, templates, and
project specs kept in a repo.

It also exposes a few existing Linear create fields so users can set
priority, labels, members, icon, and color when creating the project
instead of doing a follow-up edit.

## What changed

- `linear project create` now accepts `--content` for inline project
overview markdown.
- `--content-file` reads the project overview from a markdown file.
- `--content` and `--content-file` are mutually exclusive.
- Project create can now set priority, labels, members, icon, and color.
- The command passes these values through `ProjectCreateInput` using
Linear field names.

## Checks

- `deno task codegen`
- `deno task check`
- `deno lint`
- `deno task test`
2026-07-11 13:13:23 -07:00
Peter Schilling 4bb3bef0c7 change formatting rules: no prose wrap, no semi colons 2025-09-02 22:13:34 -07:00
Peter Schilling fae1b1319c tweak usage 2025-08-20 13:26:29 -07:00
Peter Schilling 393193b119 note usage 2025-08-20 13:08:38 -07:00