@enrichlayer/el-linear 1.7.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +82 -1
  2. package/claude-skills/linear-operations/SKILL.md +55 -0
  3. package/dist/auth/oauth-callback.js +14 -7
  4. package/dist/auth/oauth-fs.d.ts +22 -0
  5. package/dist/auth/oauth-fs.js +77 -6
  6. package/dist/auth/oauth-headless.d.ts +13 -8
  7. package/dist/auth/oauth-headless.js +18 -11
  8. package/dist/auth/oauth-storage.d.ts +11 -3
  9. package/dist/auth/oauth-storage.js +15 -8
  10. package/dist/auth/token-resolver.d.ts +9 -0
  11. package/dist/auth/token-resolver.js +57 -37
  12. package/dist/commands/attachments.js +2 -1
  13. package/dist/commands/comments.js +17 -28
  14. package/dist/commands/cycles.js +2 -1
  15. package/dist/commands/documents.js +2 -1
  16. package/dist/commands/graphql.js +4 -6
  17. package/dist/commands/init/index.js +2 -0
  18. package/dist/commands/init/oauth.d.ts +6 -0
  19. package/dist/commands/init/oauth.js +8 -2
  20. package/dist/commands/init/token.d.ts +0 -8
  21. package/dist/commands/init/token.js +13 -1
  22. package/dist/commands/issue-id.js +1 -3
  23. package/dist/commands/issues/branch.d.ts +17 -0
  24. package/dist/commands/issues/branch.js +40 -0
  25. package/dist/commands/issues/description.d.ts +89 -0
  26. package/dist/commands/issues/description.js +183 -0
  27. package/dist/commands/issues/link-references.d.ts +21 -0
  28. package/dist/commands/issues/link-references.js +171 -0
  29. package/dist/commands/issues/relations.d.ts +55 -0
  30. package/dist/commands/issues/relations.js +132 -0
  31. package/dist/commands/issues.js +97 -497
  32. package/dist/commands/labels.js +20 -28
  33. package/dist/commands/profile/migrate-legacy.js +10 -33
  34. package/dist/commands/profile.js +1 -10
  35. package/dist/commands/project-milestones.js +13 -20
  36. package/dist/commands/projects.d.ts +5 -1
  37. package/dist/commands/projects.js +202 -102
  38. package/dist/commands/read-shortcut.d.ts +6 -0
  39. package/dist/commands/read-shortcut.js +6 -1
  40. package/dist/commands/refs.js +10 -1
  41. package/dist/commands/releases.js +26 -30
  42. package/dist/commands/search.js +21 -30
  43. package/dist/commands/teams.js +2 -1
  44. package/dist/commands/templates.js +128 -7
  45. package/dist/commands/users.js +3 -1
  46. package/dist/config/config.js +31 -9
  47. package/dist/config/issue-validation.js +1 -1
  48. package/dist/config/paths.d.ts +11 -0
  49. package/dist/config/paths.js +44 -6
  50. package/dist/config/resolver.js +2 -3
  51. package/dist/config/term-enforcer.js +1 -1
  52. package/dist/main.js +37 -2
  53. package/dist/queries/attachments-types.d.ts +30 -0
  54. package/dist/queries/attachments-types.js +5 -0
  55. package/dist/queries/comments-types.d.ts +49 -0
  56. package/dist/queries/comments-types.js +5 -0
  57. package/dist/queries/documents-types.d.ts +61 -0
  58. package/dist/queries/documents-types.js +9 -0
  59. package/dist/queries/introspect-types.d.ts +57 -0
  60. package/dist/queries/introspect-types.js +10 -0
  61. package/dist/queries/issues-types.d.ts +416 -0
  62. package/dist/queries/issues-types.js +23 -0
  63. package/dist/queries/issues.d.ts +2 -0
  64. package/dist/queries/issues.js +22 -0
  65. package/dist/queries/labels-types.d.ts +64 -0
  66. package/dist/queries/labels-types.js +5 -0
  67. package/dist/queries/project-milestones-types.d.ts +91 -0
  68. package/dist/queries/project-milestones-types.js +10 -0
  69. package/dist/queries/projects-types.d.ts +75 -0
  70. package/dist/queries/projects-types.js +5 -0
  71. package/dist/queries/projects.d.ts +2 -0
  72. package/dist/queries/projects.js +22 -0
  73. package/dist/queries/releases-types.d.ts +84 -0
  74. package/dist/queries/releases-types.js +5 -0
  75. package/dist/queries/search-types.d.ts +86 -0
  76. package/dist/queries/search-types.js +6 -0
  77. package/dist/queries/templates-types.d.ts +61 -0
  78. package/dist/queries/templates-types.js +9 -0
  79. package/dist/queries/templates.d.ts +3 -0
  80. package/dist/queries/templates.js +43 -0
  81. package/dist/types/linear.d.ts +8 -2
  82. package/dist/utils/auth.js +7 -9
  83. package/dist/utils/auto-link-references.js +44 -25
  84. package/dist/utils/disk-cache.js +2 -1
  85. package/dist/utils/formatters/summary.d.ts +62 -0
  86. package/dist/utils/formatters/summary.js +755 -0
  87. package/dist/utils/graphql-attachments-service.js +6 -9
  88. package/dist/utils/graphql-documents-service.js +19 -25
  89. package/dist/utils/graphql-issues-service.d.ts +118 -5
  90. package/dist/utils/graphql-issues-service.js +202 -209
  91. package/dist/utils/issue-reference-extractor.d.ts +9 -1
  92. package/dist/utils/issue-reference-extractor.js +16 -9
  93. package/dist/utils/issue-reference-wrapper.js +1 -54
  94. package/dist/utils/linear-service.d.ts +7 -3
  95. package/dist/utils/linear-service.js +27 -5
  96. package/dist/utils/markdown-prosemirror.js +17 -1
  97. package/dist/utils/mention-resolver.js +17 -5
  98. package/dist/utils/output.d.ts +7 -1
  99. package/dist/utils/output.js +38 -1
  100. package/dist/utils/protected-ranges.d.ts +33 -0
  101. package/dist/utils/protected-ranges.js +73 -0
  102. package/dist/utils/table-formatter.d.ts +36 -0
  103. package/dist/utils/table-formatter.js +46 -24
  104. package/dist/utils/validators.d.ts +9 -1
  105. package/dist/utils/validators.js +10 -0
  106. package/dist/utils/workspace-url.d.ts +5 -1
  107. package/dist/utils/workspace-url.js +35 -5
  108. package/package.json +1 -1
@@ -6,7 +6,7 @@ import { createGraphQLService } from "../utils/graphql-service.js";
6
6
  import { createLinearService } from "../utils/linear-service.js";
7
7
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
8
8
  import { getRootOpts } from "../utils/root-opts.js";
9
- import { validateHexColor } from "../utils/validators.js";
9
+ import { parsePositiveInt, validateHexColor } from "../utils/validators.js";
10
10
  async function handleCreateLabel(name, options, command) {
11
11
  const rootOpts = getRootOpts(command);
12
12
  const teamId = resolveTeam(options.team);
@@ -23,45 +23,35 @@ async function handleCreateLabel(name, options, command) {
23
23
  name: options.parent,
24
24
  teamId,
25
25
  });
26
- const issueLabels = parentResult.issueLabels;
27
- const nodes = issueLabels?.nodes;
28
- const parentLabel = nodes?.[0];
26
+ const parentLabel = parentResult.issueLabels.nodes[0];
29
27
  if (parentLabel) {
30
28
  input.parentId = parentLabel.id;
31
29
  }
32
30
  }
33
- const result = await graphQLService.rawRequest(CREATE_LABEL_MUTATION, {
34
- input,
35
- });
36
- const createResult = result.issueLabelCreate;
37
- if (!createResult?.success) {
31
+ const result = await graphQLService.rawRequest(CREATE_LABEL_MUTATION, { input });
32
+ if (!result.issueLabelCreate.success || !result.issueLabelCreate.issueLabel) {
38
33
  throw new Error(`Failed to create label "${name}"`);
39
34
  }
40
- outputSuccess(createResult.issueLabel);
35
+ outputSuccess(result.issueLabelCreate.issueLabel);
41
36
  }
42
37
  async function handleRetireLabel(labelId, _options, command) {
43
38
  const rootOpts = getRootOpts(command);
44
39
  const graphQLService = await createGraphQLService(rootOpts);
45
- const result = await graphQLService.rawRequest(RETIRE_LABEL_MUTATION, {
46
- id: labelId,
47
- });
48
- const retireResult = result.issueLabelRetire;
49
- if (!retireResult?.success) {
40
+ const result = await graphQLService.rawRequest(RETIRE_LABEL_MUTATION, { id: labelId });
41
+ if (!result.issueLabelRetire.success || !result.issueLabelRetire.issueLabel) {
50
42
  throw new Error(`Failed to retire label "${labelId}"`);
51
43
  }
52
- outputSuccess(retireResult.issueLabel);
44
+ outputSuccess(result.issueLabelRetire.issueLabel);
53
45
  }
54
46
  async function handleRestoreLabel(labelId, _options, command) {
55
47
  const rootOpts = getRootOpts(command);
56
48
  const graphQLService = await createGraphQLService(rootOpts);
57
- const result = await graphQLService.rawRequest(RESTORE_LABEL_MUTATION, {
58
- id: labelId,
59
- });
60
- const restoreResult = result.issueLabelRestore;
61
- if (!restoreResult?.success) {
49
+ const result = await graphQLService.rawRequest(RESTORE_LABEL_MUTATION, { id: labelId });
50
+ if (!result.issueLabelRestore.success ||
51
+ !result.issueLabelRestore.issueLabel) {
62
52
  throw new Error(`Failed to restore label "${labelId}"`);
63
53
  }
64
- outputSuccess(restoreResult.issueLabel);
54
+ outputSuccess(result.issueLabelRestore.issueLabel);
65
55
  }
66
56
  export function setupLabelsCommands(program) {
67
57
  const labels = program
@@ -73,22 +63,24 @@ export function setupLabelsCommands(program) {
73
63
  .command("list")
74
64
  .description("List all available labels")
75
65
  .option("--team <team>", "filter by team key, name, or ID")
66
+ .option("--name <substring>", "filter by case-insensitive substring on label name")
76
67
  .option("-l, --limit <number>", "limit results", "100")
77
68
  .action(handleAsyncCommand(async (options, command) => {
78
69
  const rootOpts = getRootOpts(command);
79
70
  const teamFilter = options.team ? resolveTeam(options.team) : undefined;
80
- const limit = Number.parseInt(options.limit, 10);
71
+ const nameFilter = options.name;
72
+ const limit = parsePositiveInt(options.limit, "--limit");
81
73
  const ttl = resolveCacheTTL({
82
74
  configTTL: loadConfig().cacheTTLSeconds,
83
75
  noCacheFlag: rootOpts.cache === false,
84
76
  });
85
- // Cache key includes the team filter so list-with-team and list-
86
- // without-team don't collide. Same `limit` participates because
87
- // a smaller list isn't a valid cached answer for a larger ask.
88
- const cacheKey = `labels-list-team:${teamFilter ?? "_all"}-limit:${limit}`;
77
+ // Cache key includes the team filter and name filter so different
78
+ // filter combinations don't collide. Same `limit` participates
79
+ // because a smaller list isn't a valid cached answer for a larger ask.
80
+ const cacheKey = `labels-list-team:${teamFilter ?? "_all"}-name:${nameFilter ?? "_all"}-limit:${limit}`;
89
81
  const result = await cached(cacheKey, ttl, async () => {
90
82
  const service = await createLinearService(rootOpts);
91
- return service.getLabels(teamFilter, limit);
83
+ return service.getLabels(teamFilter, limit, nameFilter);
92
84
  });
93
85
  outputSuccess({
94
86
  data: result.labels,
@@ -38,7 +38,8 @@
38
38
  import { promises as fsp } from "node:fs";
39
39
  import path from "node:path";
40
40
  import { confirm, input, password } from "@inquirer/prompts";
41
- import { ACTIVE_PROFILE_FILE, CONFIG_DIR, CONFIG_PATH, PROFILES_DIR, profilePaths, TOKEN_PATH, } from "../../config/paths.js";
41
+ import { atomicWrite } from "../../auth/oauth-fs.js";
42
+ import { ACTIVE_PROFILE_FILE, CONFIG_DIR, CONFIG_PATH, isSafeProfileName as isSafeName, PROFILES_DIR, profilePaths, TOKEN_PATH, } from "../../config/paths.js";
42
43
  import { sanitizeForLog, validateToken } from "../init/token.js";
43
44
  const DEFAULT_PROMPTS = { input, password, confirm };
44
45
  /** Register `el-linear profile migrate-legacy` under the parent `profile` command. */
@@ -165,10 +166,7 @@ async function resolveAndValidateToken(options, prompts, validate) {
165
166
  async function copyConfigIntoProfile(destConfigPath, options, prompts) {
166
167
  const sourceContent = await fsp.readFile(CONFIG_PATH, "utf8");
167
168
  if (!(await pathExists(destConfigPath))) {
168
- await fsp.writeFile(destConfigPath, sourceContent, {
169
- mode: 0o644,
170
- encoding: "utf8",
171
- });
169
+ await atomicWrite(destConfigPath, sourceContent, 0o644);
172
170
  return;
173
171
  }
174
172
  const destContent = await fsp.readFile(destConfigPath, "utf8");
@@ -192,21 +190,15 @@ async function copyConfigIntoProfile(destConfigPath, options, prompts) {
192
190
  throw new Error("Aborted by user.");
193
191
  }
194
192
  }
195
- await fsp.writeFile(destConfigPath, sourceContent, {
196
- mode: 0o644,
197
- encoding: "utf8",
198
- });
193
+ await atomicWrite(destConfigPath, sourceContent, 0o644);
199
194
  }
200
195
  async function writeProfileToken(destTokenPath, token, options, prompts) {
201
196
  const newline = `${token}\n`;
202
197
  if (!(await pathExists(destTokenPath))) {
203
- await fsp.writeFile(destTokenPath, newline, {
204
- mode: 0o600,
205
- encoding: "utf8",
206
- });
207
- // fs.writeFile mode is only honored on file creation; chmod defensively
208
- // in case a parent process pre-created the file with permissive perms.
209
- await fsp.chmod(destTokenPath, 0o600);
198
+ // atomicWrite creates the tmp file with the requested mode and
199
+ // renames into place, so there is no window where the token is
200
+ // readable at a looser mode than 0o600.
201
+ await atomicWrite(destTokenPath, newline, 0o600);
210
202
  return;
211
203
  }
212
204
  const existing = (await fsp.readFile(destTokenPath, "utf8")).trim();
@@ -230,11 +222,7 @@ async function writeProfileToken(destTokenPath, token, options, prompts) {
230
222
  throw new Error("Aborted by user.");
231
223
  }
232
224
  }
233
- await fsp.writeFile(destTokenPath, newline, {
234
- mode: 0o600,
235
- encoding: "utf8",
236
- });
237
- await fsp.chmod(destTokenPath, 0o600);
225
+ await atomicWrite(destTokenPath, newline, 0o600);
238
226
  }
239
227
  async function ensureActiveProfile(name) {
240
228
  await fsp.mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
@@ -244,10 +232,7 @@ async function ensureActiveProfile(name) {
244
232
  }
245
233
  if (current === name)
246
234
  return;
247
- await fsp.writeFile(ACTIVE_PROFILE_FILE, `${name}\n`, {
248
- mode: 0o644,
249
- encoding: "utf8",
250
- });
235
+ await atomicWrite(ACTIVE_PROFILE_FILE, `${name}\n`, 0o644);
251
236
  }
252
237
  async function pathExists(p) {
253
238
  try {
@@ -258,14 +243,6 @@ async function pathExists(p) {
258
243
  return false;
259
244
  }
260
245
  }
261
- /**
262
- * Profile names land in filesystem paths and the active-profile marker.
263
- * Same conservative charset as `profile add` to avoid `..` traversal,
264
- * shell metacharacters, and Unicode lookalikes.
265
- */
266
- function isSafeName(name) {
267
- return /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(name);
268
- }
269
246
  // Re-export internals so the integration test (and follow-up commands)
270
247
  // can compose them without re-implementing the idempotency rules.
271
248
  export { CONFIG_PATH, PROFILES_DIR, TOKEN_PATH };
@@ -23,7 +23,7 @@
23
23
  import { promises as fsp } from "node:fs";
24
24
  import path from "node:path";
25
25
  import { confirm } from "@inquirer/prompts";
26
- import { ACTIVE_PROFILE_FILE, CONFIG_DIR, CONFIG_PATH, PROFILES_DIR, profilePaths, resolveActiveProfile, setActiveProfileForSession, TOKEN_PATH, } from "../config/paths.js";
26
+ import { ACTIVE_PROFILE_FILE, CONFIG_DIR, CONFIG_PATH, isSafeProfileName as isSafeName, PROFILES_DIR, profilePaths, resolveActiveProfile, setActiveProfileForSession, TOKEN_PATH, } from "../config/paths.js";
27
27
  import { outputSuccess, outputWarning } from "../utils/output.js";
28
28
  import { runFullWizard } from "./init/index.js";
29
29
  import { registerMigrateLegacy } from "./profile/migrate-legacy.js";
@@ -180,12 +180,3 @@ async function pathExists(p) {
180
180
  return false;
181
181
  }
182
182
  }
183
- /**
184
- * Profile names land in filesystem paths AND get written to a
185
- * single-line marker. Restrict to a conservative charset so we can't
186
- * accidentally pick up `..` traversal, command-line metacharacters, or
187
- * Unicode lookalikes that would confuse `profile use`.
188
- */
189
- function isSafeName(name) {
190
- return /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(name);
191
- }
@@ -5,7 +5,7 @@ import { createLinearService, } from "../utils/linear-service.js";
5
5
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
6
6
  import { getRootOpts } from "../utils/root-opts.js";
7
7
  import { isUuid } from "../utils/uuid.js";
8
- import { validateIsoDate } from "../utils/validators.js";
8
+ import { parsePositiveInt, validateIsoDate } from "../utils/validators.js";
9
9
  async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearService, projectNameOrId) {
10
10
  if (isUuid(milestoneNameOrId)) {
11
11
  return milestoneNameOrId;
@@ -17,22 +17,17 @@ async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearServi
17
17
  name: milestoneNameOrId,
18
18
  projectId,
19
19
  });
20
- const projectData = findRes.project;
21
- const milestonesData = projectData?.projectMilestones;
22
- nodes = milestonesData?.nodes || [];
20
+ nodes = findRes.project?.projectMilestones.nodes ?? [];
23
21
  }
24
22
  if (nodes.length === 0) {
25
- const globalRes = await graphQLService.rawRequest(FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, {
26
- name: milestoneNameOrId,
27
- });
28
- const globalMilestones = globalRes.projectMilestones;
29
- nodes = globalMilestones?.nodes || [];
23
+ const globalRes = await graphQLService.rawRequest(FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, { name: milestoneNameOrId });
24
+ nodes = globalRes.projectMilestones.nodes;
30
25
  }
31
26
  if (nodes.length === 0) {
32
27
  throw notFoundError("Milestone", milestoneNameOrId);
33
28
  }
34
29
  if (nodes.length > 1) {
35
- const matches = nodes.map((m) => `"${m.name}" in project "${m.project?.name}"`);
30
+ const matches = nodes.map((m) => `"${m.name}" in project "${m.project.name}"`);
36
31
  throw multipleMatchesError("milestone", milestoneNameOrId, matches, "specify --project or use the milestone ID");
37
32
  }
38
33
  return nodes[0].id;
@@ -44,11 +39,9 @@ async function handleListMilestones(options, command) {
44
39
  const projectId = await linearService.resolveProjectId(options.project);
45
40
  const result = await graphQLService.rawRequest(LIST_PROJECT_MILESTONES_QUERY, {
46
41
  projectId,
47
- first: Number.parseInt(options.limit || "50", 10),
42
+ first: parsePositiveInt(options.limit || "50", "--limit"),
48
43
  });
49
- const projectData = result.project;
50
- const milestonesData = projectData?.projectMilestones;
51
- const nodes = milestonesData?.nodes || [];
44
+ const nodes = result.project?.projectMilestones.nodes ?? [];
52
45
  outputSuccess({ data: nodes, meta: { count: nodes.length } });
53
46
  }
54
47
  async function handleReadMilestone(milestoneIdOrName, options, command) {
@@ -76,11 +69,11 @@ async function handleCreateMilestone(name, options, command) {
76
69
  description: options.description,
77
70
  targetDate: options.targetDate,
78
71
  });
79
- const createData = result.projectMilestoneCreate;
80
- if (!createData?.success) {
72
+ if (!result.projectMilestoneCreate.success ||
73
+ !result.projectMilestoneCreate.projectMilestone) {
81
74
  throw new Error("Failed to create project milestone");
82
75
  }
83
- outputSuccess(createData.projectMilestone);
76
+ outputSuccess(result.projectMilestoneCreate.projectMilestone);
84
77
  }
85
78
  async function handleUpdateMilestone(milestoneIdOrName, options, command) {
86
79
  const rootOpts = getRootOpts(command);
@@ -102,11 +95,11 @@ async function handleUpdateMilestone(milestoneIdOrName, options, command) {
102
95
  updateVars.sortOrder = Number.parseFloat(options.sortOrder);
103
96
  }
104
97
  const result = await graphQLService.rawRequest(UPDATE_PROJECT_MILESTONE_MUTATION, updateVars);
105
- const updateData = result.projectMilestoneUpdate;
106
- if (!updateData?.success) {
98
+ if (!result.projectMilestoneUpdate.success ||
99
+ !result.projectMilestoneUpdate.projectMilestone) {
107
100
  throw new Error("Failed to update project milestone");
108
101
  }
109
- outputSuccess(updateData.projectMilestone);
102
+ outputSuccess(result.projectMilestoneUpdate.projectMilestone);
110
103
  }
111
104
  export function setupProjectMilestonesCommands(program) {
112
105
  const projectMilestones = program
@@ -1,2 +1,6 @@
1
- import type { Command } from "commander";
1
+ import type { Command, OptionValues } from "commander";
2
+ export declare function resolveProjectStateFilter(options: OptionValues): {
3
+ states?: string[];
4
+ excludeStates?: string[];
5
+ };
2
6
  export declare function setupProjectsCommands(program: Command): void;