@enrichlayer/el-linear 1.5.0 → 1.6.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 (51) hide show
  1. package/dist/commands/attachments.js +11 -12
  2. package/dist/commands/batch.js +15 -14
  3. package/dist/commands/comments.js +22 -10
  4. package/dist/commands/cycles.js +5 -4
  5. package/dist/commands/documents.js +18 -15
  6. package/dist/commands/embeds.js +4 -6
  7. package/dist/commands/graphql.js +5 -4
  8. package/dist/commands/init/aliases.js +0 -14
  9. package/dist/commands/init/defaults.js +0 -6
  10. package/dist/commands/init/index.js +0 -12
  11. package/dist/commands/init/shared.js +0 -1
  12. package/dist/commands/init/token.js +0 -7
  13. package/dist/commands/init/workspace.js +0 -2
  14. package/dist/commands/issue-id.js +3 -1
  15. package/dist/commands/issues.js +87 -46
  16. package/dist/commands/labels.js +9 -8
  17. package/dist/commands/profile/migrate-legacy.js +0 -1
  18. package/dist/commands/project-milestones.js +13 -12
  19. package/dist/commands/projects.js +18 -14
  20. package/dist/commands/read-shortcut.js +8 -7
  21. package/dist/commands/refs.js +4 -3
  22. package/dist/commands/releases.js +9 -8
  23. package/dist/commands/search.js +3 -2
  24. package/dist/commands/teams.js +3 -2
  25. package/dist/commands/templates.js +5 -4
  26. package/dist/commands/users.js +3 -2
  27. package/dist/config/config.d.ts +18 -0
  28. package/dist/config/issue-validation.js +1 -1
  29. package/dist/config/resolver.js +1 -1
  30. package/dist/main.js +1 -1
  31. package/dist/utils/download-uploads.d.ts +2 -1
  32. package/dist/utils/download-uploads.js +2 -4
  33. package/dist/utils/file-service.d.ts +26 -2
  34. package/dist/utils/file-service.js +27 -5
  35. package/dist/utils/footer.d.ts +19 -0
  36. package/dist/utils/footer.js +27 -0
  37. package/dist/utils/gdoc-parser.js +1 -1
  38. package/dist/utils/graphql-attachments-service.d.ts +1 -1
  39. package/dist/utils/graphql-attachments-service.js +2 -2
  40. package/dist/utils/graphql-documents-service.d.ts +1 -1
  41. package/dist/utils/graphql-documents-service.js +2 -2
  42. package/dist/utils/graphql-service.d.ts +2 -2
  43. package/dist/utils/graphql-service.js +7 -4
  44. package/dist/utils/linear-service.d.ts +18 -3
  45. package/dist/utils/linear-service.js +21 -6
  46. package/dist/utils/markdown-prosemirror.js +16 -9
  47. package/dist/utils/root-opts.d.ts +11 -0
  48. package/dist/utils/root-opts.js +13 -0
  49. package/dist/utils/validators.d.ts +7 -0
  50. package/dist/utils/validators.js +14 -14
  51. package/package.json +1 -1
@@ -33,7 +33,6 @@ export async function runWorkspaceStep(token, workspaceUrlKey, existing) {
33
33
  for (const t of teams) {
34
34
  teamMap[t.key] = t.id;
35
35
  }
36
- // biome-ignore lint/suspicious/noConsole: wizard
37
36
  console.log(` Workspace: ${workspaceUrlKey} (${teams.length} teams)`);
38
37
  const currentDefault = existing.defaultTeam || "";
39
38
  // `change` reflects what the user agreed to: `true` means "open the picker
@@ -53,7 +52,6 @@ export async function runWorkspaceStep(token, workspaceUrlKey, existing) {
53
52
  };
54
53
  }
55
54
  if (teams.length === 0) {
56
- // biome-ignore lint/suspicious/noConsole: wizard
57
55
  console.log(" No teams visible to this token. Skipping default-team picker.");
58
56
  return {
59
57
  workspaceUrlKey,
@@ -72,7 +72,9 @@ export function setupIssueIdCommand(program) {
72
72
  return;
73
73
  }
74
74
  const rootOpts = command.parent?.opts() ?? {};
75
- const service = createGraphQLService({ apiToken: rootOpts.apiToken });
75
+ const service = await createGraphQLService({
76
+ apiToken: rootOpts.apiToken,
77
+ });
76
78
  const result = (await service.rawRequest(ISSUE_QUERY, {
77
79
  id: parsed.issueId,
78
80
  }));
@@ -7,10 +7,10 @@ import { resolveDefaultStatus } from "../config/status-defaults.js";
7
7
  import { enforceTerms } from "../config/term-enforcer.js";
8
8
  import { LIST_COMMENTS_QUERY } from "../queries/comments.js";
9
9
  import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, ISSUE_RELATION_CREATE_MUTATION, SCAN_ISSUES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
10
- import { getApiToken } from "../utils/auth.js";
11
10
  import { autoLinkReferences, } from "../utils/auto-link-references.js";
12
11
  import { downloadLinearUploads } from "../utils/download-uploads.js";
13
- import { FileService } from "../utils/file-service.js";
12
+ import { createFileService } from "../utils/file-service.js";
13
+ import { applyFooter } from "../utils/footer.js";
14
14
  import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
15
15
  import { GraphQLIssuesService } from "../utils/graphql-issues-service.js";
16
16
  import { createGraphQLService, } from "../utils/graphql-service.js";
@@ -19,6 +19,7 @@ import { wrapIssueReferencesAsLinks } from "../utils/issue-reference-wrapper.js"
19
19
  import { createLinearService, } from "../utils/linear-service.js";
20
20
  import { logger } from "../utils/logger.js";
21
21
  import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
22
+ import { getRootOpts } from "../utils/root-opts.js";
22
23
  import { formatCsv, formatMarkdown, formatTable, } from "../utils/table-formatter.js";
23
24
  import { validateReferences } from "../utils/validate-references.js";
24
25
  import { parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
@@ -78,14 +79,43 @@ function readDescriptionFile(filePath) {
78
79
  return fs.readFileSync(filePath, "utf8").trim();
79
80
  }
80
81
  /**
81
- * Resolve the description from --description, --description-file, or both.
82
- * --description-file takes precedence when both are provided.
82
+ * Resolve the description from --description, --description-file, or
83
+ * --template (looked up in `config.descriptionTemplates`).
84
+ *
85
+ * Precedence:
86
+ * --description-file > --description > --template
87
+ *
88
+ * Passing --template alongside --description or --description-file is a
89
+ * usage error — the explicit body and a template both producing content
90
+ * would be silently dropping one. We throw so the user picks one.
83
91
  */
84
92
  function resolveDescription(options) {
85
- if (options.descriptionFile) {
93
+ const hasInline = typeof options.description === "string" && options.description.length > 0;
94
+ const hasFile = Boolean(options.descriptionFile);
95
+ const hasTemplate = typeof options.template === "string" && options.template;
96
+ if (hasTemplate && (hasInline || hasFile)) {
97
+ throw new Error("--template is mutually exclusive with --description / --description-file. " +
98
+ "Pick one.");
99
+ }
100
+ if (hasFile) {
86
101
  return readDescriptionFile(options.descriptionFile);
87
102
  }
88
- return options.description;
103
+ if (hasInline) {
104
+ return options.description;
105
+ }
106
+ if (hasTemplate) {
107
+ const templates = loadConfig().descriptionTemplates ?? {};
108
+ const body = templates[options.template];
109
+ if (!body) {
110
+ const available = Object.keys(templates).sort();
111
+ const hint = available.length
112
+ ? `Available templates: ${available.join(", ")}`
113
+ : "No templates configured. Add one under `descriptionTemplates` in your config.";
114
+ throw new Error(`Template "${options.template}" not found. ${hint}`);
115
+ }
116
+ return body;
117
+ }
118
+ return undefined;
89
119
  }
90
120
  function isImageFile(filename) {
91
121
  const ext = filename.lastIndexOf(".");
@@ -296,9 +326,9 @@ async function handleListIssues(options, command) {
296
326
  if (options.label && !options.labels) {
297
327
  options.labels = options.label;
298
328
  }
299
- const rootOpts = command.parent.parent.opts();
300
- const graphQLService = createGraphQLService(rootOpts);
301
- const linearService = createLinearService(rootOpts);
329
+ const rootOpts = getRootOpts(command);
330
+ const graphQLService = await createGraphQLService(rootOpts);
331
+ const linearService = await createLinearService(rootOpts);
302
332
  const issuesService = new GraphQLIssuesService(graphQLService, linearService);
303
333
  const hasFilters = options.team ||
304
334
  options.labels ||
@@ -337,9 +367,9 @@ async function handleSearchIssues(query, options, command) {
337
367
  if (options.label && !options.labels) {
338
368
  options.labels = options.label;
339
369
  }
340
- const rootOpts = command.parent.parent.opts();
341
- const graphQLService = createGraphQLService(rootOpts);
342
- const linearService = createLinearService(rootOpts);
370
+ const rootOpts = getRootOpts(command);
371
+ const graphQLService = await createGraphQLService(rootOpts);
372
+ const linearService = await createLinearService(rootOpts);
343
373
  const issuesService = new GraphQLIssuesService(graphQLService, linearService);
344
374
  const searchArgs = {
345
375
  query,
@@ -417,8 +447,7 @@ async function uploadAttachmentsIfNeeded(options, rootOpts) {
417
447
  const paths = Array.isArray(options.attachment)
418
448
  ? options.attachment
419
449
  : [options.attachment];
420
- const apiToken = getApiToken(rootOpts);
421
- const fileService = new FileService(apiToken);
450
+ const fileService = await createFileService(rootOpts);
422
451
  const results = [];
423
452
  for (const filePath of paths) {
424
453
  const result = await fileService.uploadFile(filePath);
@@ -442,12 +471,21 @@ function buildDescriptionWithAttachments(baseDescription, uploadResults) {
442
471
  return description;
443
472
  }
444
473
  async function handleCreateIssue(title, options, command) {
445
- const rootOpts = command.parent.parent.opts();
474
+ const rootOpts = getRootOpts(command);
446
475
  const { teamInput, teamId, assigneeId, labelIds, status, subscriberIds } = await resolveCreateInputs(title, options, rootOpts);
447
476
  const uploadResults = await uploadAttachmentsIfNeeded(options, rootOpts);
448
- const description = buildDescriptionWithAttachments(resolveDescription(options) || "", uploadResults);
449
- const graphQLService = createGraphQLService(rootOpts);
450
- const linearService = createLinearService(rootOpts);
477
+ const descriptionWithAttachments = buildDescriptionWithAttachments(resolveDescription(options) || "", uploadResults);
478
+ // Append messageFooter (config or --footer flag) so auto-link picks up any
479
+ // issue refs in the footer too. --no-footer skips both flag and config.
480
+ // Commander parses --no-footer as `options.footer === false`.
481
+ const noFooter = options.footer === false;
482
+ const explicitFooter = typeof options.footer === "string" ? options.footer : undefined;
483
+ const description = applyFooter(descriptionWithAttachments, {
484
+ footer: explicitFooter,
485
+ noFooter,
486
+ }) ?? "";
487
+ const graphQLService = await createGraphQLService(rootOpts);
488
+ const linearService = await createLinearService(rootOpts);
451
489
  const issuesService = new GraphQLIssuesService(graphQLService, linearService);
452
490
  // Wrap valid issue identifiers as markdown links before creating, so the description
453
491
  // saved on Linear has clickable refs from the start. Self-reference can't apply here
@@ -486,7 +524,7 @@ async function handleCreateIssue(title, options, command) {
486
524
  // so only create separate attachment records for non-image files.
487
525
  const attachments = [];
488
526
  if (uploadResults.length > 0) {
489
- const attachmentsService = createGraphQLAttachmentsService(rootOpts);
527
+ const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
490
528
  for (const uploadResult of uploadResults) {
491
529
  if (!isImageFile(uploadResult.filename)) {
492
530
  const attachment = await attachmentsService.createAttachment({
@@ -517,9 +555,9 @@ async function handleCreateIssue(title, options, command) {
517
555
  outputSuccess(output);
518
556
  }
519
557
  async function handleHistoryIssue(issueId, _options, command) {
520
- const rootOpts = command.parent.parent.opts();
521
- const graphQLService = createGraphQLService(rootOpts);
522
- const linearService = createLinearService(rootOpts);
558
+ const rootOpts = getRootOpts(command);
559
+ const graphQLService = await createGraphQLService(rootOpts);
560
+ const linearService = await createLinearService(rootOpts);
523
561
  const resolvedId = await linearService.resolveIssueId(issueId);
524
562
  const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, {
525
563
  id: resolvedId,
@@ -547,9 +585,9 @@ async function handleHistoryIssue(issueId, _options, command) {
547
585
  });
548
586
  }
549
587
  async function handleRelateIssue(issueId, options, command) {
550
- const rootOpts = command.parent.parent.opts();
551
- const graphQLService = createGraphQLService(rootOpts);
552
- const linearService = createLinearService(rootOpts);
588
+ const rootOpts = getRootOpts(command);
589
+ const graphQLService = await createGraphQLService(rootOpts);
590
+ const linearService = await createLinearService(rootOpts);
553
591
  const sourceId = await linearService.resolveIssueId(issueId);
554
592
  const relations = await createRelations(sourceId, options, graphQLService, linearService);
555
593
  if (relations.length === 0) {
@@ -620,9 +658,9 @@ function buildRelationEntries(nodes, peerKey, direction, normalizeType) {
620
658
  return entries;
621
659
  }
622
660
  async function handleRelatedIssues(issueId, _options, command) {
623
- const rootOpts = command.parent.parent.opts();
624
- const graphQLService = createGraphQLService(rootOpts);
625
- const linearService = createLinearService(rootOpts);
661
+ const rootOpts = getRootOpts(command);
662
+ const graphQLService = await createGraphQLService(rootOpts);
663
+ const linearService = await createLinearService(rootOpts);
626
664
  const resolvedId = await linearService.resolveIssueId(issueId);
627
665
  const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
628
666
  id: resolvedId,
@@ -813,10 +851,10 @@ async function handleLinkReferencesBatch(teamInput, options, graphQLService, lin
813
851
  },
814
852
  });
815
853
  }
816
- function handleLinkReferencesIssue(issueIdOrTeamFlag, options, command) {
817
- const rootOpts = command.parent.parent.opts();
818
- const graphQLService = createGraphQLService(rootOpts);
819
- const linearService = createLinearService(rootOpts);
854
+ async function handleLinkReferencesIssue(issueIdOrTeamFlag, options, command) {
855
+ const rootOpts = getRootOpts(command);
856
+ const graphQLService = await createGraphQLService(rootOpts);
857
+ const linearService = await createLinearService(rootOpts);
820
858
  if (options.team) {
821
859
  if (issueIdOrTeamFlag) {
822
860
  throw new Error("Provide either an issueId positional argument OR --team, not both. With --team, omit the issueId.");
@@ -832,20 +870,20 @@ function handleLinkReferencesIssue(issueIdOrTeamFlag, options, command) {
832
870
  return handleLinkReferencesSingle(issueIdOrTeamFlag, options, graphQLService, linearService);
833
871
  }
834
872
  async function handleReadIssue(issueIds, _options, command) {
835
- const rootOpts = command.parent.parent.opts();
836
- const graphQLService = createGraphQLService(rootOpts);
837
- const linearService = createLinearService(rootOpts);
873
+ const rootOpts = getRootOpts(command);
874
+ const graphQLService = await createGraphQLService(rootOpts);
875
+ const linearService = await createLinearService(rootOpts);
838
876
  const issuesService = new GraphQLIssuesService(graphQLService, linearService);
839
- const apiToken = getApiToken(rootOpts);
877
+ const fileService = await createFileService(rootOpts);
840
878
  if (issueIds.length === 1) {
841
879
  const issue = await issuesService.getIssueById(issueIds[0]);
842
- const resolved = await downloadLinearUploads(issue, apiToken);
880
+ const resolved = await downloadLinearUploads(issue, fileService);
843
881
  outputSuccess(resolved);
844
882
  }
845
883
  else {
846
884
  const results = await Promise.all(issueIds.map(async (id) => {
847
885
  const issue = await issuesService.getIssueById(id);
848
- return downloadLinearUploads(issue, apiToken);
886
+ return downloadLinearUploads(issue, fileService);
849
887
  }));
850
888
  outputSuccess(results);
851
889
  }
@@ -863,9 +901,9 @@ async function handleUpdateIssue(issueId, options, command) {
863
901
  options.description = readDescriptionFile(options.descriptionFile);
864
902
  }
865
903
  validateUpdateOptions(options);
866
- const rootOpts = command.parent.parent.opts();
867
- const graphQLService = createGraphQLService(rootOpts);
868
- const linearService = createLinearService(rootOpts);
904
+ const rootOpts = getRootOpts(command);
905
+ const graphQLService = await createGraphQLService(rootOpts);
906
+ const linearService = await createLinearService(rootOpts);
869
907
  if (options.appendDescription) {
870
908
  const resolved = await linearService.resolveIssueId(issueId);
871
909
  const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", { id: resolved });
@@ -952,9 +990,9 @@ async function handleRetrolink(options, command) {
952
990
  const description = commits.length > 0
953
991
  ? commits.map((msg) => `- ${msg}`).join("\n")
954
992
  : `Retrolinked from branch: ${currentBranch}`;
955
- const rootOpts = command.parent.parent.opts();
956
- const graphQLService = createGraphQLService(rootOpts);
957
- const linearService = createLinearService(rootOpts);
993
+ const rootOpts = getRootOpts(command);
994
+ const graphQLService = await createGraphQLService(rootOpts);
995
+ const linearService = await createLinearService(rootOpts);
958
996
  const issuesService = new GraphQLIssuesService(graphQLService, linearService);
959
997
  let result;
960
998
  if (options.issue) {
@@ -1035,8 +1073,9 @@ export function setupIssuesCommands(program) {
1035
1073
  .option("-t, --title <title>", "issue title (alternative to positional argument)")
1036
1074
  .option("-d, --description <desc>", "issue description")
1037
1075
  .option("--description-file <path>", "read description from file (use - for stdin)")
1076
+ .option("--template <name>", "use a named description template from config.descriptionTemplates")
1038
1077
  .option("-a, --assignee <assignee>", "assign to user (name, alias, or UUID)")
1039
- .option("-p, --priority <priority>", "priority: name (urgent/high/medium/low) or number (1-4)")
1078
+ .option("-p, --priority <priority>", "priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
1040
1079
  .option("--project <project>", "add to project (name or ID)")
1041
1080
  .option("--team <team>", "team key or name (default: from config)")
1042
1081
  .option("--labels <labels>", "labels (comma-separated names, auto-resolved per team)")
@@ -1056,6 +1095,8 @@ export function setupIssuesCommands(program) {
1056
1095
  .option("--checkout", "create and checkout a git branch named after the issue")
1057
1096
  .option("--skip-validation", "skip all validation (labels, description, assignee, project)")
1058
1097
  .option("--no-auto-link", "skip auto-linking issue references found in the description")
1098
+ .option("--footer <text>", "text appended to the description (overrides config.messageFooter)")
1099
+ .option("--no-footer", "skip the configured messageFooter for this issue")
1059
1100
  .action(handleAsyncCommand((titleArg, options, command) => {
1060
1101
  // Normalize --label alias to --labels
1061
1102
  if (options.label && !options.labels) {
@@ -1087,7 +1128,7 @@ export function setupIssuesCommands(program) {
1087
1128
  .option("--append-description <text>", "append text to the existing description")
1088
1129
  .option("-s, --status <status>", "new status name or ID")
1089
1130
  .option("--state <status>", "alias for --status (new status name or ID)")
1090
- .option("-p, --priority <priority>", "new priority: name (urgent/high/medium/low) or number (1-4)")
1131
+ .option("-p, --priority <priority>", "new priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
1091
1132
  .option("--assignee <assignee>", "new assignee (name, alias, or UUID)")
1092
1133
  .option("--project <project>", "new project (name or ID)")
1093
1134
  .option("--labels <labels>", "labels (comma-separated names or IDs)")
@@ -3,11 +3,12 @@ import { CREATE_LABEL_MUTATION, FIND_PARENT_LABEL_QUERY, RESTORE_LABEL_MUTATION,
3
3
  import { createGraphQLService } from "../utils/graphql-service.js";
4
4
  import { createLinearService } from "../utils/linear-service.js";
5
5
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
6
+ import { getRootOpts } from "../utils/root-opts.js";
6
7
  import { validateHexColor } from "../utils/validators.js";
7
8
  async function handleCreateLabel(name, options, command) {
8
- const rootOpts = command.parent.parent.opts();
9
+ const rootOpts = getRootOpts(command);
9
10
  const teamId = resolveTeam(options.team);
10
- const graphQLService = createGraphQLService(rootOpts);
11
+ const graphQLService = await createGraphQLService(rootOpts);
11
12
  const input = { name, teamId };
12
13
  if (options.color) {
13
14
  input.color = validateHexColor(options.color);
@@ -37,8 +38,8 @@ async function handleCreateLabel(name, options, command) {
37
38
  outputSuccess(createResult.issueLabel);
38
39
  }
39
40
  async function handleRetireLabel(labelId, _options, command) {
40
- const rootOpts = command.parent.parent.opts();
41
- const graphQLService = createGraphQLService(rootOpts);
41
+ const rootOpts = getRootOpts(command);
42
+ const graphQLService = await createGraphQLService(rootOpts);
42
43
  const result = await graphQLService.rawRequest(RETIRE_LABEL_MUTATION, {
43
44
  id: labelId,
44
45
  });
@@ -49,8 +50,8 @@ async function handleRetireLabel(labelId, _options, command) {
49
50
  outputSuccess(retireResult.issueLabel);
50
51
  }
51
52
  async function handleRestoreLabel(labelId, _options, command) {
52
- const rootOpts = command.parent.parent.opts();
53
- const graphQLService = createGraphQLService(rootOpts);
53
+ const rootOpts = getRootOpts(command);
54
+ const graphQLService = await createGraphQLService(rootOpts);
54
55
  const result = await graphQLService.rawRequest(RESTORE_LABEL_MUTATION, {
55
56
  id: labelId,
56
57
  });
@@ -72,9 +73,9 @@ export function setupLabelsCommands(program) {
72
73
  .option("--team <team>", "filter by team key, name, or ID")
73
74
  .option("-l, --limit <number>", "limit results", "100")
74
75
  .action(handleAsyncCommand(async (options, command) => {
75
- const rootOpts = command.parent.parent.opts();
76
+ const rootOpts = getRootOpts(command);
76
77
  const teamFilter = options.team ? resolveTeam(options.team) : undefined;
77
- const service = createLinearService(rootOpts);
78
+ const service = await createLinearService(rootOpts);
78
79
  const result = await service.getLabels(teamFilter, Number.parseInt(options.limit, 10));
79
80
  outputSuccess({
80
81
  data: result.labels,
@@ -157,7 +157,6 @@ async function resolveAndValidateToken(options, prompts, validate) {
157
157
  }
158
158
  catch (err) {
159
159
  const raw = err instanceof Error ? err.message : String(err);
160
- // biome-ignore lint/suspicious/noConsole: interactive prompt feedback
161
160
  console.log(` ✗ ${sanitizeForLog(raw)}`);
162
161
  }
163
162
  }
@@ -3,6 +3,7 @@ import { multipleMatchesError, notFoundError, } from "../utils/error-messages.js
3
3
  import { createGraphQLService, } from "../utils/graphql-service.js";
4
4
  import { createLinearService, } from "../utils/linear-service.js";
5
5
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
6
+ import { getRootOpts } from "../utils/root-opts.js";
6
7
  import { isUuid } from "../utils/uuid.js";
7
8
  import { validateIsoDate } from "../utils/validators.js";
8
9
  async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearService, projectNameOrId) {
@@ -37,9 +38,9 @@ async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearServi
37
38
  return nodes[0].id;
38
39
  }
39
40
  async function handleListMilestones(options, command) {
40
- const rootOpts = command.parent.parent.opts();
41
- const graphQLService = createGraphQLService(rootOpts);
42
- const linearService = createLinearService(rootOpts);
41
+ const rootOpts = getRootOpts(command);
42
+ const graphQLService = await createGraphQLService(rootOpts);
43
+ const linearService = await createLinearService(rootOpts);
43
44
  const projectId = await linearService.resolveProjectId(options.project);
44
45
  const result = await graphQLService.rawRequest(LIST_PROJECT_MILESTONES_QUERY, {
45
46
  projectId,
@@ -51,9 +52,9 @@ async function handleListMilestones(options, command) {
51
52
  outputSuccess({ data: nodes, meta: { count: nodes.length } });
52
53
  }
53
54
  async function handleReadMilestone(milestoneIdOrName, options, command) {
54
- const rootOpts = command.parent.parent.opts();
55
- const graphQLService = createGraphQLService(rootOpts);
56
- const linearService = createLinearService(rootOpts);
55
+ const rootOpts = getRootOpts(command);
56
+ const graphQLService = await createGraphQLService(rootOpts);
57
+ const linearService = await createLinearService(rootOpts);
57
58
  const milestoneId = await resolveMilestoneId(milestoneIdOrName, graphQLService, linearService, options.project);
58
59
  const result = await graphQLService.rawRequest(GET_PROJECT_MILESTONE_BY_ID_QUERY, {
59
60
  id: milestoneId,
@@ -62,9 +63,9 @@ async function handleReadMilestone(milestoneIdOrName, options, command) {
62
63
  outputSuccess(result.projectMilestone);
63
64
  }
64
65
  async function handleCreateMilestone(name, options, command) {
65
- const rootOpts = command.parent.parent.opts();
66
- const graphQLService = createGraphQLService(rootOpts);
67
- const linearService = createLinearService(rootOpts);
66
+ const rootOpts = getRootOpts(command);
67
+ const graphQLService = await createGraphQLService(rootOpts);
68
+ const linearService = await createLinearService(rootOpts);
68
69
  const projectId = await linearService.resolveProjectId(options.project);
69
70
  if (options.targetDate) {
70
71
  validateIsoDate(options.targetDate);
@@ -82,9 +83,9 @@ async function handleCreateMilestone(name, options, command) {
82
83
  outputSuccess(createData.projectMilestone);
83
84
  }
84
85
  async function handleUpdateMilestone(milestoneIdOrName, options, command) {
85
- const rootOpts = command.parent.parent.opts();
86
- const graphQLService = createGraphQLService(rootOpts);
87
- const linearService = createLinearService(rootOpts);
86
+ const rootOpts = getRootOpts(command);
87
+ const graphQLService = await createGraphQLService(rootOpts);
88
+ const linearService = await createLinearService(rootOpts);
88
89
  const milestoneId = await resolveMilestoneId(milestoneIdOrName, graphQLService, linearService, options.project);
89
90
  const updateVars = { id: milestoneId };
90
91
  if (options.name !== undefined) {
@@ -4,6 +4,7 @@ import { createGraphQLService } from "../utils/graphql-service.js";
4
4
  import { createLinearService } from "../utils/linear-service.js";
5
5
  import { logger } from "../utils/logger.js";
6
6
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
7
+ import { getRootOpts } from "../utils/root-opts.js";
7
8
  import { isUuid } from "../utils/uuid.js";
8
9
  import { splitList } from "../utils/validators.js";
9
10
  function formatProjectsOutput(projects, format, fieldNames) {
@@ -126,9 +127,10 @@ function formatTeamsOutput(projectUpdate) {
126
127
  };
127
128
  }
128
129
  async function handleAddTeam(projectNameOrId, teamInput, _options, command) {
129
- const rootOpts = command.parent.parent.opts();
130
- const graphQLService = createGraphQLService(rootOpts);
131
- const finalTeamId = await createLinearService(rootOpts).resolveTeamId(resolveTeam(teamInput));
130
+ const rootOpts = getRootOpts(command);
131
+ const graphQLService = await createGraphQLService(rootOpts);
132
+ const linearService = await createLinearService(rootOpts);
133
+ const finalTeamId = await linearService.resolveTeamId(resolveTeam(teamInput));
132
134
  const { projectId, currentTeams } = await resolveProjectWithTeams(graphQLService, projectNameOrId);
133
135
  const currentTeamIds = currentTeams.map((t) => t.id);
134
136
  if (currentTeamIds.includes(finalTeamId)) {
@@ -149,9 +151,10 @@ async function handleAddTeam(projectNameOrId, teamInput, _options, command) {
149
151
  outputSuccess(formatTeamsOutput(projectUpdate));
150
152
  }
151
153
  async function handleAddTeams(projectNameOrId, teamInputs, _options, command) {
152
- const rootOpts = command.parent.parent.opts();
153
- const graphQLService = createGraphQLService(rootOpts);
154
- const resolvedTeamIds = await Promise.all(teamInputs.map((t) => createLinearService(rootOpts).resolveTeamId(resolveTeam(t))));
154
+ const rootOpts = getRootOpts(command);
155
+ const graphQLService = await createGraphQLService(rootOpts);
156
+ const linearService = await createLinearService(rootOpts);
157
+ const resolvedTeamIds = await Promise.all(teamInputs.map((t) => linearService.resolveTeamId(resolveTeam(t))));
155
158
  const { projectId, currentTeams } = await resolveProjectWithTeams(graphQLService, projectNameOrId);
156
159
  const currentTeamIds = currentTeams.map((t) => t.id);
157
160
  const newTeamIds = resolvedTeamIds.filter((id) => !currentTeamIds.includes(id));
@@ -181,9 +184,10 @@ async function handleAddTeams(projectNameOrId, teamInputs, _options, command) {
181
184
  });
182
185
  }
183
186
  async function handleRemoveTeam(projectNameOrId, teamInput, options, command) {
184
- const rootOpts = command.parent.parent.opts();
185
- const graphQLService = createGraphQLService(rootOpts);
186
- const finalTeamId = await createLinearService(rootOpts).resolveTeamId(resolveTeam(teamInput));
187
+ const rootOpts = getRootOpts(command);
188
+ const graphQLService = await createGraphQLService(rootOpts);
189
+ const linearService = await createLinearService(rootOpts);
190
+ const finalTeamId = await linearService.resolveTeamId(resolveTeam(teamInput));
187
191
  const { projectId, projectName, currentTeams } = await resolveProjectWithTeams(graphQLService, projectNameOrId);
188
192
  const currentTeamIds = currentTeams.map((t) => t.id);
189
193
  if (!currentTeamIds.includes(finalTeamId)) {
@@ -220,8 +224,8 @@ async function handleRemoveTeam(projectNameOrId, teamInput, options, command) {
220
224
  });
221
225
  }
222
226
  async function handleCreateProject(name, options, command) {
223
- const rootOpts = command.parent.parent.opts();
224
- const graphQLService = createGraphQLService(rootOpts);
227
+ const rootOpts = getRootOpts(command);
228
+ const graphQLService = await createGraphQLService(rootOpts);
225
229
  // Step 1: Check for duplicate projects (case-insensitive)
226
230
  const searchResult = await graphQLService.rawRequest(SEARCH_PROJECTS_BY_NAME_QUERY, { name });
227
231
  const existing = (searchResult.projects
@@ -236,7 +240,7 @@ async function handleCreateProject(name, options, command) {
236
240
  `Use --force to create anyway, or use the existing project.`);
237
241
  }
238
242
  // Step 2: Resolve team IDs
239
- const service = createLinearService(rootOpts);
243
+ const service = await createLinearService(rootOpts);
240
244
  const teamKeys = options.team ? splitList(options.team) : [];
241
245
  const teamIds = [];
242
246
  for (const key of teamKeys) {
@@ -300,8 +304,8 @@ export function setupProjectsCommands(program) {
300
304
  .option("--format <format>", "output format (json, table, md, csv)", "json")
301
305
  .option("--fields <fields>", "columns for table/csv (comma-separated: name,state,progress,teams,lead,targetDate)")
302
306
  .action(handleAsyncCommand(async (options, command) => {
303
- const rootOpts = command.parent.parent.opts();
304
- const service = createLinearService(rootOpts);
307
+ const rootOpts = getRootOpts(command);
308
+ const service = await createLinearService(rootOpts);
305
309
  const result = await service.getProjects(Number.parseInt(options.limit, 10));
306
310
  const format = options.format;
307
311
  if (format === "table" ||
@@ -1,9 +1,10 @@
1
- import { getApiToken } from "../utils/auth.js";
2
1
  import { downloadLinearUploads } from "../utils/download-uploads.js";
2
+ import { createFileService } from "../utils/file-service.js";
3
3
  import { GraphQLIssuesService } from "../utils/graphql-issues-service.js";
4
4
  import { createGraphQLService } from "../utils/graphql-service.js";
5
5
  import { createLinearService } from "../utils/linear-service.js";
6
6
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
7
+ import { getRootOpts } from "../utils/root-opts.js";
7
8
  /**
8
9
  * Issue ID pattern: 1-5 uppercase letters, dash, 1+ digits (e.g. ADM-652, DEV-12).
9
10
  */
@@ -49,20 +50,20 @@ export function setupReadShortcut(program) {
49
50
  };
50
51
  }
51
52
  async function readIssues(issueIds, _options, command) {
52
- const rootOpts = command.parent.opts();
53
- const graphQLService = createGraphQLService(rootOpts);
54
- const linearService = createLinearService(rootOpts);
53
+ const rootOpts = getRootOpts(command);
54
+ const graphQLService = await createGraphQLService(rootOpts);
55
+ const linearService = await createLinearService(rootOpts);
55
56
  const issuesService = new GraphQLIssuesService(graphQLService, linearService);
56
- const apiToken = getApiToken(rootOpts);
57
+ const fileService = await createFileService(rootOpts);
57
58
  if (issueIds.length === 1) {
58
59
  const issue = await issuesService.getIssueById(issueIds[0]);
59
- const resolved = await downloadLinearUploads(issue, apiToken);
60
+ const resolved = await downloadLinearUploads(issue, fileService);
60
61
  outputSuccess(resolved);
61
62
  }
62
63
  else {
63
64
  const results = await Promise.all(issueIds.map(async (id) => {
64
65
  const issue = await issuesService.getIssueById(id);
65
- return downloadLinearUploads(issue, apiToken);
66
+ return downloadLinearUploads(issue, fileService);
66
67
  }));
67
68
  outputSuccess(results);
68
69
  }
@@ -4,6 +4,7 @@ import { extractIssueReferences } from "../utils/issue-reference-extractor.js";
4
4
  import { wrapIssueReferencesAsLinks, } from "../utils/issue-reference-wrapper.js";
5
5
  import { createLinearService } from "../utils/linear-service.js";
6
6
  import { handleAsyncCommand } from "../utils/output.js";
7
+ import { getRootOpts } from "../utils/root-opts.js";
7
8
  import { validateReferences } from "../utils/validate-references.js";
8
9
  import { getWorkspaceUrlKey } from "../utils/workspace-url.js";
9
10
  const VALID_TARGETS = new Set(["markdown", "slack"]);
@@ -58,15 +59,15 @@ async function handleWrap(options, command) {
58
59
  const text = typeof options.file === "string" && options.file.length > 0
59
60
  ? readFileSync(options.file, "utf8")
60
61
  : await readAllStdin();
61
- const rootOpts = command.parent.parent.opts();
62
+ const rootOpts = getRootOpts(command);
62
63
  const deps = {
63
64
  async resolveValidIdentifiers(ids) {
64
- const linearService = createLinearService(rootOpts);
65
+ const linearService = await createLinearService(rootOpts);
65
66
  const map = await validateReferences(ids, linearService);
66
67
  return new Set(map.keys());
67
68
  },
68
69
  async resolveUrlKey() {
69
- const graphQLService = createGraphQLService(rootOpts);
70
+ const graphQLService = await createGraphQLService(rootOpts);
70
71
  return getWorkspaceUrlKey(graphQLService);
71
72
  },
72
73
  };
@@ -1,6 +1,7 @@
1
1
  import { CREATE_RELEASE_MUTATION, GET_RELEASE_BY_ID_QUERY, GET_RELEASE_PIPELINES_QUERY, GET_RELEASES_QUERY, } from "../queries/releases.js";
2
2
  import { createGraphQLService } from "../utils/graphql-service.js";
3
3
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
4
+ import { getRootOpts } from "../utils/root-opts.js";
4
5
  function transformRelease(release) {
5
6
  return {
6
7
  id: release.id,
@@ -36,8 +37,8 @@ function transformRelease(release) {
36
37
  };
37
38
  }
38
39
  async function handleCreateRelease(name, options, command) {
39
- const rootOpts = command.parent.parent.opts();
40
- const graphQLService = createGraphQLService(rootOpts);
40
+ const rootOpts = getRootOpts(command);
41
+ const graphQLService = await createGraphQLService(rootOpts);
41
42
  const pipelines = await graphQLService.rawRequest(GET_RELEASE_PIPELINES_QUERY, { first: 50 });
42
43
  const pipelineNodes = pipelines.releasePipelines
43
44
  ?.nodes;
@@ -84,8 +85,8 @@ export function setupReleasesCommands(program) {
84
85
  .option("--pipeline <pipeline>", "filter by pipeline name or ID")
85
86
  .option("-l, --limit <number>", "limit results", "25")
86
87
  .action(handleAsyncCommand(async (options, command) => {
87
- const rootOpts = command.parent.parent.opts();
88
- const graphQLService = createGraphQLService(rootOpts);
88
+ const rootOpts = getRootOpts(command);
89
+ const graphQLService = await createGraphQLService(rootOpts);
89
90
  const filter = {};
90
91
  if (options.pipeline) {
91
92
  filter.pipeline = { name: { eqIgnoreCase: options.pipeline } };
@@ -102,8 +103,8 @@ export function setupReleasesCommands(program) {
102
103
  .command("read <releaseId>")
103
104
  .description("Get release details including linked documents")
104
105
  .action(handleAsyncCommand(async (releaseId, _options, command) => {
105
- const rootOpts = command.parent.parent.opts();
106
- const graphQLService = createGraphQLService(rootOpts);
106
+ const rootOpts = getRootOpts(command);
107
+ const graphQLService = await createGraphQLService(rootOpts);
107
108
  const result = await graphQLService.rawRequest(GET_RELEASE_BY_ID_QUERY, { id: releaseId });
108
109
  if (!result.release) {
109
110
  throw new Error(`Release "${releaseId}" not found`);
@@ -122,8 +123,8 @@ export function setupReleasesCommands(program) {
122
123
  .command("pipelines")
123
124
  .description("List release pipelines and their stages")
124
125
  .action(handleAsyncCommand(async (_options, command) => {
125
- const rootOpts = command.parent.parent.opts();
126
- const graphQLService = createGraphQLService(rootOpts);
126
+ const rootOpts = getRootOpts(command);
127
+ const graphQLService = await createGraphQLService(rootOpts);
127
128
  const result = await graphQLService.rawRequest(GET_RELEASE_PIPELINES_QUERY, { first: 50 });
128
129
  const data = (result.releasePipelines
129
130
  ?.nodes ?? []).map((p) => ({
@@ -2,6 +2,7 @@ import { resolveTeam, resolveUserDisplayName } from "../config/resolver.js";
2
2
  import { SEMANTIC_SEARCH_QUERY } from "../queries/search.js";
3
3
  import { createGraphQLService } from "../utils/graphql-service.js";
4
4
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
5
+ import { getRootOpts } from "../utils/root-opts.js";
5
6
  const TEMPLATES_QUERY = `
6
7
  query {
7
8
  templates {
@@ -137,8 +138,8 @@ export function setupSearchCommands(program) {
137
138
  .option("--team <team>", "filter issue results by team key")
138
139
  .option("-l, --limit <number>", "max results", "10")
139
140
  .action(handleAsyncCommand(async (query, options, command) => {
140
- const rootOpts = command.parent.opts();
141
- const graphQLService = createGraphQLService(rootOpts);
141
+ const rootOpts = getRootOpts(command);
142
+ const graphQLService = await createGraphQLService(rootOpts);
142
143
  const limit = Number.parseInt(options.limit, 10);
143
144
  const requestedTypes = options.type
144
145
  ? options.type.split(",").map((t) => t.trim().toLowerCase())