@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,5 +1,6 @@
1
1
  import { createLinearService } from "../utils/linear-service.js";
2
2
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
3
+ import { getRootOpts } from "../utils/root-opts.js";
3
4
  export function setupTeamsCommands(program) {
4
5
  const teams = program
5
6
  .command("teams")
@@ -11,8 +12,8 @@ export function setupTeamsCommands(program) {
11
12
  .description("List all teams")
12
13
  .option("-l, --limit <number>", "limit results", "100")
13
14
  .action(handleAsyncCommand(async (options, command) => {
14
- const rootOpts = command.parent.parent.opts();
15
- const service = createLinearService(rootOpts);
15
+ const rootOpts = getRootOpts(command);
16
+ const service = await createLinearService(rootOpts);
16
17
  const result = await service.getTeams(Number.parseInt(options.limit, 10));
17
18
  outputSuccess({ data: result, meta: { count: result.length } });
18
19
  }));
@@ -1,6 +1,7 @@
1
1
  import { TEMPLATE_BY_ID_QUERY, TEMPLATES_LIST_QUERY, } from "../queries/templates.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
  import { parsePositiveInt } from "../utils/validators.js";
5
6
  function formatTemplateSummary(t) {
6
7
  return {
@@ -25,8 +26,8 @@ export function setupTemplatesCommands(program) {
25
26
  .option("--type <type>", "filter by type (issue, document, project, recurringIssue)")
26
27
  .option("-l, --limit <number>", "limit results", "50")
27
28
  .action(handleAsyncCommand(async (options, command) => {
28
- const rootOpts = command.parent.parent.opts();
29
- const graphQLService = createGraphQLService(rootOpts);
29
+ const rootOpts = getRootOpts(command);
30
+ const graphQLService = await createGraphQLService(rootOpts);
30
31
  const limit = parsePositiveInt(options.limit, "--limit");
31
32
  const result = await graphQLService.rawRequest(TEMPLATES_LIST_QUERY);
32
33
  let items = result.templates ?? [];
@@ -44,8 +45,8 @@ export function setupTemplatesCommands(program) {
44
45
  .command("read <templateId>")
45
46
  .description("Read a template including its full content (templateData)")
46
47
  .action(handleAsyncCommand(async (templateId, _options, command) => {
47
- const rootOpts = command.parent.parent.opts();
48
- const graphQLService = createGraphQLService(rootOpts);
48
+ const rootOpts = getRootOpts(command);
49
+ const graphQLService = await createGraphQLService(rootOpts);
49
50
  const result = await graphQLService.rawRequest(TEMPLATE_BY_ID_QUERY, {
50
51
  id: templateId,
51
52
  });
@@ -1,5 +1,6 @@
1
1
  import { createLinearService } from "../utils/linear-service.js";
2
2
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
3
+ import { getRootOpts } from "../utils/root-opts.js";
3
4
  export function setupUsersCommands(program) {
4
5
  const users = program.command("users").description("User operations");
5
6
  users.action(() => users.help());
@@ -9,8 +10,8 @@ export function setupUsersCommands(program) {
9
10
  .option("--active", "Only show active users")
10
11
  .option("-l, --limit <number>", "limit results", "100")
11
12
  .action(handleAsyncCommand(async (options, command) => {
12
- const rootOpts = command.parent.parent.opts();
13
- const service = createLinearService(rootOpts);
13
+ const rootOpts = getRootOpts(command);
14
+ const service = await createLinearService(rootOpts);
14
15
  const result = await service.getUsers(options.active, Number.parseInt(options.limit, 10));
15
16
  outputSuccess({ data: result, meta: { count: result.length } });
16
17
  }));
@@ -39,6 +39,24 @@ export interface ElLinearConfig {
39
39
  * once per session and caches the result.
40
40
  */
41
41
  workspaceUrlKey?: string;
42
+ /**
43
+ * Text appended to issue descriptions and comment bodies on the `create`
44
+ * paths. Treated as a literal string — include any `\n\n---\n` separator
45
+ * yourself if you want a horizontal rule. CLI flags `--footer <text>` and
46
+ * `--no-footer` override this per-invocation.
47
+ */
48
+ messageFooter?: string;
49
+ /**
50
+ * Named description boilerplates for `el-linear issues create --template <name>`.
51
+ * The template's value is used as the description body when `--template` is
52
+ * provided and neither `--description` nor `--description-file` is set.
53
+ *
54
+ * Example:
55
+ * "descriptionTemplates": {
56
+ * "bug": "## Steps to reproduce\n\n1. ...\n\n## Expected\n\n...\n\n## Actual\n\n..."
57
+ * }
58
+ */
59
+ descriptionTemplates?: Record<string, string>;
42
60
  }
43
61
  /** Test seam — resets the cache between test cases. */
44
62
  export declare function _resetConfigCacheForTests(): void;
@@ -210,7 +210,7 @@ function checkTitleVerbAlignment(title, typeLabel, result) {
210
210
  for (const verb of typeVerbs) {
211
211
  if (!verb.includes(" "))
212
212
  continue;
213
- if (title.toLowerCase().startsWith(verb.toLowerCase() + " ") ||
213
+ if (title.toLowerCase().startsWith(`${verb.toLowerCase()} `) ||
214
214
  title.toLowerCase() === verb.toLowerCase()) {
215
215
  if (type === typeLabel)
216
216
  return; // match — all good
@@ -91,7 +91,7 @@ export function resolveMember(input) {
91
91
  */
92
92
  export async function resolveAssignee(input, rootOpts) {
93
93
  if (input.toLowerCase() === "me") {
94
- const graphQLService = createGraphQLService(rootOpts);
94
+ const graphQLService = await createGraphQLService(rootOpts);
95
95
  const result = await graphQLService.rawRequest("{ viewer { id } }");
96
96
  const viewer = result.viewer;
97
97
  if (!viewer?.id) {
package/dist/main.js CHANGED
@@ -30,7 +30,7 @@ import { splitList } from "./utils/validators.js";
30
30
  program
31
31
  .name("el-linear")
32
32
  .description("A pragmatic CLI for Linear.app — deterministic resolution, structured validation, GraphQL escape hatch.")
33
- .version("1.5.0")
33
+ .version("1.6.0")
34
34
  .option("--api-token <token>", "Linear API token")
35
35
  .option("--profile <name>", "named profile (under ~/.config/el-linear/profiles/<name>/) for this invocation. Overrides EL_LINEAR_PROFILE env + the on-disk active-profile marker.")
36
36
  .option("--json", "output as JSON (default, accepted for compatibility)")
@@ -1,7 +1,8 @@
1
1
  import type { LinearIssue } from "../types/linear.js";
2
+ import type { FileService } from "./file-service.js";
2
3
  /**
3
4
  * Downloads all uploads.linear.app URLs found in an issue's description
4
5
  * and comments, replacing them with local file paths so Claude Code
5
6
  * can read the images directly.
6
7
  */
7
- export declare function downloadLinearUploads(issue: LinearIssue, apiToken: string): Promise<LinearIssue>;
8
+ export declare function downloadLinearUploads(issue: LinearIssue, fileService: FileService): Promise<LinearIssue>;
@@ -1,7 +1,6 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { FileService } from "./file-service.js";
5
4
  const DOWNLOAD_DIR = join(tmpdir(), "el-linear-downloads");
6
5
  const UPLOAD_URL_REGEX = /https:\/\/uploads\.linear\.app\/[^\s)>\]"]+/g;
7
6
  function collectUploadUrls(text) {
@@ -20,7 +19,7 @@ function replaceUrls(text, urlMap) {
20
19
  * and comments, replacing them with local file paths so Claude Code
21
20
  * can read the images directly.
22
21
  */
23
- export async function downloadLinearUploads(issue, apiToken) {
22
+ export async function downloadLinearUploads(issue, fileService) {
24
23
  const allUrls = new Set();
25
24
  if (issue.description) {
26
25
  for (const url of collectUploadUrls(issue.description)) {
@@ -38,7 +37,6 @@ export async function downloadLinearUploads(issue, apiToken) {
38
37
  return issue;
39
38
  }
40
39
  await mkdir(DOWNLOAD_DIR, { recursive: true });
41
- const fileService = new FileService(apiToken);
42
40
  const urlMap = new Map();
43
41
  await Promise.all([...allUrls].map(async (url) => {
44
42
  const filename = extractUniqueFilename(url);
@@ -78,7 +76,7 @@ function extractUniqueFilename(url) {
78
76
  // Path like /workspace-id/uuid1/uuid2/filename.png — take last two parts for uniqueness
79
77
  const parts = urlObj.pathname.split("/").filter(Boolean);
80
78
  if (parts.length >= 2) {
81
- return `${parts.at(-2).slice(0, 8)}-${parts.at(-1)}`;
79
+ return `${parts.at(-2)?.slice(0, 8)}-${parts.at(-1) ?? ""}`;
82
80
  }
83
81
  return parts.at(-1) || "download";
84
82
  }
@@ -1,7 +1,22 @@
1
1
  import type { FileDownloadResult, FileUploadResult } from "../types/linear.js";
2
+ /**
3
+ * Constructor arg shapes for `FileService`:
4
+ * - `string` → personal API token (legacy; sent as `Authorization: <token>`).
5
+ * - `{apiKey: string}` → personal API token (explicit).
6
+ * - `{oauthToken: string}` → OAuth access token (sent as
7
+ * `Authorization: Bearer <token>`).
8
+ *
9
+ * The string variant exists because dozens of tests construct `FileService`
10
+ * with a plain string. We continue to support it indefinitely.
11
+ */
12
+ export type FileServiceAuth = string | {
13
+ apiKey: string;
14
+ } | {
15
+ oauthToken: string;
16
+ };
2
17
  export declare class FileService {
3
- private readonly apiToken;
4
- constructor(apiToken: string);
18
+ private readonly authHeader;
19
+ constructor(auth: FileServiceAuth);
5
20
  downloadFile(url: string, options?: {
6
21
  output?: string;
7
22
  overwrite?: boolean;
@@ -11,3 +26,12 @@ export declare class FileService {
11
26
  private requestUploadUrl;
12
27
  private putFileToUrl;
13
28
  }
29
+ import type { AuthOptions } from "./auth.js";
30
+ /**
31
+ * OAuth-aware factory: resolves the active credential (with auto-refresh
32
+ * for OAuth tokens) and returns a `FileService` configured with the right
33
+ * `Authorization` header shape. Personal tokens use `Authorization:
34
+ * <token>` (no Bearer prefix); OAuth tokens use `Authorization: Bearer
35
+ * <token>`.
36
+ */
37
+ export declare function createFileService(options: AuthOptions): Promise<FileService>;
@@ -37,10 +37,17 @@ function getMimeType(filePath) {
37
37
  const ext = extname(filePath).toLowerCase();
38
38
  return MIME_TYPES[ext] || "application/octet-stream";
39
39
  }
40
+ function buildAuthHeader(auth) {
41
+ if (typeof auth === "string")
42
+ return auth;
43
+ if ("oauthToken" in auth)
44
+ return `Bearer ${auth.oauthToken}`;
45
+ return auth.apiKey;
46
+ }
40
47
  export class FileService {
41
- apiToken;
42
- constructor(apiToken) {
43
- this.apiToken = apiToken;
48
+ authHeader;
49
+ constructor(auth) {
50
+ this.authHeader = buildAuthHeader(auth);
44
51
  }
45
52
  async downloadFile(url, options = {}) {
46
53
  if (!isLinearUploadUrl(url)) {
@@ -67,7 +74,7 @@ export class FileService {
67
74
  const isSignedUrl = urlObj.searchParams.has("signature");
68
75
  const headers = {};
69
76
  if (!isSignedUrl) {
70
- headers.Authorization = this.apiToken;
77
+ headers.Authorization = this.authHeader;
71
78
  }
72
79
  const response = await fetch(url, { method: "GET", headers });
73
80
  if (!response.ok) {
@@ -162,7 +169,7 @@ export class FileService {
162
169
  method: "POST",
163
170
  headers: {
164
171
  "Content-Type": "application/json",
165
- Authorization: this.apiToken,
172
+ Authorization: this.authHeader,
166
173
  },
167
174
  body: JSON.stringify({
168
175
  query,
@@ -237,3 +244,18 @@ export class FileService {
237
244
  return { success: true, assetUrl: meta.assetUrl, filename };
238
245
  }
239
246
  }
247
+ import { getActiveAuth } from "../auth/token-resolver.js";
248
+ /**
249
+ * OAuth-aware factory: resolves the active credential (with auto-refresh
250
+ * for OAuth tokens) and returns a `FileService` configured with the right
251
+ * `Authorization` header shape. Personal tokens use `Authorization:
252
+ * <token>` (no Bearer prefix); OAuth tokens use `Authorization: Bearer
253
+ * <token>`.
254
+ */
255
+ export async function createFileService(options) {
256
+ const auth = await getActiveAuth(options);
257
+ if (auth.kind === "oauth") {
258
+ return new FileService({ oauthToken: auth.token });
259
+ }
260
+ return new FileService({ apiKey: auth.token });
261
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Append a footer to a body string.
3
+ *
4
+ * Resolution order:
5
+ * 1. `--no-footer` flag → return body unchanged
6
+ * 2. `--footer <text>` flag → use the explicit value
7
+ * 3. `config.messageFooter` → use the configured value
8
+ * 4. nothing set → return body unchanged
9
+ *
10
+ * The footer is treated as a literal string — callers who want a horizontal
11
+ * rule or blank line before it must include the separator in the value.
12
+ *
13
+ * Used on `create` paths only (issues create, comments create). Update paths
14
+ * take an explicit body the user has already authored, so we don't auto-inject.
15
+ */
16
+ export declare function applyFooter(body: string | undefined, options: {
17
+ footer?: string;
18
+ noFooter?: boolean;
19
+ }): string | undefined;
@@ -0,0 +1,27 @@
1
+ import { loadConfig } from "../config/config.js";
2
+ /**
3
+ * Append a footer to a body string.
4
+ *
5
+ * Resolution order:
6
+ * 1. `--no-footer` flag → return body unchanged
7
+ * 2. `--footer <text>` flag → use the explicit value
8
+ * 3. `config.messageFooter` → use the configured value
9
+ * 4. nothing set → return body unchanged
10
+ *
11
+ * The footer is treated as a literal string — callers who want a horizontal
12
+ * rule or blank line before it must include the separator in the value.
13
+ *
14
+ * Used on `create` paths only (issues create, comments create). Update paths
15
+ * take an explicit body the user has already authored, so we don't auto-inject.
16
+ */
17
+ export function applyFooter(body, options) {
18
+ if (options.noFooter) {
19
+ return body;
20
+ }
21
+ const explicit = options.footer;
22
+ const footer = explicit ?? loadConfig().messageFooter;
23
+ if (!footer) {
24
+ return body;
25
+ }
26
+ return body ? `${body}${footer}` : footer;
27
+ }
@@ -82,7 +82,7 @@ export function parseGoogleDoc(doc) {
82
82
  .filter((c) => c.paragraph)
83
83
  .map((c) => {
84
84
  let text = "";
85
- for (const el of c.paragraph.elements) {
85
+ for (const el of c.paragraph?.elements ?? []) {
86
86
  text += formatTextRun(el);
87
87
  }
88
88
  return text.replace(TRAILING_NEWLINE_RE, "");
@@ -8,5 +8,5 @@ declare class GraphQLAttachmentsService {
8
8
  deleteAttachment(id: string): Promise<boolean>;
9
9
  listAttachments(issueId: string): Promise<LinearAttachment[]>;
10
10
  }
11
- export declare function createGraphQLAttachmentsService(options: AuthOptions): GraphQLAttachmentsService;
11
+ export declare function createGraphQLAttachmentsService(options: AuthOptions): Promise<GraphQLAttachmentsService>;
12
12
  export {};
@@ -40,7 +40,7 @@ class GraphQLAttachmentsService {
40
40
  return attachments.nodes.map(transformAttachment);
41
41
  }
42
42
  }
43
- export function createGraphQLAttachmentsService(options) {
44
- const graphqlService = createGraphQLService(options);
43
+ export async function createGraphQLAttachmentsService(options) {
44
+ const graphqlService = await createGraphQLService(options);
45
45
  return new GraphQLAttachmentsService(graphqlService);
46
46
  }
@@ -14,5 +14,5 @@ declare class GraphQLDocumentsService {
14
14
  deleteDocument(id: string): Promise<boolean>;
15
15
  listDocumentsBySlugIds(slugIds: string[], limit?: number): Promise<LinearDocument[]>;
16
16
  }
17
- export declare function createGraphQLDocumentsService(options: AuthOptions): GraphQLDocumentsService;
17
+ export declare function createGraphQLDocumentsService(options: AuthOptions): Promise<GraphQLDocumentsService>;
18
18
  export {};
@@ -91,7 +91,7 @@ class GraphQLDocumentsService {
91
91
  return result.documents.nodes.map(transformDocument);
92
92
  }
93
93
  }
94
- export function createGraphQLDocumentsService(options) {
95
- const graphqlService = createGraphQLService(options);
94
+ export async function createGraphQLDocumentsService(options) {
95
+ const graphqlService = await createGraphQLService(options);
96
96
  return new GraphQLDocumentsService(graphqlService);
97
97
  }
@@ -1,5 +1,5 @@
1
1
  import type { GraphQLResponseData, GraphQLVariables } from "../types/linear.js";
2
- import { type AuthOptions } from "./auth.js";
2
+ import type { AuthOptions } from "./auth.js";
3
3
  /**
4
4
  * Constructor arg shapes for `GraphQLService`. Three variants:
5
5
  * - `string` → personal API token (legacy; sent without `Bearer` prefix).
@@ -20,4 +20,4 @@ export declare class GraphQLService {
20
20
  constructor(auth: GraphQLServiceAuth);
21
21
  rawRequest<T = GraphQLResponseData>(query: string, variables?: GraphQLVariables): Promise<T>;
22
22
  }
23
- export declare function createGraphQLService(options: AuthOptions): GraphQLService;
23
+ export declare function createGraphQLService(options: AuthOptions): Promise<GraphQLService>;
@@ -1,5 +1,5 @@
1
1
  import { LinearClient } from "@linear/sdk";
2
- import { getApiToken } from "./auth.js";
2
+ import { getActiveAuth } from "../auth/token-resolver.js";
3
3
  function buildLinearClient(auth) {
4
4
  const baseHeaders = { "public-file-urls-expire-in": "3600" };
5
5
  if (typeof auth === "string") {
@@ -41,7 +41,10 @@ export class GraphQLService {
41
41
  }
42
42
  }
43
43
  }
44
- export function createGraphQLService(options) {
45
- const apiToken = getApiToken(options);
46
- return new GraphQLService(apiToken);
44
+ export async function createGraphQLService(options) {
45
+ const auth = await getActiveAuth(options);
46
+ if (auth.kind === "oauth") {
47
+ return new GraphQLService({ oauthToken: auth.token });
48
+ }
49
+ return new GraphQLService({ apiKey: auth.token });
47
50
  }
@@ -1,8 +1,23 @@
1
1
  import type { LinearComment, LinearCycleDetail, LinearCycleSummary, LinearLabel, LinearProject, LinearTeam, LinearUser } from "../types/linear.js";
2
- import { type AuthOptions } from "./auth.js";
2
+ import type { AuthOptions } from "./auth.js";
3
+ /**
4
+ * Constructor arg shapes for `LinearService`. Three variants:
5
+ * - `string` → personal API token (legacy; sent without `Bearer` prefix).
6
+ * - `{apiKey: string}` → personal API token (explicit).
7
+ * - `{oauthToken: string}` → OAuth access token (sent as
8
+ * `Authorization: Bearer <token>` via the SDK's accessToken option).
9
+ *
10
+ * The string variant exists because hundreds of call sites and tests pass
11
+ * a plain string. We continue to support it indefinitely.
12
+ */
13
+ export type LinearServiceAuth = string | {
14
+ apiKey: string;
15
+ } | {
16
+ oauthToken: string;
17
+ };
3
18
  export declare class LinearService {
4
19
  private readonly client;
5
- constructor(apiToken: string);
20
+ constructor(auth: LinearServiceAuth);
6
21
  resolveIssueId(issueId: string): Promise<string>;
7
22
  getTeams(limit?: number): Promise<LinearTeam[]>;
8
23
  resolveUserId(nameOrEmailOrId: string): Promise<string>;
@@ -23,4 +38,4 @@ export declare class LinearService {
23
38
  resolveCycleId(cycleNameOrId: string, teamFilter?: string): Promise<string>;
24
39
  resolveProjectId(projectNameOrId: string): Promise<string>;
25
40
  }
26
- export declare function createLinearService(options: AuthOptions): LinearService;
41
+ export declare function createLinearService(options: AuthOptions): Promise<LinearService>;
@@ -1,6 +1,6 @@
1
1
  import { LinearClient } from "@linear/sdk";
2
+ import { getActiveAuth } from "../auth/token-resolver.js";
2
3
  import { resolveUserDisplayName } from "../config/resolver.js";
3
- import { getApiToken } from "./auth.js";
4
4
  import { toISOStringOrNow, toISOStringOrUndefined } from "./date-format.js";
5
5
  import { multipleMatchesError, notFoundError } from "./error-messages.js";
6
6
  import { parseIssueIdentifier } from "./identifier-parser.js";
@@ -21,10 +21,22 @@ function teamIdFilter(teamId) {
21
21
  function nonEmptyFilter(filter) {
22
22
  return Object.keys(filter).length > 0 ? filter : undefined;
23
23
  }
24
+ function buildLinearClient(auth) {
25
+ if (typeof auth === "string") {
26
+ return new LinearClient({ apiKey: auth });
27
+ }
28
+ if ("oauthToken" in auth) {
29
+ // Linear's SDK natively supports OAuth via the `accessToken` option,
30
+ // which causes the underlying transport to send
31
+ // `Authorization: Bearer <token>` instead of the personal-token shape.
32
+ return new LinearClient({ accessToken: auth.oauthToken });
33
+ }
34
+ return new LinearClient({ apiKey: auth.apiKey });
35
+ }
24
36
  export class LinearService {
25
37
  client;
26
- constructor(apiToken) {
27
- this.client = new LinearClient({ apiKey: apiToken });
38
+ constructor(auth) {
39
+ this.client = buildLinearClient(auth);
28
40
  }
29
41
  async resolveIssueId(issueId) {
30
42
  if (isUuid(issueId)) {
@@ -436,7 +448,10 @@ export class LinearService {
436
448
  return projectsConnection.nodes[0].id;
437
449
  }
438
450
  }
439
- export function createLinearService(options) {
440
- const apiToken = getApiToken(options);
441
- return new LinearService(apiToken);
451
+ export async function createLinearService(options) {
452
+ const auth = await getActiveAuth(options);
453
+ if (auth.kind === "oauth") {
454
+ return new LinearService({ oauthToken: auth.token });
455
+ }
456
+ return new LinearService({ apiKey: auth.token });
442
457
  }
@@ -106,8 +106,11 @@ function parseBulletList(state, line) {
106
106
  return false;
107
107
  }
108
108
  const items = [];
109
- while (state.i < state.lines.length && BULLET_RE.test(state.lines[state.i])) {
109
+ while (state.i < state.lines.length) {
110
110
  const m = state.lines[state.i].match(BULLET_RE);
111
+ if (!m) {
112
+ break;
113
+ }
111
114
  items.push({
112
115
  type: "listItem",
113
116
  content: [{ type: "paragraph", content: parseInline(m[1]) }],
@@ -122,9 +125,11 @@ function parseOrderedList(state, line) {
122
125
  return false;
123
126
  }
124
127
  const items = [];
125
- while (state.i < state.lines.length &&
126
- ORDERED_RE.test(state.lines[state.i])) {
128
+ while (state.i < state.lines.length) {
127
129
  const m = state.lines[state.i].match(ORDERED_RE);
130
+ if (!m) {
131
+ break;
132
+ }
128
133
  items.push({
129
134
  type: "listItem",
130
135
  content: [{ type: "paragraph", content: parseInline(m[1]) }],
@@ -139,9 +144,11 @@ function parseBlockquote(state) {
139
144
  return false;
140
145
  }
141
146
  const quoteLines = [];
142
- while (state.i < state.lines.length &&
143
- BLOCKQUOTE_RE.test(state.lines[state.i])) {
147
+ while (state.i < state.lines.length) {
144
148
  const m = state.lines[state.i].match(BLOCKQUOTE_RE);
149
+ if (!m) {
150
+ break;
151
+ }
145
152
  quoteLines.push(m[1]);
146
153
  state.i++;
147
154
  }
@@ -283,7 +290,7 @@ function findEarliestInlineMatch(text) {
283
290
  const codeMatch = text.match(INLINE_CODE_RE);
284
291
  if (codeMatch) {
285
292
  candidates.push({
286
- index: codeMatch.index,
293
+ index: codeMatch.index ?? 0,
287
294
  length: codeMatch[0].length,
288
295
  innerText: codeMatch[1],
289
296
  marks: [{ type: "code" }],
@@ -292,7 +299,7 @@ function findEarliestInlineMatch(text) {
292
299
  const linkMatch = text.match(INLINE_LINK_RE);
293
300
  if (linkMatch) {
294
301
  candidates.push({
295
- index: linkMatch.index,
302
+ index: linkMatch.index ?? 0,
296
303
  length: linkMatch[0].length,
297
304
  innerText: linkMatch[1],
298
305
  marks: [{ type: "link", attrs: { href: linkMatch[2] } }],
@@ -301,7 +308,7 @@ function findEarliestInlineMatch(text) {
301
308
  const boldMatch = text.match(INLINE_BOLD_RE);
302
309
  if (boldMatch) {
303
310
  candidates.push({
304
- index: boldMatch.index,
311
+ index: boldMatch.index ?? 0,
305
312
  length: boldMatch[0].length,
306
313
  innerText: boldMatch[1] ?? boldMatch[2],
307
314
  marks: [{ type: "bold" }],
@@ -310,7 +317,7 @@ function findEarliestInlineMatch(text) {
310
317
  const italicMatch = text.match(INLINE_ITALIC_RE);
311
318
  if (italicMatch) {
312
319
  candidates.push({
313
- index: italicMatch.index,
320
+ index: italicMatch.index ?? 0,
314
321
  length: italicMatch[0].length,
315
322
  innerText: italicMatch[1] ?? italicMatch[2],
316
323
  marks: [{ type: "italic" }],
@@ -0,0 +1,11 @@
1
+ import type { Command, OptionValues } from "commander";
2
+ /**
3
+ * Extract root command options from inside a subcommand action handler.
4
+ *
5
+ * Commander guarantees `command.parent` exists inside any subcommand action,
6
+ * and `command.parent.parent` exists inside a nested subcommand action
7
+ * (e.g. `el-linear comments create` — the `create` command is nested under
8
+ * `comments`, which is nested under the root program). The non-null
9
+ * assertions live here so call sites can stay clean and typed.
10
+ */
11
+ export declare function getRootOpts(command: Command): OptionValues;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Extract root command options from inside a subcommand action handler.
3
+ *
4
+ * Commander guarantees `command.parent` exists inside any subcommand action,
5
+ * and `command.parent.parent` exists inside a nested subcommand action
6
+ * (e.g. `el-linear comments create` — the `create` command is nested under
7
+ * `comments`, which is nested under the root program). The non-null
8
+ * assertions live here so call sites can stay clean and typed.
9
+ */
10
+ export function getRootOpts(command) {
11
+ // biome-ignore lint/style/noNonNullAssertion: command.parent / .parent.parent are guaranteed by commander inside subcommand actions; see docstring
12
+ return (command.parent?.parent ?? command.parent).opts();
13
+ }
@@ -1,4 +1,11 @@
1
1
  export declare function parsePositiveInt(value: string, flagName: string): number;
2
+ /**
3
+ * Parse a single priority value (for `issues create --priority`, `issues update --priority`).
4
+ *
5
+ * Accepts:
6
+ * - keywords: none | urgent | high | medium | normal | low
7
+ * - numbers: 0 (no priority), 1 (urgent), 2 (high), 3 (medium), 4 (low)
8
+ */
2
9
  export declare function validatePriority(value: string): number;
3
10
  export declare function validateHexColor(value: string): string;
4
11
  export declare function validateIsoDate(value: string): string;
@@ -8,14 +8,21 @@ export function parsePositiveInt(value, flagName) {
8
8
  }
9
9
  return n;
10
10
  }
11
+ /**
12
+ * Parse a single priority value (for `issues create --priority`, `issues update --priority`).
13
+ *
14
+ * Accepts:
15
+ * - keywords: none | urgent | high | medium | normal | low
16
+ * - numbers: 0 (no priority), 1 (urgent), 2 (high), 3 (medium), 4 (low)
17
+ */
11
18
  export function validatePriority(value) {
12
19
  const asName = PRIORITY_NAMES[value.toLowerCase()];
13
- if (asName !== undefined && asName >= 1) {
20
+ if (asName !== undefined) {
14
21
  return asName;
15
22
  }
16
23
  const n = Number.parseInt(value, 10);
17
- if (Number.isNaN(n) || n < 1 || n > 4) {
18
- throw invalidParameterError("--priority", `"${value}" is not valid. Use names (urgent, high, medium/normal, low) or numbers (1-4).`);
24
+ if (Number.isNaN(n) || n < 0 || n > 4) {
25
+ throw invalidParameterError("--priority", `"${value}" is not valid. Use names (none, urgent, high, medium/normal, low) or numbers (0-4).`);
19
26
  }
20
27
  return n;
21
28
  }
@@ -41,6 +48,9 @@ export function splitList(value) {
41
48
  .map((s) => s.trim())
42
49
  .filter((s) => s.length > 0);
43
50
  }
51
+ // Shared keyword → Linear priority number map. `none` and `0` mean "No priority"
52
+ // (Linear stores it as a real state, not absence). `1..4` are the rated priorities,
53
+ // `urgent` (1) being the highest.
44
54
  const PRIORITY_NAMES = {
45
55
  none: 0,
46
56
  urgent: 1,
@@ -50,17 +60,7 @@ const PRIORITY_NAMES = {
50
60
  low: 4,
51
61
  };
52
62
  export function parsePriorityFilter(value) {
53
- return splitList(value).map((item) => {
54
- const asName = PRIORITY_NAMES[item.toLowerCase()];
55
- if (asName !== undefined) {
56
- return asName;
57
- }
58
- const n = Number.parseInt(item, 10);
59
- if (Number.isNaN(n) || n < 0 || n > 4) {
60
- throw invalidParameterError("--priority", `"${item}" is not valid. Use names (urgent, high, medium, low, none) or numbers (0-4).`);
61
- }
62
- return n;
63
- });
63
+ return splitList(value).map((item) => validatePriority(item));
64
64
  }
65
65
  export const PRIORITY_LABELS = {
66
66
  0: "No priority",