@enrichlayer/el-linear 1.29.1 → 1.32.0
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.
- package/README.md +21 -3
- package/claude-skills/linear-operations/SKILL.md +30 -1
- package/dist/commands/branch.d.ts +20 -0
- package/dist/commands/branch.js +101 -0
- package/dist/commands/comments.js +73 -6
- package/dist/commands/issue-id.d.ts +1 -0
- package/dist/commands/issue-id.js +1 -1
- package/dist/commands/issues.js +1 -0
- package/dist/commands/teams.d.ts +1 -0
- package/dist/commands/teams.js +2 -1
- package/dist/main.js +2 -0
- package/dist/utils/formatters/summary.d.ts +11 -1
- package/dist/utils/formatters/summary.js +103 -0
- package/dist/utils/mention-resolver.d.ts +26 -5
- package/dist/utils/mention-resolver.js +37 -8
- package/dist/utils/output.d.ts +6 -0
- package/dist/utils/output.js +8 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -662,9 +662,9 @@ Mutually exclusive with `--field` / `--sections` / `--with`.
|
|
|
662
662
|
|
|
663
663
|
### One-line write confirmations: `-q, --quiet`
|
|
664
664
|
|
|
665
|
-
`issues create|update` and `comments create|update` accept
|
|
666
|
-
printing a single confirmation line instead of the full JSON
|
|
667
|
-
you don't have to `grep` the result:
|
|
665
|
+
`issues create|update`, `issues relate`, and `comments create|update` accept
|
|
666
|
+
`-q, --quiet`, printing a single confirmation line instead of the full JSON
|
|
667
|
+
envelope so you don't have to `grep` the result:
|
|
668
668
|
|
|
669
669
|
```bash
|
|
670
670
|
el-linear issues update DEV-123 --status "In Review" --quiet
|
|
@@ -672,6 +672,24 @@ el-linear issues update DEV-123 --status "In Review" --quiet
|
|
|
672
672
|
|
|
673
673
|
el-linear comments create DEV-123 --body "..." --quiet
|
|
674
674
|
# comment 6f1c…
|
|
675
|
+
|
|
676
|
+
el-linear issues relate DEV-123 --related-to "DEV-456,DEV-789" --quiet
|
|
677
|
+
# DEV-123 related DEV-456,DEV-789 (2)
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
For `issues relate` the line is source-oriented — relations are grouped by
|
|
681
|
+
type from the source issue's perspective, so a `--blocked-by` reads back as
|
|
682
|
+
`blockedBy`. `--format summary` renders the same source-oriented view as a
|
|
683
|
+
`TYPE / TARGET / TITLE` table (TARGET is the peer issue; the source isn't
|
|
684
|
+
repeated as a column):
|
|
685
|
+
|
|
686
|
+
```bash
|
|
687
|
+
el-linear issues relate DEV-1 --blocked-by DEV-9 --format summary
|
|
688
|
+
# TYPE TARGET TITLE
|
|
689
|
+
# ------------------------------------
|
|
690
|
+
# blockedBy DEV-9 Add IP ban system
|
|
691
|
+
#
|
|
692
|
+
# 1 relation
|
|
675
693
|
```
|
|
676
694
|
|
|
677
695
|
`--quiet` overrides `--format` and is independent of `--fields` / `--jq`.
|
|
@@ -197,7 +197,15 @@ el-linear issues search "keywords from proposed title" --include-closed 2>&1
|
|
|
197
197
|
el-linear issues create "Title" --team ENG --related-to "ENG-456,ENG-789" ... 2>&1
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
###
|
|
200
|
+
### Existence check — before an "add capability X" issue ([DEV-5097](https://linear.app/verticalint/issue/DEV-5097/))
|
|
201
|
+
|
|
202
|
+
The dup-check above guards against duplicating an *issue*. This guards against duplicating *reality*: before filing an issue to **add** a flag / guard / command / subcommand, confirm it doesn't **already exist**.
|
|
203
|
+
|
|
204
|
+
- `<cli> <subcommand> --help` (the flag may be a global option absent from the subcommand's help — check `<cli> --help` too).
|
|
205
|
+
- `el-catalog commands --search "<intent>"` / `el-catalog clis --search` (the snapshot likely already lists it).
|
|
206
|
+
- For a hook/guard, grep the source — e.g. `cli/el-hook/src/checks`.
|
|
207
|
+
|
|
208
|
+
Skipping it cost two needless branch+MR cycles in one session: a "feature" issue to add a hook guard that already existed and was firing, and one to add `--jq`/`--fields` flags that already worked — both premises only caught at implementation.
|
|
201
209
|
|
|
202
210
|
When `el-linear issues search` (or the cross-resource `search`) returns rows
|
|
203
211
|
carrying issue identifiers, the JSON envelope embeds a `_warnings` line
|
|
@@ -352,6 +360,27 @@ el-linear comments create ENG-123 --body "cc @alice — Bob can you review?" 2>&
|
|
|
352
360
|
|
|
353
361
|
Works in comments only (not descriptions).
|
|
354
362
|
|
|
363
|
+
### Confirming a mention fired (the `mentions` output field)
|
|
364
|
+
|
|
365
|
+
A real mention lives in the comment's structured `bodyData`, not in the markdown — so a comment whose body reads `@alice` may or may not have actually pinged anyone. **Don't guess: read the `mentions` field** that `comments create|update` attach to their output (the sibling of `autoLinked`):
|
|
366
|
+
|
|
367
|
+
```jsonc
|
|
368
|
+
{
|
|
369
|
+
"id": "…",
|
|
370
|
+
"mentions": {
|
|
371
|
+
"resolved": [{ "label": "alice", "userId": "…" }], // real notifications sent
|
|
372
|
+
"unresolved": ["bobby"], // explicit @names that matched nobody
|
|
373
|
+
"delivered": true // false ⇒ Linear rejected bodyData, fell back to plain text
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
- An **unresolved** explicit `@name` (typo or unknown handle) is left as plain text — **no notification** — and el-linear prints a loud `⚠ @name did not resolve …` warning to **stderr**. Fix the name and re-comment; never assume the ping landed.
|
|
379
|
+
- `delivered: false` means the structured body was rejected and the comment shipped as plain markdown, so the resolved mentions did **not** fire — also warned on stderr.
|
|
380
|
+
- Under `--quiet`, the stdout stays the one-line `comment <id>`; the `mentions: resolved=[…] unresolved=[…]` confirmation is echoed to **stderr**.
|
|
381
|
+
|
|
382
|
+
This makes "a real @mention" a verifiable, deterministic convention rather than a hope ([DEV-4987](https://linear.app/verticalint/issue/DEV-4987/)).
|
|
383
|
+
|
|
355
384
|
---
|
|
356
385
|
|
|
357
386
|
## CLI Syntax Rules
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `el-linear branch validate [branch]` — validate that a branch name carries a
|
|
3
|
+
* **real** Linear team prefix, checked against the workspace's actual teams
|
|
4
|
+
* (cached `teams list`) rather than a hand-maintained allowlist.
|
|
5
|
+
*
|
|
6
|
+
* Built so the tools-repo pre-commit gate (`check-linear-branch.sh`) can
|
|
7
|
+
* delegate team-validity here instead of hardcoding a bash regex that drifts
|
|
8
|
+
* (it already did: phantom `CS`, missing `CUS/EMW/NIC/PYT`). Branch *parsing*
|
|
9
|
+
* already lives in `issue-id`/`parseBranchName`; this adds team *validity*.
|
|
10
|
+
*
|
|
11
|
+
* Exit codes (so a caller can distinguish "block" from "fall back"):
|
|
12
|
+
* 0 — valid: a real team, or an exempt branch (main/master/detached/empty)
|
|
13
|
+
* 1 — invalid: parsed a team that isn't a real workspace team, or no Linear ID
|
|
14
|
+
* 2 — indeterminate: the team set couldn't be loaded (offline + cold cache);
|
|
15
|
+
* lets the caller fall back to its own check rather than hard-block
|
|
16
|
+
*
|
|
17
|
+
* `--exit-zero` reports the JSON verdict but always exits 0 (report-only).
|
|
18
|
+
*/
|
|
19
|
+
import type { Command } from "commander";
|
|
20
|
+
export declare function setupBranchCommands(program: Command): void;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `el-linear branch validate [branch]` — validate that a branch name carries a
|
|
3
|
+
* **real** Linear team prefix, checked against the workspace's actual teams
|
|
4
|
+
* (cached `teams list`) rather than a hand-maintained allowlist.
|
|
5
|
+
*
|
|
6
|
+
* Built so the tools-repo pre-commit gate (`check-linear-branch.sh`) can
|
|
7
|
+
* delegate team-validity here instead of hardcoding a bash regex that drifts
|
|
8
|
+
* (it already did: phantom `CS`, missing `CUS/EMW/NIC/PYT`). Branch *parsing*
|
|
9
|
+
* already lives in `issue-id`/`parseBranchName`; this adds team *validity*.
|
|
10
|
+
*
|
|
11
|
+
* Exit codes (so a caller can distinguish "block" from "fall back"):
|
|
12
|
+
* 0 — valid: a real team, or an exempt branch (main/master/detached/empty)
|
|
13
|
+
* 1 — invalid: parsed a team that isn't a real workspace team, or no Linear ID
|
|
14
|
+
* 2 — indeterminate: the team set couldn't be loaded (offline + cold cache);
|
|
15
|
+
* lets the caller fall back to its own check rather than hard-block
|
|
16
|
+
*
|
|
17
|
+
* `--exit-zero` reports the JSON verdict but always exits 0 (report-only).
|
|
18
|
+
*/
|
|
19
|
+
import { loadConfig } from "../config/config.js";
|
|
20
|
+
import { cached, resolveCacheTTL } from "../utils/disk-cache.js";
|
|
21
|
+
import { createLinearService } from "../utils/linear-service.js";
|
|
22
|
+
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
23
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
24
|
+
import { getCurrentBranch, parseBranchName } from "./issue-id.js";
|
|
25
|
+
import { TEAMS_LIST_DEFAULT_LIMIT } from "./teams.js";
|
|
26
|
+
// Branches that never carry a Linear ID and must not be blocked. Mirrors the
|
|
27
|
+
// exemptions in the tools-repo gate (main/master/detached HEAD/empty).
|
|
28
|
+
const EXEMPT_BRANCHES = new Set(["main", "master", "HEAD", ""]);
|
|
29
|
+
export function setupBranchCommands(program) {
|
|
30
|
+
const branch = program.command("branch").description("Branch-name helpers");
|
|
31
|
+
branch.action(() => branch.help());
|
|
32
|
+
branch
|
|
33
|
+
.command("validate [branch]")
|
|
34
|
+
.description("Validate a branch's team prefix against real workspace teams. Exit 0 valid, 1 invalid, 2 indeterminate.")
|
|
35
|
+
.option("--exit-zero", "report the verdict as JSON but always exit 0 (report-only)", false)
|
|
36
|
+
.action(handleAsyncCommand(async (branchArg, options, command) => {
|
|
37
|
+
const branchName = branchArg ?? getCurrentBranch();
|
|
38
|
+
if (EXEMPT_BRANCHES.has(branchName)) {
|
|
39
|
+
outputSuccess({
|
|
40
|
+
branch: branchName,
|
|
41
|
+
valid: true,
|
|
42
|
+
team: null,
|
|
43
|
+
issueId: null,
|
|
44
|
+
reason: "exempt",
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const parsed = parseBranchName(branchName);
|
|
49
|
+
if (!(parsed.issueId && parsed.team)) {
|
|
50
|
+
outputSuccess({
|
|
51
|
+
branch: branchName,
|
|
52
|
+
valid: false,
|
|
53
|
+
team: null,
|
|
54
|
+
issueId: null,
|
|
55
|
+
reason: "no-linear-id",
|
|
56
|
+
});
|
|
57
|
+
if (!options.exitZero)
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let teamKeys;
|
|
62
|
+
try {
|
|
63
|
+
const rootOpts = getRootOpts(command);
|
|
64
|
+
const ttl = resolveCacheTTL({
|
|
65
|
+
configTTL: loadConfig().cacheTTLSeconds,
|
|
66
|
+
noCacheFlag: rootOpts.cache === false,
|
|
67
|
+
});
|
|
68
|
+
// Same cache key and limit as `teams list` default — share the entry.
|
|
69
|
+
const teams = await cached(`teams-list-limit:${TEAMS_LIST_DEFAULT_LIMIT}`, ttl, async () => {
|
|
70
|
+
const service = await createLinearService(rootOpts);
|
|
71
|
+
return service.getTeams(TEAMS_LIST_DEFAULT_LIMIT);
|
|
72
|
+
});
|
|
73
|
+
teamKeys = teams.map((t) => t.key);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Couldn't load the team set (offline + cold cache). Signal
|
|
77
|
+
// indeterminate so the caller can fall back instead of blocking.
|
|
78
|
+
outputSuccess({
|
|
79
|
+
branch: branchName,
|
|
80
|
+
valid: null,
|
|
81
|
+
team: parsed.team,
|
|
82
|
+
issueId: parsed.issueId,
|
|
83
|
+
reason: "teams-unavailable",
|
|
84
|
+
});
|
|
85
|
+
if (!options.exitZero)
|
|
86
|
+
process.exitCode = 2;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const valid = teamKeys.includes(parsed.team);
|
|
90
|
+
outputSuccess({
|
|
91
|
+
branch: branchName,
|
|
92
|
+
valid,
|
|
93
|
+
team: parsed.team,
|
|
94
|
+
issueId: parsed.issueId,
|
|
95
|
+
reason: valid ? "ok" : "unknown-team",
|
|
96
|
+
...(valid ? {} : { knownTeams: teamKeys }),
|
|
97
|
+
});
|
|
98
|
+
if (!(valid || options.exitZero))
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
@@ -7,8 +7,9 @@ import { createGraphQLService, } from "../utils/graphql-service.js";
|
|
|
7
7
|
import { extractIssueReferences } from "../utils/issue-reference-extractor.js";
|
|
8
8
|
import { wrapIssueReferencesAsLinks } from "../utils/issue-reference-wrapper.js";
|
|
9
9
|
import { createLinearService, } from "../utils/linear-service.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { logger } from "../utils/logger.js";
|
|
11
|
+
import { resolveMentions, } from "../utils/mention-resolver.js";
|
|
12
|
+
import { getQuietMode, handleAsyncCommand, outputSuccess, } from "../utils/output.js";
|
|
12
13
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
13
14
|
import { validateReferences } from "../utils/validate-references.js";
|
|
14
15
|
import { parsePositiveInt } from "../utils/validators.js";
|
|
@@ -17,6 +18,54 @@ import { getWorkspaceUrlKey } from "../utils/workspace-url.js";
|
|
|
17
18
|
// tweak on their side doesn't silently regress the fallback path.
|
|
18
19
|
const BODY_DATA_ERROR_RE = /prosemirror|bodydata|invalid.*body/i;
|
|
19
20
|
const ISSUE_IDENTIFIER_REGEX = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
21
|
+
/**
|
|
22
|
+
* Turn a {@link MentionReport} into the `mentions` output field, or `undefined`
|
|
23
|
+
* when there is nothing to report. `delivered=false` records a bodyData→plain
|
|
24
|
+
* fallback so the JSON output never falsely claims a ping was sent.
|
|
25
|
+
*/
|
|
26
|
+
function buildMentionsOutput(report, delivered) {
|
|
27
|
+
if (!report) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (report.resolved.length === 0 && report.unresolvedExplicit.length === 0) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
resolved: report.resolved.map((m) => ({
|
|
35
|
+
label: m.label,
|
|
36
|
+
userId: m.userId,
|
|
37
|
+
})),
|
|
38
|
+
unresolved: report.unresolvedExplicit,
|
|
39
|
+
delivered,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Emit the human-facing side of mention resolution to **stderr** (so it never
|
|
44
|
+
* pollutes the machine-stable stdout line, and survives `--quiet`):
|
|
45
|
+
* - a loud warning for each explicit `@name` that resolved to nobody,
|
|
46
|
+
* - a warning when a bodyData rejection dropped resolved mentions,
|
|
47
|
+
* - under `--quiet`, a one-line confirmation of what resolved (since the
|
|
48
|
+
* quiet stdout line can't carry the `mentions` field).
|
|
49
|
+
*/
|
|
50
|
+
function reportMentions(mentions) {
|
|
51
|
+
if (!mentions) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const name of mentions.unresolved) {
|
|
55
|
+
logger.error(`⚠ @${name} did not resolve to a team member — left as plain text (no notification sent)`);
|
|
56
|
+
}
|
|
57
|
+
if (!mentions.delivered && mentions.resolved.length > 0) {
|
|
58
|
+
logger.error(`⚠ Linear rejected the structured comment body; fell back to plain text — ${mentions.resolved.length} mention(s) were NOT delivered as notifications`);
|
|
59
|
+
}
|
|
60
|
+
// In quiet mode stdout is a single `comment <id>` line with no room for the
|
|
61
|
+
// `mentions` field, so echo a concise confirmation to stderr.
|
|
62
|
+
if (getQuietMode()) {
|
|
63
|
+
const resolved = mentions.delivered
|
|
64
|
+
? mentions.resolved.map((m) => `${m.label}→${m.userId}`).join(", ")
|
|
65
|
+
: "(none delivered)";
|
|
66
|
+
logger.error(`mentions: resolved=[${resolved}] unresolved=[${mentions.unresolved.join(", ")}]`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
20
69
|
function readBody(options) {
|
|
21
70
|
// --body and --body-file are two sources for the same field; accepting both
|
|
22
71
|
// would silently drop one. Reject up front (DEV-4450) — the same mutual-
|
|
@@ -130,12 +179,15 @@ async function handleCreateComment(issueId, options, command) {
|
|
|
130
179
|
selfUserId,
|
|
131
180
|
});
|
|
132
181
|
const input = { issueId: resolvedIssueId };
|
|
133
|
-
if (mentionResult) {
|
|
182
|
+
if (mentionResult?.bodyData) {
|
|
134
183
|
input.bodyData = mentionResult.bodyData;
|
|
135
184
|
}
|
|
136
185
|
else {
|
|
137
186
|
input.body = body;
|
|
138
187
|
}
|
|
188
|
+
// Tracks whether resolved mentions actually shipped as structured nodes;
|
|
189
|
+
// flipped to false if the bodyData fallback below sends plain text instead.
|
|
190
|
+
let mentionsDelivered = true;
|
|
139
191
|
let result;
|
|
140
192
|
try {
|
|
141
193
|
result = await graphQLService.rawRequest(CREATE_COMMENT_MUTATION, { input });
|
|
@@ -146,6 +198,7 @@ async function handleCreateComment(issueId, options, command) {
|
|
|
146
198
|
// Linear handle the conversion server-side.
|
|
147
199
|
const msg = err instanceof Error ? err.message : String(err);
|
|
148
200
|
if (input.bodyData && BODY_DATA_ERROR_RE.test(msg)) {
|
|
201
|
+
mentionsDelivered = false;
|
|
149
202
|
const fallbackInput = {
|
|
150
203
|
issueId: resolvedIssueId,
|
|
151
204
|
body,
|
|
@@ -179,7 +232,13 @@ async function handleCreateComment(issueId, options, command) {
|
|
|
179
232
|
linearService,
|
|
180
233
|
});
|
|
181
234
|
const output = transformComment(mutation.comment);
|
|
182
|
-
|
|
235
|
+
const mentions = buildMentionsOutput(mentionResult, mentionsDelivered);
|
|
236
|
+
reportMentions(mentions);
|
|
237
|
+
outputSuccess({
|
|
238
|
+
...output,
|
|
239
|
+
...(autoLinked ? { autoLinked } : {}),
|
|
240
|
+
...(mentions ? { mentions } : {}),
|
|
241
|
+
});
|
|
183
242
|
}
|
|
184
243
|
async function handleUpdateComment(commentId, options, command) {
|
|
185
244
|
const rootOpts = getRootOpts(command);
|
|
@@ -196,12 +255,13 @@ async function handleUpdateComment(commentId, options, command) {
|
|
|
196
255
|
selfUserId,
|
|
197
256
|
});
|
|
198
257
|
const input = {};
|
|
199
|
-
if (mentionResult) {
|
|
258
|
+
if (mentionResult?.bodyData) {
|
|
200
259
|
input.bodyData = mentionResult.bodyData;
|
|
201
260
|
}
|
|
202
261
|
else {
|
|
203
262
|
input.body = body;
|
|
204
263
|
}
|
|
264
|
+
let mentionsDelivered = true;
|
|
205
265
|
let result;
|
|
206
266
|
try {
|
|
207
267
|
result = await graphQLService.rawRequest(UPDATE_COMMENT_MUTATION, { id: commentId, input });
|
|
@@ -217,6 +277,7 @@ async function handleUpdateComment(commentId, options, command) {
|
|
|
217
277
|
// extraction; copy-then-refactor on the third per the repo convention.
|
|
218
278
|
const msg = err instanceof Error ? err.message : String(err);
|
|
219
279
|
if (input.bodyData && BODY_DATA_ERROR_RE.test(msg)) {
|
|
280
|
+
mentionsDelivered = false;
|
|
220
281
|
const fallbackInput = { body };
|
|
221
282
|
result = await graphQLService.rawRequest(UPDATE_COMMENT_MUTATION, { id: commentId, input: fallbackInput });
|
|
222
283
|
}
|
|
@@ -243,7 +304,13 @@ async function handleUpdateComment(commentId, options, command) {
|
|
|
243
304
|
});
|
|
244
305
|
}
|
|
245
306
|
const output = transformComment(comment);
|
|
246
|
-
|
|
307
|
+
const mentions = buildMentionsOutput(mentionResult, mentionsDelivered);
|
|
308
|
+
reportMentions(mentions);
|
|
309
|
+
outputSuccess({
|
|
310
|
+
...output,
|
|
311
|
+
...(autoLinked ? { autoLinked } : {}),
|
|
312
|
+
...(mentions ? { mentions } : {}),
|
|
313
|
+
});
|
|
247
314
|
}
|
|
248
315
|
async function handleListComments(issueId, options, command) {
|
|
249
316
|
const rootOpts = getRootOpts(command);
|
|
@@ -24,5 +24,6 @@ interface ParsedBranch {
|
|
|
24
24
|
team: string | null;
|
|
25
25
|
}
|
|
26
26
|
export declare function parseBranchName(branch: string): ParsedBranch;
|
|
27
|
+
export declare function getCurrentBranch(): string;
|
|
27
28
|
export declare function setupIssueIdCommand(program: Command): void;
|
|
28
29
|
export {};
|
package/dist/commands/issues.js
CHANGED
|
@@ -1233,6 +1233,7 @@ export function setupIssuesCommands(program) {
|
|
|
1233
1233
|
.option("--blocks <issues>", "issues this blocks (comma-separated)")
|
|
1234
1234
|
.option("--blocked-by <issues>", "issues blocking this (comma-separated)")
|
|
1235
1235
|
.option("--duplicate-of <issue>", "mark as duplicate of another issue")
|
|
1236
|
+
.option("-q, --quiet", "print one confirmation line (SOURCE type targets (count)) instead of the full JSON")
|
|
1236
1237
|
.action(handleAsyncCommand(handleRelateIssue));
|
|
1237
1238
|
issues
|
|
1238
1239
|
.command("related <issueId>")
|
package/dist/commands/teams.d.ts
CHANGED
package/dist/commands/teams.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createLinearService } from "../utils/linear-service.js";
|
|
|
4
4
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
5
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
6
6
|
import { parsePositiveInt } from "../utils/validators.js";
|
|
7
|
+
export const TEAMS_LIST_DEFAULT_LIMIT = 100;
|
|
7
8
|
export function setupTeamsCommands(program) {
|
|
8
9
|
const teams = program
|
|
9
10
|
.command("teams")
|
|
@@ -13,7 +14,7 @@ export function setupTeamsCommands(program) {
|
|
|
13
14
|
teams
|
|
14
15
|
.command("list")
|
|
15
16
|
.description("List all teams")
|
|
16
|
-
.option("-l, --limit <number>", "limit results",
|
|
17
|
+
.option("-l, --limit <number>", "limit results", String(TEAMS_LIST_DEFAULT_LIMIT))
|
|
17
18
|
.action(handleAsyncCommand(async (options, command) => {
|
|
18
19
|
const rootOpts = getRootOpts(command);
|
|
19
20
|
const limit = parsePositiveInt(options.limit, "--limit");
|
package/dist/main.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { program } from "commander";
|
|
6
6
|
import { setupAttachmentsCommands } from "./commands/attachments.js";
|
|
7
7
|
import { setupBatchCommands } from "./commands/batch.js";
|
|
8
|
+
import { setupBranchCommands } from "./commands/branch.js";
|
|
8
9
|
import { setupCommentsCommands } from "./commands/comments.js";
|
|
9
10
|
import { setupConfigCommands } from "./commands/config.js";
|
|
10
11
|
import { setupCyclesCommands } from "./commands/cycles.js";
|
|
@@ -132,6 +133,7 @@ program.action(() => {
|
|
|
132
133
|
});
|
|
133
134
|
setupAttachmentsCommands(program);
|
|
134
135
|
setupBatchCommands(program);
|
|
136
|
+
setupBranchCommands(program);
|
|
135
137
|
setupSearchCommands(program);
|
|
136
138
|
setupIssuesCommands(program);
|
|
137
139
|
setupIssueIdCommand(program);
|
|
@@ -32,6 +32,16 @@ export declare function formatMilestoneSummary(milestone: Record<string, unknown
|
|
|
32
32
|
export declare function formatMilestoneList(milestones: unknown[]): string;
|
|
33
33
|
export declare function formatTeamList(teams: unknown[]): string;
|
|
34
34
|
export declare function formatLabelList(labels: unknown[]): string;
|
|
35
|
+
/**
|
|
36
|
+
* Table render for an `issues relate` result — the `{ data: IssueRelation[] }`
|
|
37
|
+
* envelope where each entry is `{ type, issue, relatedIssue }`. Rendered
|
|
38
|
+
* **source-oriented** (`TYPE TARGET TITLE`) to match the `--quiet` line: the
|
|
39
|
+
* type is normalized from the source's perspective (a `--blocked-by` reads as
|
|
40
|
+
* `blockedBy`) and TARGET is the peer (non-source) issue. `sourceRef` comes
|
|
41
|
+
* from the envelope's `meta.source` via `dispatch`; absent it (a bare array),
|
|
42
|
+
* the row falls back to the stored direction.
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatRelationList(relations: unknown[], sourceRef?: string): string;
|
|
35
45
|
export declare function formatUserSummary(user: Record<string, unknown>): string;
|
|
36
46
|
export declare function formatUserList(users: unknown[]): string;
|
|
37
47
|
export declare function formatDocumentSummary(doc: Record<string, unknown>): string;
|
|
@@ -49,7 +59,7 @@ export declare function formatSearchResultList(results: unknown[]): string;
|
|
|
49
59
|
* the full payload. Lists fall back to a simple bulleted list.
|
|
50
60
|
*/
|
|
51
61
|
export declare function formatGenericSummary(value: unknown): string;
|
|
52
|
-
export type ResourceKind = "issue" | "issue-list" | "project" | "project-list" | "comment" | "comment-list" | "cycle" | "cycle-list" | "milestone" | "milestone-list" | "team-list" | "label-list" | "user" | "user-list" | "document" | "document-list" | "template" | "template-list" | "attachment-list" | "release" | "release-list" | "search-result-list" | "empty-list" | "generic";
|
|
62
|
+
export type ResourceKind = "issue" | "issue-list" | "project" | "project-list" | "comment" | "comment-list" | "cycle" | "cycle-list" | "milestone" | "milestone-list" | "team-list" | "label-list" | "user" | "user-list" | "document" | "document-list" | "template" | "template-list" | "attachment-list" | "release" | "release-list" | "search-result-list" | "relation-list" | "empty-list" | "generic";
|
|
53
63
|
/**
|
|
54
64
|
* Heuristic — used by the central `outputSuccess` path which doesn't know
|
|
55
65
|
* which command produced the payload. Looks at the shape of the data to
|
|
@@ -689,6 +689,64 @@ export function formatLabelList(labels) {
|
|
|
689
689
|
{ header: "COLOR", minWidth: 5, extract: (l) => s(l.color) },
|
|
690
690
|
], { emptyText: "(no labels)", itemNoun: "label" });
|
|
691
691
|
}
|
|
692
|
+
// ── relations ──────────────────────────────────────────────────
|
|
693
|
+
/**
|
|
694
|
+
* Re-frame a stored relation row from the source issue's perspective: returns
|
|
695
|
+
* the normalized relation `type` (a reverse `blocks` becomes `blockedBy`) and
|
|
696
|
+
* the `peer` — the non-source issue node. `createRelations` stores a
|
|
697
|
+
* `--blocked-by X` row reversed (`{ type:"blocks", issue:X, relatedIssue:source }`),
|
|
698
|
+
* so the source can sit on either side; we orient by `sourceRef`.
|
|
699
|
+
*
|
|
700
|
+
* `sourceRef` is the raw arg the user passed — a canonical `TEAM-NUM`
|
|
701
|
+
* identifier or a UUID, the only two forms `resolveIssueId` admits — matched
|
|
702
|
+
* against either node's `identifier` or `id`. (Coupling note: if that resolver
|
|
703
|
+
* is ever widened to accept Linear URLs / slug-ids, as `--project` already
|
|
704
|
+
* does, this match would need the resolved `sourceId` instead.) With no source
|
|
705
|
+
* (`"—"`, e.g. a bare array with no `meta`), nothing matches, so it falls back
|
|
706
|
+
* to the stored direction: peer = `relatedIssue`, type uninverted.
|
|
707
|
+
*/
|
|
708
|
+
function relationFromSource(rel, sourceRef) {
|
|
709
|
+
const matchesSource = (node) => !!node &&
|
|
710
|
+
sourceRef !== "—" &&
|
|
711
|
+
(s(node.identifier) === sourceRef || s(node.id) === sourceRef);
|
|
712
|
+
const issue = asObj(rel.issue);
|
|
713
|
+
const related = asObj(rel.relatedIssue);
|
|
714
|
+
// Source on the relatedIssue side ⇒ this is the inverse direction.
|
|
715
|
+
const sourceIsRelated = matchesSource(related) && !matchesSource(issue);
|
|
716
|
+
const peer = sourceIsRelated ? issue : related;
|
|
717
|
+
const rawType = s(rel.type);
|
|
718
|
+
const type = sourceIsRelated && rawType === "blocks" ? "blockedBy" : rawType;
|
|
719
|
+
return { type, peer };
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Table render for an `issues relate` result — the `{ data: IssueRelation[] }`
|
|
723
|
+
* envelope where each entry is `{ type, issue, relatedIssue }`. Rendered
|
|
724
|
+
* **source-oriented** (`TYPE TARGET TITLE`) to match the `--quiet` line: the
|
|
725
|
+
* type is normalized from the source's perspective (a `--blocked-by` reads as
|
|
726
|
+
* `blockedBy`) and TARGET is the peer (non-source) issue. `sourceRef` comes
|
|
727
|
+
* from the envelope's `meta.source` via `dispatch`; absent it (a bare array),
|
|
728
|
+
* the row falls back to the stored direction.
|
|
729
|
+
*/
|
|
730
|
+
export function formatRelationList(relations, sourceRef = "—") {
|
|
731
|
+
const rows = relations.map((raw) => {
|
|
732
|
+
const { type, peer } = relationFromSource(asObj(raw) ?? {}, sourceRef);
|
|
733
|
+
return {
|
|
734
|
+
type,
|
|
735
|
+
target: s(peer?.identifier ?? peer?.id),
|
|
736
|
+
title: s(peer?.title),
|
|
737
|
+
};
|
|
738
|
+
});
|
|
739
|
+
return renderTable(rows, [
|
|
740
|
+
{ header: "TYPE", minWidth: 4, extract: (r) => r.type },
|
|
741
|
+
{ header: "TARGET", minWidth: 6, extract: (r) => r.target },
|
|
742
|
+
{
|
|
743
|
+
header: "TITLE",
|
|
744
|
+
minWidth: 5,
|
|
745
|
+
maxWidth: TITLE_TRUNC,
|
|
746
|
+
extract: (r) => r.title,
|
|
747
|
+
},
|
|
748
|
+
], { emptyText: "(no relations)", itemNoun: "relation" });
|
|
749
|
+
}
|
|
692
750
|
// ── users ──────────────────────────────────────────────────────
|
|
693
751
|
export function formatUserSummary(user) {
|
|
694
752
|
const name = s(user.name);
|
|
@@ -968,6 +1026,10 @@ function inferListKind(items) {
|
|
|
968
1026
|
return "empty-list";
|
|
969
1027
|
}
|
|
970
1028
|
const sample = asObj(items[0]) ?? {};
|
|
1029
|
+
// IssueRelation rows ({ type, issue, relatedIssue }) carry no top-level
|
|
1030
|
+
// identifier/title, so this must precede the issue-list check below.
|
|
1031
|
+
if ("relatedIssue" in sample && "issue" in sample && "type" in sample)
|
|
1032
|
+
return "relation-list";
|
|
971
1033
|
if ("identifier" in sample && "title" in sample) {
|
|
972
1034
|
// could be issue or search result
|
|
973
1035
|
if ("type" in sample && !("templateData" in sample)) {
|
|
@@ -1088,6 +1150,8 @@ export function dispatch(kind, payload, fields) {
|
|
|
1088
1150
|
return formatReleaseList(list ?? []);
|
|
1089
1151
|
case "search-result-list":
|
|
1090
1152
|
return formatSearchResultList(list ?? []);
|
|
1153
|
+
case "relation-list":
|
|
1154
|
+
return formatRelationList(list ?? [], s(asObj(obj?.meta)?.source));
|
|
1091
1155
|
case "empty-list":
|
|
1092
1156
|
return "(no results)";
|
|
1093
1157
|
case "generic":
|
|
@@ -1122,5 +1186,44 @@ export function formatLine(payload) {
|
|
|
1122
1186
|
if (kind === "comment" && obj) {
|
|
1123
1187
|
return `comment ${s(obj.id)}`;
|
|
1124
1188
|
}
|
|
1189
|
+
if (kind === "relation-list") {
|
|
1190
|
+
return formatRelationLine(payload);
|
|
1191
|
+
}
|
|
1125
1192
|
return JSON.stringify(payload);
|
|
1126
1193
|
}
|
|
1194
|
+
/**
|
|
1195
|
+
* `--quiet` render for an `issues relate` result: one source-oriented line
|
|
1196
|
+
* grouping the created relations by type from the source issue's perspective.
|
|
1197
|
+
*
|
|
1198
|
+
* EMW-331 related EMW-339,EMW-340 (2)
|
|
1199
|
+
* EMW-331 blockedBy EMW-400; related EMW-339 (2)
|
|
1200
|
+
*
|
|
1201
|
+
* The per-row reframing (peer + type inversion for reverse `--blocked-by`)
|
|
1202
|
+
* is shared with the summary table via `relationFromSource`; see its doc for
|
|
1203
|
+
* the direction and source-matching rules.
|
|
1204
|
+
*/
|
|
1205
|
+
function formatRelationLine(payload) {
|
|
1206
|
+
const obj = asObj(payload);
|
|
1207
|
+
const relations = Array.isArray(payload)
|
|
1208
|
+
? payload
|
|
1209
|
+
: Array.isArray(obj?.data)
|
|
1210
|
+
? obj.data
|
|
1211
|
+
: [];
|
|
1212
|
+
const sourceRef = s(asObj(obj?.meta)?.source);
|
|
1213
|
+
// Preserve first-seen type order for a stable line.
|
|
1214
|
+
const byType = new Map();
|
|
1215
|
+
for (const raw of relations) {
|
|
1216
|
+
const rel = asObj(raw);
|
|
1217
|
+
if (!rel)
|
|
1218
|
+
continue;
|
|
1219
|
+
const { type, peer } = relationFromSource(rel, sourceRef);
|
|
1220
|
+
const targetId = s(peer?.identifier ?? peer?.id);
|
|
1221
|
+
const bucket = byType.get(type);
|
|
1222
|
+
if (bucket)
|
|
1223
|
+
bucket.push(targetId);
|
|
1224
|
+
else
|
|
1225
|
+
byType.set(type, [targetId]);
|
|
1226
|
+
}
|
|
1227
|
+
const segments = [...byType.entries()].map(([type, ids]) => `${type} ${ids.join(",")}`);
|
|
1228
|
+
return `${sourceRef} ${segments.join("; ")} (${relations.length})`;
|
|
1229
|
+
}
|
|
@@ -12,6 +12,27 @@ export interface ResolveMentionsOptions {
|
|
|
12
12
|
*/
|
|
13
13
|
selfUserId?: string;
|
|
14
14
|
}
|
|
15
|
+
interface ResolvedMention {
|
|
16
|
+
label: string;
|
|
17
|
+
userId: string;
|
|
18
|
+
}
|
|
19
|
+
export interface MentionReport {
|
|
20
|
+
/**
|
|
21
|
+
* ProseMirror doc carrying `suggestion_userMentions` nodes — the structured
|
|
22
|
+
* body that makes Linear fire real notifications. `null` when nothing
|
|
23
|
+
* resolved (the caller should send the plain markdown `body` instead).
|
|
24
|
+
*/
|
|
25
|
+
bodyData: ProseMirrorNode | null;
|
|
26
|
+
/** Mentions that resolved to a real user (explicit `@name` + bare names). */
|
|
27
|
+
resolved: ResolvedMention[];
|
|
28
|
+
/**
|
|
29
|
+
* Explicit `@name` tokens that did NOT resolve to any team member — almost
|
|
30
|
+
* always a typo or an unknown handle. Left as plain text (no notification),
|
|
31
|
+
* so the caller surfaces them as a warning. Bare-name non-matches are not
|
|
32
|
+
* tracked here: they were never an intended ping.
|
|
33
|
+
*/
|
|
34
|
+
unresolvedExplicit: string[];
|
|
35
|
+
}
|
|
15
36
|
/**
|
|
16
37
|
* Resolve mentions in markdown body text to Linear user IDs.
|
|
17
38
|
*
|
|
@@ -23,9 +44,9 @@ export interface ResolveMentionsOptions {
|
|
|
23
44
|
* are also converted to mentions — without requiring the `@` prefix. This
|
|
24
45
|
* catches cases like "Bob owns the design" → "@bob owns the design".
|
|
25
46
|
*
|
|
26
|
-
* Returns a
|
|
27
|
-
*
|
|
47
|
+
* Returns a {@link MentionReport} describing which names resolved, which
|
|
48
|
+
* explicit `@names` did not, and the `bodyData` doc to send — or `null` when
|
|
49
|
+
* there was nothing to resolve and nothing to warn about.
|
|
28
50
|
*/
|
|
29
|
-
export declare function resolveMentions(body: string, linearService: LinearService, options?: ResolveMentionsOptions): Promise<
|
|
30
|
-
|
|
31
|
-
} | null>;
|
|
51
|
+
export declare function resolveMentions(body: string, linearService: LinearService, options?: ResolveMentionsOptions): Promise<MentionReport | null>;
|
|
52
|
+
export {};
|
|
@@ -5,7 +5,8 @@ import { isUuid } from "./uuid.js";
|
|
|
5
5
|
// Unicode-aware mention regex: ASCII-only `\w` would silently fail to
|
|
6
6
|
// match Cyrillic / accented Latin / CJK names (`@Юрий`, `@Niño`).
|
|
7
7
|
// `\p{L}` covers all Unicode letters; `\p{N}` covers all numerics.
|
|
8
|
-
const
|
|
8
|
+
const MENTION_NAME_CHARS = "\\p{L}\\p{N}_";
|
|
9
|
+
const EXPLICIT_MENTION_REGEX = new RegExp(`(?<![${MENTION_NAME_CHARS}])@([${MENTION_NAME_CHARS}]+)(?![${MENTION_NAME_CHARS}/])`, "gu");
|
|
9
10
|
const WHITESPACE_SPLIT_REGEX = /\s+/;
|
|
10
11
|
const FENCED_CODE_REGEX = /```[\s\S]*?```/g;
|
|
11
12
|
const INLINE_CODE_REGEX = /`[^`]+`/g;
|
|
@@ -21,20 +22,27 @@ const MARKDOWN_LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
|
21
22
|
* are also converted to mentions — without requiring the `@` prefix. This
|
|
22
23
|
* catches cases like "Bob owns the design" → "@bob owns the design".
|
|
23
24
|
*
|
|
24
|
-
* Returns a
|
|
25
|
-
*
|
|
25
|
+
* Returns a {@link MentionReport} describing which names resolved, which
|
|
26
|
+
* explicit `@names` did not, and the `bodyData` doc to send — or `null` when
|
|
27
|
+
* there was nothing to resolve and nothing to warn about.
|
|
26
28
|
*/
|
|
27
29
|
export async function resolveMentions(body, linearService, options = {}) {
|
|
28
30
|
const autoMention = options.autoMention !== false;
|
|
29
31
|
const selfUserId = options.selfUserId;
|
|
30
32
|
// 1. Resolve explicit @name mentions (existing behavior).
|
|
31
33
|
const explicit = new Map();
|
|
34
|
+
const unresolvedExplicit = [];
|
|
32
35
|
const explicitNames = [...body.matchAll(EXPLICIT_MENTION_REGEX)].map((m) => m[1]);
|
|
33
36
|
for (const name of new Set(explicitNames)) {
|
|
34
37
|
const userId = await resolveUserByName(name, linearService);
|
|
35
38
|
if (userId) {
|
|
36
39
|
explicit.set(name, { userId, label: name });
|
|
37
40
|
}
|
|
41
|
+
else {
|
|
42
|
+
// An explicit `@name` the author clearly meant as a ping, but it
|
|
43
|
+
// matched no team member — surface it so the ping never dies silently.
|
|
44
|
+
unresolvedExplicit.push(name);
|
|
45
|
+
}
|
|
38
46
|
}
|
|
39
47
|
// 2. Bare-name mentions: scan for candidate names that actually appear as
|
|
40
48
|
// standalone words in the body (outside code / link contexts).
|
|
@@ -61,12 +69,33 @@ export async function resolveMentions(body, linearService, options = {}) {
|
|
|
61
69
|
bare.set(candidate, { userId, label: candidate });
|
|
62
70
|
}
|
|
63
71
|
}
|
|
64
|
-
|
|
72
|
+
// Nothing resolved and no failed explicit ping → caller has nothing to do.
|
|
73
|
+
if (explicit.size === 0 &&
|
|
74
|
+
bare.size === 0 &&
|
|
75
|
+
unresolvedExplicit.length === 0) {
|
|
65
76
|
return null;
|
|
66
77
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
// De-dup by userId for the reported set: a capitalized explicit `@Bob` also
|
|
79
|
+
// matches the bare candidate "Bob" (the `@` isn't a word char, so the bare
|
|
80
|
+
// lookbehind passes), landing the same user in both maps. The injected
|
|
81
|
+
// bodyData is unaffected (the `@name` alternation consumes the span first,
|
|
82
|
+
// so only one mention node is emitted), but `resolved` would over-count and
|
|
83
|
+
// `--quiet` would print the user twice — which undercuts the whole point of
|
|
84
|
+
// a trustworthy mention report. Explicit-first so the label is the typed
|
|
85
|
+
// `@name`, not the capitalized bare candidate.
|
|
86
|
+
const resolvedByUser = new Map();
|
|
87
|
+
for (const m of [...explicit.values(), ...bare.values()]) {
|
|
88
|
+
if (!resolvedByUser.has(m.userId)) {
|
|
89
|
+
resolvedByUser.set(m.userId, m);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const resolved = [...resolvedByUser.values()];
|
|
93
|
+
// Only build the structured doc when something actually resolved; a body
|
|
94
|
+
// whose only mention was an unresolved `@typo` is sent as plain markdown.
|
|
95
|
+
const bodyData = explicit.size > 0 || bare.size > 0
|
|
96
|
+
? injectMentions(markdownToProseMirror(body), explicit, bare)
|
|
97
|
+
: null;
|
|
98
|
+
return { bodyData, resolved, unresolvedExplicit };
|
|
70
99
|
}
|
|
71
100
|
async function resolveUserByName(name, linearService) {
|
|
72
101
|
const configResult = resolveMember(name);
|
|
@@ -225,7 +254,7 @@ function buildCombinedRegex(explicit, bare) {
|
|
|
225
254
|
// Same Unicode-aware character class as EXPLICIT_MENTION_REGEX
|
|
226
255
|
// at module top so the splitter and the resolver agree on
|
|
227
256
|
// what counts as a name char.
|
|
228
|
-
parts.push(
|
|
257
|
+
parts.push(`(?<![${MENTION_NAME_CHARS}])@[${MENTION_NAME_CHARS}]+(?![${MENTION_NAME_CHARS}/])`);
|
|
229
258
|
}
|
|
230
259
|
if (bare.size > 0) {
|
|
231
260
|
const alternation = [...bare.keys()]
|
package/dist/utils/output.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export declare function setRawMode(enabled: boolean): void;
|
|
|
6
6
|
* the summary block. Set in main.ts's preAction when the flag is present.
|
|
7
7
|
*/
|
|
8
8
|
export declare function setQuietMode(enabled: boolean): void;
|
|
9
|
+
/**
|
|
10
|
+
* Whether `--quiet` is active. Lets a command decide to route extra
|
|
11
|
+
* human-facing detail (e.g. a mention-resolution confirmation) to stderr,
|
|
12
|
+
* keeping the single machine-stable stdout line intact.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getQuietMode(): boolean;
|
|
9
15
|
export declare function setJqFilter(filter: string | null): void;
|
|
10
16
|
export declare function setFieldsFilter(fields: string[] | null): void;
|
|
11
17
|
export declare function setOutputFormat(format: OutputFormat): void;
|
package/dist/utils/output.js
CHANGED
|
@@ -19,6 +19,14 @@ export function setRawMode(enabled) {
|
|
|
19
19
|
export function setQuietMode(enabled) {
|
|
20
20
|
quietMode = enabled;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether `--quiet` is active. Lets a command decide to route extra
|
|
24
|
+
* human-facing detail (e.g. a mention-resolution confirmation) to stderr,
|
|
25
|
+
* keeping the single machine-stable stdout line intact.
|
|
26
|
+
*/
|
|
27
|
+
export function getQuietMode() {
|
|
28
|
+
return quietMode;
|
|
29
|
+
}
|
|
22
30
|
export function setJqFilter(filter) {
|
|
23
31
|
jqFilter = filter;
|
|
24
32
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enrichlayer/el-linear",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.0",
|
|
4
4
|
"description": "A pragmatic CLI for Linear.app — deterministic team/label/member resolution, structured issue validation, configurable term enforcement, and a GraphQL escape hatch.",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"types": "dist/main.d.ts",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@inquirer/prompts": "^8.4.2",
|
|
55
55
|
"@linear/sdk": "^86.0.0",
|
|
56
|
-
"commander": "^
|
|
56
|
+
"commander": "^15.0.0",
|
|
57
57
|
"picocolors": "^1.1.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|