Skip to content

fix(cli): accept both UUID and name for --project/--milestone #229

Merged
schpet merged 13 commits into
schpet:mainfrom
jrschumacher:fix/221-project-milestone-resolution
Jul 11, 2026
Merged

fix(cli): accept both UUID and name for --project/--milestone #229
schpet merged 13 commits into
schpet:mainfrom
jrschumacher:fix/221-project-milestone-resolution

Conversation

@jrschumacher

Copy link
Copy Markdown
Contributor

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

jrschumacher and others added 13 commits May 22, 2026 05:53
…#221)

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 schpet#221
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 schpet#233

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR schpet#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).
## 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 schpet#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.
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 schpet#222
…pshots

schpet#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.
## 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 schpet#230, which reports that document comments are currently
unreachable from the CLI and omitted from `document view --json`.

Related:

- schpet#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.
- schpet#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`.
…date guard

schpet#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 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`
Add `labels` field to the JSON output of `linear issue view --json`.

Each label includes `id`, `name`, and `color`.

## Example

```json
{
  "identifier": "P-51",
  "labels": [
    {
      "id": "031de1e8-...",
      "name": "Work",
      "color": "#4cb782"
    }
  ]
}

---------

Co-authored-by: Hyunsu Lim <hyunsu.lim01@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.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: Peter Schilling <code@schpet.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>
Co-authored-by: Theo Gregory <theo@gregory.sh>
Co-authored-by: Bryan <bryandesigning@gmail.com>
Co-authored-by: Ryan Schumacher <j.r.schumacher@gmail.com>
Co-authored-by: Joseph <31323641+josephyooo@users.noreply.github.com>
Co-authored-by: Magnus Buvarp <magnus.buvarp@gmail.com>
schpet#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.
main landed schpet#208 (issue create refactor) and schpet#228 (milestone view pagination)
since this PR branched. Resolved by taking main's structure and re-applying the
PR's intent:
- issue-create.ts: kept schpet#208's imports, added the PR's isLinearUuid /
  resolveMilestoneId (the milestone-block change merged cleanly).
- milestone-view.ts: kept schpet#228's paginated + --all structure, re-applied the
  PR's --project option and UUID/name milestone resolution, regenerated the
  snapshot.
- reformatted under the repo's pinned deno so fmt --check passes.
@schpet
schpet merged commit dd39d9e into schpet:main Jul 11, 2026
11 checks passed
schpet added a commit to edwinhern/linear-cli that referenced this pull request Jul 14, 2026
Regenerated skill docs with the PR's new deterministic generator against
current main (post schpet#227/schpet#170/schpet#229) so the sorted output reflects the latest
CLI surface and no longer conflicts with trunk.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inconsistent UUID-vs-name resolution across commands (--project, --milestone)

7 participants