@enrichlayer/el-linear 1.9.0 → 1.15.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 (135) hide show
  1. package/README.md +139 -10
  2. package/claude-skills/linear-operations/SKILL.md +41 -1
  3. package/dist/auth/linear-credential.d.ts +27 -0
  4. package/dist/auth/linear-credential.js +1 -0
  5. package/dist/auth/oauth-app-config.d.ts +4 -3
  6. package/dist/auth/oauth-app-config.js +13 -2
  7. package/dist/auth/oauth-callback.d.ts +2 -3
  8. package/dist/auth/oauth-callback.js +2 -2
  9. package/dist/auth/oauth-client.d.ts +8 -2
  10. package/dist/auth/oauth-client.js +26 -0
  11. package/dist/auth/oauth-fs.d.ts +2 -1
  12. package/dist/auth/oauth-headless.d.ts +2 -1
  13. package/dist/auth/oauth-storage.d.ts +5 -1
  14. package/dist/auth/oauth-storage.js +1 -1
  15. package/dist/auth/oauth-token.d.ts +4 -3
  16. package/dist/auth/oauth-token.js +16 -4
  17. package/dist/auth/token-resolver.d.ts +14 -5
  18. package/dist/auth/token-resolver.js +6 -1
  19. package/dist/commands/attachments.js +2 -1
  20. package/dist/commands/batch.js +18 -21
  21. package/dist/commands/comments.js +22 -33
  22. package/dist/commands/config.js +178 -5
  23. package/dist/commands/cycles.js +2 -1
  24. package/dist/commands/documents.js +2 -1
  25. package/dist/commands/graphql.js +4 -6
  26. package/dist/commands/init/aliases.js +1 -1
  27. package/dist/commands/init/defaults.d.ts +2 -1
  28. package/dist/commands/init/index.js +45 -35
  29. package/dist/commands/init/oauth.d.ts +4 -1
  30. package/dist/commands/init/oauth.js +22 -4
  31. package/dist/commands/init/shared.d.ts +24 -2
  32. package/dist/commands/init/shared.js +35 -4
  33. package/dist/commands/init/token.d.ts +3 -3
  34. package/dist/commands/init/token.js +5 -24
  35. package/dist/commands/init/workspace.d.ts +2 -1
  36. package/dist/commands/init/workspace.js +1 -1
  37. package/dist/commands/introspect.d.ts +27 -0
  38. package/dist/commands/introspect.js +178 -0
  39. package/dist/commands/issue-id.js +1 -3
  40. package/dist/commands/issues/branch.js +9 -1
  41. package/dist/commands/issues/description.js +2 -6
  42. package/dist/commands/issues/link-references.d.ts +21 -0
  43. package/dist/commands/issues/link-references.js +171 -0
  44. package/dist/commands/issues/relations.d.ts +44 -0
  45. package/dist/commands/issues/relations.js +132 -0
  46. package/dist/commands/issues.js +269 -309
  47. package/dist/commands/labels.js +15 -24
  48. package/dist/commands/profile.js +1 -0
  49. package/dist/commands/project-milestones.js +13 -20
  50. package/dist/commands/projects.d.ts +2 -0
  51. package/dist/commands/projects.js +157 -44
  52. package/dist/commands/read-shortcut.d.ts +1 -1
  53. package/dist/commands/read-shortcut.js +28 -8
  54. package/dist/commands/refs.js +75 -8
  55. package/dist/commands/releases.js +26 -30
  56. package/dist/commands/search.js +49 -33
  57. package/dist/commands/teams.js +2 -1
  58. package/dist/commands/templates.js +9 -14
  59. package/dist/commands/users.js +5 -2
  60. package/dist/config/config.d.ts +99 -1
  61. package/dist/config/config.js +264 -52
  62. package/dist/config/error-enrichment.d.ts +62 -0
  63. package/dist/config/error-enrichment.js +417 -0
  64. package/dist/config/issue-validation.d.ts +37 -0
  65. package/dist/config/issue-validation.js +63 -1
  66. package/dist/config/paths.d.ts +2 -8
  67. package/dist/config/paths.js +4 -2
  68. package/dist/config/resolver.d.ts +8 -1
  69. package/dist/config/resolver.js +11 -5
  70. package/dist/main.js +13 -1
  71. package/dist/queries/attachments-types.d.ts +30 -0
  72. package/dist/queries/attachments-types.js +5 -0
  73. package/dist/queries/comments-types.d.ts +55 -0
  74. package/dist/queries/comments-types.js +5 -0
  75. package/dist/queries/common.d.ts +2 -2
  76. package/dist/queries/common.js +8 -0
  77. package/dist/queries/documents-types.d.ts +62 -0
  78. package/dist/queries/documents-types.js +9 -0
  79. package/dist/queries/introspect-types.d.ts +58 -0
  80. package/dist/queries/introspect-types.js +10 -0
  81. package/dist/queries/issues-types.d.ts +481 -0
  82. package/dist/queries/issues-types.js +23 -0
  83. package/dist/queries/issues.d.ts +51 -10
  84. package/dist/queries/issues.js +147 -5
  85. package/dist/queries/labels-types.d.ts +65 -0
  86. package/dist/queries/labels-types.js +5 -0
  87. package/dist/queries/project-milestones-types.d.ts +92 -0
  88. package/dist/queries/project-milestones-types.js +10 -0
  89. package/dist/queries/project-milestones.d.ts +1 -1
  90. package/dist/queries/projects-types.d.ts +76 -0
  91. package/dist/queries/projects-types.js +5 -0
  92. package/dist/queries/projects.d.ts +2 -0
  93. package/dist/queries/projects.js +22 -0
  94. package/dist/queries/releases-types.d.ts +85 -0
  95. package/dist/queries/releases-types.js +5 -0
  96. package/dist/queries/search-types.d.ts +102 -0
  97. package/dist/queries/search-types.js +6 -0
  98. package/dist/queries/templates-types.d.ts +62 -0
  99. package/dist/queries/templates-types.js +9 -0
  100. package/dist/types/linear.d.ts +21 -3
  101. package/dist/utils/auto-link-references.d.ts +3 -3
  102. package/dist/utils/auto-link-references.js +30 -34
  103. package/dist/utils/extract-field.d.ts +19 -0
  104. package/dist/utils/extract-field.js +99 -0
  105. package/dist/utils/file-service.d.ts +6 -13
  106. package/dist/utils/file-service.js +0 -2
  107. package/dist/utils/formatters/summary.js +6 -1
  108. package/dist/utils/graphql-attachments-service.js +6 -9
  109. package/dist/utils/graphql-documents-service.js +19 -25
  110. package/dist/utils/graphql-issues-service.d.ts +112 -46
  111. package/dist/utils/graphql-issues-service.js +398 -206
  112. package/dist/utils/graphql-service.d.ts +10 -12
  113. package/dist/utils/graphql-service.js +0 -3
  114. package/dist/utils/issue-reference-extractor.d.ts +7 -0
  115. package/dist/utils/issue-reference-extractor.js +5 -3
  116. package/dist/utils/issues-service-bootstrap.d.ts +28 -0
  117. package/dist/utils/issues-service-bootstrap.js +27 -0
  118. package/dist/utils/linear-service.d.ts +21 -14
  119. package/dist/utils/linear-service.js +73 -11
  120. package/dist/utils/markdown-prosemirror.js +12 -12
  121. package/dist/utils/mention-resolver.js +1 -1
  122. package/dist/utils/output.d.ts +82 -2
  123. package/dist/utils/output.js +76 -11
  124. package/dist/utils/project-slug.d.ts +21 -0
  125. package/dist/utils/project-slug.js +45 -0
  126. package/dist/utils/protected-ranges.d.ts +14 -0
  127. package/dist/utils/protected-ranges.js +88 -2
  128. package/dist/utils/sanitize-for-log.d.ts +24 -0
  129. package/dist/utils/sanitize-for-log.js +38 -0
  130. package/dist/utils/table-formatter.js +24 -0
  131. package/dist/utils/validators.d.ts +7 -2
  132. package/dist/utils/validators.js +6 -0
  133. package/dist/utils/workspace-url.d.ts +5 -1
  134. package/dist/utils/workspace-url.js +53 -7
  135. package/package.json +2 -2
@@ -4,9 +4,9 @@ import { CREATE_LABEL_MUTATION, FIND_PARENT_LABEL_QUERY, RESTORE_LABEL_MUTATION,
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";
7
- import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
7
+ import { handleAsyncCommand, outputSuccess, warnIfTruncated, } 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
@@ -79,7 +69,7 @@ export function setupLabelsCommands(program) {
79
69
  const rootOpts = getRootOpts(command);
80
70
  const teamFilter = options.team ? resolveTeam(options.team) : undefined;
81
71
  const nameFilter = options.name;
82
- const limit = Number.parseInt(options.limit, 10);
72
+ const limit = parsePositiveInt(options.limit, "--limit");
83
73
  const ttl = resolveCacheTTL({
84
74
  configTTL: loadConfig().cacheTTLSeconds,
85
75
  noCacheFlag: rootOpts.cache === false,
@@ -92,6 +82,7 @@ export function setupLabelsCommands(program) {
92
82
  const service = await createLinearService(rootOpts);
93
83
  return service.getLabels(teamFilter, limit, nameFilter);
94
84
  });
85
+ warnIfTruncated(result.labels.length, limit);
95
86
  outputSuccess({
96
87
  data: result.labels,
97
88
  meta: { count: result.labels.length },
@@ -48,6 +48,7 @@ export function setupProfileCommands(program) {
48
48
  data: {
49
49
  name: active.name ?? "<default>",
50
50
  configPath: active.configPath,
51
+ localConfigPath: active.localConfigPath,
51
52
  tokenPath: active.tokenPath,
52
53
  },
53
54
  });
@@ -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,4 +1,6 @@
1
1
  import type { Command, OptionValues } from "commander";
2
+ import type { LinearProject } from "../types/linear.js";
3
+ export declare function sortActiveFirst(projects: LinearProject[]): LinearProject[];
2
4
  export declare function resolveProjectStateFilter(options: OptionValues): {
3
5
  states?: string[];
4
6
  excludeStates?: string[];
@@ -1,15 +1,15 @@
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";
7
7
  import { logger } from "../utils/logger.js";
8
- import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
8
+ import { handleAsyncCommand, outputSuccess, outputWarning, } 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",
@@ -18,6 +18,58 @@ const VALID_PROJECT_STATES = new Set([
18
18
  "completed",
19
19
  "canceled",
20
20
  ]);
21
+ /**
22
+ * Display ordering for `projects list`: active work first, terminal states
23
+ * last. Without this, the SDK's `updatedAt` order surfaces recently-touched
24
+ * completed/canceled projects ahead of in-progress ones — so an active project
25
+ * can fall past the `--limit` window and an agent concludes it does not exist
26
+ * (the DEV-4175 silent-truncation failure). Stable sort preserves `updatedAt`
27
+ * within each rank.
28
+ *
29
+ * `Linear` exposes additional internal states (e.g. archived) that we don't
30
+ * enumerate here; anything not in this table sorts to the end via
31
+ * `UNRANKED_STATE`.
32
+ */
33
+ const PROJECT_STATE_RANK = {
34
+ started: 0,
35
+ planned: 1,
36
+ paused: 2,
37
+ backlog: 3,
38
+ completed: 4,
39
+ canceled: 5,
40
+ };
41
+ // Sentinel for any state Linear surfaces that isn't in the rank table above
42
+ // (e.g. `archived`, future additions). Number.MAX_SAFE_INTEGER reads more
43
+ // deliberately than a magic `9` and stays correct if PROJECT_STATE_RANK grows.
44
+ const UNRANKED_STATE = Number.MAX_SAFE_INTEGER;
45
+ export function sortActiveFirst(projects) {
46
+ return [...projects].sort((a, b) => (PROJECT_STATE_RANK[a.state ?? ""] ?? UNRANKED_STATE) -
47
+ (PROJECT_STATE_RANK[b.state ?? ""] ?? UNRANKED_STATE));
48
+ }
49
+ /**
50
+ * Emit a truncation warning when `projects list` returned exactly `--limit`
51
+ * rows (the same `count === limit` heuristic as `warnIfTruncated` in
52
+ * `output.ts`). JSON buffers it into `_warnings` so the payload stays a single
53
+ * parseable object; every human-facing format prints to stderr so it can't be
54
+ * silently scrolled past. DEV-4175.
55
+ *
56
+ * The exact total is deliberately omitted from the message. Returning it would
57
+ * require a second `client.projects({ first: 0 })` round-trip (the SDK has no
58
+ * `count`-only helper) or a full pagination walk — both negate the cheap-page
59
+ * default. `--all` in the hint gives the caller a deterministic way to learn
60
+ * the real total without us guessing, so the absence is a feature, not a gap.
61
+ */
62
+ function warnProjectsTruncated(count, limit, format) {
63
+ const msg = `results_truncated: returned ${count} projects matching --limit ${limit}; ` +
64
+ "more may exist. Re-run with --all to fetch every project " +
65
+ `(or --limit ${limit * 2}, or narrow with --name / --state / --team).`;
66
+ if (format === "json") {
67
+ outputWarning(msg);
68
+ }
69
+ else {
70
+ logger.error(msg);
71
+ }
72
+ }
21
73
  function parseStateList(value, flagName) {
22
74
  const states = splitList(value).map((s) => s.toLowerCase());
23
75
  for (const s of states) {
@@ -133,37 +185,29 @@ function formatProjectsOutput(projects, format, fieldNames) {
133
185
  }
134
186
  async function resolveProjectWithTeams(graphQLService, projectNameOrId) {
135
187
  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) {
188
+ const result = await graphQLService.rawRequest(PROJECT_BY_ID_QUERY, { id: projectNameOrId });
189
+ if (!result.project) {
141
190
  throw new Error(`Project "${projectNameOrId}" not found`);
142
191
  }
143
- const teams = project.teams;
144
192
  return {
145
- projectId: project.id,
146
- projectName: project.name,
147
- currentTeams: (teams?.nodes || []).map((t) => ({
193
+ projectId: result.project.id,
194
+ projectName: result.project.name,
195
+ currentTeams: result.project.teams.nodes.map((t) => ({
148
196
  id: t.id,
149
197
  key: t.key,
150
198
  name: t.name,
151
199
  })),
152
200
  };
153
201
  }
154
- const result = await graphQLService.rawRequest(GET_PROJECT_QUERY, {
155
- name: projectNameOrId,
156
- });
157
- const projectNodes = result.projects?.nodes;
158
- if (!projectNodes?.length) {
202
+ const result = await graphQLService.rawRequest(GET_PROJECT_QUERY, { name: projectNameOrId });
203
+ const project = result.projects.nodes[0];
204
+ if (!project) {
159
205
  throw new Error(`Project "${projectNameOrId}" not found`);
160
206
  }
161
- const project = projectNodes[0];
162
- const teams = project.teams;
163
207
  return {
164
208
  projectId: project.id,
165
209
  projectName: project.name,
166
- currentTeams: (teams?.nodes || []).map((t) => ({
210
+ currentTeams: project.teams.nodes.map((t) => ({
167
211
  id: t.id,
168
212
  key: t.key,
169
213
  name: t.name,
@@ -171,12 +215,14 @@ async function resolveProjectWithTeams(graphQLService, projectNameOrId) {
171
215
  };
172
216
  }
173
217
  function formatTeamsOutput(projectUpdate) {
218
+ if (!projectUpdate.project) {
219
+ throw new Error("Failed to update project");
220
+ }
174
221
  const updatedProject = projectUpdate.project;
175
- const updatedTeams = updatedProject.teams;
176
222
  return {
177
223
  id: updatedProject.id,
178
224
  name: updatedProject.name,
179
- teams: (updatedTeams.nodes || []).map((t) => ({
225
+ teams: updatedProject.teams.nodes.map((t) => ({
180
226
  id: t.id,
181
227
  key: t.key,
182
228
  name: t.name,
@@ -255,9 +301,7 @@ async function handleRemoveTeam(projectNameOrId, teamInput, options, command) {
255
301
  projectId,
256
302
  teamId: finalTeamId,
257
303
  });
258
- const project = issueCheck.project;
259
- const issues = project.issues;
260
- const issueNodes = issues.nodes || [];
304
+ const issueNodes = issueCheck.project?.issues.nodes ?? [];
261
305
  if (issueNodes.length > 0) {
262
306
  const examples = issueNodes
263
307
  .slice(0, 5)
@@ -285,14 +329,10 @@ async function handleCreateProject(name, options, command) {
285
329
  const graphQLService = await createGraphQLService(rootOpts);
286
330
  // Step 1: Check for duplicate projects (case-insensitive)
287
331
  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());
332
+ const existing = searchResult.projects.nodes.filter((p) => p.name.toLowerCase() === name.toLowerCase());
290
333
  if (existing.length > 0 && !options.force) {
291
334
  const match = existing[0];
292
- const teams = (match.teams?.nodes ??
293
- [])
294
- .map((t) => t.key)
295
- .join(", ");
335
+ const teams = match.teams.nodes.map((t) => t.key).join(", ");
296
336
  throw new Error(`Project "${match.name}" already exists (state: ${match.state}, teams: ${teams}). ` +
297
337
  `Use --force to create anyway, or use the existing project.`);
298
338
  }
@@ -310,11 +350,11 @@ async function handleCreateProject(name, options, command) {
310
350
  ...(teamIds.length > 0 ? { teamIds } : {}),
311
351
  };
312
352
  const createResult = await graphQLService.rawRequest(CREATE_PROJECT_MUTATION, { input });
313
- const created = createResult.projectCreate;
314
- if (!created?.success) {
353
+ if (!createResult.projectCreate.success ||
354
+ !createResult.projectCreate.project) {
315
355
  throw new Error("Failed to create project");
316
356
  }
317
- const project = created.project;
357
+ const project = createResult.projectCreate.project;
318
358
  // Step 4: Set content if provided (separate mutation — Linear API quirk)
319
359
  if (options.content) {
320
360
  await graphQLService.rawRequest(UPDATE_PROJECT_MUTATION, {
@@ -322,10 +362,7 @@ async function handleCreateProject(name, options, command) {
322
362
  input: { content: options.content },
323
363
  });
324
364
  }
325
- const teamList = (project.teams?.nodes ??
326
- [])
327
- .map((t) => t.key)
328
- .join(", ");
365
+ const teamList = project.teams.nodes.map((t) => t.key).join(", ");
329
366
  outputSuccess({
330
367
  id: project.id,
331
368
  name: project.name,
@@ -340,6 +377,42 @@ async function handleCreateProject(name, options, command) {
340
377
  : {}),
341
378
  });
342
379
  }
380
+ async function handleArchiveProject(projectNameOrId, _options, command) {
381
+ const rootOpts = getRootOpts(command);
382
+ const graphQLService = await createGraphQLService(rootOpts);
383
+ const linearService = await createLinearService(rootOpts);
384
+ const projectId = await linearService.resolveProjectId(projectNameOrId);
385
+ const result = await graphQLService.rawRequest(ARCHIVE_PROJECT_MUTATION, { id: projectId });
386
+ const payload = result.projectArchive;
387
+ if (!payload.success) {
388
+ throw new Error(`Failed to archive project "${projectNameOrId}"`);
389
+ }
390
+ outputSuccess({
391
+ success: true,
392
+ archived: true,
393
+ id: projectId,
394
+ entity: payload.entity ?? undefined,
395
+ lastSyncId: payload.lastSyncId,
396
+ });
397
+ }
398
+ async function handleDeleteProject(projectNameOrId, _options, command) {
399
+ const rootOpts = getRootOpts(command);
400
+ const graphQLService = await createGraphQLService(rootOpts);
401
+ const linearService = await createLinearService(rootOpts);
402
+ const projectId = await linearService.resolveProjectId(projectNameOrId);
403
+ const result = await graphQLService.rawRequest(DELETE_PROJECT_MUTATION, { id: projectId });
404
+ const payload = result.projectDelete;
405
+ if (!payload.success) {
406
+ throw new Error(`Failed to delete project "${projectNameOrId}"`);
407
+ }
408
+ outputSuccess({
409
+ success: true,
410
+ deleted: true,
411
+ id: projectId,
412
+ entity: payload.entity ?? undefined,
413
+ lastSyncId: payload.lastSyncId,
414
+ });
415
+ }
343
416
  export function setupProjectsCommands(program) {
344
417
  const projects = program
345
418
  .command("projects")
@@ -354,6 +427,14 @@ export function setupProjectsCommands(program) {
354
427
  .option("--content <markdown>", "full markdown body (shown in project panel)")
355
428
  .option("--force", "create even if a project with the same name exists")
356
429
  .action(handleAsyncCommand(handleCreateProject));
430
+ projects
431
+ .command("archive <project>")
432
+ .description("Archive a project (resolves names)")
433
+ .action(handleAsyncCommand(handleArchiveProject));
434
+ projects
435
+ .command("delete <project>")
436
+ .description("Delete (trash) a project (resolves names)")
437
+ .action(handleAsyncCommand(handleDeleteProject));
357
438
  projects
358
439
  .command("list")
359
440
  .description("List projects")
@@ -361,13 +442,22 @@ export function setupProjectsCommands(program) {
361
442
  .option("--format <format>", "output format (json, summary, table, md, csv)", "json")
362
443
  .option("--fields <fields>", "columns for table/csv (comma-separated: name,state,progress,teams,lead,targetDate)")
363
444
  .option("--name <substring>", "filter by case-insensitive substring on project name")
445
+ .option("--team <key|name>", "filter to projects belonging to a specific team (e.g. DEV, FE)")
364
446
  .option("--state <names>", "include only projects in these states (comma-separated: backlog, planned, started, paused, completed, canceled)")
365
447
  .option("--exclude-state <names>", "exclude projects in these states (comma-separated). Mutually exclusive with --state.")
366
448
  .option("--active", "shorthand for --exclude-state completed,canceled (mutually exclusive with --state / --exclude-state)")
449
+ .option("--all", "fetch every project (paginates fully). Equivalent to --limit 0; overrides --limit when both are given.")
367
450
  .action(handleAsyncCommand(async (options, command) => {
368
451
  const rootOpts = getRootOpts(command);
369
- const limit = Number.parseInt(options.limit, 10);
452
+ // `--all` / `--limit 0` mean unlimited. Stored as `limit = 0`,
453
+ // which `LinearService.getProjects` interprets as "paginate
454
+ // fully". DEV-4175.
455
+ const unlimited = options.all === true || options.limit === "0";
456
+ const limit = unlimited
457
+ ? 0
458
+ : parsePositiveInt(options.limit, "--limit");
370
459
  const nameFilter = options.name;
460
+ const teamFilter = options.team;
371
461
  const stateFilter = resolveProjectStateFilter(options);
372
462
  const ttl = resolveCacheTTL({
373
463
  configTTL: loadConfig().cacheTTLSeconds,
@@ -377,31 +467,54 @@ export function setupProjectsCommands(program) {
377
467
  // don't collide.
378
468
  const cacheKey = `projects-list-limit:${limit}` +
379
469
  `-name:${nameFilter ?? "_all"}` +
470
+ `-team:${teamFilter ?? "_all"}` +
380
471
  `-states:${stateFilter.states?.join(",") ?? "_any"}` +
381
472
  `-excl:${stateFilter.excludeStates?.join(",") ?? "_none"}`;
382
473
  const result = await cached(cacheKey, ttl, async () => {
383
474
  const service = await createLinearService(rootOpts);
475
+ let teamId;
476
+ if (teamFilter) {
477
+ const resolved = resolveTeam(teamFilter);
478
+ teamId = await service.resolveTeamId(resolved);
479
+ }
384
480
  return service.getProjects(limit, {
385
481
  nameFilter,
482
+ teamId,
386
483
  states: stateFilter.states,
387
484
  excludeStates: stateFilter.excludeStates,
388
485
  });
389
486
  });
487
+ // Active-first sort: keeps started/planned/paused/backlog
488
+ // ahead of completed/canceled in every format, so the active
489
+ // set always fits within `--limit`. DEV-4175.
490
+ const sorted = sortActiveFirst(result);
390
491
  const format = options.format;
391
- if (format === "table" ||
492
+ const isTabular = format === "table" ||
392
493
  format === "md" ||
393
494
  format === "markdown" ||
394
- format === "csv") {
495
+ format === "csv";
496
+ // Emit the truncation warning BEFORE outputting: the JSON
497
+ // branch buffers into `_warnings`, which the next
498
+ // `outputSuccess()` call drains. All other formats route to
499
+ // stderr via `logger.error` so the visible payload (table,
500
+ // markdown, summary) stays clean.
501
+ if (!unlimited && sorted.length === limit) {
502
+ warnProjectsTruncated(sorted.length, limit, format);
503
+ }
504
+ if (isTabular) {
395
505
  const fieldList = options.fields
396
506
  ? splitList(options.fields)
397
507
  : undefined;
398
- formatProjectsOutput(result, format, fieldList);
508
+ formatProjectsOutput(sorted, format, fieldList);
399
509
  if (format === "table") {
400
- logger.info(`\n${result.length} projects`);
510
+ logger.info(`\n${sorted.length} projects`);
401
511
  }
402
512
  }
403
513
  else {
404
- outputSuccess({ data: result, meta: { count: result.length } });
514
+ outputSuccess({
515
+ data: sorted,
516
+ meta: { count: sorted.length },
517
+ });
405
518
  }
406
519
  }));
407
520
  projects
@@ -9,4 +9,4 @@ export declare function setupReadShortcut(program: Command): void;
9
9
  * `issues read` (in `commands/issues.ts`) both call one implementation
10
10
  * instead of carrying byte-equivalent duplicates. ALL-938 cleanup.
11
11
  */
12
- export declare function readIssues(issueIds: string[], _options: Record<string, unknown>, command: Command): Promise<void>;
12
+ export declare function readIssues(issueIds: string[], options: Record<string, unknown>, command: Command): Promise<void>;
@@ -1,8 +1,7 @@
1
1
  import { downloadLinearUploads } from "../utils/download-uploads.js";
2
+ import { extractField } from "../utils/extract-field.js";
2
3
  import { createFileService } from "../utils/file-service.js";
3
- import { GraphQLIssuesService } from "../utils/graphql-issues-service.js";
4
- import { createGraphQLService } from "../utils/graphql-service.js";
5
- import { createLinearService } from "../utils/linear-service.js";
4
+ import { createIssuesService } from "../utils/issues-service-bootstrap.js";
6
5
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
7
6
  import { getRootOpts } from "../utils/root-opts.js";
8
7
  /**
@@ -21,7 +20,10 @@ export function setupReadShortcut(program) {
21
20
  .alias("view")
22
21
  .alias("show")
23
22
  .description("Shortcut for `issues read`. Get issue details by identifier.")
24
- .addHelpText("after", "\nExamples:\n el-linear read ADM-652\n el-linear get DEV-123 DEV-456\n el-linear ADM-652 (auto-detected)")
23
+ .option("--field <name>", 'Extract a single named section from the issue description (e.g. "Done when"). ' +
24
+ "Matches H2/H3 headers and bold pseudo-headers case-insensitively. " +
25
+ "Outputs the section text only — no JSON envelope.")
26
+ .addHelpText("after", '\nExamples:\n el-linear read ADM-652\n el-linear get DEV-123 DEV-456\n el-linear ADM-652 (auto-detected)\n el-linear read DEV-123 --field "Done when" (just that section)')
25
27
  .action(handleAsyncCommand(readIssues));
26
28
  // Catch-all: if argv looks like `el-linear ADM-652 [DEV-123 ...]`, run read
27
29
  const originalParse = program.parse.bind(program);
@@ -54,15 +56,33 @@ export function setupReadShortcut(program) {
54
56
  * `issues read` (in `commands/issues.ts`) both call one implementation
55
57
  * instead of carrying byte-equivalent duplicates. ALL-938 cleanup.
56
58
  */
57
- export async function readIssues(issueIds, _options, command) {
59
+ export async function readIssues(issueIds, options, command) {
58
60
  const rootOpts = getRootOpts(command);
59
- const graphQLService = await createGraphQLService(rootOpts);
60
- const linearService = await createLinearService(rootOpts);
61
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
61
+ const { issuesService } = await createIssuesService(rootOpts);
62
62
  const fileService = await createFileService(rootOpts);
63
+ const fieldName = typeof options.field === "string" ? options.field : null;
64
+ // --field is single-issue only. With multiple issues, a section
65
+ // extraction can't sensibly fan out to N different bodies — the
66
+ // caller almost always wants one section from one issue.
67
+ if (fieldName && issueIds.length > 1) {
68
+ throw new Error("--field is single-issue only; pass exactly one issueId. " +
69
+ "For multiple issues, drop --field and use --jq or --format summary.");
70
+ }
63
71
  if (issueIds.length === 1) {
64
72
  const issue = await issuesService.getIssueById(issueIds[0]);
65
73
  const resolved = await downloadLinearUploads(issue, fileService);
74
+ if (fieldName) {
75
+ const section = extractField(resolved.description ?? "", fieldName);
76
+ if (section === null) {
77
+ // Print nothing to stdout, exit non-zero with a stderr hint.
78
+ // Mirrors `grep` semantics for "not found" — scripts can
79
+ // branch on the exit code.
80
+ process.stderr.write(`el-linear: section "${fieldName}" not found in ${resolved.identifier}'s description\n`);
81
+ process.exit(1);
82
+ }
83
+ process.stdout.write(`${section}\n`);
84
+ return;
85
+ }
66
86
  outputSuccess(resolved);
67
87
  }
68
88
  else {