@enrichlayer/el-linear 1.9.0 → 1.10.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 +13 -0
- package/dist/commands/attachments.js +2 -1
- package/dist/commands/comments.js +17 -28
- package/dist/commands/cycles.js +2 -1
- package/dist/commands/documents.js +2 -1
- package/dist/commands/graphql.js +4 -6
- package/dist/commands/issue-id.js +1 -3
- package/dist/commands/issues/description.js +2 -6
- package/dist/commands/issues/link-references.d.ts +21 -0
- package/dist/commands/issues/link-references.js +171 -0
- package/dist/commands/issues/relations.d.ts +55 -0
- package/dist/commands/issues/relations.js +132 -0
- package/dist/commands/issues.js +65 -284
- package/dist/commands/labels.js +13 -23
- package/dist/commands/project-milestones.js +13 -20
- package/dist/commands/projects.js +67 -38
- package/dist/commands/refs.js +8 -0
- package/dist/commands/releases.js +26 -30
- package/dist/commands/search.js +21 -30
- package/dist/commands/teams.js +2 -1
- package/dist/commands/templates.js +9 -14
- package/dist/commands/users.js +2 -1
- package/dist/config/resolver.js +2 -3
- package/dist/queries/attachments-types.d.ts +30 -0
- package/dist/queries/attachments-types.js +5 -0
- package/dist/queries/comments-types.d.ts +49 -0
- package/dist/queries/comments-types.js +5 -0
- package/dist/queries/documents-types.d.ts +61 -0
- package/dist/queries/documents-types.js +9 -0
- package/dist/queries/introspect-types.d.ts +57 -0
- package/dist/queries/introspect-types.js +10 -0
- package/dist/queries/issues-types.d.ts +416 -0
- package/dist/queries/issues-types.js +23 -0
- package/dist/queries/issues.d.ts +2 -0
- package/dist/queries/issues.js +22 -0
- package/dist/queries/labels-types.d.ts +64 -0
- package/dist/queries/labels-types.js +5 -0
- package/dist/queries/project-milestones-types.d.ts +91 -0
- package/dist/queries/project-milestones-types.js +10 -0
- package/dist/queries/projects-types.d.ts +75 -0
- package/dist/queries/projects-types.js +5 -0
- package/dist/queries/projects.d.ts +2 -0
- package/dist/queries/projects.js +22 -0
- package/dist/queries/releases-types.d.ts +84 -0
- package/dist/queries/releases-types.js +5 -0
- package/dist/queries/search-types.d.ts +86 -0
- package/dist/queries/search-types.js +6 -0
- package/dist/queries/templates-types.d.ts +61 -0
- package/dist/queries/templates-types.js +9 -0
- package/dist/types/linear.d.ts +8 -2
- package/dist/utils/auto-link-references.js +29 -24
- package/dist/utils/graphql-attachments-service.js +6 -9
- package/dist/utils/graphql-documents-service.js +19 -25
- package/dist/utils/graphql-issues-service.d.ts +12 -2
- package/dist/utils/graphql-issues-service.js +151 -172
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +16 -6
- package/dist/utils/workspace-url.d.ts +5 -1
- package/dist/utils/workspace-url.js +35 -5
- package/package.json +1 -1
package/dist/commands/issues.js
CHANGED
|
@@ -4,22 +4,22 @@ import { enforceValidation, validateIssueCreation, } from "../config/issue-valid
|
|
|
4
4
|
import { resolveAssignee, resolveLabels, resolveMember, resolveTeam, } from "../config/resolver.js";
|
|
5
5
|
import { resolveDefaultStatus } from "../config/status-defaults.js";
|
|
6
6
|
import { enforceTerms } from "../config/term-enforcer.js";
|
|
7
|
-
import {
|
|
8
|
-
import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, ISSUE_RELATION_CREATE_MUTATION, SCAN_ISSUES_QUERY, } from "../queries/issues.js";
|
|
9
|
-
import { autoLinkReferences, } from "../utils/auto-link-references.js";
|
|
7
|
+
import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, } from "../queries/issues.js";
|
|
10
8
|
import { createFileService } from "../utils/file-service.js";
|
|
11
9
|
import { applyFooter } from "../utils/footer.js";
|
|
12
10
|
import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
|
|
13
11
|
import { GraphQLIssuesService, } from "../utils/graphql-issues-service.js";
|
|
14
|
-
import { createGraphQLService
|
|
15
|
-
import { createLinearService
|
|
12
|
+
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
13
|
+
import { createLinearService } from "../utils/linear-service.js";
|
|
16
14
|
import { logger } from "../utils/logger.js";
|
|
17
15
|
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
18
16
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
19
17
|
import { formatCsv, formatMarkdown, formatTable, } from "../utils/table-formatter.js";
|
|
20
|
-
import { parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
|
|
18
|
+
import { parsePositiveInt, parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
|
|
21
19
|
import { gitCheckoutBranch, toBranchName } from "./issues/branch.js";
|
|
22
|
-
import { maybeAutoLink, prepareAutoLinkedDescription,
|
|
20
|
+
import { maybeAutoLink, prepareAutoLinkedDescription, readDescriptionFile, resolveDescription, } from "./issues/description.js";
|
|
21
|
+
import { handleLinkReferencesIssue } from "./issues/link-references.js";
|
|
22
|
+
import { buildIncomingRelationEntries, buildOutgoingRelationEntries, createRelations, } from "./issues/relations.js";
|
|
23
23
|
import { readIssues } from "./read-shortcut.js";
|
|
24
24
|
const IMAGE_EXTENSIONS = new Set([
|
|
25
25
|
".png",
|
|
@@ -35,53 +35,6 @@ function isImageFile(filename) {
|
|
|
35
35
|
const ext = filename.lastIndexOf(".");
|
|
36
36
|
return ext !== -1 && IMAGE_EXTENSIONS.has(filename.slice(ext).toLowerCase());
|
|
37
37
|
}
|
|
38
|
-
function transformIssueRelation(rel) {
|
|
39
|
-
const issue = rel.issue;
|
|
40
|
-
const relatedIssue = rel.relatedIssue;
|
|
41
|
-
return {
|
|
42
|
-
id: rel.id,
|
|
43
|
-
type: rel.type,
|
|
44
|
-
issue: {
|
|
45
|
-
id: issue.id,
|
|
46
|
-
identifier: issue.identifier,
|
|
47
|
-
title: issue.title,
|
|
48
|
-
},
|
|
49
|
-
relatedIssue: {
|
|
50
|
-
id: relatedIssue.id,
|
|
51
|
-
identifier: relatedIssue.identifier,
|
|
52
|
-
title: relatedIssue.title,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
async function createRelations(sourceId, options, graphQLService, linearService) {
|
|
57
|
-
const relations = [];
|
|
58
|
-
const relationSpecs = [
|
|
59
|
-
{ option: "relatedTo", type: "related" },
|
|
60
|
-
{ option: "blocks", type: "blocks" },
|
|
61
|
-
{ option: "blockedBy", type: "blocks", reverse: true },
|
|
62
|
-
{ option: "duplicateOf", type: "duplicate" },
|
|
63
|
-
];
|
|
64
|
-
for (const spec of relationSpecs) {
|
|
65
|
-
const value = options[spec.option];
|
|
66
|
-
if (!value) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
for (const id of splitList(value)) {
|
|
70
|
-
const targetId = await linearService.resolveIssueId(id);
|
|
71
|
-
const rel = await graphQLService.rawRequest(ISSUE_RELATION_CREATE_MUTATION, {
|
|
72
|
-
input: {
|
|
73
|
-
issueId: spec.reverse ? targetId : sourceId,
|
|
74
|
-
relatedIssueId: spec.reverse ? sourceId : targetId,
|
|
75
|
-
type: spec.type,
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
const create = rel.issueRelationCreate;
|
|
79
|
-
const issueRelation = create?.issueRelation;
|
|
80
|
-
relations.push(transformIssueRelation(issueRelation));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return relations;
|
|
84
|
-
}
|
|
85
38
|
function validateUpdateOptions(options) {
|
|
86
39
|
if (options.parentTicket && options.clearParentTicket) {
|
|
87
40
|
throw new Error("Cannot use --parent-ticket and --clear-parent-ticket together");
|
|
@@ -212,7 +165,7 @@ async function handleListIssues(options, command) {
|
|
|
212
165
|
? parsePriorityFilter(options.priority)
|
|
213
166
|
: undefined,
|
|
214
167
|
orderBy: options.sort === "created" ? "createdAt" : "updatedAt",
|
|
215
|
-
limit:
|
|
168
|
+
limit: parsePositiveInt(options.limit, "--limit"),
|
|
216
169
|
};
|
|
217
170
|
const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
|
|
218
171
|
outputIssues(result, options.format, options.fields, {
|
|
@@ -220,7 +173,7 @@ async function handleListIssues(options, command) {
|
|
|
220
173
|
});
|
|
221
174
|
}
|
|
222
175
|
else {
|
|
223
|
-
const result = sortIssues(await issuesService.getIssues(
|
|
176
|
+
const result = sortIssues(await issuesService.getIssues(parsePositiveInt(options.limit, "--limit")), options.sort);
|
|
224
177
|
outputIssues(result, options.format, options.fields, {});
|
|
225
178
|
}
|
|
226
179
|
}
|
|
@@ -245,7 +198,7 @@ async function handleSearchIssues(query, options, command) {
|
|
|
245
198
|
priority: options.priority
|
|
246
199
|
? parsePriorityFilter(options.priority)
|
|
247
200
|
: undefined,
|
|
248
|
-
limit:
|
|
201
|
+
limit: parsePositiveInt(options.limit, "--limit"),
|
|
249
202
|
};
|
|
250
203
|
const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
|
|
251
204
|
outputIssues(result, options.format, options.fields, { query });
|
|
@@ -460,22 +413,19 @@ async function handleHistoryIssue(issueId, _options, command) {
|
|
|
460
413
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
461
414
|
const linearService = await createLinearService(rootOpts);
|
|
462
415
|
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
463
|
-
const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, {
|
|
464
|
-
id: resolvedId,
|
|
465
|
-
});
|
|
416
|
+
const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, { id: resolvedId });
|
|
466
417
|
if (!result.issue) {
|
|
467
418
|
throw new Error(`Issue "${issueId}" not found`);
|
|
468
419
|
}
|
|
469
420
|
const issue = result.issue;
|
|
470
|
-
const
|
|
471
|
-
const spans = (stateHistory?.nodes ?? []).map((span) => ({
|
|
421
|
+
const spans = issue.stateHistory.nodes.map((span) => ({
|
|
472
422
|
state: {
|
|
473
423
|
id: span.state.id,
|
|
474
424
|
name: span.state.name,
|
|
475
425
|
type: span.state.type,
|
|
476
426
|
},
|
|
477
427
|
startedAt: span.startedAt,
|
|
478
|
-
endedAt: span.endedAt
|
|
428
|
+
endedAt: span.endedAt ?? undefined,
|
|
479
429
|
}));
|
|
480
430
|
outputSuccess({
|
|
481
431
|
id: issue.id,
|
|
@@ -499,82 +449,19 @@ async function handleRelateIssue(issueId, options, command) {
|
|
|
499
449
|
meta: { count: relations.length, source: issueId },
|
|
500
450
|
});
|
|
501
451
|
}
|
|
502
|
-
/**
|
|
503
|
-
* Invert relation type for incoming (inverse) relations so the output
|
|
504
|
-
* reads naturally from the perspective of the queried issue.
|
|
505
|
-
* e.g. if DEV-100 "blocks" DEV-200, and we query DEV-200,
|
|
506
|
-
* the inverse relation type is "blocks" but direction is incoming → "blockedBy".
|
|
507
|
-
*/
|
|
508
|
-
function normalizeInverseType(type) {
|
|
509
|
-
if (type === "blocks") {
|
|
510
|
-
return "blockedBy";
|
|
511
|
-
}
|
|
512
|
-
return type;
|
|
513
|
-
}
|
|
514
|
-
function buildRelatedIssueSummary(peer) {
|
|
515
|
-
const state = peer.state;
|
|
516
|
-
const assignee = peer.assignee;
|
|
517
|
-
const team = peer.team;
|
|
518
|
-
return {
|
|
519
|
-
id: peer.id,
|
|
520
|
-
identifier: peer.identifier,
|
|
521
|
-
title: peer.title,
|
|
522
|
-
...(state
|
|
523
|
-
? { state: { id: state.id, name: state.name } }
|
|
524
|
-
: {}),
|
|
525
|
-
...(peer.priority == null ? {} : { priority: peer.priority }),
|
|
526
|
-
...(assignee
|
|
527
|
-
? {
|
|
528
|
-
assignee: {
|
|
529
|
-
id: assignee.id,
|
|
530
|
-
name: assignee.name,
|
|
531
|
-
},
|
|
532
|
-
}
|
|
533
|
-
: {}),
|
|
534
|
-
...(team
|
|
535
|
-
? {
|
|
536
|
-
team: {
|
|
537
|
-
id: team.id,
|
|
538
|
-
key: team.key,
|
|
539
|
-
name: team.name,
|
|
540
|
-
},
|
|
541
|
-
}
|
|
542
|
-
: {}),
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
function buildRelationEntries(nodes, peerKey, direction, normalizeType) {
|
|
546
|
-
const entries = [];
|
|
547
|
-
for (const rel of nodes ?? []) {
|
|
548
|
-
const peer = rel[peerKey];
|
|
549
|
-
if (!peer) {
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
entries.push({
|
|
553
|
-
id: rel.id,
|
|
554
|
-
type: normalizeType(rel.type),
|
|
555
|
-
direction,
|
|
556
|
-
issue: buildRelatedIssueSummary(peer),
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
return entries;
|
|
560
|
-
}
|
|
561
452
|
async function handleRelatedIssues(issueId, _options, command) {
|
|
562
453
|
const rootOpts = getRootOpts(command);
|
|
563
454
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
564
455
|
const linearService = await createLinearService(rootOpts);
|
|
565
456
|
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
566
|
-
const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
|
|
567
|
-
id: resolvedId,
|
|
568
|
-
});
|
|
457
|
+
const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, { id: resolvedId });
|
|
569
458
|
if (!result.issue) {
|
|
570
459
|
throw new Error(`Issue "${issueId}" not found`);
|
|
571
460
|
}
|
|
572
461
|
const issue = result.issue;
|
|
573
|
-
const relations = issue.relations;
|
|
574
|
-
const inverseRelations = issue.inverseRelations;
|
|
575
462
|
const entries = [
|
|
576
|
-
...
|
|
577
|
-
...
|
|
463
|
+
...buildOutgoingRelationEntries(issue.relations.nodes),
|
|
464
|
+
...buildIncomingRelationEntries(issue.inverseRelations.nodes),
|
|
578
465
|
];
|
|
579
466
|
outputSuccess({
|
|
580
467
|
id: issue.id,
|
|
@@ -584,158 +471,6 @@ async function handleRelatedIssues(issueId, _options, command) {
|
|
|
584
471
|
meta: { count: entries.length },
|
|
585
472
|
});
|
|
586
473
|
}
|
|
587
|
-
async function fetchCommentBodies(issueUuid, graphQLService) {
|
|
588
|
-
const result = await graphQLService.rawRequest(LIST_COMMENTS_QUERY, {
|
|
589
|
-
issueId: issueUuid,
|
|
590
|
-
first: 250,
|
|
591
|
-
});
|
|
592
|
-
const issue = result.issue;
|
|
593
|
-
const comments = issue?.comments;
|
|
594
|
-
const nodes = comments?.nodes ?? [];
|
|
595
|
-
return nodes
|
|
596
|
-
.map((c) => c.body)
|
|
597
|
-
.filter((b) => typeof b === "string" && b.length > 0);
|
|
598
|
-
}
|
|
599
|
-
async function linkReferencesForIssue(args) {
|
|
600
|
-
const comments = args.includeComments
|
|
601
|
-
? await fetchCommentBodies(args.issueUuid, args.graphQLService)
|
|
602
|
-
: undefined;
|
|
603
|
-
return autoLinkReferences({
|
|
604
|
-
issueId: args.issueUuid,
|
|
605
|
-
identifier: args.identifier,
|
|
606
|
-
description: args.description,
|
|
607
|
-
comments,
|
|
608
|
-
dryRun: args.dryRun,
|
|
609
|
-
graphQLService: args.graphQLService,
|
|
610
|
-
linearService: args.linearService,
|
|
611
|
-
preResolved: args.preResolved,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
async function handleLinkReferencesSingle(issueId, options, graphQLService, linearService) {
|
|
615
|
-
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
616
|
-
// GET_ISSUE_RELATIONS_QUERY also returns description, so this single call gives us everything
|
|
617
|
-
// we need for the description-only path. Comments are fetched separately on demand.
|
|
618
|
-
const issueResult = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
|
|
619
|
-
id: resolvedId,
|
|
620
|
-
});
|
|
621
|
-
if (!issueResult.issue) {
|
|
622
|
-
throw new Error(`Issue "${issueId}" not found`);
|
|
623
|
-
}
|
|
624
|
-
const issue = issueResult.issue;
|
|
625
|
-
const identifier = issue.identifier;
|
|
626
|
-
const description = issue.description ?? "";
|
|
627
|
-
const dryRun = Boolean(options.dryRun);
|
|
628
|
-
const includeComments = Boolean(options.includeComments);
|
|
629
|
-
const rewriteDescription = Boolean(options.rewriteDescription);
|
|
630
|
-
// If rewriting, validate refs once up front so we can both wrap the description and
|
|
631
|
-
// pass the resolved map down to autoLink — avoids a second resolve round trip.
|
|
632
|
-
const rewrite = rewriteDescription
|
|
633
|
-
? await prepareDescriptionRewrite(description, identifier, linearService, graphQLService)
|
|
634
|
-
: { preResolved: undefined, wrapped: undefined };
|
|
635
|
-
const autoLinked = await linkReferencesForIssue({
|
|
636
|
-
issueUuid: resolvedId,
|
|
637
|
-
identifier,
|
|
638
|
-
description,
|
|
639
|
-
includeComments,
|
|
640
|
-
dryRun,
|
|
641
|
-
graphQLService,
|
|
642
|
-
linearService,
|
|
643
|
-
preResolved: rewrite.preResolved,
|
|
644
|
-
});
|
|
645
|
-
let descriptionRewritten = false;
|
|
646
|
-
if (rewriteDescription && rewrite.wrapped && !dryRun) {
|
|
647
|
-
await pushDescriptionUpdate(resolvedId, rewrite.wrapped, graphQLService);
|
|
648
|
-
descriptionRewritten = true;
|
|
649
|
-
}
|
|
650
|
-
outputSuccess({
|
|
651
|
-
id: resolvedId,
|
|
652
|
-
identifier,
|
|
653
|
-
title: issue.title,
|
|
654
|
-
autoLinked,
|
|
655
|
-
...(rewriteDescription
|
|
656
|
-
? {
|
|
657
|
-
descriptionRewritten,
|
|
658
|
-
...(dryRun && rewrite.wrapped
|
|
659
|
-
? { descriptionPreview: rewrite.wrapped }
|
|
660
|
-
: {}),
|
|
661
|
-
}
|
|
662
|
-
: {}),
|
|
663
|
-
meta: {
|
|
664
|
-
dryRun,
|
|
665
|
-
includeComments,
|
|
666
|
-
...(rewriteDescription ? { rewriteDescription: true } : {}),
|
|
667
|
-
},
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
async function handleLinkReferencesBatch(teamInput, options, graphQLService, linearService) {
|
|
671
|
-
const teamId = resolveTeam(teamInput);
|
|
672
|
-
const limit = options.limit
|
|
673
|
-
? Number.parseInt(options.limit, 10)
|
|
674
|
-
: 100;
|
|
675
|
-
if (!Number.isFinite(limit) || limit <= 0) {
|
|
676
|
-
throw new Error(`--limit must be a positive integer, got: ${options.limit}`);
|
|
677
|
-
}
|
|
678
|
-
const dryRun = Boolean(options.dryRun);
|
|
679
|
-
const includeComments = Boolean(options.includeComments);
|
|
680
|
-
const scan = await graphQLService.rawRequest(SCAN_ISSUES_QUERY, {
|
|
681
|
-
filter: { team: { id: { eq: teamId } } },
|
|
682
|
-
first: limit,
|
|
683
|
-
});
|
|
684
|
-
const issues = scan.issues?.nodes;
|
|
685
|
-
const nodes = issues ?? [];
|
|
686
|
-
const entries = [];
|
|
687
|
-
for (const node of nodes) {
|
|
688
|
-
const issueUuid = node.id;
|
|
689
|
-
const identifier = node.identifier;
|
|
690
|
-
const description = node.description ?? "";
|
|
691
|
-
const autoLinked = await linkReferencesForIssue({
|
|
692
|
-
issueUuid,
|
|
693
|
-
identifier,
|
|
694
|
-
description,
|
|
695
|
-
includeComments,
|
|
696
|
-
dryRun,
|
|
697
|
-
graphQLService,
|
|
698
|
-
linearService,
|
|
699
|
-
});
|
|
700
|
-
entries.push({ identifier, autoLinked });
|
|
701
|
-
}
|
|
702
|
-
// Summary counts across the batch
|
|
703
|
-
const totalLinked = entries.reduce((n, e) => n + e.autoLinked.linked.length, 0);
|
|
704
|
-
const totalSkipped = entries.reduce((n, e) => n + e.autoLinked.skipped.length, 0);
|
|
705
|
-
const totalFailed = entries.reduce((n, e) => n + e.autoLinked.failed.length, 0);
|
|
706
|
-
outputSuccess({
|
|
707
|
-
data: entries,
|
|
708
|
-
meta: {
|
|
709
|
-
count: entries.length,
|
|
710
|
-
team: teamInput,
|
|
711
|
-
dryRun,
|
|
712
|
-
includeComments,
|
|
713
|
-
totals: {
|
|
714
|
-
linked: totalLinked,
|
|
715
|
-
skipped: totalSkipped,
|
|
716
|
-
failed: totalFailed,
|
|
717
|
-
},
|
|
718
|
-
},
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
async function handleLinkReferencesIssue(issueIdOrTeamFlag, options, command) {
|
|
722
|
-
const rootOpts = getRootOpts(command);
|
|
723
|
-
const graphQLService = await createGraphQLService(rootOpts);
|
|
724
|
-
const linearService = await createLinearService(rootOpts);
|
|
725
|
-
if (options.team) {
|
|
726
|
-
if (issueIdOrTeamFlag) {
|
|
727
|
-
throw new Error("Provide either an issueId positional argument OR --team, not both. With --team, omit the issueId.");
|
|
728
|
-
}
|
|
729
|
-
if (options.rewriteDescription) {
|
|
730
|
-
throw new Error("--rewrite-description is single-issue only. Use it case by case (one issueId per invocation) — bulk description rewrites bot-author the whole team's history.");
|
|
731
|
-
}
|
|
732
|
-
return handleLinkReferencesBatch(options.team, options, graphQLService, linearService);
|
|
733
|
-
}
|
|
734
|
-
if (!issueIdOrTeamFlag) {
|
|
735
|
-
throw new Error("issueId is required (or use --team <key> to scan a whole team)");
|
|
736
|
-
}
|
|
737
|
-
return handleLinkReferencesSingle(issueIdOrTeamFlag, options, graphQLService, linearService);
|
|
738
|
-
}
|
|
739
474
|
async function handleUpdateIssue(issueId, options, command) {
|
|
740
475
|
// Normalize aliases
|
|
741
476
|
if (options.state && !options.status) {
|
|
@@ -754,9 +489,10 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
754
489
|
const linearService = await createLinearService(rootOpts);
|
|
755
490
|
if (options.appendDescription) {
|
|
756
491
|
const resolved = await linearService.resolveIssueId(issueId);
|
|
757
|
-
const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", {
|
|
758
|
-
|
|
759
|
-
|
|
492
|
+
const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", {
|
|
493
|
+
id: resolved,
|
|
494
|
+
});
|
|
495
|
+
const existing = current.issue?.description ?? "";
|
|
760
496
|
options.description = `${existing}\n${options.appendDescription}`;
|
|
761
497
|
}
|
|
762
498
|
if (options.title || options.description) {
|
|
@@ -799,6 +535,34 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
799
535
|
: undefined;
|
|
800
536
|
outputSuccess(autoLinked ? { ...result, autoLinked } : result);
|
|
801
537
|
}
|
|
538
|
+
async function handleArchiveIssue(issueId, _options, command) {
|
|
539
|
+
const rootOpts = getRootOpts(command);
|
|
540
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
541
|
+
const linearService = await createLinearService(rootOpts);
|
|
542
|
+
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
543
|
+
const result = await issuesService.archiveIssue(issueId);
|
|
544
|
+
outputSuccess({
|
|
545
|
+
success: true,
|
|
546
|
+
archived: true,
|
|
547
|
+
...result,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
async function handleDeleteIssue(issueId, options, command) {
|
|
551
|
+
const rootOpts = getRootOpts(command);
|
|
552
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
553
|
+
const linearService = await createLinearService(rootOpts);
|
|
554
|
+
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
555
|
+
const permanentlyDelete = Boolean(options.permanentlyDelete || options.hard);
|
|
556
|
+
const result = await issuesService.deleteIssue(issueId, {
|
|
557
|
+
permanentlyDelete,
|
|
558
|
+
});
|
|
559
|
+
outputSuccess({
|
|
560
|
+
success: true,
|
|
561
|
+
deleted: true,
|
|
562
|
+
permanentlyDeleted: permanentlyDelete,
|
|
563
|
+
...result,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
802
566
|
async function handleRetrolink(options, command) {
|
|
803
567
|
// 1. Read current branch
|
|
804
568
|
let currentBranch;
|
|
@@ -1000,6 +764,23 @@ export function setupIssuesCommands(program) {
|
|
|
1000
764
|
.option("--strict", "strict brand validation")
|
|
1001
765
|
.option("--no-auto-link", "skip auto-linking issue references found in the description")
|
|
1002
766
|
.action(handleAsyncCommand(handleUpdateIssue));
|
|
767
|
+
issues
|
|
768
|
+
.command("archive <issueId>")
|
|
769
|
+
.description("Archive an issue.")
|
|
770
|
+
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
|
|
771
|
+
.action(handleAsyncCommand(handleArchiveIssue));
|
|
772
|
+
issues
|
|
773
|
+
.command("delete <issueId>")
|
|
774
|
+
.description("Delete (trash) an issue.")
|
|
775
|
+
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported. Use --permanently-delete only when you intend to skip Linear's grace period; Linear requires admin permissions.")
|
|
776
|
+
.option("--permanently-delete", "skip Linear's 30-day grace period (admin only)")
|
|
777
|
+
.option("--hard", "alias for --permanently-delete")
|
|
778
|
+
.action(handleAsyncCommand(handleDeleteIssue));
|
|
779
|
+
issues
|
|
780
|
+
.command("hard-delete <issueId>")
|
|
781
|
+
.description("Permanently delete an issue immediately (admin only).")
|
|
782
|
+
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
|
|
783
|
+
.action(handleAsyncCommand((issueId, options, command) => handleDeleteIssue(issueId, { ...options, permanentlyDelete: true }, command)));
|
|
1003
784
|
issues
|
|
1004
785
|
.command("history <issueId>")
|
|
1005
786
|
.description("Show issue state transition history (time in each status).")
|
package/dist/commands/labels.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createGraphQLService } from "../utils/graphql-service.js";
|
|
|
6
6
|
import { createLinearService } from "../utils/linear-service.js";
|
|
7
7
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
8
8
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
9
|
-
import { validateHexColor } from "../utils/validators.js";
|
|
9
|
+
import { parsePositiveInt, validateHexColor } from "../utils/validators.js";
|
|
10
10
|
async function handleCreateLabel(name, options, command) {
|
|
11
11
|
const rootOpts = getRootOpts(command);
|
|
12
12
|
const teamId = resolveTeam(options.team);
|
|
@@ -23,45 +23,35 @@ async function handleCreateLabel(name, options, command) {
|
|
|
23
23
|
name: options.parent,
|
|
24
24
|
teamId,
|
|
25
25
|
});
|
|
26
|
-
const
|
|
27
|
-
const nodes = issueLabels?.nodes;
|
|
28
|
-
const parentLabel = nodes?.[0];
|
|
26
|
+
const parentLabel = parentResult.issueLabels.nodes[0];
|
|
29
27
|
if (parentLabel) {
|
|
30
28
|
input.parentId = parentLabel.id;
|
|
31
29
|
}
|
|
32
30
|
}
|
|
33
|
-
const result = await graphQLService.rawRequest(CREATE_LABEL_MUTATION, {
|
|
34
|
-
|
|
35
|
-
});
|
|
36
|
-
const createResult = result.issueLabelCreate;
|
|
37
|
-
if (!createResult?.success) {
|
|
31
|
+
const result = await graphQLService.rawRequest(CREATE_LABEL_MUTATION, { input });
|
|
32
|
+
if (!result.issueLabelCreate.success || !result.issueLabelCreate.issueLabel) {
|
|
38
33
|
throw new Error(`Failed to create label "${name}"`);
|
|
39
34
|
}
|
|
40
|
-
outputSuccess(
|
|
35
|
+
outputSuccess(result.issueLabelCreate.issueLabel);
|
|
41
36
|
}
|
|
42
37
|
async function handleRetireLabel(labelId, _options, command) {
|
|
43
38
|
const rootOpts = getRootOpts(command);
|
|
44
39
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
45
|
-
const result = await graphQLService.rawRequest(RETIRE_LABEL_MUTATION, {
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
const retireResult = result.issueLabelRetire;
|
|
49
|
-
if (!retireResult?.success) {
|
|
40
|
+
const result = await graphQLService.rawRequest(RETIRE_LABEL_MUTATION, { id: labelId });
|
|
41
|
+
if (!result.issueLabelRetire.success || !result.issueLabelRetire.issueLabel) {
|
|
50
42
|
throw new Error(`Failed to retire label "${labelId}"`);
|
|
51
43
|
}
|
|
52
|
-
outputSuccess(
|
|
44
|
+
outputSuccess(result.issueLabelRetire.issueLabel);
|
|
53
45
|
}
|
|
54
46
|
async function handleRestoreLabel(labelId, _options, command) {
|
|
55
47
|
const rootOpts = getRootOpts(command);
|
|
56
48
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
57
|
-
const result = await graphQLService.rawRequest(RESTORE_LABEL_MUTATION, {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const restoreResult = result.issueLabelRestore;
|
|
61
|
-
if (!restoreResult?.success) {
|
|
49
|
+
const result = await graphQLService.rawRequest(RESTORE_LABEL_MUTATION, { id: labelId });
|
|
50
|
+
if (!result.issueLabelRestore.success ||
|
|
51
|
+
!result.issueLabelRestore.issueLabel) {
|
|
62
52
|
throw new Error(`Failed to restore label "${labelId}"`);
|
|
63
53
|
}
|
|
64
|
-
outputSuccess(
|
|
54
|
+
outputSuccess(result.issueLabelRestore.issueLabel);
|
|
65
55
|
}
|
|
66
56
|
export function setupLabelsCommands(program) {
|
|
67
57
|
const labels = program
|
|
@@ -79,7 +69,7 @@ export function setupLabelsCommands(program) {
|
|
|
79
69
|
const rootOpts = getRootOpts(command);
|
|
80
70
|
const teamFilter = options.team ? resolveTeam(options.team) : undefined;
|
|
81
71
|
const nameFilter = options.name;
|
|
82
|
-
const limit =
|
|
72
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
83
73
|
const ttl = resolveCacheTTL({
|
|
84
74
|
configTTL: loadConfig().cacheTTLSeconds,
|
|
85
75
|
noCacheFlag: rootOpts.cache === false,
|
|
@@ -5,7 +5,7 @@ import { createLinearService, } from "../utils/linear-service.js";
|
|
|
5
5
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
6
6
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
7
7
|
import { isUuid } from "../utils/uuid.js";
|
|
8
|
-
import { validateIsoDate } from "../utils/validators.js";
|
|
8
|
+
import { parsePositiveInt, validateIsoDate } from "../utils/validators.js";
|
|
9
9
|
async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearService, projectNameOrId) {
|
|
10
10
|
if (isUuid(milestoneNameOrId)) {
|
|
11
11
|
return milestoneNameOrId;
|
|
@@ -17,22 +17,17 @@ async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearServi
|
|
|
17
17
|
name: milestoneNameOrId,
|
|
18
18
|
projectId,
|
|
19
19
|
});
|
|
20
|
-
|
|
21
|
-
const milestonesData = projectData?.projectMilestones;
|
|
22
|
-
nodes = milestonesData?.nodes || [];
|
|
20
|
+
nodes = findRes.project?.projectMilestones.nodes ?? [];
|
|
23
21
|
}
|
|
24
22
|
if (nodes.length === 0) {
|
|
25
|
-
const globalRes = await graphQLService.rawRequest(FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, {
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
const globalMilestones = globalRes.projectMilestones;
|
|
29
|
-
nodes = globalMilestones?.nodes || [];
|
|
23
|
+
const globalRes = await graphQLService.rawRequest(FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, { name: milestoneNameOrId });
|
|
24
|
+
nodes = globalRes.projectMilestones.nodes;
|
|
30
25
|
}
|
|
31
26
|
if (nodes.length === 0) {
|
|
32
27
|
throw notFoundError("Milestone", milestoneNameOrId);
|
|
33
28
|
}
|
|
34
29
|
if (nodes.length > 1) {
|
|
35
|
-
const matches = nodes.map((m) => `"${m.name}" in project "${m.project
|
|
30
|
+
const matches = nodes.map((m) => `"${m.name}" in project "${m.project.name}"`);
|
|
36
31
|
throw multipleMatchesError("milestone", milestoneNameOrId, matches, "specify --project or use the milestone ID");
|
|
37
32
|
}
|
|
38
33
|
return nodes[0].id;
|
|
@@ -44,11 +39,9 @@ async function handleListMilestones(options, command) {
|
|
|
44
39
|
const projectId = await linearService.resolveProjectId(options.project);
|
|
45
40
|
const result = await graphQLService.rawRequest(LIST_PROJECT_MILESTONES_QUERY, {
|
|
46
41
|
projectId,
|
|
47
|
-
first:
|
|
42
|
+
first: parsePositiveInt(options.limit || "50", "--limit"),
|
|
48
43
|
});
|
|
49
|
-
const
|
|
50
|
-
const milestonesData = projectData?.projectMilestones;
|
|
51
|
-
const nodes = milestonesData?.nodes || [];
|
|
44
|
+
const nodes = result.project?.projectMilestones.nodes ?? [];
|
|
52
45
|
outputSuccess({ data: nodes, meta: { count: nodes.length } });
|
|
53
46
|
}
|
|
54
47
|
async function handleReadMilestone(milestoneIdOrName, options, command) {
|
|
@@ -76,11 +69,11 @@ async function handleCreateMilestone(name, options, command) {
|
|
|
76
69
|
description: options.description,
|
|
77
70
|
targetDate: options.targetDate,
|
|
78
71
|
});
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
if (!result.projectMilestoneCreate.success ||
|
|
73
|
+
!result.projectMilestoneCreate.projectMilestone) {
|
|
81
74
|
throw new Error("Failed to create project milestone");
|
|
82
75
|
}
|
|
83
|
-
outputSuccess(
|
|
76
|
+
outputSuccess(result.projectMilestoneCreate.projectMilestone);
|
|
84
77
|
}
|
|
85
78
|
async function handleUpdateMilestone(milestoneIdOrName, options, command) {
|
|
86
79
|
const rootOpts = getRootOpts(command);
|
|
@@ -102,11 +95,11 @@ async function handleUpdateMilestone(milestoneIdOrName, options, command) {
|
|
|
102
95
|
updateVars.sortOrder = Number.parseFloat(options.sortOrder);
|
|
103
96
|
}
|
|
104
97
|
const result = await graphQLService.rawRequest(UPDATE_PROJECT_MILESTONE_MUTATION, updateVars);
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
if (!result.projectMilestoneUpdate.success ||
|
|
99
|
+
!result.projectMilestoneUpdate.projectMilestone) {
|
|
107
100
|
throw new Error("Failed to update project milestone");
|
|
108
101
|
}
|
|
109
|
-
outputSuccess(
|
|
102
|
+
outputSuccess(result.projectMilestoneUpdate.projectMilestone);
|
|
110
103
|
}
|
|
111
104
|
export function setupProjectMilestonesCommands(program) {
|
|
112
105
|
const projectMilestones = program
|