@enrichlayer/el-linear 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +13 -0
  2. package/dist/commands/attachments.js +2 -1
  3. package/dist/commands/comments.js +17 -28
  4. package/dist/commands/cycles.js +2 -1
  5. package/dist/commands/documents.js +2 -1
  6. package/dist/commands/graphql.js +4 -6
  7. package/dist/commands/issue-id.js +1 -3
  8. package/dist/commands/issues/description.js +2 -6
  9. package/dist/commands/issues/link-references.d.ts +21 -0
  10. package/dist/commands/issues/link-references.js +171 -0
  11. package/dist/commands/issues/relations.d.ts +55 -0
  12. package/dist/commands/issues/relations.js +132 -0
  13. package/dist/commands/issues.js +65 -284
  14. package/dist/commands/labels.js +13 -23
  15. package/dist/commands/project-milestones.js +13 -20
  16. package/dist/commands/projects.js +67 -38
  17. package/dist/commands/refs.js +8 -0
  18. package/dist/commands/releases.js +26 -30
  19. package/dist/commands/search.js +21 -30
  20. package/dist/commands/teams.js +2 -1
  21. package/dist/commands/templates.js +9 -14
  22. package/dist/commands/users.js +2 -1
  23. package/dist/config/resolver.js +2 -3
  24. package/dist/queries/attachments-types.d.ts +30 -0
  25. package/dist/queries/attachments-types.js +5 -0
  26. package/dist/queries/comments-types.d.ts +49 -0
  27. package/dist/queries/comments-types.js +5 -0
  28. package/dist/queries/documents-types.d.ts +61 -0
  29. package/dist/queries/documents-types.js +9 -0
  30. package/dist/queries/introspect-types.d.ts +57 -0
  31. package/dist/queries/introspect-types.js +10 -0
  32. package/dist/queries/issues-types.d.ts +416 -0
  33. package/dist/queries/issues-types.js +23 -0
  34. package/dist/queries/issues.d.ts +2 -0
  35. package/dist/queries/issues.js +22 -0
  36. package/dist/queries/labels-types.d.ts +64 -0
  37. package/dist/queries/labels-types.js +5 -0
  38. package/dist/queries/project-milestones-types.d.ts +91 -0
  39. package/dist/queries/project-milestones-types.js +10 -0
  40. package/dist/queries/projects-types.d.ts +75 -0
  41. package/dist/queries/projects-types.js +5 -0
  42. package/dist/queries/projects.d.ts +2 -0
  43. package/dist/queries/projects.js +22 -0
  44. package/dist/queries/releases-types.d.ts +84 -0
  45. package/dist/queries/releases-types.js +5 -0
  46. package/dist/queries/search-types.d.ts +86 -0
  47. package/dist/queries/search-types.js +6 -0
  48. package/dist/queries/templates-types.d.ts +61 -0
  49. package/dist/queries/templates-types.js +9 -0
  50. package/dist/types/linear.d.ts +8 -2
  51. package/dist/utils/auto-link-references.js +29 -24
  52. package/dist/utils/graphql-attachments-service.js +6 -9
  53. package/dist/utils/graphql-documents-service.js +19 -25
  54. package/dist/utils/graphql-issues-service.d.ts +12 -2
  55. package/dist/utils/graphql-issues-service.js +151 -172
  56. package/dist/utils/output.d.ts +2 -0
  57. package/dist/utils/output.js +16 -6
  58. package/dist/utils/workspace-url.d.ts +5 -1
  59. package/dist/utils/workspace-url.js +35 -5
  60. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { loadConfig } from "../config/config.js";
2
2
  import { resolveTeam } from "../config/resolver.js";
3
- import { CREATE_PROJECT_MUTATION, GET_PROJECT_QUERY, GET_PROJECT_TEAM_ISSUES_QUERY, PROJECT_BY_ID_QUERY, SEARCH_PROJECTS_BY_NAME_QUERY, UPDATE_PROJECT_MUTATION, } from "../queries/projects.js";
3
+ import { ARCHIVE_PROJECT_MUTATION, CREATE_PROJECT_MUTATION, DELETE_PROJECT_MUTATION, GET_PROJECT_QUERY, GET_PROJECT_TEAM_ISSUES_QUERY, PROJECT_BY_ID_QUERY, SEARCH_PROJECTS_BY_NAME_QUERY, UPDATE_PROJECT_MUTATION, } from "../queries/projects.js";
4
4
  import { cached, resolveCacheTTL } from "../utils/disk-cache.js";
5
5
  import { createGraphQLService } from "../utils/graphql-service.js";
6
6
  import { createLinearService } from "../utils/linear-service.js";
@@ -9,7 +9,7 @@ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
9
9
  import { getRootOpts } from "../utils/root-opts.js";
10
10
  import { renderCsv, renderFixedWidthTable, renderMarkdownTable, } from "../utils/table-formatter.js";
11
11
  import { isUuid } from "../utils/uuid.js";
12
- import { splitList } from "../utils/validators.js";
12
+ import { parsePositiveInt, splitList } from "../utils/validators.js";
13
13
  const VALID_PROJECT_STATES = new Set([
14
14
  "backlog",
15
15
  "planned",
@@ -133,37 +133,29 @@ function formatProjectsOutput(projects, format, fieldNames) {
133
133
  }
134
134
  async function resolveProjectWithTeams(graphQLService, projectNameOrId) {
135
135
  if (isUuid(projectNameOrId)) {
136
- const result = await graphQLService.rawRequest(PROJECT_BY_ID_QUERY, {
137
- id: projectNameOrId,
138
- });
139
- const project = result.project;
140
- if (!project) {
136
+ const result = await graphQLService.rawRequest(PROJECT_BY_ID_QUERY, { id: projectNameOrId });
137
+ if (!result.project) {
141
138
  throw new Error(`Project "${projectNameOrId}" not found`);
142
139
  }
143
- const teams = project.teams;
144
140
  return {
145
- projectId: project.id,
146
- projectName: project.name,
147
- currentTeams: (teams?.nodes || []).map((t) => ({
141
+ projectId: result.project.id,
142
+ projectName: result.project.name,
143
+ currentTeams: result.project.teams.nodes.map((t) => ({
148
144
  id: t.id,
149
145
  key: t.key,
150
146
  name: t.name,
151
147
  })),
152
148
  };
153
149
  }
154
- const result = await graphQLService.rawRequest(GET_PROJECT_QUERY, {
155
- name: projectNameOrId,
156
- });
157
- const projectNodes = result.projects?.nodes;
158
- if (!projectNodes?.length) {
150
+ const result = await graphQLService.rawRequest(GET_PROJECT_QUERY, { name: projectNameOrId });
151
+ const project = result.projects.nodes[0];
152
+ if (!project) {
159
153
  throw new Error(`Project "${projectNameOrId}" not found`);
160
154
  }
161
- const project = projectNodes[0];
162
- const teams = project.teams;
163
155
  return {
164
156
  projectId: project.id,
165
157
  projectName: project.name,
166
- currentTeams: (teams?.nodes || []).map((t) => ({
158
+ currentTeams: project.teams.nodes.map((t) => ({
167
159
  id: t.id,
168
160
  key: t.key,
169
161
  name: t.name,
@@ -171,12 +163,14 @@ async function resolveProjectWithTeams(graphQLService, projectNameOrId) {
171
163
  };
172
164
  }
173
165
  function formatTeamsOutput(projectUpdate) {
166
+ if (!projectUpdate.project) {
167
+ throw new Error("Failed to update project");
168
+ }
174
169
  const updatedProject = projectUpdate.project;
175
- const updatedTeams = updatedProject.teams;
176
170
  return {
177
171
  id: updatedProject.id,
178
172
  name: updatedProject.name,
179
- teams: (updatedTeams.nodes || []).map((t) => ({
173
+ teams: updatedProject.teams.nodes.map((t) => ({
180
174
  id: t.id,
181
175
  key: t.key,
182
176
  name: t.name,
@@ -255,9 +249,7 @@ async function handleRemoveTeam(projectNameOrId, teamInput, options, command) {
255
249
  projectId,
256
250
  teamId: finalTeamId,
257
251
  });
258
- const project = issueCheck.project;
259
- const issues = project.issues;
260
- const issueNodes = issues.nodes || [];
252
+ const issueNodes = issueCheck.project?.issues.nodes ?? [];
261
253
  if (issueNodes.length > 0) {
262
254
  const examples = issueNodes
263
255
  .slice(0, 5)
@@ -285,14 +277,10 @@ async function handleCreateProject(name, options, command) {
285
277
  const graphQLService = await createGraphQLService(rootOpts);
286
278
  // Step 1: Check for duplicate projects (case-insensitive)
287
279
  const searchResult = await graphQLService.rawRequest(SEARCH_PROJECTS_BY_NAME_QUERY, { name });
288
- const existing = (searchResult.projects
289
- ?.nodes ?? []).filter((p) => p.name.toLowerCase() === name.toLowerCase());
280
+ const existing = searchResult.projects.nodes.filter((p) => p.name.toLowerCase() === name.toLowerCase());
290
281
  if (existing.length > 0 && !options.force) {
291
282
  const match = existing[0];
292
- const teams = (match.teams?.nodes ??
293
- [])
294
- .map((t) => t.key)
295
- .join(", ");
283
+ const teams = match.teams.nodes.map((t) => t.key).join(", ");
296
284
  throw new Error(`Project "${match.name}" already exists (state: ${match.state}, teams: ${teams}). ` +
297
285
  `Use --force to create anyway, or use the existing project.`);
298
286
  }
@@ -310,11 +298,11 @@ async function handleCreateProject(name, options, command) {
310
298
  ...(teamIds.length > 0 ? { teamIds } : {}),
311
299
  };
312
300
  const createResult = await graphQLService.rawRequest(CREATE_PROJECT_MUTATION, { input });
313
- const created = createResult.projectCreate;
314
- if (!created?.success) {
301
+ if (!createResult.projectCreate.success ||
302
+ !createResult.projectCreate.project) {
315
303
  throw new Error("Failed to create project");
316
304
  }
317
- const project = created.project;
305
+ const project = createResult.projectCreate.project;
318
306
  // Step 4: Set content if provided (separate mutation — Linear API quirk)
319
307
  if (options.content) {
320
308
  await graphQLService.rawRequest(UPDATE_PROJECT_MUTATION, {
@@ -322,10 +310,7 @@ async function handleCreateProject(name, options, command) {
322
310
  input: { content: options.content },
323
311
  });
324
312
  }
325
- const teamList = (project.teams?.nodes ??
326
- [])
327
- .map((t) => t.key)
328
- .join(", ");
313
+ const teamList = project.teams.nodes.map((t) => t.key).join(", ");
329
314
  outputSuccess({
330
315
  id: project.id,
331
316
  name: project.name,
@@ -340,6 +325,42 @@ async function handleCreateProject(name, options, command) {
340
325
  : {}),
341
326
  });
342
327
  }
328
+ async function handleArchiveProject(projectNameOrId, _options, command) {
329
+ const rootOpts = getRootOpts(command);
330
+ const graphQLService = await createGraphQLService(rootOpts);
331
+ const linearService = await createLinearService(rootOpts);
332
+ const projectId = await linearService.resolveProjectId(projectNameOrId);
333
+ const result = await graphQLService.rawRequest(ARCHIVE_PROJECT_MUTATION, { id: projectId });
334
+ const payload = result.projectArchive;
335
+ if (!payload.success) {
336
+ throw new Error(`Failed to archive project "${projectNameOrId}"`);
337
+ }
338
+ outputSuccess({
339
+ success: true,
340
+ archived: true,
341
+ id: projectId,
342
+ entity: payload.entity ?? undefined,
343
+ lastSyncId: payload.lastSyncId,
344
+ });
345
+ }
346
+ async function handleDeleteProject(projectNameOrId, _options, command) {
347
+ const rootOpts = getRootOpts(command);
348
+ const graphQLService = await createGraphQLService(rootOpts);
349
+ const linearService = await createLinearService(rootOpts);
350
+ const projectId = await linearService.resolveProjectId(projectNameOrId);
351
+ const result = await graphQLService.rawRequest(DELETE_PROJECT_MUTATION, { id: projectId });
352
+ const payload = result.projectDelete;
353
+ if (!payload.success) {
354
+ throw new Error(`Failed to delete project "${projectNameOrId}"`);
355
+ }
356
+ outputSuccess({
357
+ success: true,
358
+ deleted: true,
359
+ id: projectId,
360
+ entity: payload.entity ?? undefined,
361
+ lastSyncId: payload.lastSyncId,
362
+ });
363
+ }
343
364
  export function setupProjectsCommands(program) {
344
365
  const projects = program
345
366
  .command("projects")
@@ -354,6 +375,14 @@ export function setupProjectsCommands(program) {
354
375
  .option("--content <markdown>", "full markdown body (shown in project panel)")
355
376
  .option("--force", "create even if a project with the same name exists")
356
377
  .action(handleAsyncCommand(handleCreateProject));
378
+ projects
379
+ .command("archive <project>")
380
+ .description("Archive a project (resolves names)")
381
+ .action(handleAsyncCommand(handleArchiveProject));
382
+ projects
383
+ .command("delete <project>")
384
+ .description("Delete (trash) a project (resolves names)")
385
+ .action(handleAsyncCommand(handleDeleteProject));
357
386
  projects
358
387
  .command("list")
359
388
  .description("List projects")
@@ -366,7 +395,7 @@ export function setupProjectsCommands(program) {
366
395
  .option("--active", "shorthand for --exclude-state completed,canceled (mutually exclusive with --state / --exclude-state)")
367
396
  .action(handleAsyncCommand(async (options, command) => {
368
397
  const rootOpts = getRootOpts(command);
369
- const limit = Number.parseInt(options.limit, 10);
398
+ const limit = parsePositiveInt(options.limit, "--limit");
370
399
  const nameFilter = options.name;
371
400
  const stateFilter = resolveProjectStateFilter(options);
372
401
  const ttl = resolveCacheTTL({
@@ -68,6 +68,13 @@ async function handleWrap(options, command) {
68
68
  return new Set(map.keys());
69
69
  },
70
70
  async resolveUrlKey() {
71
+ // When --workspace-url-key or EL_LINEAR_WORKSPACE_URL_KEY supplies
72
+ // the key, the live lookup never fires — skip the GraphQL service
73
+ // instantiation entirely so `refs wrap --no-validate` works offline.
74
+ const override = options.workspaceUrlKey;
75
+ if (override || process.env.EL_LINEAR_WORKSPACE_URL_KEY) {
76
+ return getWorkspaceUrlKey(undefined, { override });
77
+ }
71
78
  const graphQLService = await createGraphQLService(rootOpts);
72
79
  return getWorkspaceUrlKey(graphQLService);
73
80
  },
@@ -93,5 +100,6 @@ export function setupRefsCommands(program) {
93
100
  .option("--file <path>", "read input from a file instead of stdin")
94
101
  .option("--target <target>", "output format: markdown (default) or slack", "markdown")
95
102
  .option("--no-validate", "skip workspace validation; wrap every regex match. Faster, but may produce broken links for IDs that don't exist.")
103
+ .option("--workspace-url-key <key>", "workspace URL key (the segment after linear.app/). Overrides config + env var. With --no-validate, makes the command fully offline.")
96
104
  .action(handleAsyncCommand(handleWrap));
97
105
  }
@@ -2,18 +2,24 @@ import { CREATE_RELEASE_MUTATION, GET_RELEASE_BY_ID_QUERY, GET_RELEASE_PIPELINES
2
2
  import { createGraphQLService } from "../utils/graphql-service.js";
3
3
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
4
4
  import { getRootOpts } from "../utils/root-opts.js";
5
+ import { parsePositiveInt } from "../utils/validators.js";
5
6
  function transformRelease(release) {
7
+ // Lifecycle timestamps + dates only exist on the list/detail node shapes,
8
+ // not on the create-mutation shape; read defensively rather than splitting
9
+ // the function in two.
10
+ const r = release;
11
+ const documents = release.documents?.nodes;
6
12
  return {
7
13
  id: release.id,
8
14
  name: release.name,
9
- description: release.description || undefined,
10
- version: release.version || undefined,
11
- url: release.url || undefined,
12
- startDate: release.startDate || undefined,
13
- targetDate: release.targetDate || undefined,
14
- startedAt: release.startedAt || undefined,
15
- completedAt: release.completedAt || undefined,
16
- canceledAt: release.canceledAt || undefined,
15
+ description: release.description ?? undefined,
16
+ version: release.version ?? undefined,
17
+ url: release.url ?? undefined,
18
+ startDate: r.startDate ?? undefined,
19
+ targetDate: r.targetDate ?? undefined,
20
+ startedAt: r.startedAt ?? undefined,
21
+ completedAt: r.completedAt ?? undefined,
22
+ canceledAt: r.canceledAt ?? undefined,
17
23
  stage: release.stage
18
24
  ? {
19
25
  id: release.stage.id,
@@ -27,7 +33,7 @@ function transformRelease(release) {
27
33
  name: release.pipeline.name,
28
34
  }
29
35
  : undefined,
30
- documents: release.documents?.nodes?.map((d) => ({
36
+ documents: documents?.map((d) => ({
31
37
  id: d.id,
32
38
  title: d.title,
33
39
  slugId: d.slugId,
@@ -40,14 +46,11 @@ async function handleCreateRelease(name, options, command) {
40
46
  const rootOpts = getRootOpts(command);
41
47
  const graphQLService = await createGraphQLService(rootOpts);
42
48
  const pipelines = await graphQLService.rawRequest(GET_RELEASE_PIPELINES_QUERY, { first: 50 });
43
- const pipelineNodes = pipelines.releasePipelines
44
- ?.nodes;
45
- const pipeline = pipelineNodes?.find((p) => p.id === options.pipeline ||
49
+ const pipelineNodes = pipelines.releasePipelines.nodes;
50
+ const pipeline = pipelineNodes.find((p) => p.id === options.pipeline ||
46
51
  p.name.toLowerCase() === options.pipeline.toLowerCase());
47
52
  if (!pipeline) {
48
- const available = (pipelineNodes ?? [])
49
- .map((p) => p.name)
50
- .join(", ");
53
+ const available = pipelineNodes.map((p) => p.name).join(", ");
51
54
  throw new Error(`Pipeline "${options.pipeline}" not found. Available: ${available || "none"}`);
52
55
  }
53
56
  const input = { name, pipelineId: pipeline.id };
@@ -58,21 +61,17 @@ async function handleCreateRelease(name, options, command) {
58
61
  input.version = options.version;
59
62
  }
60
63
  if (options.stage) {
61
- const stageNodes = pipeline.stages?.nodes;
62
- const stage = stageNodes?.find((s) => s.id === options.stage ||
64
+ const stage = pipeline.stages.nodes.find((s) => s.id === options.stage ||
63
65
  s.name.toLowerCase() === options.stage.toLowerCase());
64
66
  if (stage) {
65
67
  input.stageId = stage.id;
66
68
  }
67
69
  }
68
- const result = await graphQLService.rawRequest(CREATE_RELEASE_MUTATION, {
69
- input,
70
- });
71
- const releaseCreate = result.releaseCreate;
72
- if (!releaseCreate?.success) {
70
+ const result = await graphQLService.rawRequest(CREATE_RELEASE_MUTATION, { input });
71
+ if (!result.releaseCreate.success || !result.releaseCreate.release) {
73
72
  throw new Error(`Failed to create release "${name}"`);
74
73
  }
75
- outputSuccess(transformRelease(releaseCreate.release));
74
+ outputSuccess(transformRelease(result.releaseCreate.release));
76
75
  }
77
76
  export function setupReleasesCommands(program) {
78
77
  const releases = program
@@ -92,11 +91,10 @@ export function setupReleasesCommands(program) {
92
91
  filter.pipeline = { name: { eqIgnoreCase: options.pipeline } };
93
92
  }
94
93
  const result = await graphQLService.rawRequest(GET_RELEASES_QUERY, {
95
- first: Number.parseInt(options.limit, 10),
94
+ first: parsePositiveInt(options.limit, "--limit"),
96
95
  filter: Object.keys(filter).length > 0 ? filter : undefined,
97
96
  });
98
- const data = (result.releases
99
- ?.nodes ?? []).map(transformRelease);
97
+ const data = result.releases.nodes.map(transformRelease);
100
98
  outputSuccess({ data, meta: { count: data.length } });
101
99
  }));
102
100
  releases
@@ -126,12 +124,10 @@ export function setupReleasesCommands(program) {
126
124
  const rootOpts = getRootOpts(command);
127
125
  const graphQLService = await createGraphQLService(rootOpts);
128
126
  const result = await graphQLService.rawRequest(GET_RELEASE_PIPELINES_QUERY, { first: 50 });
129
- const data = (result.releasePipelines
130
- ?.nodes ?? []).map((p) => ({
127
+ const data = result.releasePipelines.nodes.map((p) => ({
131
128
  id: p.id,
132
129
  name: p.name,
133
- stages: (p.stages
134
- ?.nodes ?? []).map((s) => ({
130
+ stages: p.stages.nodes.map((s) => ({
135
131
  id: s.id,
136
132
  name: s.name,
137
133
  type: s.type,
@@ -3,6 +3,7 @@ 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
5
  import { getRootOpts } from "../utils/root-opts.js";
6
+ import { parsePositiveInt } from "../utils/validators.js";
6
7
  const TEMPLATES_QUERY = `
7
8
  query {
8
9
  templates {
@@ -24,59 +25,50 @@ const VALID_TYPES = new Set([
24
25
  ]);
25
26
  const WHITESPACE_RE = /\s+/;
26
27
  function transformSearchResult(r) {
27
- const rType = r.type;
28
- switch (rType) {
28
+ switch (r.type) {
29
29
  case "issue": {
30
30
  const issue = r.issue;
31
- const state = issue?.state;
32
- const team = issue?.team;
33
- const assignee = issue?.assignee;
34
- const project = issue?.project;
35
31
  return {
36
32
  type: "issue",
37
33
  identifier: issue?.identifier,
38
34
  title: issue?.title,
39
- state: state?.name,
40
- team: team?.key,
41
- assignee: assignee
42
- ? resolveUserDisplayName(assignee.id, assignee.name)
35
+ state: issue?.state?.name,
36
+ team: issue?.team?.key,
37
+ assignee: issue?.assignee
38
+ ? resolveUserDisplayName(issue.assignee.id, issue.assignee.name)
43
39
  : undefined,
44
- priority: issue?.priority,
45
- project: project?.name,
40
+ priority: issue?.priority ?? undefined,
41
+ project: issue?.project?.name,
46
42
  id: issue?.id,
47
43
  };
48
44
  }
49
45
  case "project": {
50
- const project = r.project;
51
46
  return {
52
47
  type: "project",
53
- name: project?.name,
54
- state: project?.state,
55
- id: project?.id,
48
+ name: r.project?.name,
49
+ state: r.project?.state,
50
+ id: r.project?.id,
56
51
  };
57
52
  }
58
53
  case "initiative": {
59
- const initiative = r.initiative;
60
54
  return {
61
55
  type: "initiative",
62
- name: initiative?.name,
63
- status: initiative?.status,
64
- id: initiative?.id,
56
+ name: r.initiative?.name,
57
+ status: r.initiative?.status ?? undefined,
58
+ id: r.initiative?.id,
65
59
  };
66
60
  }
67
61
  case "document": {
68
- const doc = r.document;
69
- const docProject = doc?.project;
70
62
  return {
71
63
  type: "document",
72
- title: doc?.title,
73
- project: docProject?.name,
74
- slugId: doc?.slugId,
75
- id: doc?.id,
64
+ title: r.document?.title,
65
+ project: r.document?.project?.name,
66
+ slugId: r.document?.slugId ?? undefined,
67
+ id: r.document?.id,
76
68
  };
77
69
  }
78
70
  default:
79
- return { type: rType, id: null };
71
+ return { type: r.type, id: null };
80
72
  }
81
73
  }
82
74
  function matchesQuery(name, query) {
@@ -122,8 +114,7 @@ function buildSemanticQuery(graphQLService, query, limit, teamOption) {
122
114
  });
123
115
  }
124
116
  function extractSemanticResults(semanticResult, requestedTypes, onlyTemplates) {
125
- const semanticSearch = semanticResult.semanticSearch;
126
- let results = semanticSearch?.results ?? [];
117
+ let results = semanticResult.semanticSearch?.results ?? [];
127
118
  if (requestedTypes && !onlyTemplates) {
128
119
  const semanticTypes = requestedTypes.filter((t) => t !== "template");
129
120
  results = results.filter((r) => semanticTypes.includes(r.type));
@@ -140,7 +131,7 @@ export function setupSearchCommands(program) {
140
131
  .action(handleAsyncCommand(async (query, options, command) => {
141
132
  const rootOpts = getRootOpts(command);
142
133
  const graphQLService = await createGraphQLService(rootOpts);
143
- const limit = Number.parseInt(options.limit, 10);
134
+ const limit = parsePositiveInt(options.limit, "--limit");
144
135
  const requestedTypes = options.type
145
136
  ? options.type.split(",").map((t) => t.trim().toLowerCase())
146
137
  : null;
@@ -3,6 +3,7 @@ import { cached, resolveCacheTTL } from "../utils/disk-cache.js";
3
3
  import { createLinearService } from "../utils/linear-service.js";
4
4
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
5
5
  import { getRootOpts } from "../utils/root-opts.js";
6
+ import { parsePositiveInt } from "../utils/validators.js";
6
7
  export function setupTeamsCommands(program) {
7
8
  const teams = program
8
9
  .command("teams")
@@ -15,7 +16,7 @@ export function setupTeamsCommands(program) {
15
16
  .option("-l, --limit <number>", "limit results", "100")
16
17
  .action(handleAsyncCommand(async (options, command) => {
17
18
  const rootOpts = getRootOpts(command);
18
- const limit = Number.parseInt(options.limit, 10);
19
+ const limit = parsePositiveInt(options.limit, "--limit");
19
20
  const ttl = resolveCacheTTL({
20
21
  configTTL: loadConfig().cacheTTLSeconds,
21
22
  // commander's `--no-cache` produces `cache: false` on the root opts.
@@ -48,14 +48,11 @@ export function setupTemplatesCommands(program) {
48
48
  .action(handleAsyncCommand(async (templateId, _options, command) => {
49
49
  const rootOpts = getRootOpts(command);
50
50
  const graphQLService = await createGraphQLService(rootOpts);
51
- const result = await graphQLService.rawRequest(TEMPLATE_BY_ID_QUERY, {
52
- id: templateId,
53
- });
54
- const template = result.template;
55
- if (!template) {
51
+ const result = await graphQLService.rawRequest(TEMPLATE_BY_ID_QUERY, { id: templateId });
52
+ if (!result.template) {
56
53
  throw new Error(`Template "${templateId}" not found`);
57
54
  }
58
- outputSuccess(template);
55
+ outputSuccess(result.template);
59
56
  }));
60
57
  templates
61
58
  .command("create")
@@ -87,11 +84,10 @@ export function setupTemplatesCommands(program) {
87
84
  if (options.color)
88
85
  input.color = options.color;
89
86
  const result = await graphQLService.rawRequest(TEMPLATE_CREATE_MUTATION, { input });
90
- const payload = result.templateCreate;
91
- if (!payload?.success) {
87
+ if (!result.templateCreate.success || !result.templateCreate.template) {
92
88
  throw new Error("templateCreate returned success=false");
93
89
  }
94
- outputSuccess(payload.template);
90
+ outputSuccess(result.templateCreate.template);
95
91
  }));
96
92
  templates
97
93
  .command("update <templateId>")
@@ -126,11 +122,11 @@ export function setupTemplatesCommands(program) {
126
122
  throw new Error("templates update: at least one of --name, --description, --data, --data-file, --icon, --color, --team-id is required");
127
123
  }
128
124
  const result = await graphQLService.rawRequest(TEMPLATE_UPDATE_MUTATION, { id: templateId, input });
129
- const payload = result.templateUpdate;
130
- if (!payload?.success) {
125
+ if (!result.templateUpdate.success ||
126
+ !result.templateUpdate.template) {
131
127
  throw new Error("templateUpdate returned success=false");
132
128
  }
133
- outputSuccess(payload.template);
129
+ outputSuccess(result.templateUpdate.template);
134
130
  }));
135
131
  templates
136
132
  .command("delete <templateId>")
@@ -139,8 +135,7 @@ export function setupTemplatesCommands(program) {
139
135
  const rootOpts = getRootOpts(command);
140
136
  const graphQLService = await createGraphQLService(rootOpts);
141
137
  const result = await graphQLService.rawRequest(TEMPLATE_DELETE_MUTATION, { id: templateId });
142
- const payload = result.templateDelete;
143
- if (!payload?.success) {
138
+ if (!result.templateDelete.success) {
144
139
  throw new Error("templateDelete returned success=false");
145
140
  }
146
141
  outputSuccess({ id: templateId, deleted: true });
@@ -1,6 +1,7 @@
1
1
  import { createLinearService } from "../utils/linear-service.js";
2
2
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
3
3
  import { getRootOpts } from "../utils/root-opts.js";
4
+ import { parsePositiveInt } from "../utils/validators.js";
4
5
  export function setupUsersCommands(program) {
5
6
  const users = program.command("users").description("User operations");
6
7
  users.action(() => users.help());
@@ -13,7 +14,7 @@ export function setupUsersCommands(program) {
13
14
  .action(handleAsyncCommand(async (options, command) => {
14
15
  const rootOpts = getRootOpts(command);
15
16
  const service = await createLinearService(rootOpts);
16
- const result = await service.getUsers(options.active, Number.parseInt(options.limit, 10), options.name);
17
+ const result = await service.getUsers(options.active, parsePositiveInt(options.limit, "--limit"), options.name);
17
18
  outputSuccess({ data: result, meta: { count: result.length } });
18
19
  }));
19
20
  }
@@ -93,11 +93,10 @@ export async function resolveAssignee(input, rootOpts) {
93
93
  if (input.toLowerCase() === "me") {
94
94
  const graphQLService = await createGraphQLService(rootOpts);
95
95
  const result = await graphQLService.rawRequest("{ viewer { id } }");
96
- const viewer = result.viewer;
97
- if (!viewer?.id) {
96
+ if (!result.viewer?.id) {
98
97
  throw new Error('Could not resolve "me" — viewer query returned no user. Check your API token.');
99
98
  }
100
- return viewer.id;
99
+ return result.viewer.id;
101
100
  }
102
101
  return resolveMember(input);
103
102
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./attachments.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ /** Mirrors `ATTACHMENT_FRAGMENT` from `src/queries/attachments.ts`. */
6
+ export interface AttachmentNode {
7
+ id: string;
8
+ title: string | null;
9
+ url: string;
10
+ createdAt: string | null;
11
+ updatedAt: string | null;
12
+ }
13
+ export interface CreateAttachmentResponse {
14
+ attachmentCreate: {
15
+ success: boolean;
16
+ attachment: AttachmentNode | null;
17
+ };
18
+ }
19
+ export interface DeleteAttachmentResponse {
20
+ attachmentDelete: {
21
+ success: boolean;
22
+ };
23
+ }
24
+ export interface ListAttachmentsResponse {
25
+ issue: {
26
+ attachments: {
27
+ nodes: AttachmentNode[];
28
+ };
29
+ } | null;
30
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./attachments.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ export {};
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./comments.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ export interface CommentUserRef {
6
+ id: string;
7
+ name: string;
8
+ displayName: string | null;
9
+ url: string | null;
10
+ }
11
+ /**
12
+ * Mirrors the comment selection set used by `LIST_COMMENTS_QUERY`,
13
+ * `CREATE_COMMENT_MUTATION`, and (with `issue` overlay)
14
+ * `UPDATE_COMMENT_MUTATION`.
15
+ */
16
+ export interface CommentNode {
17
+ id: string;
18
+ body: string;
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ user: CommentUserRef;
22
+ }
23
+ export interface UpdatedCommentNode extends CommentNode {
24
+ issue: {
25
+ id: string;
26
+ identifier: string;
27
+ } | null;
28
+ }
29
+ export interface ListCommentsResponse {
30
+ issue: {
31
+ id: string;
32
+ identifier: string;
33
+ comments: {
34
+ nodes: CommentNode[];
35
+ };
36
+ } | null;
37
+ }
38
+ export interface CreateCommentResponse {
39
+ commentCreate: {
40
+ success: boolean;
41
+ comment: CommentNode | null;
42
+ };
43
+ }
44
+ export interface UpdateCommentResponse {
45
+ commentUpdate: {
46
+ success: boolean;
47
+ comment: UpdatedCommentNode | null;
48
+ };
49
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./comments.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ export {};