@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.
Files changed (60) hide show
  1. package/README.md +13 -0
  2. package/dist/commands/attachments.js +2 -1
  3. package/dist/commands/comments.js +17 -28
  4. package/dist/commands/cycles.js +2 -1
  5. package/dist/commands/documents.js +2 -1
  6. package/dist/commands/graphql.js +4 -6
  7. package/dist/commands/issue-id.js +1 -3
  8. package/dist/commands/issues/description.js +2 -6
  9. package/dist/commands/issues/link-references.d.ts +21 -0
  10. package/dist/commands/issues/link-references.js +171 -0
  11. package/dist/commands/issues/relations.d.ts +55 -0
  12. package/dist/commands/issues/relations.js +132 -0
  13. package/dist/commands/issues.js +65 -284
  14. package/dist/commands/labels.js +13 -23
  15. package/dist/commands/project-milestones.js +13 -20
  16. package/dist/commands/projects.js +67 -38
  17. package/dist/commands/refs.js +8 -0
  18. package/dist/commands/releases.js +26 -30
  19. package/dist/commands/search.js +21 -30
  20. package/dist/commands/teams.js +2 -1
  21. package/dist/commands/templates.js +9 -14
  22. package/dist/commands/users.js +2 -1
  23. package/dist/config/resolver.js +2 -3
  24. package/dist/queries/attachments-types.d.ts +30 -0
  25. package/dist/queries/attachments-types.js +5 -0
  26. package/dist/queries/comments-types.d.ts +49 -0
  27. package/dist/queries/comments-types.js +5 -0
  28. package/dist/queries/documents-types.d.ts +61 -0
  29. package/dist/queries/documents-types.js +9 -0
  30. package/dist/queries/introspect-types.d.ts +57 -0
  31. package/dist/queries/introspect-types.js +10 -0
  32. package/dist/queries/issues-types.d.ts +416 -0
  33. package/dist/queries/issues-types.js +23 -0
  34. package/dist/queries/issues.d.ts +2 -0
  35. package/dist/queries/issues.js +22 -0
  36. package/dist/queries/labels-types.d.ts +64 -0
  37. package/dist/queries/labels-types.js +5 -0
  38. package/dist/queries/project-milestones-types.d.ts +91 -0
  39. package/dist/queries/project-milestones-types.js +10 -0
  40. package/dist/queries/projects-types.d.ts +75 -0
  41. package/dist/queries/projects-types.js +5 -0
  42. package/dist/queries/projects.d.ts +2 -0
  43. package/dist/queries/projects.js +22 -0
  44. package/dist/queries/releases-types.d.ts +84 -0
  45. package/dist/queries/releases-types.js +5 -0
  46. package/dist/queries/search-types.d.ts +86 -0
  47. package/dist/queries/search-types.js +6 -0
  48. package/dist/queries/templates-types.d.ts +61 -0
  49. package/dist/queries/templates-types.js +9 -0
  50. package/dist/types/linear.d.ts +8 -2
  51. package/dist/utils/auto-link-references.js +29 -24
  52. package/dist/utils/graphql-attachments-service.js +6 -9
  53. package/dist/utils/graphql-documents-service.js +19 -25
  54. package/dist/utils/graphql-issues-service.d.ts +12 -2
  55. package/dist/utils/graphql-issues-service.js +151 -172
  56. package/dist/utils/output.d.ts +2 -0
  57. package/dist/utils/output.js +16 -6
  58. package/dist/utils/workspace-url.d.ts +5 -1
  59. package/dist/utils/workspace-url.js +35 -5
  60. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { resolveUserDisplayName } from "../config/resolver.js";
2
- import { BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
2
+ import { ARCHIVE_ISSUE_MUTATION, BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, DELETE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
3
3
  import { CREATE_LABEL_MUTATION } from "../queries/labels.js";
4
4
  import { toISOStringOrNow } from "./date-format.js";
5
5
  import { extractEmbeds } from "./embed-parser.js";
@@ -14,7 +14,7 @@ function extractSummaryText(summary) {
14
14
  }
15
15
  const parts = [];
16
16
  const walk = (node) => {
17
- if (node.text) {
17
+ if (typeof node.text === "string") {
18
18
  parts.push(node.text);
19
19
  }
20
20
  if (Array.isArray(node.content)) {
@@ -34,15 +34,12 @@ export class GraphQLIssuesService {
34
34
  this.linearService = linearService;
35
35
  }
36
36
  async getIssues(limit = 25) {
37
- const result = await this.graphQLService.rawRequest(GET_ISSUES_QUERY, {
38
- first: limit,
39
- orderBy: "updatedAt",
40
- });
41
- const issues = result.issues;
42
- if (!issues?.nodes) {
37
+ const result = await this.graphQLService.rawRequest(GET_ISSUES_QUERY, { first: limit, orderBy: "updatedAt" });
38
+ const nodes = result.issues?.nodes;
39
+ if (!nodes?.length) {
43
40
  return [];
44
41
  }
45
- return issues.nodes.map((issue) => this.transformIssueData(issue));
42
+ return nodes.map((issue) => this.transformIssueData(issue));
46
43
  }
47
44
  async getIssueById(issueId) {
48
45
  let issueData;
@@ -55,12 +52,8 @@ export class GraphQLIssuesService {
55
52
  }
56
53
  else {
57
54
  const { teamKey, issueNumber } = parseIssueIdentifier(issueId);
58
- const result = await this.graphQLService.rawRequest(GET_ISSUE_BY_IDENTIFIER_QUERY, {
59
- teamKey,
60
- number: issueNumber,
61
- });
62
- const issues = result.issues;
63
- const nodes = issues?.nodes;
55
+ const result = await this.graphQLService.rawRequest(GET_ISSUE_BY_IDENTIFIER_QUERY, { teamKey, number: issueNumber });
56
+ const nodes = result.issues?.nodes;
64
57
  if (!nodes?.length) {
65
58
  throw notFoundError("Issue", issueId);
66
59
  }
@@ -97,15 +90,40 @@ export class GraphQLIssuesService {
97
90
  });
98
91
  return this.executeUpdateMutation(resolvedIssueId, args.id, updateInput);
99
92
  }
93
+ async archiveIssue(issueId) {
94
+ const resolvedIssueId = await this.linearService.resolveIssueId(issueId);
95
+ const result = await this.graphQLService.rawRequest(ARCHIVE_ISSUE_MUTATION, { id: resolvedIssueId });
96
+ const payload = result.issueArchive;
97
+ if (!payload.success) {
98
+ throw new Error(`Failed to archive issue ${issueId}`);
99
+ }
100
+ return {
101
+ id: resolvedIssueId,
102
+ entity: payload.entity ?? undefined,
103
+ lastSyncId: payload.lastSyncId,
104
+ };
105
+ }
106
+ async deleteIssue(issueId, options = {}) {
107
+ const resolvedIssueId = await this.linearService.resolveIssueId(issueId);
108
+ const result = await this.graphQLService.rawRequest(DELETE_ISSUE_MUTATION, {
109
+ id: resolvedIssueId,
110
+ permanentlyDelete: Boolean(options.permanentlyDelete),
111
+ });
112
+ const payload = result.issueDelete;
113
+ if (!payload.success) {
114
+ throw new Error(`Failed to delete issue ${issueId}`);
115
+ }
116
+ return {
117
+ id: resolvedIssueId,
118
+ entity: payload.entity ?? undefined,
119
+ lastSyncId: payload.lastSyncId,
120
+ };
121
+ }
100
122
  extractMilestoneNodes(args, resolveResult) {
101
- const projectNodes = resolveResult.projects?.nodes;
102
123
  const projectMilestoneNodes = args.projectId
103
- ? projectNodes?.[0]?.projectMilestones?.nodes
124
+ ? resolveResult.projects?.nodes?.[0]?.projectMilestones?.nodes
104
125
  : undefined;
105
- const issueNodes = resolveResult.issues
106
- ?.nodes;
107
- const issueProjectMilestoneNodes = issueNodes?.[0]?.project
108
- ?.projectMilestones?.nodes;
126
+ const issueProjectMilestoneNodes = resolveResult.issues?.nodes?.[0]?.project?.projectMilestones.nodes;
109
127
  return { projectMilestoneNodes, issueProjectMilestoneNodes };
110
128
  }
111
129
  async executeUpdateMutation(resolvedIssueId, originalId, updateInput) {
@@ -140,17 +158,17 @@ export class GraphQLIssuesService {
140
158
  let resolveResult = {};
141
159
  if (Object.keys(resolveVariables).length > 0) {
142
160
  const { __labelNames, ...queryVars } = resolveVariables;
143
- const requests = [
144
- this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_CREATE_QUERY, queryVars),
145
- ];
161
+ const batchPromise = this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_CREATE_QUERY, queryVars);
146
162
  if (__labelNames) {
147
163
  const labelQuery = buildResolveLabelsByNameQuery(__labelNames);
148
- requests.push(this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables));
164
+ const [batch, labels] = await Promise.all([
165
+ batchPromise,
166
+ this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables),
167
+ ]);
168
+ resolveResult = { ...batch, labels: labels.labels };
149
169
  }
150
- const results = await Promise.all(requests);
151
- resolveResult = results[0];
152
- if (results[1]) {
153
- resolveResult.labels = results[1].labels;
170
+ else {
171
+ resolveResult = await batchPromise;
154
172
  }
155
173
  }
156
174
  const resolved = await this.resolveCreateFields(args, resolveResult);
@@ -231,7 +249,8 @@ export class GraphQLIssuesService {
231
249
  }
232
250
  let resolveResult = {};
233
251
  if (Object.keys(resolveVariables).length > 0) {
234
- resolveResult = await this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_SEARCH_QUERY, resolveVariables);
252
+ resolveResult =
253
+ await this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_SEARCH_QUERY, resolveVariables);
235
254
  }
236
255
  const finalTeamId = args.teamId
237
256
  ? await this.resolveTeamId(args.teamId, resolveResult)
@@ -246,11 +265,11 @@ export class GraphQLIssuesService {
246
265
  term: args.query,
247
266
  first: limit,
248
267
  });
249
- const searchIssues = searchResult.searchIssues;
250
- if (!searchIssues?.nodes) {
268
+ const nodes = searchResult.searchIssues?.nodes;
269
+ if (!nodes?.length) {
251
270
  return [];
252
271
  }
253
- const results = searchIssues.nodes.map((issue) => this.transformIssueData(issue));
272
+ const results = nodes.map((issue) => this.transformIssueData(issue));
254
273
  return this.applySearchFilters(results, {
255
274
  teamId: finalTeamId,
256
275
  assigneeId: finalAssigneeId,
@@ -285,8 +304,7 @@ export class GraphQLIssuesService {
285
304
  if (isUuid(teamId)) {
286
305
  return teamId;
287
306
  }
288
- const teams = resolveResult.teams;
289
- const teamNodes = teams?.nodes;
307
+ const teamNodes = resolveResult.teams?.nodes;
290
308
  const resolvedTeam = teamNodes?.[0];
291
309
  if (resolvedTeam &&
292
310
  (resolvedTeam.key.toUpperCase() === teamId.toUpperCase() ||
@@ -300,8 +318,7 @@ export class GraphQLIssuesService {
300
318
  if (isUuid(projectId)) {
301
319
  return projectId;
302
320
  }
303
- const projects = resolveResult.projects;
304
- const projectNodes = projects?.nodes;
321
+ const projectNodes = resolveResult.projects?.nodes;
305
322
  if (!projectNodes?.length) {
306
323
  throw notFoundError("Project", projectId);
307
324
  }
@@ -312,13 +329,11 @@ export class GraphQLIssuesService {
312
329
  * If not, auto-switch when the project belongs to exactly one team.
313
330
  */
314
331
  validateProjectTeam(teamId, projectInput, resolveResult) {
315
- const projects = resolveResult.projects;
316
- const projectNode = projects?.nodes?.[0];
332
+ const projectNode = resolveResult.projects?.nodes?.[0];
317
333
  if (!projectNode) {
318
334
  return teamId;
319
335
  }
320
- const teamsConn = projectNode.teams;
321
- const teamNodes = teamsConn?.nodes;
336
+ const teamNodes = projectNode.teams?.nodes;
322
337
  if (!teamNodes?.length) {
323
338
  return teamId;
324
339
  }
@@ -346,8 +361,7 @@ export class GraphQLIssuesService {
346
361
  return labelIds;
347
362
  }
348
363
  const resolvedLabels = [];
349
- const labels = resolveResult.labels;
350
- const labelNodes = labels?.nodes;
364
+ const labelNodes = resolveResult.labels?.nodes;
351
365
  for (const labelIdOrName of labelIds) {
352
366
  if (isUuid(labelIdOrName)) {
353
367
  resolvedLabels.push(labelIdOrName);
@@ -357,10 +371,7 @@ export class GraphQLIssuesService {
357
371
  const candidates = labelNodes?.filter((l) => l.name.toLowerCase() === lowerName);
358
372
  let label;
359
373
  if (candidates && candidates.length > 1 && teamId) {
360
- label = candidates.find((l) => {
361
- const team = l.team;
362
- return team?.id === teamId;
363
- });
374
+ label = candidates.find((l) => l.team?.id === teamId);
364
375
  }
365
376
  if (!label) {
366
377
  label = candidates?.[0];
@@ -402,12 +413,10 @@ export class GraphQLIssuesService {
402
413
  }
403
414
  const result = await this.graphQLService.rawRequest(CREATE_LABEL_MUTATION, { input });
404
415
  const created = result.issueLabelCreate;
405
- if (created?.success && created.issueLabel) {
406
- const labelData = created.issueLabel;
407
- const teamInfo = labelData.team;
408
- const teamKey = teamInfo?.key ?? "";
416
+ if (created.success && created.issueLabel) {
417
+ const teamKey = created.issueLabel.team?.key ?? "";
409
418
  logger.error(`Auto-created label "${name}" on team ${teamKey}`);
410
- return labelData.id;
419
+ return created.issueLabel.id;
411
420
  }
412
421
  return null;
413
422
  }
@@ -435,37 +444,27 @@ export class GraphQLIssuesService {
435
444
  // Only fall back to first team if there's exactly one (unambiguous).
436
445
  // With multiple teams, returning an arbitrary one produces misleading error hints.
437
446
  const topLevelNodes = resolveResult.teams?.nodes;
438
- return topLevelNodes?.length === 1
439
- ? topLevelNodes[0].key
440
- : undefined;
447
+ return topLevelNodes?.length === 1 ? topLevelNodes[0].key : undefined;
441
448
  }
442
449
  collectTeamNodes(resolveResult) {
443
450
  const nodes = [];
444
451
  const seen = new Set();
445
- const topTeams = resolveResult.teams;
446
- const topNodes = topTeams?.nodes;
452
+ const topNodes = resolveResult.teams?.nodes;
447
453
  if (topNodes) {
448
454
  for (const t of topNodes) {
449
- const id = t.id;
450
- if (!seen.has(id)) {
451
- seen.add(id);
455
+ if (!seen.has(t.id)) {
456
+ seen.add(t.id);
452
457
  nodes.push(t);
453
458
  }
454
459
  }
455
460
  }
456
461
  // Also check project teams (which are always fetched when --project is used)
457
- const projects = resolveResult.projects;
458
- const projectNode = projects?.nodes?.[0];
459
- if (projectNode) {
460
- const projectTeams = projectNode.teams;
461
- const projectTeamNodes = projectTeams?.nodes;
462
- if (projectTeamNodes) {
463
- for (const t of projectTeamNodes) {
464
- const id = t.id;
465
- if (!seen.has(id)) {
466
- seen.add(id);
467
- nodes.push(t);
468
- }
462
+ const projectTeamNodes = resolveResult.projects?.nodes?.[0]?.teams?.nodes;
463
+ if (projectTeamNodes) {
464
+ for (const t of projectTeamNodes) {
465
+ if (!seen.has(t.id)) {
466
+ seen.add(t.id);
467
+ nodes.push(t);
469
468
  }
470
469
  }
471
470
  }
@@ -488,8 +487,7 @@ export class GraphQLIssuesService {
488
487
  resolved = issueMilestone.id;
489
488
  }
490
489
  }
491
- const milestones = resolveResult.milestones;
492
- const milestoneNodes = milestones?.nodes;
490
+ const milestoneNodes = resolveResult.milestones?.nodes;
493
491
  if (!isUuid(resolved) && milestoneNodes?.length) {
494
492
  resolved = milestoneNodes[0].id;
495
493
  }
@@ -503,19 +501,14 @@ export class GraphQLIssuesService {
503
501
  return milestoneId;
504
502
  }
505
503
  let resolved;
506
- const createProjects = resolveResult.projects;
507
- const createProjectNodes = createProjects?.nodes;
508
- const createProjectMilestones = createProjectNodes?.[0]
509
- ?.projectMilestones;
510
- const createProjectMilestoneNodes = createProjectMilestones?.nodes;
504
+ const createProjectMilestoneNodes = resolveResult.projects?.nodes?.[0]?.projectMilestones?.nodes;
511
505
  if (createProjectMilestoneNodes) {
512
506
  const projectMilestone = createProjectMilestoneNodes.find((m) => m.name === milestoneId);
513
507
  if (projectMilestone) {
514
508
  resolved = projectMilestone.id;
515
509
  }
516
510
  }
517
- const createMilestones = resolveResult.milestones;
518
- const createMilestoneNodes = createMilestones?.nodes;
511
+ const createMilestoneNodes = resolveResult.milestones?.nodes;
519
512
  if (!resolved && createMilestoneNodes?.length) {
520
513
  resolved = createMilestoneNodes[0].id;
521
514
  }
@@ -528,12 +521,8 @@ export class GraphQLIssuesService {
528
521
  return resolved;
529
522
  }
530
523
  async fetchIssueTeamId(issueId) {
531
- const result = await this.graphQLService.rawRequest(GET_ISSUE_TEAM_QUERY, {
532
- issueId,
533
- });
534
- const fetchedIssue = result.issue;
535
- const fetchedTeam = fetchedIssue?.team;
536
- return fetchedTeam?.id;
524
+ const result = await this.graphQLService.rawRequest(GET_ISSUE_TEAM_QUERY, { issueId });
525
+ return result.issue?.team?.id;
537
526
  }
538
527
  buildCreateInput(args, resolved) {
539
528
  const input = {};
@@ -704,8 +693,7 @@ export class GraphQLIssuesService {
704
693
  if (!assigneeId || isUuid(assigneeId) || !assigneeId.includes("@")) {
705
694
  return assigneeId;
706
695
  }
707
- const users = resolveResult.users;
708
- const userNodes = users?.nodes;
696
+ const userNodes = resolveResult.users?.nodes;
709
697
  if (!userNodes?.length) {
710
698
  throw notFoundError("User", assigneeId);
711
699
  }
@@ -734,29 +722,27 @@ export class GraphQLIssuesService {
734
722
  resolveVariables.milestoneName = args.milestoneId;
735
723
  }
736
724
  const { __labelNames, ...updateQueryVars } = resolveVariables;
737
- const requests = [
738
- this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_UPDATE_QUERY, updateQueryVars),
739
- ];
725
+ const batchPromise = this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_UPDATE_QUERY, updateQueryVars);
726
+ let resolveResult;
740
727
  if (__labelNames) {
741
728
  const labelQuery = buildResolveLabelsByNameQuery(__labelNames);
742
- requests.push(this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables));
729
+ const [batch, labels] = await Promise.all([
730
+ batchPromise,
731
+ this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables),
732
+ ]);
733
+ resolveResult = { ...batch, labels: labels.labels };
743
734
  }
744
- const results = await Promise.all(requests);
745
- const resolveResult = results[0];
746
- if (results[1]) {
747
- resolveResult.labels = results[1].labels;
735
+ else {
736
+ resolveResult = await batchPromise;
748
737
  }
749
738
  if (!isUuid(args.id)) {
750
- const resolvedIssues = resolveResult.issues;
751
- const resolvedIssueNodes = resolvedIssues.nodes;
739
+ const resolvedIssueNodes = resolveResult.issues?.nodes ?? [];
752
740
  if (!resolvedIssueNodes.length) {
753
741
  throw notFoundError("Issue", args.id);
754
742
  }
755
743
  resolvedIssueId = resolvedIssueNodes[0].id;
756
- const team = resolvedIssueNodes[0].team;
757
- issueTeamId = team?.id;
758
- const issueLabels = resolvedIssueNodes[0].labels;
759
- currentIssueLabels = issueLabels.nodes.map((l) => l.id);
744
+ issueTeamId = resolvedIssueNodes[0].team?.id;
745
+ currentIssueLabels = resolvedIssueNodes[0].labels.nodes.map((l) => l.id);
760
746
  }
761
747
  return { resolvedIssueId, issueTeamId, currentIssueLabels, resolveResult };
762
748
  }
@@ -813,30 +799,32 @@ export class GraphQLIssuesService {
813
799
  if (!parentId || isUuid(parentId)) {
814
800
  return parentId;
815
801
  }
816
- const parentIssues = resolveResult.parentIssues;
817
- const parentNodes = parentIssues?.nodes;
802
+ const parentNodes = resolveResult.parentIssues?.nodes;
818
803
  if (!parentNodes?.length) {
819
804
  throw notFoundError("Parent issue", parentId);
820
805
  }
821
806
  return parentNodes[0].id;
822
807
  }
823
808
  transformIssueData(issue) {
824
- const labels = issue.labels;
809
+ // `labels`/`children`/`comments` are connection fields that the
810
+ // fragment always selects, so production responses populate them.
811
+ // Test mocks pass partial objects, so read defensively to keep
812
+ // those test fixtures small.
825
813
  return {
826
814
  ...this.transformIssueCoreFields(issue),
827
815
  ...this.transformIssueRelations(issue),
828
- summary: issue.summary
829
- ? extractSummaryText(issue.summary)
830
- : undefined,
816
+ summary: issue.summary ? extractSummaryText(issue.summary) : undefined,
831
817
  priority: issue.priority,
832
- estimate: issue.estimate || undefined,
833
- dueDate: issue.dueDate || undefined,
834
- labels: labels.nodes.map((label) => ({
818
+ estimate: issue.estimate ?? undefined,
819
+ dueDate: issue.dueDate ?? undefined,
820
+ labels: (issue.labels?.nodes ?? []).map((label) => ({
835
821
  id: label.id,
836
822
  name: label.name,
837
823
  })),
838
824
  ...this.transformIssueHierarchy(issue),
839
- comments: this.transformIssueComments(issue),
825
+ comments: "comments" in issue && issue.comments
826
+ ? this.transformIssueComments(issue)
827
+ : [],
840
828
  createdAt: toISOStringOrNow(issue.createdAt),
841
829
  updatedAt: toISOStringOrNow(issue.updatedAt),
842
830
  };
@@ -847,93 +835,84 @@ export class GraphQLIssuesService {
847
835
  identifier: issue.identifier,
848
836
  url: issue.url,
849
837
  title: issue.title,
850
- description: issue.description || undefined,
838
+ description: issue.description ?? undefined,
851
839
  branchName: issue.branchName || undefined,
852
- embeds: issue.description
853
- ? extractEmbeds(issue.description)
854
- : undefined,
840
+ embeds: issue.description ? extractEmbeds(issue.description) : undefined,
855
841
  };
856
842
  }
857
843
  transformIssueRelations(issue) {
858
- const state = issue.state;
859
- const assignee = issue.assignee;
860
- const team = issue.team;
861
- const project = issue.project;
862
- const cycle = issue.cycle;
863
- const milestone = issue.projectMilestone;
864
844
  return {
865
- state: state
866
- ? { id: state.id, name: state.name }
845
+ state: issue.state
846
+ ? { id: issue.state.id, name: issue.state.name }
867
847
  : undefined,
868
- assignee: assignee
848
+ assignee: issue.assignee
869
849
  ? {
870
- id: assignee.id,
871
- name: resolveUserDisplayName(assignee.id, assignee.name),
872
- url: assignee.url || undefined,
850
+ id: issue.assignee.id,
851
+ name: resolveUserDisplayName(issue.assignee.id, issue.assignee.name),
852
+ url: issue.assignee.url ?? undefined,
873
853
  }
874
854
  : undefined,
875
- team: team
855
+ team: issue.team
876
856
  ? {
877
- id: team.id,
878
- key: team.key,
879
- name: team.name,
857
+ id: issue.team.id,
858
+ key: issue.team.key,
859
+ name: issue.team.name,
880
860
  }
881
861
  : undefined,
882
- project: project
883
- ? { id: project.id, name: project.name }
862
+ project: issue.project
863
+ ? { id: issue.project.id, name: issue.project.name }
884
864
  : undefined,
885
- cycle: cycle
865
+ cycle: issue.cycle
886
866
  ? {
887
- id: cycle.id,
888
- name: cycle.name,
889
- number: cycle.number,
867
+ id: issue.cycle.id,
868
+ name: issue.cycle.name ?? "",
869
+ number: issue.cycle.number,
890
870
  }
891
871
  : undefined,
892
- projectMilestone: milestone
872
+ projectMilestone: issue.projectMilestone
893
873
  ? {
894
- id: milestone.id,
895
- name: milestone.name,
896
- targetDate: milestone.targetDate || undefined,
874
+ id: issue.projectMilestone.id,
875
+ name: issue.projectMilestone.name,
876
+ targetDate: issue.projectMilestone.targetDate ?? undefined,
897
877
  }
898
878
  : undefined,
899
879
  };
900
880
  }
901
881
  transformIssueHierarchy(issue) {
902
- const parent = issue.parent;
903
- const children = issue.children;
882
+ // children is a connection field — always present in production
883
+ // responses. Defensive read keeps test mocks small.
884
+ const childNodes = issue.children?.nodes ?? [];
904
885
  return {
905
- parentIssue: parent
886
+ parentIssue: issue.parent
906
887
  ? {
907
- id: parent.id,
908
- identifier: parent.identifier,
909
- title: parent.title,
888
+ id: issue.parent.id,
889
+ identifier: issue.parent.identifier,
890
+ title: issue.parent.title,
910
891
  }
911
892
  : undefined,
912
- subIssues: children?.nodes?.map((child) => ({
913
- id: child.id,
914
- identifier: child.identifier,
915
- title: child.title,
916
- })) || undefined,
893
+ subIssues: childNodes.length > 0
894
+ ? childNodes.map((child) => ({
895
+ id: child.id,
896
+ identifier: child.identifier,
897
+ title: child.title,
898
+ }))
899
+ : undefined,
917
900
  };
918
901
  }
919
902
  transformIssueComments(issue) {
920
- const comments = issue.comments;
921
- return (comments?.nodes?.map((comment) => {
922
- const commentUser = comment.user;
923
- return {
924
- id: comment.id,
925
- body: comment.body,
926
- embeds: extractEmbeds(comment.body),
927
- user: commentUser
928
- ? {
929
- id: commentUser.id,
930
- name: resolveUserDisplayName(commentUser.id, commentUser.name),
931
- url: commentUser.url || undefined,
932
- }
933
- : undefined,
934
- createdAt: toISOStringOrNow(comment.createdAt),
935
- updatedAt: toISOStringOrNow(comment.updatedAt),
936
- };
937
- }) || []);
903
+ return issue.comments.nodes.map((comment) => ({
904
+ id: comment.id,
905
+ body: comment.body,
906
+ embeds: extractEmbeds(comment.body),
907
+ user: comment.user
908
+ ? {
909
+ id: comment.user.id,
910
+ name: resolveUserDisplayName(comment.user.id, comment.user.name),
911
+ url: comment.user.url ?? undefined,
912
+ }
913
+ : undefined,
914
+ createdAt: toISOStringOrNow(comment.createdAt),
915
+ updatedAt: toISOStringOrNow(comment.updatedAt),
916
+ }));
938
917
  }
939
918
  }
@@ -3,9 +3,11 @@ export declare function setRawMode(enabled: boolean): void;
3
3
  export declare function setJqFilter(filter: string | null): void;
4
4
  export declare function setFieldsFilter(fields: string[] | null): void;
5
5
  export declare function setOutputFormat(format: OutputFormat): void;
6
+ /** @internal Test seam — consumers should not depend on the format state. */
6
7
  export declare function getOutputFormat(): OutputFormat;
7
8
  export declare function outputSuccess(data: unknown): void;
8
9
  export declare function outputWarning(message: string | string[]): void;
9
10
  export declare function resetWarnings(): void;
11
+ /** @internal Test seam — call between tests that toggle `setOutputFormat`. */
10
12
  export declare function resetOutputFormat(): void;
11
13
  export declare function handleAsyncCommand<TArgs extends unknown[]>(asyncFn: (...args: TArgs) => Promise<void>): (...args: TArgs) => Promise<void>;
@@ -18,6 +18,7 @@ export function setFieldsFilter(fields) {
18
18
  export function setOutputFormat(format) {
19
19
  outputFormat = format;
20
20
  }
21
+ /** @internal Test seam — consumers should not depend on the format state. */
21
22
  export function getOutputFormat() {
22
23
  return outputFormat;
23
24
  }
@@ -38,12 +39,15 @@ function filterFields(obj, fields) {
38
39
  return obj;
39
40
  }
40
41
  /**
41
- * Emit a summary-format render to stdout for the given payload. The
42
- * caller (`outputSuccess`) has already applied `--raw` and `--fields`,
43
- * so the value here is post-filter — no need to unwrap again.
42
+ * Emit a summary-format render to stdout for the given payload.
43
+ *
44
+ * `kind` is captured upstream from the **pre-filter** payload — if we
45
+ * inferred here, `--fields identifier,url` would strip `title` and
46
+ * the heuristic would fall through to "generic", silently breaking
47
+ * the issue-list table. Caching the pre-filter shape keeps the
48
+ * formatter accurate regardless of how the user pared the JSON.
44
49
  */
45
- function emitSummary(payload) {
46
- const kind = inferKindFromPayload(payload);
50
+ function emitSummary(payload, kind) {
47
51
  logger.info(dispatchSummary(kind, payload));
48
52
  }
49
53
  export function outputSuccess(data) {
@@ -58,6 +62,11 @@ export function outputSuccess(data) {
58
62
  else {
59
63
  output = data;
60
64
  }
65
+ // Capture the kind from the original envelope shape BEFORE --raw /
66
+ // --fields stripping. Otherwise filtering away signature fields (e.g.
67
+ // `title` on an issue) breaks shape inference and the summary
68
+ // formatter falls back to the generic key-value dump.
69
+ const inferredKind = outputFormat === "summary" ? inferKindFromPayload(output) : "generic";
61
70
  // --raw: unwrap { data: [...] } to just the array
62
71
  if (rawMode &&
63
72
  output !== null &&
@@ -87,7 +96,7 @@ export function outputSuccess(data) {
87
96
  // as a human-readable block. We bypass the jq path because jq is a
88
97
  // JSON-shape filter — it doesn't compose with text output.
89
98
  if (outputFormat === "summary") {
90
- emitSummary(output);
99
+ emitSummary(output, inferredKind);
91
100
  return;
92
101
  }
93
102
  if (jqFilter) {
@@ -127,6 +136,7 @@ function drainWarnings() {
127
136
  export function resetWarnings() {
128
137
  warningBuffer.length = 0;
129
138
  }
139
+ /** @internal Test seam — call between tests that toggle `setOutputFormat`. */
130
140
  export function resetOutputFormat() {
131
141
  outputFormat = "json";
132
142
  }
@@ -1,4 +1,8 @@
1
1
  import type { GraphQLService } from "./graphql-service.js";
2
- export declare function getWorkspaceUrlKey(graphQLService: GraphQLService): Promise<string>;
2
+ export interface GetWorkspaceUrlKeyOptions {
3
+ /** Highest-priority source. Typically the `--workspace-url-key` flag. */
4
+ override?: string;
5
+ }
6
+ export declare function getWorkspaceUrlKey(graphQLService?: GraphQLService, options?: GetWorkspaceUrlKeyOptions): Promise<string>;
3
7
  /** Test helper: clear the cached URL key. Not called from production code. */
4
8
  export declare function _resetWorkspaceUrlKeyCache(): void;