@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
@@ -1,8 +1,8 @@
1
- import { getApiToken } from "../utils/auth.js";
2
- import { FileService } from "../utils/file-service.js";
1
+ import { createFileService } from "../utils/file-service.js";
3
2
  import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
4
3
  import { createLinearService } from "../utils/linear-service.js";
5
4
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
5
+ import { getRootOpts } from "../utils/root-opts.js";
6
6
  export function setupAttachmentsCommands(program) {
7
7
  const attachments = program
8
8
  .command("attachments")
@@ -14,16 +14,15 @@ export function setupAttachmentsCommands(program) {
14
14
  .requiredOption("--file <path>", "path to file to upload")
15
15
  .option("--title <title>", "attachment title (defaults to filename)")
16
16
  .action(handleAsyncCommand(async (issueId, options, command) => {
17
- const rootOpts = command.parent.parent.opts();
18
- const apiToken = getApiToken(rootOpts);
19
- const linearService = createLinearService(rootOpts);
17
+ const rootOpts = getRootOpts(command);
18
+ const linearService = await createLinearService(rootOpts);
20
19
  const resolvedIssueId = await linearService.resolveIssueId(issueId);
21
- const fileService = new FileService(apiToken);
20
+ const fileService = await createFileService(rootOpts);
22
21
  const uploadResult = await fileService.uploadFile(options.file);
23
22
  if (!uploadResult.success) {
24
23
  throw new Error(uploadResult.error);
25
24
  }
26
- const attachmentsService = createGraphQLAttachmentsService(rootOpts);
25
+ const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
27
26
  const attachment = await attachmentsService.createAttachment({
28
27
  issueId: resolvedIssueId,
29
28
  url: uploadResult.assetUrl,
@@ -36,10 +35,10 @@ export function setupAttachmentsCommands(program) {
36
35
  .description("List attachments on an issue.")
37
36
  .option("-l, --limit <number>", "maximum number of attachments", "50")
38
37
  .action(handleAsyncCommand(async (issueId, options, command) => {
39
- const rootOpts = command.parent.parent.opts();
40
- const linearService = createLinearService(rootOpts);
38
+ const rootOpts = getRootOpts(command);
39
+ const linearService = await createLinearService(rootOpts);
41
40
  const resolvedIssueId = await linearService.resolveIssueId(issueId);
42
- const attachmentsService = createGraphQLAttachmentsService(rootOpts);
41
+ const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
43
42
  const allAttachments = await attachmentsService.listAttachments(resolvedIssueId);
44
43
  const limit = Number.parseInt(options.limit, 10);
45
44
  const data = allAttachments.slice(0, limit);
@@ -49,8 +48,8 @@ export function setupAttachmentsCommands(program) {
49
48
  .command("delete <attachmentId>")
50
49
  .description("Delete an attachment.")
51
50
  .action(handleAsyncCommand(async (attachmentId, _options, command) => {
52
- const rootOpts = command.parent.parent.opts();
53
- const attachmentsService = createGraphQLAttachmentsService(rootOpts);
51
+ const rootOpts = getRootOpts(command);
52
+ const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
54
53
  await attachmentsService.deleteAttachment(attachmentId);
55
54
  outputSuccess({ success: true, message: "Attachment deleted" });
56
55
  }));
@@ -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, outputWarning, } from "../utils/output.js";
7
+ import { getRootOpts } from "../utils/root-opts.js";
7
8
  import { splitList } from "../utils/validators.js";
8
9
  /**
9
10
  * Parse the --filter string into structured search arguments.
@@ -29,8 +30,8 @@ function parseFilterString(filter) {
29
30
  * Returns LinearIssue[] so we can show a preview.
30
31
  */
31
32
  async function resolveTargetIssues(options, rootOpts) {
32
- const graphQLService = createGraphQLService(rootOpts);
33
- const linearService = createLinearService(rootOpts);
33
+ const graphQLService = await createGraphQLService(rootOpts);
34
+ const linearService = await createLinearService(rootOpts);
34
35
  const issuesService = new GraphQLIssuesService(graphQLService, linearService);
35
36
  if (options.issues) {
36
37
  const ids = splitList(options.issues);
@@ -95,7 +96,7 @@ async function executeBatch(issues, operation) {
95
96
  return { results };
96
97
  }
97
98
  async function handleBatchAssign(options, command) {
98
- const rootOpts = command.parent.parent.opts();
99
+ const rootOpts = getRootOpts(command);
99
100
  if (!options.assignee) {
100
101
  throw new Error("--assignee is required.");
101
102
  }
@@ -116,8 +117,8 @@ async function handleBatchAssign(options, command) {
116
117
  });
117
118
  return;
118
119
  }
119
- const graphQLService = createGraphQLService(rootOpts);
120
- const linearService = createLinearService(rootOpts);
120
+ const graphQLService = await createGraphQLService(rootOpts);
121
+ const linearService = await createLinearService(rootOpts);
121
122
  const issuesService = new GraphQLIssuesService(graphQLService, linearService);
122
123
  const { results } = await executeBatch(issues, (issue) => issuesService.updateIssue({ id: issue.identifier, assigneeId }, "adding"));
123
124
  outputSuccess({
@@ -132,7 +133,7 @@ async function handleBatchAssign(options, command) {
132
133
  });
133
134
  }
134
135
  async function handleBatchLabel(options, command) {
135
- const rootOpts = command.parent.parent.opts();
136
+ const rootOpts = getRootOpts(command);
136
137
  if (!(options.add || options.remove)) {
137
138
  throw new Error("Specify --add and/or --remove for labels.");
138
139
  }
@@ -163,8 +164,8 @@ async function handleBatchLabel(options, command) {
163
164
  });
164
165
  return;
165
166
  }
166
- const graphQLService = createGraphQLService(rootOpts);
167
- const linearService = createLinearService(rootOpts);
167
+ const graphQLService = await createGraphQLService(rootOpts);
168
+ const linearService = await createLinearService(rootOpts);
168
169
  const issuesService = new GraphQLIssuesService(graphQLService, linearService);
169
170
  const removeLower = removeLabels.map((l) => l.toLowerCase());
170
171
  const { results } = await executeBatch(issues, (issue) => {
@@ -197,7 +198,7 @@ async function handleBatchLabel(options, command) {
197
198
  });
198
199
  }
199
200
  async function handleBatchMove(options, command) {
200
- const rootOpts = command.parent.parent.opts();
201
+ const rootOpts = getRootOpts(command);
201
202
  if (!options.project) {
202
203
  throw new Error("--project is required.");
203
204
  }
@@ -217,8 +218,8 @@ async function handleBatchMove(options, command) {
217
218
  });
218
219
  return;
219
220
  }
220
- const graphQLService = createGraphQLService(rootOpts);
221
- const linearService = createLinearService(rootOpts);
221
+ const graphQLService = await createGraphQLService(rootOpts);
222
+ const linearService = await createLinearService(rootOpts);
222
223
  const issuesService = new GraphQLIssuesService(graphQLService, linearService);
223
224
  const { results } = await executeBatch(issues, (issue) => issuesService.updateIssue({ id: issue.identifier, projectId: options.project }, "adding"));
224
225
  outputSuccess({
@@ -233,7 +234,7 @@ async function handleBatchMove(options, command) {
233
234
  });
234
235
  }
235
236
  async function handleBatchStatus(options, command) {
236
- const rootOpts = command.parent.parent.opts();
237
+ const rootOpts = getRootOpts(command);
237
238
  if (!options.status) {
238
239
  throw new Error("--status is required.");
239
240
  }
@@ -253,8 +254,8 @@ async function handleBatchStatus(options, command) {
253
254
  });
254
255
  return;
255
256
  }
256
- const graphQLService = createGraphQLService(rootOpts);
257
- const linearService = createLinearService(rootOpts);
257
+ const graphQLService = await createGraphQLService(rootOpts);
258
+ const linearService = await createLinearService(rootOpts);
258
259
  const issuesService = new GraphQLIssuesService(graphQLService, linearService);
259
260
  const { results } = await executeBatch(issues, (issue) => issuesService.updateIssue({ id: issue.identifier, statusId: options.status }, "adding"));
260
261
  outputSuccess({
@@ -2,12 +2,14 @@ import { readFileSync } from "node:fs";
2
2
  import { resolveUserDisplayName } from "../config/resolver.js";
3
3
  import { CREATE_COMMENT_MUTATION, LIST_COMMENTS_QUERY, UPDATE_COMMENT_MUTATION, } from "../queries/comments.js";
4
4
  import { autoLinkReferences, } from "../utils/auto-link-references.js";
5
+ import { applyFooter } from "../utils/footer.js";
5
6
  import { createGraphQLService, } from "../utils/graphql-service.js";
6
7
  import { extractIssueReferences } from "../utils/issue-reference-extractor.js";
7
8
  import { wrapIssueReferencesAsLinks } from "../utils/issue-reference-wrapper.js";
8
9
  import { createLinearService, } from "../utils/linear-service.js";
9
10
  import { resolveMentions } from "../utils/mention-resolver.js";
10
11
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
12
+ import { getRootOpts } from "../utils/root-opts.js";
11
13
  import { validateReferences } from "../utils/validate-references.js";
12
14
  import { getWorkspaceUrlKey } from "../utils/workspace-url.js";
13
15
  // Match Linear's bodyData validation error in multiple phrasings so a wording
@@ -98,10 +100,18 @@ async function autoLinkCommentReferences(args) {
98
100
  return result;
99
101
  }
100
102
  async function handleCreateComment(issueId, options, command) {
101
- const rootOpts = command.parent.parent.opts();
102
- const graphQLService = createGraphQLService(rootOpts);
103
- const linearService = createLinearService(rootOpts);
104
- const rawBody = readBody(options);
103
+ const rootOpts = getRootOpts(command);
104
+ const graphQLService = await createGraphQLService(rootOpts);
105
+ const linearService = await createLinearService(rootOpts);
106
+ // Apply messageFooter (config or --footer flag) before any further
107
+ // processing, so auto-link / mention-resolution see it as part of the body.
108
+ // Commander parses --no-footer as `options.footer === false`.
109
+ const noFooter = options.footer === false;
110
+ const explicitFooter = typeof options.footer === "string" ? options.footer : undefined;
111
+ const rawBody = applyFooter(readBody(options), {
112
+ footer: explicitFooter,
113
+ noFooter,
114
+ }) ?? "";
105
115
  const resolvedIssueId = await linearService.resolveIssueId(issueId);
106
116
  // Wrap valid refs as markdown links before mention resolution. Idempotent against
107
117
  // already-wrapped refs.
@@ -171,9 +181,9 @@ async function handleCreateComment(issueId, options, command) {
171
181
  outputSuccess(autoLinked ? { ...output, autoLinked } : output);
172
182
  }
173
183
  async function handleUpdateComment(commentId, options, command) {
174
- const rootOpts = command.parent.parent.opts();
175
- const graphQLService = createGraphQLService(rootOpts);
176
- const linearService = createLinearService(rootOpts);
184
+ const rootOpts = getRootOpts(command);
185
+ const graphQLService = await createGraphQLService(rootOpts);
186
+ const linearService = await createLinearService(rootOpts);
177
187
  const rawBody = readBody(options);
178
188
  const { body, preResolved } = await prepareCommentBodyWithLinks(rawBody, options, linearService, graphQLService);
179
189
  const autoMention = options.autoMention !== false;
@@ -218,9 +228,9 @@ async function handleUpdateComment(commentId, options, command) {
218
228
  outputSuccess(autoLinked ? { ...output, autoLinked } : output);
219
229
  }
220
230
  async function handleListComments(issueId, options, command) {
221
- const rootOpts = command.parent.parent.opts();
222
- const graphQLService = createGraphQLService(rootOpts);
223
- const linearService = createLinearService(rootOpts);
231
+ const rootOpts = getRootOpts(command);
232
+ const graphQLService = await createGraphQLService(rootOpts);
233
+ const linearService = await createLinearService(rootOpts);
224
234
  const resolvedId = await linearService.resolveIssueId(issueId);
225
235
  const result = await graphQLService.rawRequest(LIST_COMMENTS_QUERY, {
226
236
  issueId: resolvedId,
@@ -254,6 +264,8 @@ export function setupCommentsCommands(program) {
254
264
  .option("--file <path>", "read comment body from file")
255
265
  .option("--no-auto-mention", "do not auto-convert bare team-member names to @mentions")
256
266
  .option("--no-auto-link", "skip wrapping issue refs as markdown links and creating sidebar relations")
267
+ .option("--footer <text>", "text appended to the comment body (overrides config.messageFooter)")
268
+ .option("--no-footer", "skip the configured messageFooter for this comment")
257
269
  .action(handleAsyncCommand(handleCreateComment));
258
270
  comments
259
271
  .command("update <commentId>")
@@ -2,6 +2,7 @@ import { resolveTeam } from "../config/resolver.js";
2
2
  import { invalidParameterError, notFoundError, requiresParameterError, } from "../utils/error-messages.js";
3
3
  import { createLinearService } from "../utils/linear-service.js";
4
4
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
5
+ import { getRootOpts } from "../utils/root-opts.js";
5
6
  export function setupCyclesCommands(program) {
6
7
  const cycles = program
7
8
  .command("cycles")
@@ -19,9 +20,9 @@ export function setupCyclesCommands(program) {
19
20
  if (options.aroundActive && !options.team) {
20
21
  throw requiresParameterError("--around-active", "--team");
21
22
  }
22
- const rootOpts = command.parent.parent.opts();
23
+ const rootOpts = getRootOpts(command);
23
24
  const teamFilter = options.team ? resolveTeam(options.team) : undefined;
24
- const linearService = createLinearService(rootOpts);
25
+ const linearService = await createLinearService(rootOpts);
25
26
  const allCycles = await linearService.getCycles(teamFilter, options.active || undefined, Number.parseInt(options.limit, 10));
26
27
  if (options.aroundActive) {
27
28
  const n = Number.parseInt(options.aroundActive, 10);
@@ -51,11 +52,11 @@ export function setupCyclesCommands(program) {
51
52
  .option("--team <team>", "team key, name, or ID to scope name lookup")
52
53
  .option("--issues-first <n>", "how many issues to fetch (default 50)", "50")
53
54
  .action(handleAsyncCommand(async (cycleIdOrName, options, command) => {
54
- const rootOpts = command.parent.parent.opts();
55
+ const rootOpts = getRootOpts(command);
55
56
  const teamFilter = options.team
56
57
  ? resolveTeam(options.team)
57
58
  : undefined;
58
- const linearService = createLinearService(rootOpts);
59
+ const linearService = await createLinearService(rootOpts);
59
60
  const cycleId = await linearService.resolveCycleId(cycleIdOrName, teamFilter);
60
61
  const cycle = await linearService.getCycleById(cycleId, Number.parseInt(options.issuesFirst || "50", 10));
61
62
  outputSuccess(cycle);
@@ -2,6 +2,7 @@ import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-se
2
2
  import { createGraphQLDocumentsService } from "../utils/graphql-documents-service.js";
3
3
  import { createLinearService } from "../utils/linear-service.js";
4
4
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
5
+ import { getRootOpts } from "../utils/root-opts.js";
5
6
  function extractDocumentIdFromUrl(url) {
6
7
  try {
7
8
  const parsed = new URL(url);
@@ -25,9 +26,9 @@ function extractDocumentIdFromUrl(url) {
25
26
  }
26
27
  }
27
28
  async function handleCreateDocument(options, command) {
28
- const rootOpts = command.parent.parent.opts();
29
- const documentsService = createGraphQLDocumentsService(rootOpts);
30
- const linearService = createLinearService(rootOpts);
29
+ const rootOpts = getRootOpts(command);
30
+ const documentsService = await createGraphQLDocumentsService(rootOpts);
31
+ const linearService = await createLinearService(rootOpts);
31
32
  const document = await documentsService.createDocument({
32
33
  title: options.title,
33
34
  content: options.content,
@@ -44,7 +45,7 @@ async function handleCreateDocument(options, command) {
44
45
  color: options.color,
45
46
  });
46
47
  if (options.attachTo) {
47
- const attachmentsService = createGraphQLAttachmentsService(rootOpts);
48
+ const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
48
49
  const issueId = await linearService.resolveIssueId(options.attachTo);
49
50
  try {
50
51
  await attachmentsService.createAttachment({
@@ -66,15 +67,15 @@ async function handleListDocuments(options, command) {
66
67
  if (options.project && options.issue) {
67
68
  throw new Error("Cannot use --project and --issue together. Choose one filter.");
68
69
  }
69
- const rootOpts = command.parent.parent.opts();
70
- const documentsService = createGraphQLDocumentsService(rootOpts);
71
- const linearService = createLinearService(rootOpts);
70
+ const rootOpts = getRootOpts(command);
71
+ const documentsService = await createGraphQLDocumentsService(rootOpts);
72
+ const linearService = await createLinearService(rootOpts);
72
73
  const limit = Number.parseInt(options.limit || "50", 10);
73
74
  if (Number.isNaN(limit) || limit < 1) {
74
75
  throw new Error(`Invalid limit "${options.limit}": must be a positive number`);
75
76
  }
76
77
  if (options.issue) {
77
- const attachmentsService = createGraphQLAttachmentsService(rootOpts);
78
+ const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
78
79
  const issueId = await linearService.resolveIssueId(options.issue);
79
80
  const attachments = await attachmentsService.listAttachments(issueId);
80
81
  const documentSlugIds = [
@@ -129,9 +130,9 @@ export function setupDocumentsCommands(program) {
129
130
  .option("--icon <icon>", "document icon")
130
131
  .option("--color <color>", "icon color")
131
132
  .action(handleAsyncCommand(async (documentId, options, command) => {
132
- const rootOpts = command.parent.parent.opts();
133
- const documentsService = createGraphQLDocumentsService(rootOpts);
134
- const linearService = createLinearService(rootOpts);
133
+ const rootOpts = getRootOpts(command);
134
+ const documentsService = await createGraphQLDocumentsService(rootOpts);
135
+ const linearService = await createLinearService(rootOpts);
135
136
  const input = {};
136
137
  if (options.title) {
137
138
  input.title = options.title;
@@ -154,8 +155,9 @@ export function setupDocumentsCommands(program) {
154
155
  .command("read <documentId>")
155
156
  .description("Read a document")
156
157
  .action(handleAsyncCommand(async (documentId, _options, command) => {
157
- const rootOpts = command.parent.parent.opts();
158
- outputSuccess(await createGraphQLDocumentsService(rootOpts).getDocument(documentId));
158
+ const rootOpts = getRootOpts(command);
159
+ const documentsService = await createGraphQLDocumentsService(rootOpts);
160
+ outputSuccess(await documentsService.getDocument(documentId));
159
161
  }));
160
162
  documents
161
163
  .command("list")
@@ -168,8 +170,9 @@ export function setupDocumentsCommands(program) {
168
170
  .command("delete <documentId>")
169
171
  .description("Delete (trash) a document")
170
172
  .action(handleAsyncCommand(async (documentId, _options, command) => {
171
- const rootOpts = command.parent.parent.opts();
172
- await createGraphQLDocumentsService(rootOpts).deleteDocument(documentId);
173
+ const rootOpts = getRootOpts(command);
174
+ const documentsService = await createGraphQLDocumentsService(rootOpts);
175
+ await documentsService.deleteDocument(documentId);
173
176
  outputSuccess({ success: true, message: "Document moved to trash" });
174
177
  }));
175
178
  }
@@ -1,6 +1,6 @@
1
- import { getApiToken } from "../utils/auth.js";
2
- import { FileService } from "../utils/file-service.js";
1
+ import { createFileService } from "../utils/file-service.js";
3
2
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
3
+ import { getRootOpts } from "../utils/root-opts.js";
4
4
  export function setupEmbedsCommands(program) {
5
5
  const embeds = program
6
6
  .command("embeds")
@@ -12,8 +12,7 @@ export function setupEmbedsCommands(program) {
12
12
  .option("--output <path>", "output file path")
13
13
  .option("--overwrite", "overwrite existing file", false)
14
14
  .action(handleAsyncCommand(async (url, options, command) => {
15
- const apiToken = getApiToken(command.parent.parent.opts());
16
- const fileService = new FileService(apiToken);
15
+ const fileService = await createFileService(getRootOpts(command));
17
16
  const result = await fileService.downloadFile(url, {
18
17
  output: options.output,
19
18
  overwrite: options.overwrite,
@@ -40,8 +39,7 @@ export function setupEmbedsCommands(program) {
40
39
  .command("upload <file>")
41
40
  .description("Upload a file to Linear storage.")
42
41
  .action(handleAsyncCommand(async (filePath, _options, command) => {
43
- const apiToken = getApiToken(command.parent.parent.opts());
44
- const fileService = new FileService(apiToken);
42
+ const fileService = await createFileService(getRootOpts(command));
45
43
  const result = await fileService.uploadFile(filePath);
46
44
  if (result.success) {
47
45
  outputSuccess({
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import { INTROSPECT_ROOT_QUERY, INTROSPECT_TYPE_QUERY, } from "../queries/introspect.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
  async function executeQuery(query, options, command) {
6
7
  let finalQuery = query;
7
8
  if (options.file) {
@@ -22,8 +23,8 @@ async function executeQuery(query, options, command) {
22
23
  throw new Error(`Invalid JSON in --variables: ${options.variables}`);
23
24
  }
24
25
  }
25
- const rootOpts = command.parent.opts();
26
- const graphQLService = createGraphQLService(rootOpts);
26
+ const rootOpts = getRootOpts(command);
27
+ const graphQLService = await createGraphQLService(rootOpts);
27
28
  const result = await graphQLService.rawRequest(finalQuery, variables);
28
29
  outputSuccess(result);
29
30
  }
@@ -41,8 +42,8 @@ export function setupGraphQLCommands(program) {
41
42
  .description("Introspect the Linear GraphQL schema. Without args lists root query fields; with a type name shows its fields and args.")
42
43
  .option("--filter <pattern>", "filter fields by name (case-insensitive substring match)")
43
44
  .action(handleAsyncCommand(async (typeName, options, command) => {
44
- const rootOpts = command.parent.opts();
45
- const graphQLService = createGraphQLService(rootOpts);
45
+ const rootOpts = getRootOpts(command);
46
+ const graphQLService = await createGraphQLService(rootOpts);
46
47
  if (typeName) {
47
48
  const result = await graphQLService.rawRequest(INTROSPECT_TYPE_QUERY, { typeName });
48
49
  const typeInfo = result.__type;
@@ -159,13 +159,11 @@ export async function runAliasesStep(token, existing, options = {}) {
159
159
  default: false,
160
160
  });
161
161
  if (!proceed) {
162
- // biome-ignore lint/suspicious/noConsole: wizard
163
162
  console.log(" Skipped — run `el-linear init aliases` to add or edit aliases later.");
164
163
  return new Map();
165
164
  }
166
165
  const allUsers = await fetchAllUsers(token);
167
166
  if (allUsers.length === 0) {
168
- // biome-ignore lint/suspicious/noConsole: wizard
169
167
  console.log(" No active users visible to this token.");
170
168
  return new Map();
171
169
  }
@@ -176,25 +174,20 @@ export async function runAliasesStep(token, existing, options = {}) {
176
174
  const collisions = findDisplayNameCollisions(allUsers);
177
175
  const collidingIds = new Set();
178
176
  if (collisions.size > 0) {
179
- // biome-ignore lint/suspicious/noConsole: wizard
180
177
  console.log(` ⚠ ${collisions.size} display name(s) are shared by multiple active users — skipping those to avoid alias corruption:`);
181
178
  for (const [name, group] of collisions) {
182
- // biome-ignore lint/suspicious/noConsole: wizard
183
179
  console.log(` "${name}" — ${group.map((u) => u.email ?? u.id).join(", ")}`);
184
180
  for (const u of group)
185
181
  collidingIds.add(u.id);
186
182
  }
187
- // biome-ignore lint/suspicious/noConsole: wizard
188
183
  console.log(" (Add aliases manually in ~/.config/el-linear/config.json for now; schema migration in 1.3.0.)");
189
184
  }
190
185
  const users = allUsers.filter((u) => !collidingIds.has(u.id));
191
186
  if (users.length === 0) {
192
- // biome-ignore lint/suspicious/noConsole: wizard
193
187
  console.log(" No safe-to-alias users remain after collision filter.");
194
188
  return new Map();
195
189
  }
196
190
  const startIdx = await resolveResumePoint(users);
197
- // biome-ignore lint/suspicious/noConsole: wizard
198
191
  console.log(` Walking ${users.length - startIdx}/${users.length} users. Type 'q' at any prompt to stop and save progress.`);
199
192
  const updates = new Map();
200
193
  let lastCompletedIdx = startIdx - 1;
@@ -213,7 +206,6 @@ export async function runAliasesStep(token, existing, options = {}) {
213
206
  const update = await promptForUser(i + 1, users.length, u, existing);
214
207
  if (update === "quit") {
215
208
  await saveProgress();
216
- // biome-ignore lint/suspicious/noConsole: wizard
217
209
  console.log(` Saved progress at ${lastCompletedIdx + 1}/${users.length}. Resume with \`el-linear init aliases\`.`);
218
210
  return updates;
219
211
  }
@@ -225,7 +217,6 @@ export async function runAliasesStep(token, existing, options = {}) {
225
217
  lastCompletedIdx = i;
226
218
  }
227
219
  await clearAliasesProgress();
228
- // biome-ignore lint/suspicious/noConsole: wizard
229
220
  console.log(` ✓ Walked all ${users.length} users.`);
230
221
  return updates;
231
222
  }
@@ -253,7 +244,6 @@ async function resolveResumePoint(users) {
253
244
  return 0;
254
245
  const idx = users.findIndex((u) => u.id === progress.lastCompletedUserId);
255
246
  if (idx < 0) {
256
- // biome-ignore lint/suspicious/noConsole: wizard
257
247
  console.log(" Saved progress points at a user no longer in the workspace. Starting over.");
258
248
  await clearAliasesProgress();
259
249
  return 0;
@@ -272,18 +262,14 @@ async function promptForUser(index, total, user, existing) {
272
262
  const currentAliases = currentAliasesFor(existing, fullName);
273
263
  const currentGithub = currentHandleFor(existing, "github", fullName);
274
264
  const currentGitlab = currentHandleFor(existing, "gitlab", fullName);
275
- // biome-ignore lint/suspicious/noConsole: wizard
276
265
  console.log(`\n [${index}/${total}] ${fullName}${user.email ? ` <${user.email}>` : ""}`);
277
266
  if (currentAliases.length > 0) {
278
- // biome-ignore lint/suspicious/noConsole: wizard
279
267
  console.log(` Current aliases: ${currentAliases.join(", ")}`);
280
268
  }
281
269
  if (currentGithub) {
282
- // biome-ignore lint/suspicious/noConsole: wizard
283
270
  console.log(` Current GitHub: ${currentGithub}`);
284
271
  }
285
272
  if (currentGitlab) {
286
- // biome-ignore lint/suspicious/noConsole: wizard
287
273
  console.log(` Current GitLab: ${currentGitlab}`);
288
274
  }
289
275
  const action = await select({
@@ -19,7 +19,6 @@ export async function runDefaultsStep(existing) {
19
19
  };
20
20
  // ── Default labels ────────────────────────────────────────────────
21
21
  const currentLabels = existing.defaultLabels ?? [];
22
- // biome-ignore lint/suspicious/noConsole: wizard
23
22
  console.log(` Current default labels: ${currentLabels.length > 0 ? currentLabels.join(", ") : "(none)"}`);
24
23
  const editLabels = await confirm({
25
24
  message: "Change default labels for new issues?",
@@ -35,7 +34,6 @@ export async function runDefaultsStep(existing) {
35
34
  }
36
35
  // ── Status defaults ────────────────────────────────────────────────
37
36
  const cur = existing.statusDefaults;
38
- // biome-ignore lint/suspicious/noConsole: wizard
39
37
  console.log(` Current status defaults: noProject=${cur?.noProject ?? STATUS_FALLBACK.noProject}, ` +
40
38
  `withAssigneeAndProject=${cur?.withAssigneeAndProject ?? STATUS_FALLBACK.withAssigneeAndProject}`);
41
39
  const editStatus = await confirm({
@@ -58,10 +56,8 @@ export async function runDefaultsStep(existing) {
58
56
  }
59
57
  // ── Term enforcement ───────────────────────────────────────────────
60
58
  const currentTerms = existing.terms ?? [];
61
- // biome-ignore lint/suspicious/noConsole: wizard
62
59
  console.log(` Current term-enforcement rules: ${currentTerms.length}`);
63
60
  for (const t of currentTerms) {
64
- // biome-ignore lint/suspicious/noConsole: wizard
65
61
  console.log(` "${t.canonical}" rejects: ${t.reject.join(", ")}`);
66
62
  }
67
63
  const editTerms = await confirm({
@@ -77,7 +73,6 @@ export async function runDefaultsStep(existing) {
77
73
  }
78
74
  async function collectTermsInteractively(initial) {
79
75
  const terms = [...initial];
80
- // biome-ignore lint/suspicious/noConsole: wizard
81
76
  console.log(" Term enforcement catches misspellings of brand or product names in issue titles " +
82
77
  "and descriptions. Define the canonical form and a list of rejected variants.");
83
78
  if (terms.length > 0) {
@@ -102,7 +97,6 @@ async function collectTermsInteractively(initial) {
102
97
  });
103
98
  const reject = parseCsvList(rejectRaw);
104
99
  if (reject.length === 0) {
105
- // biome-ignore lint/suspicious/noConsole: wizard
106
100
  console.log(" No variants given — skipping this rule.");
107
101
  continue;
108
102
  }
@@ -33,7 +33,6 @@ function withCleanExit(fn) {
33
33
  }
34
34
  catch (err) {
35
35
  if (err instanceof Error && err.name === "ExitPromptError") {
36
- // biome-ignore lint/suspicious/noConsole: wizard
37
36
  console.log("\n Cancelled.");
38
37
  process.exit(130);
39
38
  }
@@ -94,7 +93,6 @@ export function setupInitCommands(program) {
94
93
  workspaceUrlKey: existing.workspaceUrlKey ?? ws.workspaceUrlKey,
95
94
  });
96
95
  await writeConfig(merged);
97
- // biome-ignore lint/suspicious/noConsole: wizard
98
96
  console.log(" ✓ Workspace defaults saved.");
99
97
  }));
100
98
  init
@@ -111,7 +109,6 @@ export function setupInitCommands(program) {
111
109
  const result = await runAliasesImport(tokenResult.token, options.import);
112
110
  updates = result.updates;
113
111
  if (result.skipped.length > 0) {
114
- // biome-ignore lint/suspicious/noConsole: wizard
115
112
  console.log(` Skipped ${result.skipped.length} unmatched email(s): ${result.skipped.join(", ")}`);
116
113
  }
117
114
  }
@@ -121,13 +118,11 @@ export function setupInitCommands(program) {
121
118
  });
122
119
  }
123
120
  if (updates.size === 0) {
124
- // biome-ignore lint/suspicious/noConsole: wizard
125
121
  console.log(" No alias changes — config unchanged.");
126
122
  return;
127
123
  }
128
124
  const merged = mergeAliasesIntoConfig(existing, updates);
129
125
  await writeConfig(merged);
130
- // biome-ignore lint/suspicious/noConsole: wizard
131
126
  console.log(` ✓ Updated aliases for ${updates.size} user(s).`);
132
127
  }));
133
128
  init
@@ -143,7 +138,6 @@ export function setupInitCommands(program) {
143
138
  terms: result.terms,
144
139
  });
145
140
  await writeConfig(merged);
146
- // biome-ignore lint/suspicious/noConsole: wizard
147
141
  console.log(" ✓ Defaults saved.");
148
142
  }));
149
143
  }
@@ -156,9 +150,7 @@ export async function runFullWizard(options = {}) {
156
150
  await runFullWizardImpl(opts);
157
151
  }
158
152
  async function runFullWizardImpl(options) {
159
- // biome-ignore lint/suspicious/noConsole: wizard
160
153
  console.log("Welcome to el-linear. This wizard will set up your config.");
161
- // biome-ignore lint/suspicious/noConsole: wizard
162
154
  console.log("Skip is the default at every prompt; only the API token is required.\n");
163
155
  const existing = await readConfig();
164
156
  // Step 1: token (required)
@@ -197,12 +189,8 @@ async function runFullWizardImpl(options) {
197
189
  merged = mergeAliasesIntoConfig(merged, aliasUpdates);
198
190
  }
199
191
  await writeConfig(merged);
200
- // biome-ignore lint/suspicious/noConsole: wizard
201
192
  console.log("\n✓ Setup complete.");
202
- // biome-ignore lint/suspicious/noConsole: wizard
203
193
  console.log(" Token: ~/.config/el-linear/token (mode 0600)");
204
- // biome-ignore lint/suspicious/noConsole: wizard
205
194
  console.log(" Config: ~/.config/el-linear/config.json");
206
- // biome-ignore lint/suspicious/noConsole: wizard
207
195
  console.log("\nTry: el-linear teams list");
208
196
  }
@@ -174,7 +174,6 @@ export async function clearAliasesProgress() {
174
174
  * Print a step header in the wizard. Step strings like "1/4" or "2/4 (skipped)".
175
175
  */
176
176
  export function printStep(label, title) {
177
- // biome-ignore lint/suspicious/noConsole: wizard output is meant for stdout
178
177
  console.log(`\n[${label}] ${title}`);
179
178
  }
180
179
  /**
@@ -91,7 +91,6 @@ export async function runTokenStep(options = {}) {
91
91
  if (existing && !options.force) {
92
92
  try {
93
93
  const viewer = await validateToken(existing);
94
- // biome-ignore lint/suspicious/noConsole: wizard
95
94
  console.log(` Existing token works — authenticated as ${viewer.displayName} <${viewer.email}>.`);
96
95
  const replace = await confirm({
97
96
  message: "Replace this token?",
@@ -104,13 +103,10 @@ export async function runTokenStep(options = {}) {
104
103
  catch (err) {
105
104
  const raw = err instanceof Error ? err.message : String(err);
106
105
  const message = sanitizeForLog(raw);
107
- // biome-ignore lint/suspicious/noConsole: wizard
108
106
  console.log(` Existing token failed validation (${message}). You'll need to provide a new one.`);
109
107
  }
110
108
  }
111
- // biome-ignore lint/suspicious/noConsole: wizard
112
109
  console.log(` Generate a personal API token: ${TOKEN_GENERATION_URL}`);
113
- // biome-ignore lint/suspicious/noConsole: wizard
114
110
  console.log(" The token stays on your machine. el-linear only sends it to Linear's API. " +
115
111
  "It's stored at ~/.config/el-linear/token (mode 0600).");
116
112
  // Up to three attempts before giving up.
@@ -123,17 +119,14 @@ export async function runTokenStep(options = {}) {
123
119
  validate: (input) => input.trim().length > 0 || "Token cannot be empty",
124
120
  })).trim();
125
121
  try {
126
- // biome-ignore lint/suspicious/noConsole: wizard
127
122
  console.log(" Validating…");
128
123
  const viewer = await validateToken(token);
129
124
  await writeToken(token);
130
- // biome-ignore lint/suspicious/noConsole: wizard
131
125
  console.log(` ✓ Authenticated as ${viewer.displayName} <${viewer.email}> (${viewer.organization.name}).`);
132
126
  return { token, viewer };
133
127
  }
134
128
  catch (err) {
135
129
  const raw = err instanceof Error ? err.message : String(err);
136
- // biome-ignore lint/suspicious/noConsole: wizard
137
130
  console.log(` ✗ ${sanitizeForLog(raw)}`);
138
131
  }
139
132
  }