Issue tables gain a compact CYC column (only when a listed team has
cycles enabled): 'now' for the active cycle, +1/-1 from the API's
next/previous flags, +N/-N anchored on team.activeCycle, and #N when no
anchor exists (cooldown or pre-first-cycle). issue view annotates its
Cycle meta part with the same token, and issue JSON output now carries
the cycle flags and team anchor in raw GraphQL shape.
The shared cycle resolver understands now/next/previous and signed
offsets, so --cycle on query/mine/create/update and cycle view all
speak the relative vocabulary. issue update gains --clear-cycle
(explicit cycleId: null, mirroring --unassign). Also fixes issue query
--search silently dropping the --cycle filter.
Three related gaps in member listing:
`team members` had no --json output. It now emits the connection shape
({ nodes, pageInfo }) with GraphQL field names preserved, matching label list
and project list.
There was no way to list everyone in the workspace, only per-team. Adds
`linear user list` (alias `u`) querying viewer.organization.users. This is a
sibling command rather than a `team members --organization` flag: workspace
members are definitionally not team members, and a flag that has to error when
combined with the positional team key is the design saying it's two commands.
Members now show admin, owner, and you markers alongside the existing
inactive/guest/not-assignable ones, via a shared renderer.
Along the way this fixes `team members --all`, which was a no-op. getTeamMembers
never passed includeDisabled, so Linear defaulted it to false and disabled users
were never fetched — the client-side `active` filter was narrowing a set that
could not contain them. The regression test pins includeDisabled in the mock
variables, so it fails loudly if the flag stops reaching the API.
Note: the --all fix could not be exercised against real data; the workspace used
for QA has no disabled users.
There was no way to unassign an issue. The mutation input was built as a
`Record<string, string | number | string[] | undefined>`, a type that
structurally cannot hold null, so `IssueUpdateInput.assigneeId` could never
be set to null no matter what flags were added.
Swap that hand-rolled Record for the codegen'd `IssueUpdateInput` — matching
what `project update` already does — and add an explicit `--unassign` flag.
Passing both --assignee and --unassign is a ValidationError rather than one
silently winning.
No short alias: `-U, --unassigned` is already bound in three sibling commands
as a read-side filter, and putting a data-clearing mutation one shift-key away
from a harmless filter invites accidents.
Verified against the real API that Linear honors `assigneeId: null` — worth
checking explicitly, since it silently ignores `projectId: null` elsewhere.
`document create` accepts --project but `document update` did not, so a
document's project attachment was fixed at creation time — changing it required
the web UI. Add --project to update (UUID, slug ID, or name), reusing the same
resolveProjectId path as create so resolution and errors stay consistent.
The reporter framed this around Linear's web UI supporting multiple project
links per document with add/remove semantics (--project X --remove). The API
tells a different story: a Document has a single related project (the scalar
DocumentUpdateInput.projectId), and create requires exactly one anchor — so a
document always has one project and setting merely replaces it. A detach flag
was prototyped, but live testing showed the API silently ignores
`projectId: null` (returns success, keeps the project), so shipping it would
have been a silent no-op; it was dropped rather than lie about detaching.
Github-Issue: Fixes#225
Github-Issue-Url: https://github.com/schpet/linear-cli/issues/225
Discovering a team's valid workflow state names required a raw GraphQL query,
and passing a wrong `--state` to `issue create`/`issue update` failed with a
bare "Workflow state not found" and no hint at the valid options.
Add `linear team states [teamKey]` (table + `--json`) reusing the existing
getWorkflowStates helper, and make the wrong-state failure actionable: both
issue commands now fetch the states once, resolve against them, and on a miss
throw an error that lists the valid states and points at `linear team states`.
Resolution moves to a pure resolveWorkflowState + a shared
workflowStateNotFoundError factory so both call sites stay identical and the
matching logic is unit-testable; the fetched list is reused for the suggestion
(no second round-trip).
The reporter also hypothesized a raw TypeError for unknown teams; verified this
is false — team(id) is non-null in the schema and an unknown team already
yields a clean "Could not find referenced Team." error, so no null guard is
added. The `--state` help text was left unchanged to avoid churning the
(width-sensitive, globally stale) generated skill docs; the enriched error is
the load-bearing discovery path.
Three user-facing strings advertised a `linear configure` command that does
not exist — the interactive setup command is registered as `config`, so
`linear configure` failed with "Unknown command". The reporter hit this via
`linear team id` with no team configured.
Fix all three suggestions to name the canonical `config` command, and add
`configure` as an alias so the natural command people (and the CLI's own help
text) reach for just works instead of erroring. Keeping `config` canonical in
every string and doc while tolerating `configure` is more robust than a bare
text swap: the alias matches the exact instinct that produced the bug.
Also upgrade the bare `Error` thrown for an integer id with no team to a
`ValidationError` so it renders with the standard ✗ + suggestion treatment.
The root command moves from src/main.ts into src/cli.ts so it can be imported
by tests (to assert the alias resolves) without its complex inferred cliffy
type entering the published public API and tripping no-slow-types; main.ts
stays the entry point and only runs it under import.meta.main.
Detect secret-tool availability by whether the executable can be launched,
rather than by probing the Secret Service.
Distinguish missing lookup results from operational failures using stderr,
and stop treating clear failures as successful deletions. Extend the
integration test to cover missing D-Bus and missing executable cases.
Fixesschpet/linear-cli#231
The comment pointed at astral-sh/cargo-dist v0.28.3, but that repo stops at
0.28.7 and the version this workspace pins (cargo-dist-version = 0.31.0) is
only published from axodotdev/cargo-dist, so the documented command fails with
'failed to find tag v0.31.0'.
Point at the version dist-workspace.toml actually pins, and note why matching
it matters: 'dist generate' rewrites cargo-dist-version to whatever version
generated the file, so running a mismatched dist silently repins the workspace.
027f25e pinned deno to 2.7.9 in mise.toml and ci.yaml, but the two workflows
that actually build and ship artifacts were left on 'v2.x': release.yml runs
'deno compile' for every target triple, and publish.yaml runs codegen before
'jsr publish'. So the binaries users install were produced by whatever v2.x
resolved to at build time, not the toolchain the project pins and tests on.
build-setup.yml is the dist 'github-build-setup' hook that generates the deno
step in release.yml, so it and the generated line are updated together to keep
'dist generate' a no-op.
setup-deno@v2 already runs an exact 2.7.9 in both ci.yaml jobs, so this is the
version resolution those jobs have been proving all along.
isKeyringAvailable() only probed on Linux and returned true unconditionally
everywhere else, so on macOS the keyring round-trip always attempted a real
write. Under ssh or an agent shell the login keychain has no UI session to
unlock it, so 'security add-generic-password' fails with exit 36
(errSecInteractionNotAllowed) and 'deno task test' fails locally even though
nothing is wrong with the code.
Probe with 'security show-keychain-info', which is read-only and returns 36 in
exactly that state. A read-only probe is not enough on its own: reads still
succeed while locked (find-generic-password returns 44), so only a
write-capable check detects it. The guard keys on 36 specifically rather than
any nonzero exit, so a genuine keyring bug still fails the test loudly.
CI is unaffected: its macOS keychain is unlocked, and the keyring job now sets
LINEAR_KEYRING_INTEGRATION=1 to force the test to run, so a probe that ever
wrongly reports 'unavailable' cannot quietly turn that job into a no-op.
Adds a repeatable `--label` flag to `linear project update`, mirroring the
existing `project create --label`: names resolve case-insensitively and an
unknown label raises NotFoundError (no auto-create).
The flag uses replace semantics — the supplied labels become the project's
complete label set — consistent with `project update --team` and
`issue update --label`. Empty/whitespace labels are rejected up front, and
case-insensitive duplicates collapse to a single ID.
The project-label lookup is extracted from project-create into a shared
getProjectLabelIdByName helper in utils/linear.ts so create and update stay
identical.
This reshapes the update half of #226 to the repo's existing label
conventions; the create half of #226 already shipped in #216, and the PR's
auto-create/interactive-create behavior is intentionally dropped.
Co-authored-by: KinomotoMio <200703522+KinomotoMio@users.noreply.github.com>
Corrective delta on top of #236's generator hardening:
- deno fmt failure is now fatal instead of a logged warning; unformatted
committed docs would otherwise break `deno fmt --check` in CI.
- The top-level error boundary prints a concise message instead of the raw
error (and its stack trace) on every abort path.
- SKILL.md is rendered from its template before writeReferences prunes any
stale docs, so a missing or broken template aborts before touching the
references directory.
This reworks `skills/linear-cli/scripts/generate-docs.ts` for robustness and stable output, and regenerates the references with the new ordering.
Robustness:
- `run()` now catches the error `Deno.Command` throws when a binary is missing (e.g. `NotFound`) instead of crashing, returning a failed result.
- Help-fetch failures are collected and the run aborts before writing, so an error string can never be embedded into committed docs.
- Reference files are written before stale ones are pruned, so a partial failure can no longer gut the `references/` directory.
- `main()` runs under an `import.meta.main` guard with a `.catch` that logs and exits non-zero.
- The two `pop()` non-null assertions are replaced with a safe `lastSegment()` helper.
Output stability:
- Top-level commands and each subcommand list are sorted by name before rendering, so generation is deterministic and stops producing reordering churn between runs (related to the motivation behind dropping the staleness check in #218). This is the source of the large but reorder-only docs diff in this PR. Happy to drop the sort if you'd rather keep CLI help order.
Cleanup:
- The markdown builders are rewritten as pure `map`/`join`/`flatMap` helpers, and the unused `formatCommandMarkdown` helper is removed.
Verified with the repo-pinned Deno (2.7.9): `deno fmt --check`, `deno lint`, `deno check`, and `deno task generate-skill-docs` run twice with an empty diff between runs. The docs diff is reordering only (identical sorted line sets per file).
The CLI was inconsistent about whether --project and --milestone flags
accept a UUID, slug, or name depending on which command you called.
Several commands silently rejected one form with a misleading "not
found" error.
- Extend resolveProjectId to accept UUID, slug, or name uniformly.
- Add resolveMilestoneId that accepts a UUID directly, or a name when
--project is supplied so the milestone lookup can be scoped.
- Wire the resolvers into issue create/update/mine/query, milestone
create/update/list, and milestone view (which now also accepts an
optional --project for name-based lookup).
- Remove document create's duplicate local resolver in favor of the
shared one.
- Update --help to say "UUID, slug ID, or name" everywhere.
Closes#221
---------
Co-authored-by: Theo Gregory <theo@gregory.sh>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Schilling <code@schpet.com>
Co-authored-by: Bryan <bryandesigning@gmail.com>
Co-authored-by: Joseph <31323641+josephyooo@users.noreply.github.com>
Co-authored-by: Magnus Buvarp <magnus.buvarp@gmail.com>
Co-authored-by: Rengar Lee <Rengarlee@163.com>
Co-authored-by: Hyunsu Lim <hyunsu.lim01@gmail.com>
Co-authored-by: Luc Leray <luc.leray@gmail.com>
Co-authored-by: c-99-e <268417377+c-99-e@users.noreply.github.com>
Co-authored-by: Al Johri <al.johri@gmail.com>
Co-authored-by: Mihai Chiorean <mihai.v.chiorean@gmail.com>
Co-authored-by: Mihai Chiorean <mihai-chiorean@users.noreply.github.com>
Co-authored-by: Jeffrey Holm <jeff.holm@scale.com>
Co-authored-by: Paymahn Moghadasian <paymahn1@gmail.com>
Co-authored-by: Evan Jacobson <evanjacobson3@gmail.com>
Co-authored-by: schpetbot <bot@schpet.com>
Co-authored-by: Alex Broekhof <alex@quilt.com>
#170 adds `labels` to `issue view --json` by selecting them in both
GetIssueDetails and GetIssueDetailsWithComments. Its dedicated
labels-with-content test only exercises the --no-comments (GetIssueDetails)
path; the with-comments path was covered only with empty labels. Add a
--json (with comments) test carrying real labels so both fetchIssueDetailsRaw
code paths are verified to surface labels — the consistency both were meant to
guarantee.
## Summary
This improves `linear issue create` in three related areas:
- allow `--project` to keep issue creation interactive, the same way
`--parent` already does
- add optional project selection in interactive issue creation
- add configurable default self-assignment behavior for created issues
Before this change, `linear issue create --project "Dashboard"` skipped
interactive mode and failed with "Title is required when not using
interactive mode". With this PR, `--project` is treated as an
interactive-safe input, so users can preselect a project without also
needing `--title`.
Project prompting in interactive mode remains opt-in.
`issue_create_ask_project` defaults to `false`, so existing interactive
behavior is unchanged unless users explicitly enable the new prompt.
## Changes
- allow interactive issue creation when the only flags are `--project`,
`--parent`, or both
- add a team-scoped project picker during interactive issue creation
- gate the direct project prompt behind a new config option:
`issue_create_ask_project`
- keep `issue_create_ask_project = false` as the default, so project
selection stays out of the main interactive flow unless enabled
- when `issue_create_ask_project = false`, expose `Project` through the
existing "Add more fields" flow instead
- continue inheriting the parent issue's project when `--parent` is used
without an explicit `--project`
- allow explicit `--project` together with `--parent` and defer any
invalid combination checks to the Linear API
- add a new config option, `issue_create_assign_self`, with these modes:
- `auto` (default): respect Linear's `autoAssignToSelf` setting
- `always`: default-assign created issues to self
- `never`: never default-assign created issues
- update docs for the new interactive behavior and config options
## Notes
- `issue_create_ask_project` defaults to `false`, so users who do not
set it will keep the previous interactive flow
- project selection only shows projects available to the selected team
- the interactive project picker is not shown when a parent issue is
present unless the project was explicitly provided
- explicit `--assignee`, the interactive assignee flow, and `--start`
still override the default assignment behavior
## Testing
- `deno task codegen`
- `deno task check`
- `deno lint`
- `deno fmt`
- `deno task generate-skill-docs`
- `deno test --allow-all --quiet test/config.test.ts`
- `deno test --allow-all --quiet
test/commands/issue/issue-create.test.ts`
#235 makes `document update` refuse a content replacement when the document has
active inline comments (whose anchors the replacement could orphan), with
--force to override, and exposes comments in `document view --json`. Two
corrections to the guard in document-update.ts:
- Exclude resolved/archived inline comments. The guard query only selected
`quotedText`, so it blocked on ANY inline comment — including resolved
(closed) threads whose anchor detaching loses no live context. It now also
selects `resolvedAt`/`archivedAt` and ignores comments that are resolved or
archived, so a closed thread no longer forces users to pass --force.
- Drop the hand-written `DocumentInlineComment*` interfaces in favor of the
codegen-inferred type (`DocumentInlineCommentGuardQuery`), per the repo's
no-`any`/typed-GraphQL convention. The annotation also breaks the circular
result-type inference that the reused `after` cursor variable introduces
(the reason the hand-written interface existed).
Adds a test that a resolved inline comment lets the update proceed without
--force.
## Summary
This makes Linear document comments visible to agents/automation and
prevents `linear document update` from silently orphaning inline comment
anchors.
- include paginated document comments in `linear document view <id>
--json`
- guard Markdown content updates when active inline document comments
are present
- allow top-level document comments to pass through, since they do not
carry losable inline anchors
- add `--force` to opt back into the existing replacement behavior when
the caller intentionally accepts the risk
## Background / related work
Closes#230, which reports that document comments are currently
unreachable from the CLI and omitted from `document view --json`.
Related:
- #219 added richer `document view` behavior for downloaded inline
images; this PR keeps normal/raw rendered views lean and only expands
`--json` with comment metadata.
- #121 added the raw `linear api` GraphQL escape hatch. That is useful
for inspection, but it does not make `documentUpdate(content)` safe for
inline document comments.
## API limitation
This is intentionally a safety guard plus read fix, not a
comment-preserving writer.
From the schema used by this CLI:
- `Document.comments(...)` exposes document comments and
`Comment.quotedText`, which is enough to detect inline comments.
- `DocumentUpdateInput` only accepts Markdown `content` for document
body writes; it does not accept `contentState`/YJS/ProseMirror data or
comment anchor metadata.
- `CommentCreateInput`/`CommentUpdateInput` expose `quotedText`, but
live testing showed raw GraphQL `commentUpdate(quotedText: ...)` returns
success without recreating an inline anchor. Writing
`<linear-comment>`-style tags through `documentUpdate(content)` stores
literal text, not an anchor.
So the CLI cannot preserve or restore inline anchors through the public
GraphQL write path it uses. The best safe behavior here is to make
comments visible and convert silent data loss into an explicit stop.
`--force` remains the old replacement behavior behind an intentional
flag.
## Behavior
`linear document view <id> --json` now returns a flattened, paginated
`comments.nodes` list with fields useful to agents:
- `id`, `body`, `quotedText`, `documentContentId`
- timestamps / archive / resolution metadata
- `url`, `user`, and `parent.id`
- final `pageInfo`
`linear document update <id> --content...` now scans active document
comments page-by-page. It blocks only when it finds a comment with
`quotedText`, i.e. an inline comment anchor that can be detached by
replacing Markdown content. Top-level document comments with
`quotedText: null` do not block.
## Testing
- `deno task validate`
- `deno test --allow-all --quiet`
Full suite result locally: `338 passed`, `0 failed`, `5 ignored`.
#228 fixes milestone view silently capping its issue list, adding --all to
paginate. Two corrections:
1. Fail loudly on inconsistent --all pagination. The loop guarded on
`while (pageInfo.hasNextPage && pageInfo.endCursor)` and `if (next == null)
break`, so if Linear advertised another page but returned no cursor (or the
milestone vanished mid-pagination), --all would stop early and return a
*partial* list with no error — reintroducing the exact silent-truncation bug
this command exists to fix. Now throws CliError / NotFoundError instead, with
a regression test.
2. Fix timezone-flaky snapshots. The new "Truncated" and "--all paginates" tests
used midnight-UTC timestamps (2020-01-01T00:00:00Z) whose snapshots were
generated in a US timezone, so `formatRelativeTime`'s toLocaleDateString
fallback rendered 12/31/2019 there but 1/1/2020 under CI's UTC — failing CI
(fork CI never ran, so it slipped through). Moved the fixtures to noon UTC so
they render the same date in every timezone; verified passing under TZ=UTC and
TZ=America/Los_Angeles.
milestone view silently capped its issues list at 10 items. A user with
12 issues would see only 10 and could miss issues entirely when
iterating over the visible output in a script. The cap was also
undocumented in --help.
- Request first: 50 issues with pageInfo on the connection.
- Add --all to paginate through every page when needed.
- Show a prominent truncation footer pointing at --all and at
`linear issue query --milestone X --json` for full programmatic
access.
- Document the default cap in --help.
Closes#222
PR #216 adds --content/--content-file plus priority/label/member/icon/color
to `linear project create`, with happy-path snapshot tests and a
mutual-exclusion unit test. This adds the missing failure-path coverage that
both blind-planning passes called for:
- invalid --priority is rejected with a ValidationError before any network call
- an unknown --label surfaces a NotFoundError
- an unknown --member surfaces a NotFoundError
These use a stubbed Deno.exit and capture stderr (mirroring the validation
tests in issue-query.test.ts), and run against a mock Linear server so they
never touch the real API. No product code changes — the contributor's feature
is correct as written; this only locks in the error behavior.
## 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`
PR #234 makes attachment uploads private by default and adds a --public
opt-in that errors (rather than silently downgrades) for non-image types.
Two gaps remained in `issue comment add`, both flagged independently while
blind-planning the fix:
- The public/type check only ran inside uploadFile, per file, at upload
time. With `--public` and multiple attachments, an earlier valid image
was uploaded *publicly* before a later non-image threw — a partial, and
unwanted-public, upload. Pre-flight resolveMakePublic() for every
attachment in the existing existence-validation loop so a mixed batch
fails before anything is uploaded.
- `--public` with no `--attach` was a silent no-op. Per the repo's
"explicit invalid input should error" convention, reject it.
Adds command-level tests for both validation paths (they short-circuit
before any network call, so no upload mock is needed).
Image attachments uploaded via `issue attach` and `issue comment add
--attach` were sent with makePublic auto-detected to true for raster
images, producing a public.linear.app URL readable by anyone,
unauthenticated, with no way to opt out. This silently published
screenshots of internal data from private workspaces.
Default all uploads to private (uploads.linear.app), matching the Linear
web app. Add a --public flag to both commands to opt into a public URL,
which is only valid for raster images; requesting it for other types is
now an error rather than a silent downgrade. Print a warning whenever an
upload lands on a public URL.
Also document the attachment commands and their privacy behaviour in the
README (previously undocumented) and regenerate the skill reference.
Fixes#233
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a 'B' column (with ⊘) to 'linear issue mine' and 'linear issue query'
that flags issues with at least one active blocker — a blocked-by relation
whose blocker is not in a completed or canceled workflow state. The
inverseRelations connection is also surfaced in --json output.
Document view's rendered/raw output now downloads inline images and
Linear-upload links to the same /tmp cache used by issue view, so terminal
renderers and downstream tools see local file paths instead of remote URLs
that require auth.
Image helpers (extractImageInfo, extractLinearLinkInfo, replaceImageUrls,
getUrlHash, getLinearUploadHost, downloadMarkdownImages) move from
src/commands/issue/issue-view.ts to src/utils/markdown-images.ts so both
commands share one implementation. downloadIssueImages becomes
downloadMarkdownImages, which takes an array of markdown sources rather
than an issue-specific (description, comments) tuple.
Adds --no-download to document view (mirroring issue view) and reorders
the command so the download step runs after the --json early return but
before --raw, matching issue view's behavior where piped output also gets
local paths.
Also adds remark-gfm to the parse/stringify pipeline used by
replaceImageUrls so GFM constructs (task lists, tables, strikethrough)
survive the URL rewrite. Without it, remark-stringify would re-escape
`- [ ] todo` as `* \[ ] todo` whenever an image is rewritten, mangling
documents and issue descriptions that lean on GFM syntax.
The skill docs check was failing because `Deno.consoleSize()` returns
different values depending on the runner's pseudo-terminal dimensions
(CI is wide, local terminals vary). Since the docs are for LLM
consumption and not correctness, dropping the `git diff` check and just
verifying generation doesn't crash.
Co-authored-by: Alex Broekhof <alex@quilt.com>
Co-authored-by: Peter Schilling <code@schpet.com>
Linear's comments connection returns newest-first and the GraphQL schema
doesn't expose a direction argument, so sort root comments ascending
client-side to match the order shown in Linear's UI.
Introduce 'issue mine' for personal work queue and 'issue query' for
structured retrieval with optional full-text search via --search.
Keep 'issue list' as a separate backwards-compatible command.
Remove standalone 'issue search' registration in favor of query --search.
query supports multi-team filtering, --all-teams, --json output,
--search-comments, and verifies GraphQL variables in tests.