@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.
- package/README.md +139 -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/attachments.js +2 -1
- package/dist/commands/batch.js +18 -21
- package/dist/commands/comments.js +22 -33
- package/dist/commands/config.js +178 -5
- package/dist/commands/cycles.js +2 -1
- package/dist/commands/documents.js +2 -1
- package/dist/commands/graphql.js +4 -6
- 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/issue-id.js +1 -3
- package/dist/commands/issues/branch.js +9 -1
- package/dist/commands/issues/description.js +2 -6
- package/dist/commands/issues/link-references.d.ts +21 -0
- package/dist/commands/issues/link-references.js +171 -0
- package/dist/commands/issues/relations.d.ts +44 -0
- package/dist/commands/issues/relations.js +132 -0
- package/dist/commands/issues.js +269 -309
- package/dist/commands/labels.js +15 -24
- package/dist/commands/profile.js +1 -0
- package/dist/commands/project-milestones.js +13 -20
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +157 -44
- package/dist/commands/read-shortcut.d.ts +1 -1
- package/dist/commands/read-shortcut.js +28 -8
- package/dist/commands/refs.js +75 -8
- package/dist/commands/releases.js +26 -30
- package/dist/commands/search.js +49 -33
- package/dist/commands/teams.js +2 -1
- package/dist/commands/templates.js +9 -14
- package/dist/commands/users.js +5 -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 +11 -5
- package/dist/main.js +13 -1
- package/dist/queries/attachments-types.d.ts +30 -0
- package/dist/queries/attachments-types.js +5 -0
- package/dist/queries/comments-types.d.ts +55 -0
- package/dist/queries/comments-types.js +5 -0
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +8 -0
- package/dist/queries/documents-types.d.ts +62 -0
- package/dist/queries/documents-types.js +9 -0
- package/dist/queries/introspect-types.d.ts +58 -0
- package/dist/queries/introspect-types.js +10 -0
- package/dist/queries/issues-types.d.ts +481 -0
- package/dist/queries/issues-types.js +23 -0
- package/dist/queries/issues.d.ts +51 -10
- package/dist/queries/issues.js +147 -5
- package/dist/queries/labels-types.d.ts +65 -0
- package/dist/queries/labels-types.js +5 -0
- package/dist/queries/project-milestones-types.d.ts +92 -0
- package/dist/queries/project-milestones-types.js +10 -0
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/queries/projects-types.d.ts +76 -0
- package/dist/queries/projects-types.js +5 -0
- package/dist/queries/projects.d.ts +2 -0
- package/dist/queries/projects.js +22 -0
- package/dist/queries/releases-types.d.ts +85 -0
- package/dist/queries/releases-types.js +5 -0
- package/dist/queries/search-types.d.ts +102 -0
- package/dist/queries/search-types.js +6 -0
- package/dist/queries/templates-types.d.ts +62 -0
- package/dist/queries/templates-types.js +9 -0
- package/dist/types/linear.d.ts +21 -3
- package/dist/utils/auto-link-references.d.ts +3 -3
- package/dist/utils/auto-link-references.js +30 -34
- 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-attachments-service.js +6 -9
- package/dist/utils/graphql-documents-service.js +19 -25
- package/dist/utils/graphql-issues-service.d.ts +112 -46
- package/dist/utils/graphql-issues-service.js +398 -206
- 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 +82 -2
- package/dist/utils/output.js +76 -11
- 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.d.ts +5 -1
- package/dist/utils/workspace-url.js +53 -7
- package/package.json +2 -2
package/dist/commands/issues.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
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";
|
|
6
7
|
import { enforceTerms } from "../config/term-enforcer.js";
|
|
7
|
-
import {
|
|
8
|
-
import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, ISSUE_RELATION_CREATE_MUTATION, SCAN_ISSUES_QUERY, } from "../queries/issues.js";
|
|
9
|
-
import { autoLinkReferences, } from "../utils/auto-link-references.js";
|
|
8
|
+
import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, } from "../queries/issues.js";
|
|
10
9
|
import { createFileService } from "../utils/file-service.js";
|
|
11
10
|
import { applyFooter } from "../utils/footer.js";
|
|
12
11
|
import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
|
|
13
|
-
import { GraphQLIssuesService, } from "../utils/graphql-issues-service.js";
|
|
14
12
|
import { createGraphQLService, } from "../utils/graphql-service.js";
|
|
13
|
+
import { createIssuesService } from "../utils/issues-service-bootstrap.js";
|
|
15
14
|
import { createLinearService, } from "../utils/linear-service.js";
|
|
16
15
|
import { logger } from "../utils/logger.js";
|
|
17
|
-
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
16
|
+
import { handleAsyncCommand, outputSuccess, outputWarning, warnIfTruncated, } from "../utils/output.js";
|
|
18
17
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
19
18
|
import { formatCsv, formatMarkdown, formatTable, } from "../utils/table-formatter.js";
|
|
20
|
-
import { parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
|
|
19
|
+
import { parsePositiveInt, parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
|
|
21
20
|
import { gitCheckoutBranch, toBranchName } from "./issues/branch.js";
|
|
22
|
-
import { maybeAutoLink, prepareAutoLinkedDescription,
|
|
21
|
+
import { maybeAutoLink, prepareAutoLinkedDescription, readDescriptionFile, resolveDescription, } from "./issues/description.js";
|
|
22
|
+
import { handleLinkReferencesIssue } from "./issues/link-references.js";
|
|
23
|
+
import { buildIncomingRelationEntries, buildOutgoingRelationEntries, createRelations, } from "./issues/relations.js";
|
|
23
24
|
import { readIssues } from "./read-shortcut.js";
|
|
24
25
|
const IMAGE_EXTENSIONS = new Set([
|
|
25
26
|
".png",
|
|
@@ -35,54 +36,10 @@ function isImageFile(filename) {
|
|
|
35
36
|
const ext = filename.lastIndexOf(".");
|
|
36
37
|
return ext !== -1 && IMAGE_EXTENSIONS.has(filename.slice(ext).toLowerCase());
|
|
37
38
|
}
|
|
38
|
-
function transformIssueRelation(rel) {
|
|
39
|
-
const issue = rel.issue;
|
|
40
|
-
const relatedIssue = rel.relatedIssue;
|
|
41
|
-
return {
|
|
42
|
-
id: rel.id,
|
|
43
|
-
type: rel.type,
|
|
44
|
-
issue: {
|
|
45
|
-
id: issue.id,
|
|
46
|
-
identifier: issue.identifier,
|
|
47
|
-
title: issue.title,
|
|
48
|
-
},
|
|
49
|
-
relatedIssue: {
|
|
50
|
-
id: relatedIssue.id,
|
|
51
|
-
identifier: relatedIssue.identifier,
|
|
52
|
-
title: relatedIssue.title,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
async function createRelations(sourceId, options, graphQLService, linearService) {
|
|
57
|
-
const relations = [];
|
|
58
|
-
const relationSpecs = [
|
|
59
|
-
{ option: "relatedTo", type: "related" },
|
|
60
|
-
{ option: "blocks", type: "blocks" },
|
|
61
|
-
{ option: "blockedBy", type: "blocks", reverse: true },
|
|
62
|
-
{ option: "duplicateOf", type: "duplicate" },
|
|
63
|
-
];
|
|
64
|
-
for (const spec of relationSpecs) {
|
|
65
|
-
const value = options[spec.option];
|
|
66
|
-
if (!value) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
for (const id of splitList(value)) {
|
|
70
|
-
const targetId = await linearService.resolveIssueId(id);
|
|
71
|
-
const rel = await graphQLService.rawRequest(ISSUE_RELATION_CREATE_MUTATION, {
|
|
72
|
-
input: {
|
|
73
|
-
issueId: spec.reverse ? targetId : sourceId,
|
|
74
|
-
relatedIssueId: spec.reverse ? sourceId : targetId,
|
|
75
|
-
type: spec.type,
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
const create = rel.issueRelationCreate;
|
|
79
|
-
const issueRelation = create?.issueRelation;
|
|
80
|
-
relations.push(transformIssueRelation(issueRelation));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return relations;
|
|
84
|
-
}
|
|
85
39
|
function validateUpdateOptions(options) {
|
|
40
|
+
if (options.delegate && options.clearDelegate) {
|
|
41
|
+
throw new Error("Cannot use --delegate and --clear-delegate together");
|
|
42
|
+
}
|
|
86
43
|
if (options.parentTicket && options.clearParentTicket) {
|
|
87
44
|
throw new Error("Cannot use --parent-ticket and --clear-parent-ticket together");
|
|
88
45
|
}
|
|
@@ -108,7 +65,7 @@ function validateUpdateOptions(options) {
|
|
|
108
65
|
throw new Error("Cannot use --description and --append-description together");
|
|
109
66
|
}
|
|
110
67
|
}
|
|
111
|
-
function buildUpdateArgs(issueId, options, assigneeId) {
|
|
68
|
+
function buildUpdateArgs(issueId, options, assigneeId, delegateId) {
|
|
112
69
|
let labelIds;
|
|
113
70
|
if (options.clearLabels) {
|
|
114
71
|
labelIds = [];
|
|
@@ -131,6 +88,7 @@ function buildUpdateArgs(issueId, options, assigneeId) {
|
|
|
131
88
|
statusId: options.status,
|
|
132
89
|
priority: priorityInput ? validatePriority(priorityInput) : undefined,
|
|
133
90
|
assigneeId,
|
|
91
|
+
delegateId,
|
|
134
92
|
projectId: options.project,
|
|
135
93
|
labelIds,
|
|
136
94
|
parentId: options.parentTicket || (options.clearParentTicket ? null : undefined),
|
|
@@ -183,44 +141,61 @@ function sortIssues(issues, sort) {
|
|
|
183
141
|
}
|
|
184
142
|
return sorted;
|
|
185
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
|
+
}
|
|
186
157
|
async function handleListIssues(options, command) {
|
|
187
158
|
if (options.label && !options.labels) {
|
|
188
159
|
options.labels = options.label;
|
|
189
160
|
}
|
|
190
161
|
const rootOpts = getRootOpts(command);
|
|
191
|
-
const
|
|
192
|
-
const linearService = await createLinearService(rootOpts);
|
|
193
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
162
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
194
163
|
const hasFilters = options.team ||
|
|
195
164
|
options.labels ||
|
|
196
165
|
options.status ||
|
|
197
166
|
options.assignee ||
|
|
167
|
+
options.delegate ||
|
|
198
168
|
options.project ||
|
|
199
169
|
options.project === false ||
|
|
200
170
|
options.priority;
|
|
171
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
201
172
|
if (hasFilters) {
|
|
202
173
|
const searchArgs = {
|
|
203
174
|
teamId: options.team ? resolveTeam(options.team) : undefined,
|
|
204
175
|
assigneeId: options.assignee
|
|
205
176
|
? await resolveAssignee(options.assignee, rootOpts)
|
|
206
177
|
: undefined,
|
|
207
|
-
|
|
208
|
-
|
|
178
|
+
delegateId: options.delegate
|
|
179
|
+
? resolveMember(options.delegate)
|
|
180
|
+
: undefined,
|
|
181
|
+
project: resolveProjectFlag(options.project),
|
|
209
182
|
labelNames: options.labels ? splitList(options.labels) : undefined,
|
|
210
183
|
status: options.status ? splitList(options.status) : undefined,
|
|
211
184
|
priority: options.priority
|
|
212
185
|
? parsePriorityFilter(options.priority)
|
|
213
186
|
: undefined,
|
|
214
187
|
orderBy: options.sort === "created" ? "createdAt" : "updatedAt",
|
|
215
|
-
limit
|
|
188
|
+
limit,
|
|
216
189
|
};
|
|
217
190
|
const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
|
|
191
|
+
warnIfTruncated(result.length, limit);
|
|
218
192
|
outputIssues(result, options.format, options.fields, {
|
|
219
193
|
team: options.team,
|
|
220
194
|
});
|
|
221
195
|
}
|
|
222
196
|
else {
|
|
223
|
-
const result = sortIssues(await issuesService.getIssues(
|
|
197
|
+
const result = sortIssues(await issuesService.getIssues(limit), options.sort);
|
|
198
|
+
warnIfTruncated(result.length, limit);
|
|
224
199
|
outputIssues(result, options.format, options.fields, {});
|
|
225
200
|
}
|
|
226
201
|
}
|
|
@@ -229,27 +204,58 @@ async function handleSearchIssues(query, options, command) {
|
|
|
229
204
|
options.labels = options.label;
|
|
230
205
|
}
|
|
231
206
|
const rootOpts = getRootOpts(command);
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
207
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
208
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
235
209
|
const searchArgs = {
|
|
236
210
|
query,
|
|
237
211
|
teamId: options.team ? resolveTeam(options.team) : undefined,
|
|
238
212
|
assigneeId: options.assignee
|
|
239
213
|
? await resolveAssignee(options.assignee, rootOpts)
|
|
240
214
|
: undefined,
|
|
241
|
-
|
|
242
|
-
|
|
215
|
+
delegateId: options.delegate
|
|
216
|
+
? resolveMember(options.delegate)
|
|
217
|
+
: undefined,
|
|
218
|
+
project: resolveProjectFlag(options.project),
|
|
243
219
|
status: options.status ? splitList(options.status) : undefined,
|
|
244
220
|
labelNames: options.labels ? splitList(options.labels) : undefined,
|
|
245
221
|
priority: options.priority
|
|
246
222
|
? parsePriorityFilter(options.priority)
|
|
247
223
|
: undefined,
|
|
248
|
-
limit
|
|
224
|
+
limit,
|
|
249
225
|
};
|
|
250
226
|
const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
|
|
227
|
+
warnIfTruncated(result.length, limit);
|
|
251
228
|
outputIssues(result, options.format, options.fields, { query });
|
|
252
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
|
+
}
|
|
253
259
|
async function resolveCreateInputs(title, options, rootOpts) {
|
|
254
260
|
const config = loadConfig();
|
|
255
261
|
enforceTerms([title, options.description], { strict: options.strict });
|
|
@@ -291,6 +297,22 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
291
297
|
if (validationResult.normalizedLabels) {
|
|
292
298
|
options.labels = validationResult.normalizedLabels.join(",");
|
|
293
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
|
+
}
|
|
294
316
|
enforceValidation(validationResult);
|
|
295
317
|
}
|
|
296
318
|
if (!effectivePriorityInput) {
|
|
@@ -301,9 +323,16 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
301
323
|
const assigneeId = effectiveAssignee
|
|
302
324
|
? await resolveAssignee(effectiveAssignee, rootOpts)
|
|
303
325
|
: undefined;
|
|
326
|
+
const delegateId = options.delegate
|
|
327
|
+
? resolveMember(options.delegate)
|
|
328
|
+
: undefined;
|
|
304
329
|
let labelIds = [];
|
|
305
330
|
if (options.labels) {
|
|
306
|
-
|
|
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));
|
|
307
336
|
}
|
|
308
337
|
if (options.claude) {
|
|
309
338
|
const claudeId = config.labels.workspace.claude;
|
|
@@ -327,6 +356,7 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
327
356
|
teamInput,
|
|
328
357
|
teamId,
|
|
329
358
|
assigneeId,
|
|
359
|
+
delegateId,
|
|
330
360
|
labelIds,
|
|
331
361
|
status,
|
|
332
362
|
subscriberIds,
|
|
@@ -365,7 +395,7 @@ function buildDescriptionWithAttachments(baseDescription, uploadResults) {
|
|
|
365
395
|
}
|
|
366
396
|
async function handleCreateIssue(title, options, command) {
|
|
367
397
|
const rootOpts = getRootOpts(command);
|
|
368
|
-
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);
|
|
369
399
|
const uploadResults = await uploadAttachmentsIfNeeded(options, rootOpts);
|
|
370
400
|
const descriptionWithAttachments = buildDescriptionWithAttachments(resolveDescription(options) || "", uploadResults);
|
|
371
401
|
// Append messageFooter (config or --footer flag) so auto-link picks up any
|
|
@@ -377,14 +407,12 @@ async function handleCreateIssue(title, options, command) {
|
|
|
377
407
|
footer: explicitFooter,
|
|
378
408
|
noFooter,
|
|
379
409
|
}) ?? "";
|
|
380
|
-
const graphQLService = await
|
|
381
|
-
const linearService = await createLinearService(rootOpts);
|
|
382
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
410
|
+
const { graphQLService, linearService, issuesService } = await createIssuesService(rootOpts);
|
|
383
411
|
// Wrap valid issue identifiers as markdown links before creating, so the description
|
|
384
412
|
// saved on Linear has clickable refs from the start. Self-reference can't apply here
|
|
385
413
|
// because the issue doesn't exist yet — pass undefined.
|
|
386
414
|
const prepared = await prepareAutoLinkedDescription(description || undefined, options, undefined, linearService, graphQLService);
|
|
387
|
-
const result = await issuesService.createIssue({
|
|
415
|
+
const result = await withProjectResolverEnrichment(() => issuesService.createIssue({
|
|
388
416
|
// title is omitted when --from-template is set without an override,
|
|
389
417
|
// so Linear copies the template's title server-side.
|
|
390
418
|
...(title ? { title } : {}),
|
|
@@ -392,6 +420,7 @@ async function handleCreateIssue(title, options, command) {
|
|
|
392
420
|
teamInput,
|
|
393
421
|
description: prepared.description,
|
|
394
422
|
assigneeId,
|
|
423
|
+
delegateId,
|
|
395
424
|
priority,
|
|
396
425
|
projectId: options.project,
|
|
397
426
|
statusId: status,
|
|
@@ -407,7 +436,7 @@ async function handleCreateIssue(title, options, command) {
|
|
|
407
436
|
...(typeof options.fromTemplate === "string" && options.fromTemplate
|
|
408
437
|
? { templateId: options.fromTemplate }
|
|
409
438
|
: {}),
|
|
410
|
-
});
|
|
439
|
+
}), { team: options.team ?? loadConfig().defaultTeam, title }, { graphQLService, linearService });
|
|
411
440
|
const relations = await createRelations(result.id, options, graphQLService, linearService);
|
|
412
441
|
// Pass the ORIGINAL description (pre-wrap) so the extractor's prose-keyword inference
|
|
413
442
|
// ("blocked by", "duplicates", etc.) sees `keyword DEV-100` instead of `keyword [DEV-100](url)`.
|
|
@@ -460,22 +489,19 @@ async function handleHistoryIssue(issueId, _options, command) {
|
|
|
460
489
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
461
490
|
const linearService = await createLinearService(rootOpts);
|
|
462
491
|
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
463
|
-
const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, {
|
|
464
|
-
id: resolvedId,
|
|
465
|
-
});
|
|
492
|
+
const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, { id: resolvedId });
|
|
466
493
|
if (!result.issue) {
|
|
467
494
|
throw new Error(`Issue "${issueId}" not found`);
|
|
468
495
|
}
|
|
469
496
|
const issue = result.issue;
|
|
470
|
-
const
|
|
471
|
-
const spans = (stateHistory?.nodes ?? []).map((span) => ({
|
|
497
|
+
const spans = issue.stateHistory.nodes.map((span) => ({
|
|
472
498
|
state: {
|
|
473
499
|
id: span.state.id,
|
|
474
500
|
name: span.state.name,
|
|
475
501
|
type: span.state.type,
|
|
476
502
|
},
|
|
477
503
|
startedAt: span.startedAt,
|
|
478
|
-
endedAt: span.endedAt
|
|
504
|
+
endedAt: span.endedAt ?? undefined,
|
|
479
505
|
}));
|
|
480
506
|
outputSuccess({
|
|
481
507
|
id: issue.id,
|
|
@@ -499,82 +525,19 @@ async function handleRelateIssue(issueId, options, command) {
|
|
|
499
525
|
meta: { count: relations.length, source: issueId },
|
|
500
526
|
});
|
|
501
527
|
}
|
|
502
|
-
/**
|
|
503
|
-
* Invert relation type for incoming (inverse) relations so the output
|
|
504
|
-
* reads naturally from the perspective of the queried issue.
|
|
505
|
-
* e.g. if DEV-100 "blocks" DEV-200, and we query DEV-200,
|
|
506
|
-
* the inverse relation type is "blocks" but direction is incoming → "blockedBy".
|
|
507
|
-
*/
|
|
508
|
-
function normalizeInverseType(type) {
|
|
509
|
-
if (type === "blocks") {
|
|
510
|
-
return "blockedBy";
|
|
511
|
-
}
|
|
512
|
-
return type;
|
|
513
|
-
}
|
|
514
|
-
function buildRelatedIssueSummary(peer) {
|
|
515
|
-
const state = peer.state;
|
|
516
|
-
const assignee = peer.assignee;
|
|
517
|
-
const team = peer.team;
|
|
518
|
-
return {
|
|
519
|
-
id: peer.id,
|
|
520
|
-
identifier: peer.identifier,
|
|
521
|
-
title: peer.title,
|
|
522
|
-
...(state
|
|
523
|
-
? { state: { id: state.id, name: state.name } }
|
|
524
|
-
: {}),
|
|
525
|
-
...(peer.priority == null ? {} : { priority: peer.priority }),
|
|
526
|
-
...(assignee
|
|
527
|
-
? {
|
|
528
|
-
assignee: {
|
|
529
|
-
id: assignee.id,
|
|
530
|
-
name: assignee.name,
|
|
531
|
-
},
|
|
532
|
-
}
|
|
533
|
-
: {}),
|
|
534
|
-
...(team
|
|
535
|
-
? {
|
|
536
|
-
team: {
|
|
537
|
-
id: team.id,
|
|
538
|
-
key: team.key,
|
|
539
|
-
name: team.name,
|
|
540
|
-
},
|
|
541
|
-
}
|
|
542
|
-
: {}),
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
function buildRelationEntries(nodes, peerKey, direction, normalizeType) {
|
|
546
|
-
const entries = [];
|
|
547
|
-
for (const rel of nodes ?? []) {
|
|
548
|
-
const peer = rel[peerKey];
|
|
549
|
-
if (!peer) {
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
entries.push({
|
|
553
|
-
id: rel.id,
|
|
554
|
-
type: normalizeType(rel.type),
|
|
555
|
-
direction,
|
|
556
|
-
issue: buildRelatedIssueSummary(peer),
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
return entries;
|
|
560
|
-
}
|
|
561
528
|
async function handleRelatedIssues(issueId, _options, command) {
|
|
562
529
|
const rootOpts = getRootOpts(command);
|
|
563
530
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
564
531
|
const linearService = await createLinearService(rootOpts);
|
|
565
532
|
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
566
|
-
const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
|
|
567
|
-
id: resolvedId,
|
|
568
|
-
});
|
|
533
|
+
const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, { id: resolvedId });
|
|
569
534
|
if (!result.issue) {
|
|
570
535
|
throw new Error(`Issue "${issueId}" not found`);
|
|
571
536
|
}
|
|
572
537
|
const issue = result.issue;
|
|
573
|
-
const relations = issue.relations;
|
|
574
|
-
const inverseRelations = issue.inverseRelations;
|
|
575
538
|
const entries = [
|
|
576
|
-
...
|
|
577
|
-
...
|
|
539
|
+
...buildOutgoingRelationEntries(issue.relations.nodes),
|
|
540
|
+
...buildIncomingRelationEntries(issue.inverseRelations.nodes),
|
|
578
541
|
];
|
|
579
542
|
outputSuccess({
|
|
580
543
|
id: issue.id,
|
|
@@ -584,157 +547,52 @@ async function handleRelatedIssues(issueId, _options, command) {
|
|
|
584
547
|
meta: { count: entries.length },
|
|
585
548
|
});
|
|
586
549
|
}
|
|
587
|
-
async function
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
});
|
|
592
|
-
const issue = result.issue;
|
|
593
|
-
const comments = issue?.comments;
|
|
594
|
-
const nodes = comments?.nodes ?? [];
|
|
595
|
-
return nodes
|
|
596
|
-
.map((c) => c.body)
|
|
597
|
-
.filter((b) => typeof b === "string" && b.length > 0);
|
|
598
|
-
}
|
|
599
|
-
async function linkReferencesForIssue(args) {
|
|
600
|
-
const comments = args.includeComments
|
|
601
|
-
? await fetchCommentBodies(args.issueUuid, args.graphQLService)
|
|
602
|
-
: undefined;
|
|
603
|
-
return autoLinkReferences({
|
|
604
|
-
issueId: args.issueUuid,
|
|
605
|
-
identifier: args.identifier,
|
|
606
|
-
description: args.description,
|
|
607
|
-
comments,
|
|
608
|
-
dryRun: args.dryRun,
|
|
609
|
-
graphQLService: args.graphQLService,
|
|
610
|
-
linearService: args.linearService,
|
|
611
|
-
preResolved: args.preResolved,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
async function handleLinkReferencesSingle(issueId, options, graphQLService, linearService) {
|
|
615
|
-
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
616
|
-
// GET_ISSUE_RELATIONS_QUERY also returns description, so this single call gives us everything
|
|
617
|
-
// we need for the description-only path. Comments are fetched separately on demand.
|
|
618
|
-
const issueResult = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
|
|
619
|
-
id: resolvedId,
|
|
620
|
-
});
|
|
621
|
-
if (!issueResult.issue) {
|
|
622
|
-
throw new Error(`Issue "${issueId}" not found`);
|
|
623
|
-
}
|
|
624
|
-
const issue = issueResult.issue;
|
|
625
|
-
const identifier = issue.identifier;
|
|
626
|
-
const description = issue.description ?? "";
|
|
627
|
-
const dryRun = Boolean(options.dryRun);
|
|
628
|
-
const includeComments = Boolean(options.includeComments);
|
|
629
|
-
const rewriteDescription = Boolean(options.rewriteDescription);
|
|
630
|
-
// If rewriting, validate refs once up front so we can both wrap the description and
|
|
631
|
-
// pass the resolved map down to autoLink — avoids a second resolve round trip.
|
|
632
|
-
const rewrite = rewriteDescription
|
|
633
|
-
? await prepareDescriptionRewrite(description, identifier, linearService, graphQLService)
|
|
634
|
-
: { preResolved: undefined, wrapped: undefined };
|
|
635
|
-
const autoLinked = await linkReferencesForIssue({
|
|
636
|
-
issueUuid: resolvedId,
|
|
637
|
-
identifier,
|
|
638
|
-
description,
|
|
639
|
-
includeComments,
|
|
640
|
-
dryRun,
|
|
641
|
-
graphQLService,
|
|
642
|
-
linearService,
|
|
643
|
-
preResolved: rewrite.preResolved,
|
|
644
|
-
});
|
|
645
|
-
let descriptionRewritten = false;
|
|
646
|
-
if (rewriteDescription && rewrite.wrapped && !dryRun) {
|
|
647
|
-
await pushDescriptionUpdate(resolvedId, rewrite.wrapped, graphQLService);
|
|
648
|
-
descriptionRewritten = true;
|
|
649
|
-
}
|
|
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);
|
|
650
554
|
outputSuccess({
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
...(rewriteDescription
|
|
656
|
-
? {
|
|
657
|
-
descriptionRewritten,
|
|
658
|
-
...(dryRun && rewrite.wrapped
|
|
659
|
-
? { descriptionPreview: rewrite.wrapped }
|
|
660
|
-
: {}),
|
|
661
|
-
}
|
|
662
|
-
: {}),
|
|
663
|
-
meta: {
|
|
664
|
-
dryRun,
|
|
665
|
-
includeComments,
|
|
666
|
-
...(rewriteDescription ? { rewriteDescription: true } : {}),
|
|
667
|
-
},
|
|
555
|
+
...result.issue,
|
|
556
|
+
started: result.started,
|
|
557
|
+
...(result.previousState ? { previousState: result.previousState } : {}),
|
|
558
|
+
...(result.targetState ? { targetState: result.targetState } : {}),
|
|
668
559
|
});
|
|
669
560
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
});
|
|
700
|
-
entries.push({ identifier, autoLinked });
|
|
701
|
-
}
|
|
702
|
-
// Summary counts across the batch
|
|
703
|
-
const totalLinked = entries.reduce((n, e) => n + e.autoLinked.linked.length, 0);
|
|
704
|
-
const totalSkipped = entries.reduce((n, e) => n + e.autoLinked.skipped.length, 0);
|
|
705
|
-
const totalFailed = entries.reduce((n, e) => n + e.autoLinked.failed.length, 0);
|
|
706
|
-
outputSuccess({
|
|
707
|
-
data: entries,
|
|
708
|
-
meta: {
|
|
709
|
-
count: entries.length,
|
|
710
|
-
team: teamInput,
|
|
711
|
-
dryRun,
|
|
712
|
-
includeComments,
|
|
713
|
-
totals: {
|
|
714
|
-
linked: totalLinked,
|
|
715
|
-
skipped: totalSkipped,
|
|
716
|
-
failed: totalFailed,
|
|
717
|
-
},
|
|
718
|
-
},
|
|
719
|
-
});
|
|
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);
|
|
720
590
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
if (issueIdOrTeamFlag) {
|
|
727
|
-
throw new Error("Provide either an issueId positional argument OR --team, not both. With --team, omit the issueId.");
|
|
728
|
-
}
|
|
729
|
-
if (options.rewriteDescription) {
|
|
730
|
-
throw new Error("--rewrite-description is single-issue only. Use it case by case (one issueId per invocation) — bulk description rewrites bot-author the whole team's history.");
|
|
731
|
-
}
|
|
732
|
-
return handleLinkReferencesBatch(options.team, options, graphQLService, linearService);
|
|
733
|
-
}
|
|
734
|
-
if (!issueIdOrTeamFlag) {
|
|
735
|
-
throw new Error("issueId is required (or use --team <key> to scan a whole team)");
|
|
736
|
-
}
|
|
737
|
-
return handleLinkReferencesSingle(issueIdOrTeamFlag, options, graphQLService, linearService);
|
|
591
|
+
function hasRelationFlags(options) {
|
|
592
|
+
return Boolean(options.relatedTo ||
|
|
593
|
+
options.blocks ||
|
|
594
|
+
options.blockedBy ||
|
|
595
|
+
options.duplicateOf);
|
|
738
596
|
}
|
|
739
597
|
async function handleUpdateIssue(issueId, options, command) {
|
|
740
598
|
// Normalize aliases
|
|
@@ -744,19 +602,37 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
744
602
|
if (options.label && !options.labels) {
|
|
745
603
|
options.labels = options.label;
|
|
746
604
|
}
|
|
605
|
+
if (options.parent && !options.parentTicket) {
|
|
606
|
+
options.parentTicket = options.parent;
|
|
607
|
+
}
|
|
747
608
|
// Resolve --description-file before validation
|
|
748
609
|
if (options.descriptionFile) {
|
|
749
610
|
options.description = readDescriptionFile(options.descriptionFile);
|
|
750
611
|
}
|
|
751
612
|
validateUpdateOptions(options);
|
|
752
613
|
const rootOpts = getRootOpts(command);
|
|
753
|
-
const graphQLService = await
|
|
754
|
-
|
|
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
|
+
}
|
|
755
630
|
if (options.appendDescription) {
|
|
756
631
|
const resolved = await linearService.resolveIssueId(issueId);
|
|
757
|
-
const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", {
|
|
758
|
-
|
|
759
|
-
|
|
632
|
+
const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", {
|
|
633
|
+
id: resolved,
|
|
634
|
+
});
|
|
635
|
+
const existing = current.issue?.description ?? "";
|
|
760
636
|
options.description = `${existing}\n${options.appendDescription}`;
|
|
761
637
|
}
|
|
762
638
|
if (options.title || options.description) {
|
|
@@ -780,12 +656,24 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
780
656
|
if (prepared.description !== undefined) {
|
|
781
657
|
options.description = prepared.description;
|
|
782
658
|
}
|
|
783
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
784
659
|
const assigneeId = options.assignee
|
|
785
660
|
? await resolveAssignee(options.assignee, rootOpts)
|
|
786
661
|
: undefined;
|
|
787
|
-
const
|
|
788
|
-
|
|
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);
|
|
789
677
|
const autoLinked = originalDescription
|
|
790
678
|
? await maybeAutoLink({
|
|
791
679
|
issueId: result.id,
|
|
@@ -797,7 +685,35 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
797
685
|
linearService,
|
|
798
686
|
})
|
|
799
687
|
: undefined;
|
|
800
|
-
outputSuccess(
|
|
688
|
+
outputSuccess({
|
|
689
|
+
...result,
|
|
690
|
+
...(relations.length > 0 ? { relations } : {}),
|
|
691
|
+
...(autoLinked ? { autoLinked } : {}),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
async function handleArchiveIssue(issueId, _options, command) {
|
|
695
|
+
const rootOpts = getRootOpts(command);
|
|
696
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
697
|
+
const result = await issuesService.archiveIssue(issueId);
|
|
698
|
+
outputSuccess({
|
|
699
|
+
success: true,
|
|
700
|
+
archived: true,
|
|
701
|
+
...result,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
async function handleDeleteIssue(issueId, options, command) {
|
|
705
|
+
const rootOpts = getRootOpts(command);
|
|
706
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
707
|
+
const permanentlyDelete = Boolean(options.permanentlyDelete || options.hard);
|
|
708
|
+
const result = await issuesService.deleteIssue(issueId, {
|
|
709
|
+
permanentlyDelete,
|
|
710
|
+
});
|
|
711
|
+
outputSuccess({
|
|
712
|
+
success: true,
|
|
713
|
+
deleted: true,
|
|
714
|
+
permanentlyDeleted: permanentlyDelete,
|
|
715
|
+
...result,
|
|
716
|
+
});
|
|
801
717
|
}
|
|
802
718
|
async function handleRetrolink(options, command) {
|
|
803
719
|
// 1. Read current branch
|
|
@@ -839,9 +755,7 @@ async function handleRetrolink(options, command) {
|
|
|
839
755
|
? commits.map((msg) => `- ${msg}`).join("\n")
|
|
840
756
|
: `Retrolinked from branch: ${currentBranch}`;
|
|
841
757
|
const rootOpts = getRootOpts(command);
|
|
842
|
-
const
|
|
843
|
-
const linearService = await createLinearService(rootOpts);
|
|
844
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
758
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
845
759
|
let result;
|
|
846
760
|
if (options.issue) {
|
|
847
761
|
// Link to existing issue — fetch it and rename branch
|
|
@@ -867,7 +781,13 @@ async function handleRetrolink(options, command) {
|
|
|
867
781
|
let newBranch;
|
|
868
782
|
if (result.branchName) {
|
|
869
783
|
newBranch = toBranchName(result.branchName);
|
|
870
|
-
|
|
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], {
|
|
871
791
|
stdio: "pipe",
|
|
872
792
|
});
|
|
873
793
|
}
|
|
@@ -889,6 +809,7 @@ export function setupIssuesCommands(program) {
|
|
|
889
809
|
.option("-l, --limit <number>", "limit results", "25")
|
|
890
810
|
.option("--team <team>", "filter by team key (EL: resolves names)")
|
|
891
811
|
.option("--assignee <assignee>", "filter by assignee (name, alias, or ID)")
|
|
812
|
+
.option("--delegate <delegate>", "filter by delegated agent (name, alias, or ID)")
|
|
892
813
|
.option("--project <project>", "filter by project name or ID")
|
|
893
814
|
.option("--no-project", "filter issues with no project assigned")
|
|
894
815
|
.option("--labels <labels>", "filter by labels (comma-separated names)")
|
|
@@ -897,13 +818,14 @@ export function setupIssuesCommands(program) {
|
|
|
897
818
|
.option("--priority <priority>", "filter by priority (comma-separated: urgent,high,medium,low,none or 0-4)")
|
|
898
819
|
.option("--sort <field>", "sort results (priority, status, created, updated)")
|
|
899
820
|
.option("--format <format>", "output format (json, summary, table, md, csv)", "json")
|
|
900
|
-
.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)")
|
|
901
822
|
.action(handleAsyncCommand(handleListIssues));
|
|
902
823
|
issues
|
|
903
824
|
.command("search <query>")
|
|
904
825
|
.description("Search issues.")
|
|
905
826
|
.option("--team <team>", "filter by team key, name, or ID")
|
|
906
827
|
.option("--assignee <assignee>", "filter by assignee (name, alias, or ID)")
|
|
828
|
+
.option("--delegate <delegate>", "filter by delegated agent (name, alias, or ID)")
|
|
907
829
|
.option("--project <project>", "filter by project name or ID")
|
|
908
830
|
.option("--no-project", "filter issues with no project assigned")
|
|
909
831
|
.option("--status <status>", "filter by status (comma-separated)")
|
|
@@ -924,6 +846,7 @@ export function setupIssuesCommands(program) {
|
|
|
924
846
|
.option("--template <name>", "use a named description template from config.descriptionTemplates")
|
|
925
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.")
|
|
926
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)")
|
|
927
850
|
.option("--no-assignee", "create unassigned even when config.defaultAssignee is set")
|
|
928
851
|
.option("-p, --priority <priority>", "priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
|
|
929
852
|
.option("--project <project>", "add to project (name or ID)")
|
|
@@ -935,6 +858,7 @@ export function setupIssuesCommands(program) {
|
|
|
935
858
|
.option("--cycle <cycle>", "cycle name or ID")
|
|
936
859
|
.option("--status <status>", "status name or ID")
|
|
937
860
|
.option("--parent-ticket <parentId>", "parent issue ID or identifier")
|
|
861
|
+
.option("--parent <parentId>", "alias for --parent-ticket")
|
|
938
862
|
.option("--subscriber <subscribers>", "subscribers (comma-separated names, aliases, or UUIDs)")
|
|
939
863
|
.option("--strict", "strict brand validation (error instead of warning)")
|
|
940
864
|
.option("--related-to <issues>", "related issues (comma-separated identifiers)")
|
|
@@ -952,6 +876,10 @@ export function setupIssuesCommands(program) {
|
|
|
952
876
|
if (options.label && !options.labels) {
|
|
953
877
|
options.labels = options.label;
|
|
954
878
|
}
|
|
879
|
+
// Normalize --parent alias to --parent-ticket
|
|
880
|
+
if (options.parent && !options.parentTicket) {
|
|
881
|
+
options.parentTicket = options.parent;
|
|
882
|
+
}
|
|
955
883
|
const title = options.title || titleArg;
|
|
956
884
|
// --from-template lets Linear copy the template's title, so a
|
|
957
885
|
// local title is optional in that path. Without --from-template,
|
|
@@ -968,7 +896,10 @@ export function setupIssuesCommands(program) {
|
|
|
968
896
|
.alias("get")
|
|
969
897
|
.alias("show")
|
|
970
898
|
.description("Get issue details. Accepts multiple IDs for batch retrieval.")
|
|
971
|
-
.
|
|
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"')
|
|
972
903
|
.action(handleAsyncCommand(readIssues));
|
|
973
904
|
issues
|
|
974
905
|
.command("update <issueId>")
|
|
@@ -984,12 +915,15 @@ export function setupIssuesCommands(program) {
|
|
|
984
915
|
.option("--state <status>", "alias for --status (new status name or ID)")
|
|
985
916
|
.option("-p, --priority <priority>", "new priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
|
|
986
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")
|
|
987
920
|
.option("--project <project>", "new project (name or ID)")
|
|
988
921
|
.option("--labels <labels>", "labels (comma-separated names or IDs)")
|
|
989
922
|
.option("--label <labels>", "alias for --labels")
|
|
990
923
|
.option("--label-by <mode>", "how to apply labels: 'adding' (default) or 'overwriting'")
|
|
991
924
|
.option("--clear-labels", "remove all labels from issue")
|
|
992
925
|
.option("--parent-ticket <parentId>", "set parent issue")
|
|
926
|
+
.option("--parent <parentId>", "alias for --parent-ticket")
|
|
993
927
|
.option("--clear-parent-ticket", "clear parent relationship")
|
|
994
928
|
.option("--project-milestone <milestone>", "set project milestone")
|
|
995
929
|
.option("--clear-project-milestone", "clear project milestone")
|
|
@@ -999,12 +933,38 @@ export function setupIssuesCommands(program) {
|
|
|
999
933
|
.option("--clear-due-date", "clear due date")
|
|
1000
934
|
.option("--strict", "strict brand validation")
|
|
1001
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")
|
|
1002
940
|
.action(handleAsyncCommand(handleUpdateIssue));
|
|
941
|
+
issues
|
|
942
|
+
.command("archive <issueId>")
|
|
943
|
+
.description("Archive an issue.")
|
|
944
|
+
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
|
|
945
|
+
.action(handleAsyncCommand(handleArchiveIssue));
|
|
946
|
+
issues
|
|
947
|
+
.command("delete <issueId>")
|
|
948
|
+
.description("Delete (trash) an issue.")
|
|
949
|
+
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported. Use --permanently-delete only when you intend to skip Linear's grace period; Linear requires admin permissions.")
|
|
950
|
+
.option("--permanently-delete", "skip Linear's 30-day grace period (admin only)")
|
|
951
|
+
.option("--hard", "alias for --permanently-delete")
|
|
952
|
+
.action(handleAsyncCommand(handleDeleteIssue));
|
|
953
|
+
issues
|
|
954
|
+
.command("hard-delete <issueId>")
|
|
955
|
+
.description("Permanently delete an issue immediately (admin only).")
|
|
956
|
+
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
|
|
957
|
+
.action(handleAsyncCommand((issueId, options, command) => handleDeleteIssue(issueId, { ...options, permanentlyDelete: true }, command)));
|
|
1003
958
|
issues
|
|
1004
959
|
.command("history <issueId>")
|
|
1005
960
|
.description("Show issue state transition history (time in each status).")
|
|
1006
961
|
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
|
|
1007
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));
|
|
1008
968
|
issues
|
|
1009
969
|
.command("relate <issueId>")
|
|
1010
970
|
.description("Create issue relations.")
|