@enrichlayer/el-linear 1.10.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.
- package/README.md +126 -10
- package/claude-skills/linear-operations/SKILL.md +41 -1
- package/dist/auth/linear-credential.d.ts +27 -0
- package/dist/auth/linear-credential.js +1 -0
- package/dist/auth/oauth-app-config.d.ts +4 -3
- package/dist/auth/oauth-app-config.js +13 -2
- package/dist/auth/oauth-callback.d.ts +2 -3
- package/dist/auth/oauth-callback.js +2 -2
- package/dist/auth/oauth-client.d.ts +8 -2
- package/dist/auth/oauth-client.js +26 -0
- package/dist/auth/oauth-fs.d.ts +2 -1
- package/dist/auth/oauth-headless.d.ts +2 -1
- package/dist/auth/oauth-storage.d.ts +5 -1
- package/dist/auth/oauth-storage.js +1 -1
- package/dist/auth/oauth-token.d.ts +4 -3
- package/dist/auth/oauth-token.js +16 -4
- package/dist/auth/token-resolver.d.ts +14 -5
- package/dist/auth/token-resolver.js +6 -1
- package/dist/commands/batch.js +18 -21
- package/dist/commands/comments.js +5 -5
- package/dist/commands/config.js +178 -5
- package/dist/commands/init/aliases.js +1 -1
- package/dist/commands/init/defaults.d.ts +2 -1
- package/dist/commands/init/index.js +45 -35
- package/dist/commands/init/oauth.d.ts +4 -1
- package/dist/commands/init/oauth.js +22 -4
- package/dist/commands/init/shared.d.ts +24 -2
- package/dist/commands/init/shared.js +35 -4
- package/dist/commands/init/token.d.ts +3 -3
- package/dist/commands/init/token.js +5 -24
- package/dist/commands/init/workspace.d.ts +2 -1
- package/dist/commands/init/workspace.js +1 -1
- package/dist/commands/introspect.d.ts +27 -0
- package/dist/commands/introspect.js +178 -0
- package/dist/commands/issues/branch.js +9 -1
- package/dist/commands/issues/relations.d.ts +3 -14
- package/dist/commands/issues/relations.js +3 -3
- package/dist/commands/issues.js +222 -43
- package/dist/commands/labels.js +2 -1
- package/dist/commands/profile.js +1 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +91 -7
- package/dist/commands/read-shortcut.d.ts +1 -1
- package/dist/commands/read-shortcut.js +28 -8
- package/dist/commands/refs.js +67 -8
- package/dist/commands/search.js +30 -5
- package/dist/commands/users.js +4 -2
- package/dist/config/config.d.ts +99 -1
- package/dist/config/config.js +264 -52
- package/dist/config/error-enrichment.d.ts +62 -0
- package/dist/config/error-enrichment.js +417 -0
- package/dist/config/issue-validation.d.ts +37 -0
- package/dist/config/issue-validation.js +63 -1
- package/dist/config/paths.d.ts +2 -8
- package/dist/config/paths.js +4 -2
- package/dist/config/resolver.d.ts +8 -1
- package/dist/config/resolver.js +9 -2
- package/dist/main.js +13 -1
- package/dist/queries/comments-types.d.ts +15 -9
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +8 -0
- package/dist/queries/documents-types.d.ts +4 -3
- package/dist/queries/introspect-types.d.ts +8 -7
- package/dist/queries/issues-types.d.ts +92 -27
- package/dist/queries/issues.d.ts +49 -10
- package/dist/queries/issues.js +125 -5
- package/dist/queries/labels-types.d.ts +7 -6
- package/dist/queries/project-milestones-types.d.ts +5 -4
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/queries/projects-types.d.ts +8 -7
- package/dist/queries/releases-types.d.ts +5 -4
- package/dist/queries/search-types.d.ts +28 -12
- package/dist/queries/templates-types.d.ts +3 -2
- package/dist/types/linear.d.ts +13 -1
- package/dist/utils/auto-link-references.d.ts +3 -3
- package/dist/utils/auto-link-references.js +1 -10
- package/dist/utils/extract-field.d.ts +19 -0
- package/dist/utils/extract-field.js +99 -0
- package/dist/utils/file-service.d.ts +6 -13
- package/dist/utils/file-service.js +0 -2
- package/dist/utils/formatters/summary.js +6 -1
- package/dist/utils/graphql-issues-service.d.ts +101 -45
- package/dist/utils/graphql-issues-service.js +252 -39
- package/dist/utils/graphql-service.d.ts +10 -12
- package/dist/utils/graphql-service.js +0 -3
- package/dist/utils/issue-reference-extractor.d.ts +7 -0
- package/dist/utils/issue-reference-extractor.js +5 -3
- package/dist/utils/issues-service-bootstrap.d.ts +28 -0
- package/dist/utils/issues-service-bootstrap.js +27 -0
- package/dist/utils/linear-service.d.ts +21 -14
- package/dist/utils/linear-service.js +73 -11
- package/dist/utils/markdown-prosemirror.js +12 -12
- package/dist/utils/mention-resolver.js +1 -1
- package/dist/utils/output.d.ts +81 -3
- package/dist/utils/output.js +61 -6
- package/dist/utils/project-slug.d.ts +21 -0
- package/dist/utils/project-slug.js +45 -0
- package/dist/utils/protected-ranges.d.ts +14 -0
- package/dist/utils/protected-ranges.js +88 -2
- package/dist/utils/sanitize-for-log.d.ts +24 -0
- package/dist/utils/sanitize-for-log.js +38 -0
- package/dist/utils/table-formatter.js +24 -0
- package/dist/utils/validators.d.ts +7 -2
- package/dist/utils/validators.js +6 -0
- package/dist/utils/workspace-url.js +20 -4
- package/package.json +2 -2
package/dist/commands/issues.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { loadConfig } from "../config/config.js";
|
|
3
|
+
import { enrichProjectResolverError, enrichValidationErrors, } from "../config/error-enrichment.js";
|
|
3
4
|
import { enforceValidation, validateIssueCreation, } from "../config/issue-validation.js";
|
|
4
5
|
import { resolveAssignee, resolveLabels, resolveMember, resolveTeam, } from "../config/resolver.js";
|
|
5
6
|
import { resolveDefaultStatus } from "../config/status-defaults.js";
|
|
@@ -8,11 +9,11 @@ import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, } from "../qu
|
|
|
8
9
|
import { createFileService } from "../utils/file-service.js";
|
|
9
10
|
import { applyFooter } from "../utils/footer.js";
|
|
10
11
|
import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { createLinearService } from "../utils/linear-service.js";
|
|
12
|
+
import { createGraphQLService, } from "../utils/graphql-service.js";
|
|
13
|
+
import { createIssuesService } from "../utils/issues-service-bootstrap.js";
|
|
14
|
+
import { createLinearService, } from "../utils/linear-service.js";
|
|
14
15
|
import { logger } from "../utils/logger.js";
|
|
15
|
-
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
16
|
+
import { handleAsyncCommand, outputSuccess, outputWarning, warnIfTruncated, } from "../utils/output.js";
|
|
16
17
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
17
18
|
import { formatCsv, formatMarkdown, formatTable, } from "../utils/table-formatter.js";
|
|
18
19
|
import { parsePositiveInt, parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
|
|
@@ -36,6 +37,9 @@ function isImageFile(filename) {
|
|
|
36
37
|
return ext !== -1 && IMAGE_EXTENSIONS.has(filename.slice(ext).toLowerCase());
|
|
37
38
|
}
|
|
38
39
|
function validateUpdateOptions(options) {
|
|
40
|
+
if (options.delegate && options.clearDelegate) {
|
|
41
|
+
throw new Error("Cannot use --delegate and --clear-delegate together");
|
|
42
|
+
}
|
|
39
43
|
if (options.parentTicket && options.clearParentTicket) {
|
|
40
44
|
throw new Error("Cannot use --parent-ticket and --clear-parent-ticket together");
|
|
41
45
|
}
|
|
@@ -61,7 +65,7 @@ function validateUpdateOptions(options) {
|
|
|
61
65
|
throw new Error("Cannot use --description and --append-description together");
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
|
-
function buildUpdateArgs(issueId, options, assigneeId) {
|
|
68
|
+
function buildUpdateArgs(issueId, options, assigneeId, delegateId) {
|
|
65
69
|
let labelIds;
|
|
66
70
|
if (options.clearLabels) {
|
|
67
71
|
labelIds = [];
|
|
@@ -84,6 +88,7 @@ function buildUpdateArgs(issueId, options, assigneeId) {
|
|
|
84
88
|
statusId: options.status,
|
|
85
89
|
priority: priorityInput ? validatePriority(priorityInput) : undefined,
|
|
86
90
|
assigneeId,
|
|
91
|
+
delegateId,
|
|
87
92
|
projectId: options.project,
|
|
88
93
|
labelIds,
|
|
89
94
|
parentId: options.parentTicket || (options.clearParentTicket ? null : undefined),
|
|
@@ -136,44 +141,61 @@ function sortIssues(issues, sort) {
|
|
|
136
141
|
}
|
|
137
142
|
return sorted;
|
|
138
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Map commander's `options.project` (string | false | undefined) onto the
|
|
146
|
+
* `SearchIssueArgs.project` discriminant. Commander turns `--no-project`
|
|
147
|
+
* into `false`; a missing flag is `undefined`; a string is the project
|
|
148
|
+
* name or UUID to filter to.
|
|
149
|
+
*/
|
|
150
|
+
function resolveProjectFlag(value) {
|
|
151
|
+
if (value === false)
|
|
152
|
+
return { kind: "none" };
|
|
153
|
+
if (value)
|
|
154
|
+
return { kind: "id", id: value };
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
139
157
|
async function handleListIssues(options, command) {
|
|
140
158
|
if (options.label && !options.labels) {
|
|
141
159
|
options.labels = options.label;
|
|
142
160
|
}
|
|
143
161
|
const rootOpts = getRootOpts(command);
|
|
144
|
-
const
|
|
145
|
-
const linearService = await createLinearService(rootOpts);
|
|
146
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
162
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
147
163
|
const hasFilters = options.team ||
|
|
148
164
|
options.labels ||
|
|
149
165
|
options.status ||
|
|
150
166
|
options.assignee ||
|
|
167
|
+
options.delegate ||
|
|
151
168
|
options.project ||
|
|
152
169
|
options.project === false ||
|
|
153
170
|
options.priority;
|
|
171
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
154
172
|
if (hasFilters) {
|
|
155
173
|
const searchArgs = {
|
|
156
174
|
teamId: options.team ? resolveTeam(options.team) : undefined,
|
|
157
175
|
assigneeId: options.assignee
|
|
158
176
|
? await resolveAssignee(options.assignee, rootOpts)
|
|
159
177
|
: undefined,
|
|
160
|
-
|
|
161
|
-
|
|
178
|
+
delegateId: options.delegate
|
|
179
|
+
? resolveMember(options.delegate)
|
|
180
|
+
: undefined,
|
|
181
|
+
project: resolveProjectFlag(options.project),
|
|
162
182
|
labelNames: options.labels ? splitList(options.labels) : undefined,
|
|
163
183
|
status: options.status ? splitList(options.status) : undefined,
|
|
164
184
|
priority: options.priority
|
|
165
185
|
? parsePriorityFilter(options.priority)
|
|
166
186
|
: undefined,
|
|
167
187
|
orderBy: options.sort === "created" ? "createdAt" : "updatedAt",
|
|
168
|
-
limit
|
|
188
|
+
limit,
|
|
169
189
|
};
|
|
170
190
|
const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
|
|
191
|
+
warnIfTruncated(result.length, limit);
|
|
171
192
|
outputIssues(result, options.format, options.fields, {
|
|
172
193
|
team: options.team,
|
|
173
194
|
});
|
|
174
195
|
}
|
|
175
196
|
else {
|
|
176
|
-
const result = sortIssues(await issuesService.getIssues(
|
|
197
|
+
const result = sortIssues(await issuesService.getIssues(limit), options.sort);
|
|
198
|
+
warnIfTruncated(result.length, limit);
|
|
177
199
|
outputIssues(result, options.format, options.fields, {});
|
|
178
200
|
}
|
|
179
201
|
}
|
|
@@ -182,27 +204,58 @@ async function handleSearchIssues(query, options, command) {
|
|
|
182
204
|
options.labels = options.label;
|
|
183
205
|
}
|
|
184
206
|
const rootOpts = getRootOpts(command);
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
207
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
208
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
188
209
|
const searchArgs = {
|
|
189
210
|
query,
|
|
190
211
|
teamId: options.team ? resolveTeam(options.team) : undefined,
|
|
191
212
|
assigneeId: options.assignee
|
|
192
213
|
? await resolveAssignee(options.assignee, rootOpts)
|
|
193
214
|
: undefined,
|
|
194
|
-
|
|
195
|
-
|
|
215
|
+
delegateId: options.delegate
|
|
216
|
+
? resolveMember(options.delegate)
|
|
217
|
+
: undefined,
|
|
218
|
+
project: resolveProjectFlag(options.project),
|
|
196
219
|
status: options.status ? splitList(options.status) : undefined,
|
|
197
220
|
labelNames: options.labels ? splitList(options.labels) : undefined,
|
|
198
221
|
priority: options.priority
|
|
199
222
|
? parsePriorityFilter(options.priority)
|
|
200
223
|
: undefined,
|
|
201
|
-
limit
|
|
224
|
+
limit,
|
|
202
225
|
};
|
|
203
226
|
const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
|
|
227
|
+
warnIfTruncated(result.length, limit);
|
|
204
228
|
outputIssues(result, options.format, options.fields, { query });
|
|
205
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Run an `issuesService.createIssue` / `.updateIssue` call and, if it
|
|
232
|
+
* throws a `Project "<input>" not found` resolver error, enrich the
|
|
233
|
+
* message with team-scoped project suggestions before re-throwing. Same
|
|
234
|
+
* suggestion shape as the `Missing --project` validation path — agents
|
|
235
|
+
* get a concrete retry list whether the failure was "forgot to pass
|
|
236
|
+
* --project" or "passed --project but it didn't resolve".
|
|
237
|
+
*
|
|
238
|
+
* Best-effort: any other error class flows through unchanged, and any
|
|
239
|
+
* failure inside enrichment falls back to the original message.
|
|
240
|
+
*/
|
|
241
|
+
async function withProjectResolverEnrichment(fn, options, services) {
|
|
242
|
+
try {
|
|
243
|
+
return await fn();
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
if (err instanceof Error && options.team) {
|
|
247
|
+
const enriched = await enrichProjectResolverError(err.message, { team: options.team, title: options.title }, services);
|
|
248
|
+
if (enriched !== err.message) {
|
|
249
|
+
// Mutate in place rather than throwing a fresh Error — preserves
|
|
250
|
+
// the original error subclass and stack trace so consumers that
|
|
251
|
+
// rely on `instanceof` (or read `.stack` via EL_LINEAR_DEBUG=1)
|
|
252
|
+
// keep working. Only the user-facing message changes.
|
|
253
|
+
err.message = enriched;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
206
259
|
async function resolveCreateInputs(title, options, rootOpts) {
|
|
207
260
|
const config = loadConfig();
|
|
208
261
|
enforceTerms([title, options.description], { strict: options.strict });
|
|
@@ -244,6 +297,22 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
244
297
|
if (validationResult.normalizedLabels) {
|
|
245
298
|
options.labels = validationResult.normalizedLabels.join(",");
|
|
246
299
|
}
|
|
300
|
+
// Match the team-resolution rule used for the create call below
|
|
301
|
+
// (line ~389): fall back to config.defaultTeam when --team is omitted.
|
|
302
|
+
// Without this, users with a configured default team would see bare
|
|
303
|
+
// validation errors with no suggestions — the exact opposite of what
|
|
304
|
+
// the AI-coding-agent use case needs.
|
|
305
|
+
const enrichTeam = options.team || config.defaultTeam;
|
|
306
|
+
if (validationResult.errors.length > 0 && enrichTeam) {
|
|
307
|
+
try {
|
|
308
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
309
|
+
const linearService = await createLinearService(rootOpts);
|
|
310
|
+
await enrichValidationErrors(validationResult, { team: enrichTeam, title }, { graphQLService, linearService });
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Best-effort enrichment — never block the original error.
|
|
314
|
+
}
|
|
315
|
+
}
|
|
247
316
|
enforceValidation(validationResult);
|
|
248
317
|
}
|
|
249
318
|
if (!effectivePriorityInput) {
|
|
@@ -254,9 +323,16 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
254
323
|
const assigneeId = effectiveAssignee
|
|
255
324
|
? await resolveAssignee(effectiveAssignee, rootOpts)
|
|
256
325
|
: undefined;
|
|
326
|
+
const delegateId = options.delegate
|
|
327
|
+
? resolveMember(options.delegate)
|
|
328
|
+
: undefined;
|
|
257
329
|
let labelIds = [];
|
|
258
330
|
if (options.labels) {
|
|
259
|
-
|
|
331
|
+
// No `teamInput`: team-scoped labels are resolved API-side against the
|
|
332
|
+
// issue's *final* team, which `createIssue` may auto-switch to match
|
|
333
|
+
// the project. Resolving a team-scoped config UUID here would pin the
|
|
334
|
+
// label to the pre-switch team and Linear would reject the mismatch.
|
|
335
|
+
labelIds = resolveLabels(splitList(options.labels));
|
|
260
336
|
}
|
|
261
337
|
if (options.claude) {
|
|
262
338
|
const claudeId = config.labels.workspace.claude;
|
|
@@ -280,6 +356,7 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
280
356
|
teamInput,
|
|
281
357
|
teamId,
|
|
282
358
|
assigneeId,
|
|
359
|
+
delegateId,
|
|
283
360
|
labelIds,
|
|
284
361
|
status,
|
|
285
362
|
subscriberIds,
|
|
@@ -318,7 +395,7 @@ function buildDescriptionWithAttachments(baseDescription, uploadResults) {
|
|
|
318
395
|
}
|
|
319
396
|
async function handleCreateIssue(title, options, command) {
|
|
320
397
|
const rootOpts = getRootOpts(command);
|
|
321
|
-
const { teamInput, teamId, assigneeId, labelIds, status, subscriberIds, priority, } = await resolveCreateInputs(title ?? "", options, rootOpts);
|
|
398
|
+
const { teamInput, teamId, assigneeId, delegateId, labelIds, status, subscriberIds, priority, } = await resolveCreateInputs(title ?? "", options, rootOpts);
|
|
322
399
|
const uploadResults = await uploadAttachmentsIfNeeded(options, rootOpts);
|
|
323
400
|
const descriptionWithAttachments = buildDescriptionWithAttachments(resolveDescription(options) || "", uploadResults);
|
|
324
401
|
// Append messageFooter (config or --footer flag) so auto-link picks up any
|
|
@@ -330,14 +407,12 @@ async function handleCreateIssue(title, options, command) {
|
|
|
330
407
|
footer: explicitFooter,
|
|
331
408
|
noFooter,
|
|
332
409
|
}) ?? "";
|
|
333
|
-
const graphQLService = await
|
|
334
|
-
const linearService = await createLinearService(rootOpts);
|
|
335
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
410
|
+
const { graphQLService, linearService, issuesService } = await createIssuesService(rootOpts);
|
|
336
411
|
// Wrap valid issue identifiers as markdown links before creating, so the description
|
|
337
412
|
// saved on Linear has clickable refs from the start. Self-reference can't apply here
|
|
338
413
|
// because the issue doesn't exist yet — pass undefined.
|
|
339
414
|
const prepared = await prepareAutoLinkedDescription(description || undefined, options, undefined, linearService, graphQLService);
|
|
340
|
-
const result = await issuesService.createIssue({
|
|
415
|
+
const result = await withProjectResolverEnrichment(() => issuesService.createIssue({
|
|
341
416
|
// title is omitted when --from-template is set without an override,
|
|
342
417
|
// so Linear copies the template's title server-side.
|
|
343
418
|
...(title ? { title } : {}),
|
|
@@ -345,6 +420,7 @@ async function handleCreateIssue(title, options, command) {
|
|
|
345
420
|
teamInput,
|
|
346
421
|
description: prepared.description,
|
|
347
422
|
assigneeId,
|
|
423
|
+
delegateId,
|
|
348
424
|
priority,
|
|
349
425
|
projectId: options.project,
|
|
350
426
|
statusId: status,
|
|
@@ -360,7 +436,7 @@ async function handleCreateIssue(title, options, command) {
|
|
|
360
436
|
...(typeof options.fromTemplate === "string" && options.fromTemplate
|
|
361
437
|
? { templateId: options.fromTemplate }
|
|
362
438
|
: {}),
|
|
363
|
-
});
|
|
439
|
+
}), { team: options.team ?? loadConfig().defaultTeam, title }, { graphQLService, linearService });
|
|
364
440
|
const relations = await createRelations(result.id, options, graphQLService, linearService);
|
|
365
441
|
// Pass the ORIGINAL description (pre-wrap) so the extractor's prose-keyword inference
|
|
366
442
|
// ("blocked by", "duplicates", etc.) sees `keyword DEV-100` instead of `keyword [DEV-100](url)`.
|
|
@@ -471,6 +547,53 @@ async function handleRelatedIssues(issueId, _options, command) {
|
|
|
471
547
|
meta: { count: entries.length },
|
|
472
548
|
});
|
|
473
549
|
}
|
|
550
|
+
async function handleStartIssue(issueId, _options, command) {
|
|
551
|
+
const rootOpts = getRootOpts(command);
|
|
552
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
553
|
+
const result = await issuesService.startIssue(issueId);
|
|
554
|
+
outputSuccess({
|
|
555
|
+
...result.issue,
|
|
556
|
+
started: result.started,
|
|
557
|
+
...(result.previousState ? { previousState: result.previousState } : {}),
|
|
558
|
+
...(result.targetState ? { targetState: result.targetState } : {}),
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
// Field-mutating options on `issues update`, post alias-normalization.
|
|
562
|
+
// If none of these are set, an `issues update` invocation that carries only
|
|
563
|
+
// relation flags is treated as relation-only (see handleUpdateIssue): it must
|
|
564
|
+
// behave exactly like `issues relate` and NOT issue an issueUpdate mutation,
|
|
565
|
+
// which would otherwise apply the config defaultPriority fallback and bump the
|
|
566
|
+
// issue's activity for what the user intended as a pure relation add.
|
|
567
|
+
const UPDATE_FIELD_OPTION_KEYS = [
|
|
568
|
+
"title",
|
|
569
|
+
"description",
|
|
570
|
+
"appendDescription",
|
|
571
|
+
"status",
|
|
572
|
+
"priority",
|
|
573
|
+
"assignee",
|
|
574
|
+
"delegate",
|
|
575
|
+
"clearDelegate",
|
|
576
|
+
"project",
|
|
577
|
+
"labels",
|
|
578
|
+
"clearLabels",
|
|
579
|
+
"parentTicket",
|
|
580
|
+
"clearParentTicket",
|
|
581
|
+
"projectMilestone",
|
|
582
|
+
"clearProjectMilestone",
|
|
583
|
+
"cycle",
|
|
584
|
+
"clearCycle",
|
|
585
|
+
"dueDate",
|
|
586
|
+
"clearDueDate",
|
|
587
|
+
];
|
|
588
|
+
function hasFieldUpdates(options) {
|
|
589
|
+
return UPDATE_FIELD_OPTION_KEYS.some((key) => options[key] !== undefined && options[key] !== false);
|
|
590
|
+
}
|
|
591
|
+
function hasRelationFlags(options) {
|
|
592
|
+
return Boolean(options.relatedTo ||
|
|
593
|
+
options.blocks ||
|
|
594
|
+
options.blockedBy ||
|
|
595
|
+
options.duplicateOf);
|
|
596
|
+
}
|
|
474
597
|
async function handleUpdateIssue(issueId, options, command) {
|
|
475
598
|
// Normalize aliases
|
|
476
599
|
if (options.state && !options.status) {
|
|
@@ -479,14 +602,31 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
479
602
|
if (options.label && !options.labels) {
|
|
480
603
|
options.labels = options.label;
|
|
481
604
|
}
|
|
605
|
+
if (options.parent && !options.parentTicket) {
|
|
606
|
+
options.parentTicket = options.parent;
|
|
607
|
+
}
|
|
482
608
|
// Resolve --description-file before validation
|
|
483
609
|
if (options.descriptionFile) {
|
|
484
610
|
options.description = readDescriptionFile(options.descriptionFile);
|
|
485
611
|
}
|
|
486
612
|
validateUpdateOptions(options);
|
|
487
613
|
const rootOpts = getRootOpts(command);
|
|
488
|
-
const graphQLService = await
|
|
489
|
-
|
|
614
|
+
const { graphQLService, linearService, issuesService } = await createIssuesService(rootOpts);
|
|
615
|
+
// Relation-only update: no field changes, just relation flags. Behave
|
|
616
|
+
// identically to `issues relate` — never call updateIssue, so no
|
|
617
|
+
// defaultPriority side effect and no spurious issue activity.
|
|
618
|
+
if (!hasFieldUpdates(options) && hasRelationFlags(options)) {
|
|
619
|
+
const sourceId = await linearService.resolveIssueId(issueId);
|
|
620
|
+
const relations = await createRelations(sourceId, options, graphQLService, linearService);
|
|
621
|
+
if (relations.length === 0) {
|
|
622
|
+
throw new Error("Specify at least one of: --related-to, --blocks, --blocked-by, --duplicate-of");
|
|
623
|
+
}
|
|
624
|
+
outputSuccess({
|
|
625
|
+
data: relations,
|
|
626
|
+
meta: { count: relations.length, source: issueId },
|
|
627
|
+
});
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
490
630
|
if (options.appendDescription) {
|
|
491
631
|
const resolved = await linearService.resolveIssueId(issueId);
|
|
492
632
|
const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", {
|
|
@@ -516,12 +656,24 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
516
656
|
if (prepared.description !== undefined) {
|
|
517
657
|
options.description = prepared.description;
|
|
518
658
|
}
|
|
519
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
520
659
|
const assigneeId = options.assignee
|
|
521
660
|
? await resolveAssignee(options.assignee, rootOpts)
|
|
522
661
|
: undefined;
|
|
523
|
-
const
|
|
524
|
-
|
|
662
|
+
const delegateId = options.clearDelegate
|
|
663
|
+
? null
|
|
664
|
+
: options.delegate
|
|
665
|
+
? resolveMember(options.delegate)
|
|
666
|
+
: undefined;
|
|
667
|
+
const updateArgs = buildUpdateArgs(issueId, options, assigneeId, delegateId);
|
|
668
|
+
const result = await withProjectResolverEnrichment(() => issuesService.updateIssue(updateArgs, options.labelBy || "adding"), {
|
|
669
|
+
team: options.team ?? loadConfig().defaultTeam,
|
|
670
|
+
title: options.title,
|
|
671
|
+
}, { graphQLService, linearService });
|
|
672
|
+
// Mirror `issues create` / `issues relate`: the --related-to / --blocks /
|
|
673
|
+
// --blocked-by / --duplicate-of flags create sidebar relations on the
|
|
674
|
+
// updated issue. Without this, callers wanting to add a relation to an
|
|
675
|
+
// existing issue have to make a second `issues relate` call.
|
|
676
|
+
const relations = await createRelations(result.id, options, graphQLService, linearService);
|
|
525
677
|
const autoLinked = originalDescription
|
|
526
678
|
? await maybeAutoLink({
|
|
527
679
|
issueId: result.id,
|
|
@@ -533,13 +685,15 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
533
685
|
linearService,
|
|
534
686
|
})
|
|
535
687
|
: undefined;
|
|
536
|
-
outputSuccess(
|
|
688
|
+
outputSuccess({
|
|
689
|
+
...result,
|
|
690
|
+
...(relations.length > 0 ? { relations } : {}),
|
|
691
|
+
...(autoLinked ? { autoLinked } : {}),
|
|
692
|
+
});
|
|
537
693
|
}
|
|
538
694
|
async function handleArchiveIssue(issueId, _options, command) {
|
|
539
695
|
const rootOpts = getRootOpts(command);
|
|
540
|
-
const
|
|
541
|
-
const linearService = await createLinearService(rootOpts);
|
|
542
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
696
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
543
697
|
const result = await issuesService.archiveIssue(issueId);
|
|
544
698
|
outputSuccess({
|
|
545
699
|
success: true,
|
|
@@ -549,9 +703,7 @@ async function handleArchiveIssue(issueId, _options, command) {
|
|
|
549
703
|
}
|
|
550
704
|
async function handleDeleteIssue(issueId, options, command) {
|
|
551
705
|
const rootOpts = getRootOpts(command);
|
|
552
|
-
const
|
|
553
|
-
const linearService = await createLinearService(rootOpts);
|
|
554
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
706
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
555
707
|
const permanentlyDelete = Boolean(options.permanentlyDelete || options.hard);
|
|
556
708
|
const result = await issuesService.deleteIssue(issueId, {
|
|
557
709
|
permanentlyDelete,
|
|
@@ -603,9 +755,7 @@ async function handleRetrolink(options, command) {
|
|
|
603
755
|
? commits.map((msg) => `- ${msg}`).join("\n")
|
|
604
756
|
: `Retrolinked from branch: ${currentBranch}`;
|
|
605
757
|
const rootOpts = getRootOpts(command);
|
|
606
|
-
const
|
|
607
|
-
const linearService = await createLinearService(rootOpts);
|
|
608
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
758
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
609
759
|
let result;
|
|
610
760
|
if (options.issue) {
|
|
611
761
|
// Link to existing issue — fetch it and rename branch
|
|
@@ -631,7 +781,13 @@ async function handleRetrolink(options, command) {
|
|
|
631
781
|
let newBranch;
|
|
632
782
|
if (result.branchName) {
|
|
633
783
|
newBranch = toBranchName(result.branchName);
|
|
634
|
-
|
|
784
|
+
// `--` BEFORE the ref operands terminates option parsing so a
|
|
785
|
+
// name starting with `-` (server bug, malicious slug, forked
|
|
786
|
+
// Linear-compatible API) is parsed as a ref name rather than a
|
|
787
|
+
// git flag. Defense in depth — the default feature/ prefix
|
|
788
|
+
// already blocks the common case, but the prefix is configurable
|
|
789
|
+
// (DEV-4064).
|
|
790
|
+
execFileSync("git", ["branch", "-m", "--", oldBranch, newBranch], {
|
|
635
791
|
stdio: "pipe",
|
|
636
792
|
});
|
|
637
793
|
}
|
|
@@ -653,6 +809,7 @@ export function setupIssuesCommands(program) {
|
|
|
653
809
|
.option("-l, --limit <number>", "limit results", "25")
|
|
654
810
|
.option("--team <team>", "filter by team key (EL: resolves names)")
|
|
655
811
|
.option("--assignee <assignee>", "filter by assignee (name, alias, or ID)")
|
|
812
|
+
.option("--delegate <delegate>", "filter by delegated agent (name, alias, or ID)")
|
|
656
813
|
.option("--project <project>", "filter by project name or ID")
|
|
657
814
|
.option("--no-project", "filter issues with no project assigned")
|
|
658
815
|
.option("--labels <labels>", "filter by labels (comma-separated names)")
|
|
@@ -661,13 +818,14 @@ export function setupIssuesCommands(program) {
|
|
|
661
818
|
.option("--priority <priority>", "filter by priority (comma-separated: urgent,high,medium,low,none or 0-4)")
|
|
662
819
|
.option("--sort <field>", "sort results (priority, status, created, updated)")
|
|
663
820
|
.option("--format <format>", "output format (json, summary, table, md, csv)", "json")
|
|
664
|
-
.option("--fields <fields>", "columns for table/csv (comma-separated: identifier,title,status,priority,assignee,project,team,labels,updated)")
|
|
821
|
+
.option("--fields <fields>", "columns for table/csv (comma-separated: identifier,title,status,priority,assignee,delegate,project,team,labels,updated)")
|
|
665
822
|
.action(handleAsyncCommand(handleListIssues));
|
|
666
823
|
issues
|
|
667
824
|
.command("search <query>")
|
|
668
825
|
.description("Search issues.")
|
|
669
826
|
.option("--team <team>", "filter by team key, name, or ID")
|
|
670
827
|
.option("--assignee <assignee>", "filter by assignee (name, alias, or ID)")
|
|
828
|
+
.option("--delegate <delegate>", "filter by delegated agent (name, alias, or ID)")
|
|
671
829
|
.option("--project <project>", "filter by project name or ID")
|
|
672
830
|
.option("--no-project", "filter issues with no project assigned")
|
|
673
831
|
.option("--status <status>", "filter by status (comma-separated)")
|
|
@@ -688,6 +846,7 @@ export function setupIssuesCommands(program) {
|
|
|
688
846
|
.option("--template <name>", "use a named description template from config.descriptionTemplates")
|
|
689
847
|
.option("--from-template <id>", "instantiate the issue from a Linear server-side template (UUID from `el-linear templates list`). Sets templateId on the underlying issueCreate mutation; Linear copies the template's title, description, labels, priority, etc. as the new issue's defaults. Override any field with the matching --title / --description / --labels flag.")
|
|
690
848
|
.option("-a, --assignee <assignee>", "assign to user (name, alias, or UUID)")
|
|
849
|
+
.option("--delegate <delegate>", "delegate implementation to an agent app user (name, alias, or UUID)")
|
|
691
850
|
.option("--no-assignee", "create unassigned even when config.defaultAssignee is set")
|
|
692
851
|
.option("-p, --priority <priority>", "priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
|
|
693
852
|
.option("--project <project>", "add to project (name or ID)")
|
|
@@ -699,6 +858,7 @@ export function setupIssuesCommands(program) {
|
|
|
699
858
|
.option("--cycle <cycle>", "cycle name or ID")
|
|
700
859
|
.option("--status <status>", "status name or ID")
|
|
701
860
|
.option("--parent-ticket <parentId>", "parent issue ID or identifier")
|
|
861
|
+
.option("--parent <parentId>", "alias for --parent-ticket")
|
|
702
862
|
.option("--subscriber <subscribers>", "subscribers (comma-separated names, aliases, or UUIDs)")
|
|
703
863
|
.option("--strict", "strict brand validation (error instead of warning)")
|
|
704
864
|
.option("--related-to <issues>", "related issues (comma-separated identifiers)")
|
|
@@ -716,6 +876,10 @@ export function setupIssuesCommands(program) {
|
|
|
716
876
|
if (options.label && !options.labels) {
|
|
717
877
|
options.labels = options.label;
|
|
718
878
|
}
|
|
879
|
+
// Normalize --parent alias to --parent-ticket
|
|
880
|
+
if (options.parent && !options.parentTicket) {
|
|
881
|
+
options.parentTicket = options.parent;
|
|
882
|
+
}
|
|
719
883
|
const title = options.title || titleArg;
|
|
720
884
|
// --from-template lets Linear copy the template's title, so a
|
|
721
885
|
// local title is optional in that path. Without --from-template,
|
|
@@ -732,7 +896,10 @@ export function setupIssuesCommands(program) {
|
|
|
732
896
|
.alias("get")
|
|
733
897
|
.alias("show")
|
|
734
898
|
.description("Get issue details. Accepts multiple IDs for batch retrieval.")
|
|
735
|
-
.
|
|
899
|
+
.option("--field <name>", 'Extract a single named section from the issue description (e.g. "Done when"). ' +
|
|
900
|
+
"Matches H2/H3 headers and bold pseudo-headers case-insensitively. " +
|
|
901
|
+
"Outputs the section text only — no JSON envelope. Single-issue only.")
|
|
902
|
+
.addHelpText("after", '\nBoth UUID and identifiers like ABC-123 are supported.\nMultiple IDs: el-linear issue get DEV-123 DEV-456 DEV-789\nExtract a section: el-linear issue read DEV-123 --field "Done when"')
|
|
736
903
|
.action(handleAsyncCommand(readIssues));
|
|
737
904
|
issues
|
|
738
905
|
.command("update <issueId>")
|
|
@@ -748,12 +915,15 @@ export function setupIssuesCommands(program) {
|
|
|
748
915
|
.option("--state <status>", "alias for --status (new status name or ID)")
|
|
749
916
|
.option("-p, --priority <priority>", "new priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
|
|
750
917
|
.option("--assignee <assignee>", "new assignee (name, alias, or UUID)")
|
|
918
|
+
.option("--delegate <delegate>", "delegate implementation to an agent app user (name, alias, or UUID)")
|
|
919
|
+
.option("--clear-delegate", "remove the issue delegate")
|
|
751
920
|
.option("--project <project>", "new project (name or ID)")
|
|
752
921
|
.option("--labels <labels>", "labels (comma-separated names or IDs)")
|
|
753
922
|
.option("--label <labels>", "alias for --labels")
|
|
754
923
|
.option("--label-by <mode>", "how to apply labels: 'adding' (default) or 'overwriting'")
|
|
755
924
|
.option("--clear-labels", "remove all labels from issue")
|
|
756
925
|
.option("--parent-ticket <parentId>", "set parent issue")
|
|
926
|
+
.option("--parent <parentId>", "alias for --parent-ticket")
|
|
757
927
|
.option("--clear-parent-ticket", "clear parent relationship")
|
|
758
928
|
.option("--project-milestone <milestone>", "set project milestone")
|
|
759
929
|
.option("--clear-project-milestone", "clear project milestone")
|
|
@@ -763,6 +933,10 @@ export function setupIssuesCommands(program) {
|
|
|
763
933
|
.option("--clear-due-date", "clear due date")
|
|
764
934
|
.option("--strict", "strict brand validation")
|
|
765
935
|
.option("--no-auto-link", "skip auto-linking issue references found in the description")
|
|
936
|
+
.option("--related-to <issues>", "related issues (comma-separated)")
|
|
937
|
+
.option("--blocks <issues>", "issues this blocks (comma-separated)")
|
|
938
|
+
.option("--blocked-by <issues>", "issues blocking this (comma-separated)")
|
|
939
|
+
.option("--duplicate-of <issue>", "mark as duplicate of another issue")
|
|
766
940
|
.action(handleAsyncCommand(handleUpdateIssue));
|
|
767
941
|
issues
|
|
768
942
|
.command("archive <issueId>")
|
|
@@ -786,6 +960,11 @@ export function setupIssuesCommands(program) {
|
|
|
786
960
|
.description("Show issue state transition history (time in each status).")
|
|
787
961
|
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
|
|
788
962
|
.action(handleAsyncCommand(handleHistoryIssue));
|
|
963
|
+
issues
|
|
964
|
+
.command("start <issueId>")
|
|
965
|
+
.description("Move an issue to the team's first started workflow state, unless it is already started or terminal.")
|
|
966
|
+
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
|
|
967
|
+
.action(handleAsyncCommand(handleStartIssue));
|
|
789
968
|
issues
|
|
790
969
|
.command("relate <issueId>")
|
|
791
970
|
.description("Create issue relations.")
|
package/dist/commands/labels.js
CHANGED
|
@@ -4,7 +4,7 @@ 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
9
|
import { parsePositiveInt, validateHexColor } from "../utils/validators.js";
|
|
10
10
|
async function handleCreateLabel(name, options, command) {
|
|
@@ -82,6 +82,7 @@ export function setupLabelsCommands(program) {
|
|
|
82
82
|
const service = await createLinearService(rootOpts);
|
|
83
83
|
return service.getLabels(teamFilter, limit, nameFilter);
|
|
84
84
|
});
|
|
85
|
+
warnIfTruncated(result.labels.length, limit);
|
|
85
86
|
outputSuccess({
|
|
86
87
|
data: result.labels,
|
|
87
88
|
meta: { count: result.labels.length },
|
package/dist/commands/profile.js
CHANGED
|
@@ -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[];
|