linear-sdk-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +46 -46
  2. package/COVERAGE.md +5 -5
  3. package/MIGRATING.md +69 -71
  4. package/README.md +88 -96
  5. package/package.json +3 -4
  6. package/skills/linear-sdk-cli/SKILL.md +3 -13
  7. package/skills/linear-sdk-cli/references/api.md +9 -9
  8. package/skills/linear-sdk-cli/references/auth.md +0 -18
  9. package/skills/linear-sdk-cli/references/commands.md +2 -2
  10. package/skills/linear-sdk-cli/references/config.md +11 -9
  11. package/skills/linear-sdk-cli/references/issue.md +12 -63
  12. package/skills/linear-sdk-cli/references/team.md +0 -16
  13. package/skills/linear-sdk-cli/scripts/generate-docs.ts +2 -6
  14. package/src/bin/linear.ts +6 -16
  15. package/src/cli.ts +1 -3
  16. package/src/commands/agent-session.ts +5 -13
  17. package/src/commands/api.ts +9 -15
  18. package/src/commands/attach.ts +4 -15
  19. package/src/commands/comment.ts +2 -6
  20. package/src/commands/completion.ts +6 -2
  21. package/src/commands/cycle.ts +1 -6
  22. package/src/commands/discover.ts +2 -6
  23. package/src/commands/document.ts +1 -4
  24. package/src/commands/initiative.ts +1 -5
  25. package/src/commands/issue.ts +88 -376
  26. package/src/commands/meta.ts +31 -139
  27. package/src/commands/milestone.ts +1 -4
  28. package/src/commands/notification.ts +6 -15
  29. package/src/commands/project.ts +4 -17
  30. package/src/commands/team.ts +0 -13
  31. package/src/commands/webhook.ts +2 -12
  32. package/src/config.ts +183 -500
  33. package/src/context.ts +4 -13
  34. package/src/git.ts +3 -28
  35. package/src/lib/body.ts +2 -26
  36. package/src/lib/errors.ts +7 -32
  37. package/src/lib/introspect.ts +7 -76
  38. package/src/lib/keyring.ts +9 -51
  39. package/src/lib/options.ts +12 -108
  40. package/src/lib/output-shapes.ts +9 -81
  41. package/src/lib/pagination.ts +17 -50
  42. package/src/lib/resolve.ts +7 -40
  43. package/src/lib/shape.ts +12 -16
  44. package/src/lib/upload.ts +1 -3
  45. package/src/output/format.ts +18 -96
  46. package/src/output/table.ts +7 -7
  47. package/src/services/agent-session.ts +3 -11
  48. package/src/services/attachment.ts +5 -14
  49. package/src/services/comment.ts +4 -12
  50. package/src/services/cycle.ts +4 -11
  51. package/src/services/document.ts +10 -29
  52. package/src/services/favorite.ts +9 -15
  53. package/src/services/initiative-update.ts +15 -16
  54. package/src/services/initiative.ts +10 -15
  55. package/src/services/issue.ts +18 -262
  56. package/src/services/label.ts +2 -12
  57. package/src/services/milestone.ts +5 -8
  58. package/src/services/notification.ts +2 -16
  59. package/src/services/organization.ts +18 -23
  60. package/src/services/project.ts +34 -55
  61. package/src/services/roadmap.ts +6 -7
  62. package/src/services/state.ts +3 -9
  63. package/src/services/team.ts +24 -67
  64. package/src/services/user.ts +2 -2
  65. package/src/services/webhook.ts +17 -28
  66. package/skills/linear-sdk-cli/references/open.md +0 -29
  67. package/src/commands/open.ts +0 -84
  68. package/src/lib/open.ts +0 -32
  69. package/src/output/color.ts +0 -52
  70. package/src/output/markdown.ts +0 -133
  71. package/src/output/pager.ts +0 -131
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "linear-sdk-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Ergonomic command-line interface for Linear, built on @linear/sdk",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Eugene Beloded <belodedevgenij@gmail.com>",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/zhendalf/linear-sdk-cli.git"
10
+ "url": "https://github.com/zhendalf/linear-sdk-cli"
11
11
  },
12
12
  "homepage": "https://github.com/zhendalf/linear-sdk-cli#readme",
13
13
  "bugs": {
@@ -55,13 +55,12 @@
55
55
  "typecheck": "tsc --noEmit",
56
56
  "lint": "eslint .",
57
57
  "format": "prettier --write .",
58
- "format:check": "prettier --check .",
59
58
  "test": "bun test test/unit test/contract",
60
59
  "test:unit": "bun test test/unit",
61
60
  "test:contract": "bun test test/contract",
62
61
  "test:live": "LINEAR_CLI_LIVE=1 bun test --timeout 60000 test/integration",
63
62
  "test:live:admin": "LINEAR_CLI_LIVE=1 LINEAR_CLI_LIVE_ADMIN=1 bun test --timeout 60000 test/integration",
64
- "verify": "bun run typecheck && bun run lint && bun run audit:coverage && bun run format:check && bun run test",
63
+ "verify": "bun run typecheck && bun run lint && bun run test",
65
64
  "skill:docs": "bun run skills/linear-sdk-cli/scripts/generate-docs.ts",
66
65
  "audit:coverage": "bun run scripts/coverage-audit.ts",
67
66
  "janitor": "bun run test/janitor.ts"
@@ -24,13 +24,10 @@ If that fails, or prints `2.x` (that is `schpet/linear-cli`, a different tool th
24
24
  `linear` name), install this one — it needs Bun ≥ 1.1:
25
25
 
26
26
  ```bash
27
- git clone https://github.com/zhendalf/linear-sdk-cli.git
28
- cd linear-sdk-cli && bun install
29
- bun run dev -- --version
27
+ bun add -g linear-sdk-cli # installs `linear` and `lin`
28
+ lin --version # `lin` never collides with the other CLI
30
29
  ```
31
30
 
32
- The npm package has not been published yet; use a source clone until its first release.
33
-
34
31
  `lin` is the safe spelling when both might be present. If the user is coming from
35
32
  `schpet/linear-cli`, their credentials and `.linear.toml` are picked up automatically — see
36
33
  `MIGRATING.md` in the package; do not ask them to re-enter an API key before running
@@ -198,9 +195,7 @@ EOF
198
195
 
199
196
  For short, single-line content, use inline `-d/--description` (issues/initiatives),
200
197
  the positional `[body]` arg (comments: `linear comment add TES-42 "lgtm"`), or
201
- `--body` (status updates). There is no `-b` short flag. The CLI rejects an inline
202
- body containing literal `\n` sequences and points back to stdin; never encode a
203
- multiline body by inserting `\n` inside an ordinary quoted argument.
198
+ `--body` (status updates). There is no `-b` short flag.
204
199
 
205
200
  ### Discovery — learn the surface without scraping help
206
201
 
@@ -240,7 +235,6 @@ linear attachment delete
240
235
  linear attachment list
241
236
 
242
237
  linear auth
243
- linear auth adopt
244
238
  linear auth default
245
239
  linear auth list
246
240
  linear auth login
@@ -357,8 +351,6 @@ linear notification read-all
357
351
  linear notification snooze
358
352
  linear notification unread
359
353
 
360
- linear open
361
-
362
354
  linear organization
363
355
  linear organization invites
364
356
  linear organization members
@@ -394,7 +386,6 @@ linear team
394
386
  linear team create
395
387
  linear team cycles
396
388
  linear team delete
397
- linear team id
398
389
  linear team labels
399
390
  linear team list
400
391
  linear team members
@@ -441,7 +432,6 @@ prints the same). These are supplementary — `--help` on any command is authori
441
432
  - [label](references/label.md) — Work with issue labels
442
433
  - [milestone](references/milestone.md) — Work with project milestones
443
434
  - [notification](references/notification.md) — Work with your notifications
444
- - [open](references/open.md) — Open the workspace, an issue, team, project, or URL
445
435
  - [organization](references/organization.md) — Inspect the current workspace
446
436
  - [project](references/project.md) — Work with projects
447
437
  - [project-update](references/project-update.md) — Post and list project status updates
@@ -14,14 +14,14 @@ Run a raw GraphQL query or mutation against the Linear API
14
14
  linear api [options] [query]
15
15
  ```
16
16
 
17
- | Option | Description |
18
- | --------------------- | ------------------------------------------------------------------------------ |
19
- | `--query-file <path>` | read the query from a file ('-' for stdin) |
20
- | `--var <k=v...>` | set a variable (repeatable; JSON literals are typed, other values are strings) |
21
- | `--vars <json>` | variables as a JSON object |
22
- | `--vars-file <path>` | read variables from a JSON file ('-' for stdin) |
23
- | `--operation <name>` | run this operation of a multi-operation document |
24
- | `--paginate` | auto-follow the first connection's pageInfo cursor (queries only) |
25
- | `--raw` | print the full GraphQL response (data + extensions) |
17
+ | Option | Description |
18
+ | --------------------- | ----------------------------------------------------------------- |
19
+ | `--query-file <path>` | read the query from a file ('-' for stdin) |
20
+ | `--var <k=v...>` | set a variable (repeatable; string value) |
21
+ | `--vars <json>` | variables as a JSON object |
22
+ | `--vars-file <path>` | read variables from a JSON file ('-' for stdin) |
23
+ | `--operation <name>` | run this operation of a multi-operation document |
24
+ | `--paginate` | auto-follow the first connection's pageInfo cursor (queries only) |
25
+ | `--raw` | print the full GraphQL response (data + extensions) |
26
26
 
27
27
  **Output (`--json`)**: raw JSON (keys depend on the request) — the response `data` as the API returned it (`--raw`: {data, extensions}); `--paginate` prints one array of every page's nodes
@@ -14,24 +14,6 @@ Manage authentication
14
14
  linear auth [options]
15
15
  ```
16
16
 
17
- ### `linear auth adopt`
18
-
19
- Adopt an existing named credential from the shared OS keyring
20
-
21
- ```
22
- linear auth adopt [options] <slug>
23
- ```
24
-
25
- **Output (`--json`)**: a receipt object
26
-
27
- ```text
28
- success: boolean
29
- workspace: string
30
- user: {id: string, name: string, email: string}
31
- storage: string
32
- path: string
33
- ```
34
-
35
17
  ### `linear auth default`
36
18
 
37
19
  Set the default workspace credential
@@ -21,7 +21,7 @@ path: string
21
21
  description: string
22
22
  aliases: string[]
23
23
  arguments: Array<{name: string, required: boolean, variadic: boolean}>
24
- options: Array<{flags: string, description: string, attribute: string, valueRequired: boolean, valueOptional: boolean, variadic: boolean, choices?: string[], defaultValue?: unknown, global: boolean, applicable: boolean}>
24
+ options: Array<{flags: string, description: string}>
25
25
  output?: {kind: string, fields?: object, note?: string, variants?: object}
26
26
  ```
27
27
 
@@ -32,6 +32,6 @@ path: string
32
32
  description: string
33
33
  aliases: string[]
34
34
  arguments: Array<{name: string, required: boolean, variadic: boolean}>
35
- options: Array<{flags: string, description: string, attribute: string, valueRequired: boolean, valueOptional: boolean, variadic: boolean, choices?: string[], defaultValue?: unknown, global: boolean, applicable: boolean}>
35
+ options: Array<{flags: string, description: string}>
36
36
  output?: {kind: string, fields?: object, note?: string, variants?: object}
37
37
  ```
@@ -23,7 +23,8 @@ credentialWorkspace: string | null
23
23
  team: string | null
24
24
  workspace: string | null
25
25
  sort: string
26
- origins: {team: {source: string, path?: string, key?: string}, workspace: {source: string, path?: string, key?: string}, sort: {source: string, path?: string, key?: string}}
26
+ vcs: string
27
+ origins: {team: {source: string, path?: string}, workspace: {source: string, path?: string}, sort: {source: string, path?: string}, vcs: {source: string, path?: string}}
27
28
  userConfigPath: string
28
29
  projectConfigPath: string | null
29
30
  globalConfigPath: string | null
@@ -37,12 +38,12 @@ Write a project .linear.toml (at the git root, or here outside a repository)
37
38
  linear config init [options]
38
39
  ```
39
40
 
40
- | Option | Description |
41
- | ---------------- | ------------------------------------------------------------------ |
42
- | `--team <key>` | default team key (otherwise chosen from a list) |
43
- | `--sort <order>` | default issue-list sort (priority \| manual \| updated \| created) |
44
- | `--path <file>` | write this file instead of <git root>/.linear.toml |
45
- | `--force` | overwrite an existing file |
41
+ | Option | Description |
42
+ | ---------------- | -------------------------------------------------------- |
43
+ | `--team <key>` | default team key (otherwise chosen from a list) |
44
+ | `--sort <order>` | default issue-list sort (priority \| updated \| created) |
45
+ | `--path <file>` | write this file instead of <git root>/.linear.toml |
46
+ | `--force` | overwrite an existing file |
46
47
 
47
48
  **Output (`--json`)**: a receipt object
48
49
 
@@ -55,7 +56,7 @@ sort?: string
55
56
 
56
57
  ### `linear config set`
57
58
 
58
- Set one project setting (team, workspace, sort) in the project config
59
+ Set one project setting (team, workspace, sort, vcs) in the project config
59
60
 
60
61
  ```
61
62
  linear config set [options] <key> <value>
@@ -92,7 +93,8 @@ credentialWorkspace: string | null
92
93
  team: string | null
93
94
  workspace: string | null
94
95
  sort: string
95
- origins: {team: {source: string, path?: string, key?: string}, workspace: {source: string, path?: string, key?: string}, sort: {source: string, path?: string, key?: string}}
96
+ vcs: string
97
+ origins: {team: {source: string, path?: string}, workspace: {source: string, path?: string}, sort: {source: string, path?: string}, vcs: {source: string, path?: string}}
96
98
  userConfigPath: string
97
99
  projectConfigPath: string | null
98
100
  globalConfigPath: string | null
@@ -44,27 +44,18 @@ team: {id: string, key: string, name: string} | null
44
44
  project: {id: string, name: string} | null
45
45
  milestone: {id: string, name: string} | null
46
46
  cycle: {id: string, number: number, name: string | null} | null
47
- parent: {id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null} | null
48
- children: Array<{id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null}>
47
+ parent: {id: string, identifier: string} | null
49
48
  labels: Array<{id: string, name: string}>
50
49
  subscribers: Array<{id: string, displayName: string}>
51
- attachments: Array<{id: string, title: string, url: string, subtitle: string | null, sourceType: string | null, createdAt: string}>
52
- documents: Array<{id: string, title: string, slugId: string, url: string, createdAt: string, updatedAt: string}>
53
- relations: Array<{id: string, type: string, issue: {id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null}, relatedIssue: {id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null}}>
54
- inverseRelations: Array<{id: string, type: string, issue: {id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null}, relatedIssue: {id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null}}>
55
- comments: Array<{id: string, body: string, url: string, createdAt: string, editedAt: string | null, resolvedAt: string | null, parent: {id: string} | null, user: {id: string, displayName: string} | null, externalUser: {id: string, displayName: string} | null, resolvingCommentId: string | null, resolvingUser: {id: string, displayName: string} | null}>
56
50
  ```
57
51
 
58
- With `--web`: a receipt object
52
+ With `--comments`: the same, plus:
59
53
 
60
54
  ```text
61
- id: string
62
- identifier: string
63
- url: string
64
- opened: boolean
55
+ comments: Array<{id: string, body: string, user: {id: string, displayName: string} | null, createdAt: string, editedAt: string | null, resolvedAt: string | null, parent: {id: string} | null, url: string}>
65
56
  ```
66
57
 
67
- With `--app`: a receipt object
58
+ With `--web`: a receipt object
68
59
 
69
60
  ```text
70
61
  id: string
@@ -152,12 +143,6 @@ Archive an issue
152
143
  linear issue archive [options] [id]
153
144
  ```
154
145
 
155
- | Option | Description |
156
- | -------------------- | ------------------------------------------------ |
157
- | `--bulk <ids>` | archive comma-separated issue ids (repeatable) |
158
- | `--bulk-file <path>` | archive issue ids from a file (one per line) |
159
- | `--bulk-stdin` | archive issue ids read from stdin (one per line) |
160
-
161
146
  **Output (`--json`)**: a receipt object
162
147
 
163
148
  ```text
@@ -166,15 +151,6 @@ identifier: string
166
151
  archived: boolean
167
152
  ```
168
153
 
169
- With `--bulk`: a receipt object
170
-
171
- ```text
172
- action: string
173
- results: Array<{input: string, id?: string, identifier?: string, archived?: boolean, deleted?: boolean, error?: {message: string, code: string}}>
174
- succeeded: number
175
- failed: number
176
- ```
177
-
178
154
  ### `linear issue assign`
179
155
 
180
156
  Assign an issue (use 'me', email, name, or id). Issue defaults to the branch.
@@ -404,12 +380,6 @@ Aliases: `rm`
404
380
  linear issue delete [options] [id]
405
381
  ```
406
382
 
407
- | Option | Description |
408
- | -------------------- | ----------------------------------------------- |
409
- | `--bulk <ids>` | delete comma-separated issue ids (repeatable) |
410
- | `--bulk-file <path>` | delete issue ids from a file (one per line) |
411
- | `--bulk-stdin` | delete issue ids read from stdin (one per line) |
412
-
413
383
  **Output (`--json`)**: a receipt object
414
384
 
415
385
  ```text
@@ -418,15 +388,6 @@ identifier: string
418
388
  deleted: boolean
419
389
  ```
420
390
 
421
- With `--bulk`: a receipt object
422
-
423
- ```text
424
- action: string
425
- results: Array<{input: string, id?: string, identifier?: string, archived?: boolean, deleted?: boolean, error?: {message: string, code: string}}>
426
- succeeded: number
427
- failed: number
428
- ```
429
-
430
391
  ### `linear issue describe`
431
392
 
432
393
  Print a commit message for the issue: 'ID Title' plus Linear-issue trailers
@@ -829,7 +790,6 @@ linear issue update [options] [id]
829
790
  | `--estimate <n>` | estimate points |
830
791
  | `--parent <id>` | parent issue id |
831
792
  | `--due <date>` | due date (YYYY-MM-DD) |
832
- | `-l, --label <name>` | replace all labels (repeatable / comma-separated) |
833
793
  | `--add-label <name>` | add a label (repeatable) |
834
794
  | `--remove-label <name>` | remove a label (repeatable) |
835
795
  | `--unassign` | clear the assignee |
@@ -865,12 +825,10 @@ Show an issue (defaults to the current branch's issue)
865
825
  linear issue view [options] [id]
866
826
  ```
867
827
 
868
- | Option | Description |
869
- | ------------------------- | ------------------------------------------------- |
870
- | `-w, --web` | open the issue in the browser instead of printing |
871
- | `--app` | open the issue in Linear.app instead of printing |
872
- | `--no-comments` | exclude comments from the output |
873
- | `--show-resolved-threads` | include resolved comment threads |
828
+ | Option | Description |
829
+ | ------------ | ------------------------------------------------- |
830
+ | `-w, --web` | open the issue in the browser instead of printing |
831
+ | `--comments` | include recent comments |
874
832
 
875
833
  **Output (`--json`)**: a bare object
876
834
 
@@ -898,27 +856,18 @@ team: {id: string, key: string, name: string} | null
898
856
  project: {id: string, name: string} | null
899
857
  milestone: {id: string, name: string} | null
900
858
  cycle: {id: string, number: number, name: string | null} | null
901
- parent: {id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null} | null
902
- children: Array<{id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null}>
859
+ parent: {id: string, identifier: string} | null
903
860
  labels: Array<{id: string, name: string}>
904
861
  subscribers: Array<{id: string, displayName: string}>
905
- attachments: Array<{id: string, title: string, url: string, subtitle: string | null, sourceType: string | null, createdAt: string}>
906
- documents: Array<{id: string, title: string, slugId: string, url: string, createdAt: string, updatedAt: string}>
907
- relations: Array<{id: string, type: string, issue: {id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null}, relatedIssue: {id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null}}>
908
- inverseRelations: Array<{id: string, type: string, issue: {id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null}, relatedIssue: {id: string, identifier: string, title: string, state: {id: string, name: string, type: string} | null}}>
909
- comments: Array<{id: string, body: string, url: string, createdAt: string, editedAt: string | null, resolvedAt: string | null, parent: {id: string} | null, user: {id: string, displayName: string} | null, externalUser: {id: string, displayName: string} | null, resolvingCommentId: string | null, resolvingUser: {id: string, displayName: string} | null}>
910
862
  ```
911
863
 
912
- With `--web`: a receipt object
864
+ With `--comments`: the same, plus:
913
865
 
914
866
  ```text
915
- id: string
916
- identifier: string
917
- url: string
918
- opened: boolean
867
+ comments: Array<{id: string, body: string, user: {id: string, displayName: string} | null, createdAt: string, editedAt: string | null, resolvedAt: string | null, parent: {id: string} | null, url: string}>
919
868
  ```
920
869
 
921
- With `--app`: a receipt object
870
+ With `--web`: a receipt object
922
871
 
923
872
  ```text
924
873
  id: string
@@ -86,22 +86,6 @@ movedIssues: number
86
86
  movedTo: {id: string, key: string, name: string} | null
87
87
  ```
88
88
 
89
- ### `linear team id`
90
-
91
- Print a team's UUID (defaults to the configured team)
92
-
93
- ```
94
- linear team id [options] [key]
95
- ```
96
-
97
- **Output (`--json`)**: a receipt object
98
-
99
- ```text
100
- id: string
101
- key: string
102
- name: string
103
- ```
104
-
105
89
  ### `linear team labels`
106
90
 
107
91
  List a team's labels
@@ -70,10 +70,7 @@ const GLOBAL_OPTIONS = new Map([
70
70
  ["-t, --team <key>", "default team key (e.g. TES)"],
71
71
  ["-n, --limit <n>", "max results (positive integer; 0 = all)"],
72
72
  ["--all", "fetch all results (exhaust pagination)"],
73
- [
74
- "-f, --fields <a,b,c>",
75
- "select fields: table columns or detail lines (human), top-level keys (--json)",
76
- ],
73
+ ["-f, --fields <a,b,c>", "select fields: table columns or detail lines (human), top-level keys (--json)"],
77
74
  ["-y, --yes", "skip confirmation prompts"],
78
75
  ["-q, --quiet", "suppress status output"],
79
76
  ["--no-input", "never prompt; fail instead"],
@@ -193,8 +190,7 @@ function formatOutput(out: OutputShape, heading = "**Output (`--json`)**"): stri
193
190
  lines.push(`With \`${when}\`: the same${drop}, plus:`);
194
191
  lines.push("");
195
192
  lines.push("```text");
196
- for (const [key, shape] of Object.entries(delta.added))
197
- lines.push(`${key}: ${renderShape(shape)}`);
193
+ for (const [key, shape] of Object.entries(delta.added)) lines.push(`${key}: ${renderShape(shape)}`);
198
194
  lines.push("```");
199
195
  lines.push("");
200
196
  continue;
package/src/bin/linear.ts CHANGED
@@ -10,7 +10,6 @@ import { createProgram, parsedGlobalOptions, usageHint, suppressedHelp } from ".
10
10
  import { Output } from "../output/format.js";
11
11
  import { CliError, ExitCode, normalizeError } from "../lib/errors.js";
12
12
  import { commandPath } from "../lib/options.js";
13
- import { isDebugEnabled, shouldUseColor } from "../output/color.js";
14
13
 
15
14
  // `linear schema | head -2` is a reader that stops early, and stdout closing
16
15
  // under us is that reader's business, not an error: exit quietly, as every
@@ -54,14 +53,9 @@ program.parseAsync(process.argv).catch((err) => {
54
53
  const json = globals.json === true;
55
54
  const out = new Output({
56
55
  json,
57
- color: shouldUseColor({
58
- disabled: globals.noAnsi,
59
- json,
60
- isTTY: process.stderr.isTTY === true,
61
- }),
56
+ color: globals.noAnsi !== true && !json && process.stderr.isTTY === true,
62
57
  quiet: globals.quiet === true,
63
- debug: isDebugEnabled(globals.debug),
64
- isTTY: process.stderr.isTTY === true,
58
+ debug: globals.debug === true,
65
59
  });
66
60
  out.error(cliError);
67
61
  process.exit(cliError.exitCode);
@@ -76,18 +70,14 @@ function fromCommander(err: CommanderError): CliError {
76
70
  if (err.code === "commander.help") {
77
71
  const help = suppressedHelp(program);
78
72
  const path = help ? commandPath(help.command) : "linear";
79
- return new CliError(
80
- "Missing subcommand.",
81
- "usage",
82
- err.code,
83
- `Run '${path} --help' to see the commands.`,
84
- );
73
+ return new CliError(`Missing subcommand. Run '${path} --help' to see the commands.`, "usage", err.code);
85
74
  }
86
75
  return new CliError(
87
- stripErrorPrefix(err.message).replace(/\.?$/, "."),
76
+ [stripErrorPrefix(err.message).replace(/\.?$/, "."), usageHint(program)]
77
+ .filter(Boolean)
78
+ .join(" "),
88
79
  "usage",
89
80
  err.code,
90
- usageHint(program),
91
81
  );
92
82
  }
93
83
 
package/src/cli.ts CHANGED
@@ -29,7 +29,6 @@ import { registerRoadmap } from "./commands/roadmap.js";
29
29
  import { registerNotification } from "./commands/notification.js";
30
30
  import { registerOrganization } from "./commands/organization.js";
31
31
  import { registerWebhook } from "./commands/webhook.js";
32
- import { registerOpen } from "./commands/open.js";
33
32
  import { registerCommands, registerSchema } from "./commands/discover.js";
34
33
  import { Context, type GlobalOptions } from "./context.js";
35
34
  import { currentIssueId } from "./git.js";
@@ -59,7 +58,6 @@ export function createProgram(): Command {
59
58
  registerMeta(program);
60
59
  registerApi(program);
61
60
  registerCompletion(program);
62
- registerOpen(program);
63
61
  // Phase 1: issues. Agent sessions live on issues, so their group is mounted
64
62
  // under `issue` (`linear issue agent-session …`); so is file upload
65
63
  // (`linear issue attach <issue> <file...>`, TES-602).
@@ -110,7 +108,7 @@ export function createProgram(): Command {
110
108
  return;
111
109
  }
112
110
  const detail = await getIssueDetail(ctx.client, id);
113
- await renderIssueDetail(ctx, detail, true);
111
+ await renderIssueDetail(ctx, detail, false);
114
112
  });
115
113
 
116
114
  // Make global options usable in any position (e.g. `linear whoami --json`),
@@ -18,12 +18,7 @@ const ROW_COLUMNS: Column<svc.AgentSessionRow>[] = [
18
18
  { key: "agent", header: "Agent", value: (r) => r.agent?.displayName ?? "—", max: 20 },
19
19
  { key: "issue", header: "Issue", value: (r) => r.issue?.identifier ?? "—" },
20
20
  { key: "createdAt", header: "Created", value: (r) => r.createdAt.slice(0, 10) },
21
- {
22
- key: "summary",
23
- header: "Summary",
24
- value: (r) => r.summary?.replace(/\n/g, " ") ?? "—",
25
- max: 60,
26
- },
21
+ { key: "summary", header: "Summary", value: (r) => r.summary?.replace(/\n/g, " ") ?? "—", max: 60 },
27
22
  { key: "id", header: "ID", value: (r) => r.id },
28
23
  ];
29
24
 
@@ -93,12 +88,7 @@ export function registerAgentSession(issue: Command): void {
93
88
  ["Created", d.createdAt],
94
89
  ["Started", d.startedAt],
95
90
  ["Ended", d.endedAt],
96
- [
97
- "Dismissed",
98
- d.dismissedAt
99
- ? `${d.dismissedAt}${d.dismissedBy ? ` by ${d.dismissedBy.displayName}` : ""}`
100
- : null,
101
- ],
91
+ ["Dismissed", d.dismissedAt ? `${d.dismissedAt}${d.dismissedBy ? ` by ${d.dismissedBy.displayName}` : ""}` : null],
102
92
  ["External link", d.externalLink],
103
93
  ["URL", d.url],
104
94
  ["Summary", d.summary ? `\n${d.summary}` : null],
@@ -113,7 +103,9 @@ function renderActivities(d: svc.AgentSessionDetail): string {
113
103
  const lines = d.activities.map((a) => {
114
104
  const when = a.createdAt.slice(11, 19);
115
105
  const text =
116
- a.action !== null ? `${a.action}${a.parameter ? `: ${a.parameter}` : ""}` : (a.body ?? "");
106
+ a.action !== null
107
+ ? `${a.action}${a.parameter ? `: ${a.parameter}` : ""}`
108
+ : (a.body ?? "");
117
109
  return `- ${when} ${a.type}: ${text.replace(/\s*\n\s*/g, " ")}`;
118
110
  });
119
111
  if (d.activitiesTruncated) lines.push(`… more activities not shown (use --json)`);
@@ -32,12 +32,7 @@ export function registerApi(program: Command): void {
32
32
  .command("api [query]")
33
33
  .description("Run a raw GraphQL query or mutation against the Linear API")
34
34
  .option("--query-file <path>", "read the query from a file ('-' for stdin)")
35
- .option(
36
- "--var <k=v...>",
37
- "set a variable (repeatable; JSON literals are typed, other values are strings)",
38
- collectKeyVal,
39
- {},
40
- )
35
+ .option("--var <k=v...>", "set a variable (repeatable; string value)", collectKeyVal, {})
41
36
  .option("--vars <json>", "variables as a JSON object")
42
37
  .option("--vars-file <path>", "read variables from a JSON file ('-' for stdin)")
43
38
  .option("--operation <name>", "run this operation of a multi-operation document")
@@ -107,11 +102,6 @@ function readFileOrThrow(path: string, label: string): string {
107
102
  }
108
103
 
109
104
  function resolveQuery(arg: string | undefined, file: string | undefined): string {
110
- if (arg !== undefined && file !== undefined) {
111
- throw usageError(
112
- "Pass the GraphQL query either as an argument or with --query-file, not both.",
113
- );
114
- }
115
105
  let query: string | undefined;
116
106
  if (arg !== undefined) query = arg;
117
107
  else if (file !== undefined) query = readFileOrThrow(file, "--query-file");
@@ -129,7 +119,7 @@ function resolveVariables(opts: Record<string, any>): Record<string, unknown> {
129
119
  vars = { ...vars, ...parseJsonObject(text, "--vars-file") };
130
120
  }
131
121
  if (opts.vars) vars = { ...vars, ...parseJsonObject(opts.vars, "--vars") };
132
- if (opts.var) vars = { ...vars, ...(opts.var as Record<string, unknown>) };
122
+ if (opts.var) vars = { ...vars, ...(opts.var as Record<string, string>) };
133
123
  return vars;
134
124
  }
135
125
 
@@ -227,7 +217,10 @@ function describeNames(operations: OperationDefinitionNode[]): string {
227
217
  return operations.map((o) => o.name?.value ?? "<unnamed>").join(", ");
228
218
  }
229
219
 
230
- function unknownOperationMessage(requested: string, operations: OperationDefinitionNode[]): string {
220
+ function unknownOperationMessage(
221
+ requested: string,
222
+ operations: OperationDefinitionNode[],
223
+ ): string {
231
224
  const named = operations.filter((o) => o.name).map((o) => o.name!.value);
232
225
  const available = named.length
233
226
  ? `Available: ${named.join(", ")}.`
@@ -276,6 +269,7 @@ async function paginate(
276
269
  variables: Record<string, unknown>,
277
270
  ): Promise<{ nodes: unknown[]; pageCount: number; truncated: boolean }> {
278
271
  let after: string | undefined = variables.after as string | undefined;
272
+ let firstData: any;
279
273
  const allNodes: unknown[] = [];
280
274
  let pages = 0;
281
275
  let truncated = false;
@@ -284,10 +278,10 @@ async function paginate(
284
278
  const result: any = await withRetry(() =>
285
279
  (ctx.client.client as any).rawRequest(query, { ...variables, after }),
286
280
  );
281
+ if (!firstData) firstData = result.data;
287
282
  const conn = findConnection(result.data);
288
283
  if (!conn) {
289
- if (pages === 0)
290
- throw new CliError("No connection (nodes/pageInfo) found to paginate.", "usage");
284
+ if (pages === 0) throw new CliError("No connection (nodes/pageInfo) found to paginate.", "usage");
291
285
  break;
292
286
  }
293
287
  allNodes.push(...conn.nodes);
@@ -27,14 +27,8 @@ export function registerAttach(issue: Command): void {
27
27
  .command("attach <issue> <file...>")
28
28
  .description("Upload files and attach them to an issue (private by default)")
29
29
  .option("--title <title>", "attachment title (single file only; default: the file name)")
30
- .option(
31
- "--comment <body>",
32
- "also post a comment with this body embedding the files as markdown",
33
- )
34
- .option(
35
- "--public",
36
- "upload to a public, world-readable URL (raster images only; default: private)",
37
- )
30
+ .option("--comment <body>", "also post a comment with this body embedding the files as markdown")
31
+ .option("--public", "upload to a public, world-readable URL (raster images only; default: private)")
38
32
  .addHelpText(
39
33
  "after",
40
34
  [
@@ -56,13 +50,8 @@ export function registerAttach(issue: Command): void {
56
50
  comment: opts.comment,
57
51
  public: opts.public === true,
58
52
  onAttached: (a, iss) => {
59
- ctx.output.success(
60
- `Attached ${a.filename} (${formatBytes(a.size)}) to ${iss.identifier}`,
61
- );
62
- if (a.public)
63
- ctx.output.warn(
64
- `${a.filename} is on a public URL, readable by anyone: ${a.assetUrl}`,
65
- );
53
+ ctx.output.success(`Attached ${a.filename} (${formatBytes(a.size)}) to ${iss.identifier}`);
54
+ if (a.public) ctx.output.warn(`${a.filename} is on a public URL, readable by anyone: ${a.assetUrl}`);
66
55
  },
67
56
  });
68
57
  const identifier = result.issue.identifier;
@@ -57,10 +57,7 @@ function buildAdd(_o: MountOptions): Command {
57
57
  "upload a file and embed it in the comment (images inline; repeatable; private by default)",
58
58
  collectArray,
59
59
  )
60
- .option(
61
- "--public",
62
- "upload the attachments to public, world-readable URLs (raster images only)",
63
- )
60
+ .option("--public", "upload the attachments to public, world-readable URLs (raster images only)")
64
61
  .action(
65
62
  action(async (ctx: Context, opts, issueArg: string, bodyArg?: string) => {
66
63
  const attachments: string[] = opts.attach ?? [];
@@ -84,8 +81,7 @@ function buildAdd(_o: MountOptions): Command {
84
81
  public: opts.public === true,
85
82
  });
86
83
  for (const u of uploads) {
87
- if (u.public)
88
- ctx.output.warn(`${u.filename} is on a public URL, readable by anyone: ${u.assetUrl}`);
84
+ if (u.public) ctx.output.warn(`${u.filename} is on a public URL, readable by anyone: ${u.assetUrl}`);
89
85
  }
90
86
  ctx.output.emit(
91
87
  {
@@ -47,8 +47,12 @@ compdef _linear linear lin
47
47
  `;
48
48
  case "fish":
49
49
  return `# linear fish completion: linear completion fish > ~/.config/fish/completions/linear.fish
50
- ${commands.map((c) => `complete -c linear -n '__fish_use_subcommand' -a '${c}'`).join("\n")}
51
- ${commands.map((c) => `complete -c lin -n '__fish_use_subcommand' -a '${c}'`).join("\n")}
50
+ ${commands
51
+ .map((c) => `complete -c linear -n '__fish_use_subcommand' -a '${c}'`)
52
+ .join("\n")}
53
+ ${commands
54
+ .map((c) => `complete -c lin -n '__fish_use_subcommand' -a '${c}'`)
55
+ .join("\n")}
52
56
  `;
53
57
  }
54
58
  }