@enrichlayer/el-linear 1.7.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -1
- package/claude-skills/linear-operations/SKILL.md +55 -0
- package/dist/auth/oauth-callback.js +14 -7
- package/dist/auth/oauth-fs.d.ts +22 -0
- package/dist/auth/oauth-fs.js +77 -6
- package/dist/auth/oauth-headless.d.ts +13 -8
- package/dist/auth/oauth-headless.js +18 -11
- package/dist/auth/oauth-storage.d.ts +11 -3
- package/dist/auth/oauth-storage.js +15 -8
- package/dist/auth/token-resolver.d.ts +9 -0
- package/dist/auth/token-resolver.js +57 -37
- package/dist/commands/attachments.js +2 -1
- package/dist/commands/comments.js +17 -28
- 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/index.js +2 -0
- package/dist/commands/init/oauth.d.ts +6 -0
- package/dist/commands/init/oauth.js +8 -2
- package/dist/commands/init/token.d.ts +0 -8
- package/dist/commands/init/token.js +13 -1
- package/dist/commands/issue-id.js +1 -3
- package/dist/commands/issues/branch.d.ts +17 -0
- package/dist/commands/issues/branch.js +40 -0
- package/dist/commands/issues/description.d.ts +89 -0
- package/dist/commands/issues/description.js +183 -0
- 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 +55 -0
- package/dist/commands/issues/relations.js +132 -0
- package/dist/commands/issues.js +97 -497
- package/dist/commands/labels.js +20 -28
- package/dist/commands/profile/migrate-legacy.js +10 -33
- package/dist/commands/profile.js +1 -10
- package/dist/commands/project-milestones.js +13 -20
- package/dist/commands/projects.d.ts +5 -1
- package/dist/commands/projects.js +202 -102
- package/dist/commands/read-shortcut.d.ts +6 -0
- package/dist/commands/read-shortcut.js +6 -1
- package/dist/commands/refs.js +10 -1
- package/dist/commands/releases.js +26 -30
- package/dist/commands/search.js +21 -30
- package/dist/commands/teams.js +2 -1
- package/dist/commands/templates.js +128 -7
- package/dist/commands/users.js +3 -1
- package/dist/config/config.js +31 -9
- package/dist/config/issue-validation.js +1 -1
- package/dist/config/paths.d.ts +11 -0
- package/dist/config/paths.js +44 -6
- package/dist/config/resolver.js +2 -3
- package/dist/config/term-enforcer.js +1 -1
- package/dist/main.js +37 -2
- package/dist/queries/attachments-types.d.ts +30 -0
- package/dist/queries/attachments-types.js +5 -0
- package/dist/queries/comments-types.d.ts +49 -0
- package/dist/queries/comments-types.js +5 -0
- package/dist/queries/documents-types.d.ts +61 -0
- package/dist/queries/documents-types.js +9 -0
- package/dist/queries/introspect-types.d.ts +57 -0
- package/dist/queries/introspect-types.js +10 -0
- package/dist/queries/issues-types.d.ts +416 -0
- package/dist/queries/issues-types.js +23 -0
- package/dist/queries/issues.d.ts +2 -0
- package/dist/queries/issues.js +22 -0
- package/dist/queries/labels-types.d.ts +64 -0
- package/dist/queries/labels-types.js +5 -0
- package/dist/queries/project-milestones-types.d.ts +91 -0
- package/dist/queries/project-milestones-types.js +10 -0
- package/dist/queries/projects-types.d.ts +75 -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 +84 -0
- package/dist/queries/releases-types.js +5 -0
- package/dist/queries/search-types.d.ts +86 -0
- package/dist/queries/search-types.js +6 -0
- package/dist/queries/templates-types.d.ts +61 -0
- package/dist/queries/templates-types.js +9 -0
- package/dist/queries/templates.d.ts +3 -0
- package/dist/queries/templates.js +43 -0
- package/dist/types/linear.d.ts +8 -2
- package/dist/utils/auth.js +7 -9
- package/dist/utils/auto-link-references.js +44 -25
- package/dist/utils/disk-cache.js +2 -1
- package/dist/utils/formatters/summary.d.ts +62 -0
- package/dist/utils/formatters/summary.js +755 -0
- 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 +118 -5
- package/dist/utils/graphql-issues-service.js +202 -209
- package/dist/utils/issue-reference-extractor.d.ts +9 -1
- package/dist/utils/issue-reference-extractor.js +16 -9
- package/dist/utils/issue-reference-wrapper.js +1 -54
- package/dist/utils/linear-service.d.ts +7 -3
- package/dist/utils/linear-service.js +27 -5
- package/dist/utils/markdown-prosemirror.js +17 -1
- package/dist/utils/mention-resolver.js +17 -5
- package/dist/utils/output.d.ts +7 -1
- package/dist/utils/output.js +38 -1
- package/dist/utils/protected-ranges.d.ts +33 -0
- package/dist/utils/protected-ranges.js +73 -0
- package/dist/utils/table-formatter.d.ts +36 -0
- package/dist/utils/table-formatter.js +46 -24
- package/dist/utils/validators.d.ts +9 -1
- package/dist/utils/validators.js +10 -0
- package/dist/utils/workspace-url.d.ts +5 -1
- package/dist/utils/workspace-url.js +35 -5
- package/package.json +1 -1
package/dist/commands/issues.js
CHANGED
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
2
|
import { loadConfig } from "../config/config.js";
|
|
4
3
|
import { enforceValidation, validateIssueCreation, } from "../config/issue-validation.js";
|
|
5
4
|
import { resolveAssignee, resolveLabels, resolveMember, resolveTeam, } from "../config/resolver.js";
|
|
6
5
|
import { resolveDefaultStatus } from "../config/status-defaults.js";
|
|
7
6
|
import { enforceTerms } from "../config/term-enforcer.js";
|
|
8
|
-
import {
|
|
9
|
-
import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, ISSUE_RELATION_CREATE_MUTATION, SCAN_ISSUES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
|
|
10
|
-
import { autoLinkReferences, } from "../utils/auto-link-references.js";
|
|
11
|
-
import { downloadLinearUploads } from "../utils/download-uploads.js";
|
|
7
|
+
import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, } from "../queries/issues.js";
|
|
12
8
|
import { createFileService } from "../utils/file-service.js";
|
|
13
9
|
import { applyFooter } from "../utils/footer.js";
|
|
14
10
|
import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
|
|
15
|
-
import { GraphQLIssuesService } from "../utils/graphql-issues-service.js";
|
|
16
|
-
import { createGraphQLService
|
|
17
|
-
import {
|
|
18
|
-
import { wrapIssueReferencesAsLinks } from "../utils/issue-reference-wrapper.js";
|
|
19
|
-
import { createLinearService, } from "../utils/linear-service.js";
|
|
11
|
+
import { GraphQLIssuesService, } from "../utils/graphql-issues-service.js";
|
|
12
|
+
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
13
|
+
import { createLinearService } from "../utils/linear-service.js";
|
|
20
14
|
import { logger } from "../utils/logger.js";
|
|
21
15
|
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
22
16
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
23
17
|
import { formatCsv, formatMarkdown, formatTable, } from "../utils/table-formatter.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
18
|
+
import { parsePositiveInt, parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
|
|
19
|
+
import { gitCheckoutBranch, toBranchName } from "./issues/branch.js";
|
|
20
|
+
import { maybeAutoLink, prepareAutoLinkedDescription, readDescriptionFile, resolveDescription, } from "./issues/description.js";
|
|
21
|
+
import { handleLinkReferencesIssue } from "./issues/link-references.js";
|
|
22
|
+
import { buildIncomingRelationEntries, buildOutgoingRelationEntries, createRelations, } from "./issues/relations.js";
|
|
23
|
+
import { readIssues } from "./read-shortcut.js";
|
|
27
24
|
const IMAGE_EXTENSIONS = new Set([
|
|
28
25
|
".png",
|
|
29
26
|
".jpg",
|
|
@@ -33,202 +30,11 @@ const IMAGE_EXTENSIONS = new Set([
|
|
|
33
30
|
".svg",
|
|
34
31
|
".ico",
|
|
35
32
|
]);
|
|
36
|
-
const LINEAR_BRANCH_REGEX = /^([a-zA-Z]+)-(\d+)-(.+)$/;
|
|
37
33
|
const COMMIT_HASH_PREFIX_REGEX = /^[a-f0-9]+ /;
|
|
38
|
-
/**
|
|
39
|
-
* Transform Linear's branchName (e.g. "dev-3549-slug") into our convention:
|
|
40
|
-
* "feature/DEV-3549-slug" — uppercase team key with a configurable prefix.
|
|
41
|
-
*/
|
|
42
|
-
function toBranchName(linearBranchName, prefix = "feature/") {
|
|
43
|
-
// Linear branch names look like "dev-123-some-slug"
|
|
44
|
-
// We need to uppercase the team key: "DEV-123-some-slug"
|
|
45
|
-
const match = linearBranchName.match(LINEAR_BRANCH_REGEX);
|
|
46
|
-
if (!match) {
|
|
47
|
-
return `${prefix}${linearBranchName}`;
|
|
48
|
-
}
|
|
49
|
-
const [, teamKey, number, slug] = match;
|
|
50
|
-
return `${prefix}${teamKey.toUpperCase()}-${number}-${slug}`;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Check out a new git branch. Warns and skips if not in a git repo.
|
|
54
|
-
* Throws if the branch already exists.
|
|
55
|
-
*/
|
|
56
|
-
function gitCheckoutBranch(branchName) {
|
|
57
|
-
try {
|
|
58
|
-
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
59
|
-
stdio: "pipe",
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
outputWarning("Not inside a git repository — skipping branch checkout.");
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
execFileSync("git", ["checkout", "-b", branchName], { stdio: "pipe" });
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Read description from a file path or stdin ("-").
|
|
70
|
-
* Avoids shell escaping issues when descriptions contain special characters.
|
|
71
|
-
*/
|
|
72
|
-
function readDescriptionFile(filePath) {
|
|
73
|
-
if (filePath === "-") {
|
|
74
|
-
return fs.readFileSync(0, "utf8").trim();
|
|
75
|
-
}
|
|
76
|
-
if (!fs.existsSync(filePath)) {
|
|
77
|
-
throw new Error(`Description file not found: ${filePath}`);
|
|
78
|
-
}
|
|
79
|
-
return fs.readFileSync(filePath, "utf8").trim();
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Resolve the description from --description, --description-file, or
|
|
83
|
-
* --template (looked up in `config.descriptionTemplates`).
|
|
84
|
-
*
|
|
85
|
-
* Precedence:
|
|
86
|
-
* --description-file > --description > --template
|
|
87
|
-
*
|
|
88
|
-
* Passing --template alongside --description or --description-file is a
|
|
89
|
-
* usage error — the explicit body and a template both producing content
|
|
90
|
-
* would be silently dropping one. We throw so the user picks one.
|
|
91
|
-
*/
|
|
92
|
-
function resolveDescription(options) {
|
|
93
|
-
const hasInline = typeof options.description === "string" && options.description.length > 0;
|
|
94
|
-
const hasFile = Boolean(options.descriptionFile);
|
|
95
|
-
const hasTemplate = typeof options.template === "string" && options.template;
|
|
96
|
-
if (hasTemplate && (hasInline || hasFile)) {
|
|
97
|
-
throw new Error("--template is mutually exclusive with --description / --description-file. " +
|
|
98
|
-
"Pick one.");
|
|
99
|
-
}
|
|
100
|
-
if (hasFile) {
|
|
101
|
-
return readDescriptionFile(options.descriptionFile);
|
|
102
|
-
}
|
|
103
|
-
if (hasInline) {
|
|
104
|
-
return options.description;
|
|
105
|
-
}
|
|
106
|
-
if (hasTemplate) {
|
|
107
|
-
const templates = loadConfig().descriptionTemplates ?? {};
|
|
108
|
-
const body = templates[options.template];
|
|
109
|
-
if (!body) {
|
|
110
|
-
const available = Object.keys(templates).sort();
|
|
111
|
-
const hint = available.length
|
|
112
|
-
? `Available templates: ${available.join(", ")}`
|
|
113
|
-
: "No templates configured. Add one under `descriptionTemplates` in your config.";
|
|
114
|
-
throw new Error(`Template "${options.template}" not found. ${hint}`);
|
|
115
|
-
}
|
|
116
|
-
return body;
|
|
117
|
-
}
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
34
|
function isImageFile(filename) {
|
|
121
35
|
const ext = filename.lastIndexOf(".");
|
|
122
36
|
return ext !== -1 && IMAGE_EXTENSIONS.has(filename.slice(ext).toLowerCase());
|
|
123
37
|
}
|
|
124
|
-
function transformIssueRelation(rel) {
|
|
125
|
-
const issue = rel.issue;
|
|
126
|
-
const relatedIssue = rel.relatedIssue;
|
|
127
|
-
return {
|
|
128
|
-
id: rel.id,
|
|
129
|
-
type: rel.type,
|
|
130
|
-
issue: {
|
|
131
|
-
id: issue.id,
|
|
132
|
-
identifier: issue.identifier,
|
|
133
|
-
title: issue.title,
|
|
134
|
-
},
|
|
135
|
-
relatedIssue: {
|
|
136
|
-
id: relatedIssue.id,
|
|
137
|
-
identifier: relatedIssue.identifier,
|
|
138
|
-
title: relatedIssue.title,
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
async function createRelations(sourceId, options, graphQLService, linearService) {
|
|
143
|
-
const relations = [];
|
|
144
|
-
const relationSpecs = [
|
|
145
|
-
{ option: "relatedTo", type: "related" },
|
|
146
|
-
{ option: "blocks", type: "blocks" },
|
|
147
|
-
{ option: "blockedBy", type: "blocks", reverse: true },
|
|
148
|
-
{ option: "duplicateOf", type: "duplicate" },
|
|
149
|
-
];
|
|
150
|
-
for (const spec of relationSpecs) {
|
|
151
|
-
const value = options[spec.option];
|
|
152
|
-
if (!value) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
for (const id of splitList(value)) {
|
|
156
|
-
const targetId = await linearService.resolveIssueId(id);
|
|
157
|
-
const rel = await graphQLService.rawRequest(ISSUE_RELATION_CREATE_MUTATION, {
|
|
158
|
-
input: {
|
|
159
|
-
issueId: spec.reverse ? targetId : sourceId,
|
|
160
|
-
relatedIssueId: spec.reverse ? sourceId : targetId,
|
|
161
|
-
type: spec.type,
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
const create = rel.issueRelationCreate;
|
|
165
|
-
const issueRelation = create?.issueRelation;
|
|
166
|
-
relations.push(transformIssueRelation(issueRelation));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return relations;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Pre-process a description before sending to Linear:
|
|
173
|
-
* 1. Extract all issue references from the description.
|
|
174
|
-
* 2. Validate them (drop those that don't resolve in this workspace — handles "ISO-1424"-style false positives).
|
|
175
|
-
* 3. Wrap valid identifiers as markdown links — skipping any already inside a link, code block, or backtick span.
|
|
176
|
-
*
|
|
177
|
-
* Returns the rewritten description plus the validated map for downstream auto-linking.
|
|
178
|
-
*
|
|
179
|
-
* No-ops (returns the original description with an empty map) when:
|
|
180
|
-
* - description is empty/undefined
|
|
181
|
-
* - the user passed `--no-auto-link`
|
|
182
|
-
*/
|
|
183
|
-
async function prepareAutoLinkedDescription(description, options, selfIdentifier, linearService, graphQLService) {
|
|
184
|
-
if (!description || options.autoLink === false) {
|
|
185
|
-
return { description, preResolved: new Map(), rewritten: false };
|
|
186
|
-
}
|
|
187
|
-
const refs = extractIssueReferences(description, selfIdentifier);
|
|
188
|
-
if (refs.length === 0) {
|
|
189
|
-
return { description, preResolved: new Map(), rewritten: false };
|
|
190
|
-
}
|
|
191
|
-
const preResolved = await validateReferences(refs.map((r) => r.identifier), linearService);
|
|
192
|
-
if (preResolved.size === 0) {
|
|
193
|
-
return { description, preResolved, rewritten: false };
|
|
194
|
-
}
|
|
195
|
-
const validIds = new Set(preResolved.keys());
|
|
196
|
-
const urlKey = await getWorkspaceUrlKey(graphQLService);
|
|
197
|
-
const wrapped = wrapIssueReferencesAsLinks(description, validIds, urlKey);
|
|
198
|
-
return {
|
|
199
|
-
description: wrapped,
|
|
200
|
-
preResolved,
|
|
201
|
-
rewritten: wrapped !== description,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Run auto-linking for issue references found in a description, unless the user opted out
|
|
206
|
-
* with --no-auto-link or no description was provided. Returns undefined when nothing was linked
|
|
207
|
-
* and nothing was skipped/failed (so callers can omit the field from JSON output).
|
|
208
|
-
*/
|
|
209
|
-
async function maybeAutoLink(input) {
|
|
210
|
-
const { issueId, identifier, description, options, graphQLService, linearService, preResolved, } = input;
|
|
211
|
-
if (options.autoLink === false) {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (!description) {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
const result = await autoLinkReferences({
|
|
218
|
-
issueId,
|
|
219
|
-
identifier,
|
|
220
|
-
description,
|
|
221
|
-
graphQLService,
|
|
222
|
-
linearService,
|
|
223
|
-
preResolved,
|
|
224
|
-
});
|
|
225
|
-
if (result.linked.length === 0 &&
|
|
226
|
-
result.skipped.length === 0 &&
|
|
227
|
-
result.failed.length === 0) {
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
return result;
|
|
231
|
-
}
|
|
232
38
|
function validateUpdateOptions(options) {
|
|
233
39
|
if (options.parentTicket && options.clearParentTicket) {
|
|
234
40
|
throw new Error("Cannot use --parent-ticket and --clear-parent-ticket together");
|
|
@@ -359,7 +165,7 @@ async function handleListIssues(options, command) {
|
|
|
359
165
|
? parsePriorityFilter(options.priority)
|
|
360
166
|
: undefined,
|
|
361
167
|
orderBy: options.sort === "created" ? "createdAt" : "updatedAt",
|
|
362
|
-
limit:
|
|
168
|
+
limit: parsePositiveInt(options.limit, "--limit"),
|
|
363
169
|
};
|
|
364
170
|
const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
|
|
365
171
|
outputIssues(result, options.format, options.fields, {
|
|
@@ -367,7 +173,7 @@ async function handleListIssues(options, command) {
|
|
|
367
173
|
});
|
|
368
174
|
}
|
|
369
175
|
else {
|
|
370
|
-
const result = sortIssues(await issuesService.getIssues(
|
|
176
|
+
const result = sortIssues(await issuesService.getIssues(parsePositiveInt(options.limit, "--limit")), options.sort);
|
|
371
177
|
outputIssues(result, options.format, options.fields, {});
|
|
372
178
|
}
|
|
373
179
|
}
|
|
@@ -392,7 +198,7 @@ async function handleSearchIssues(query, options, command) {
|
|
|
392
198
|
priority: options.priority
|
|
393
199
|
? parsePriorityFilter(options.priority)
|
|
394
200
|
: undefined,
|
|
395
|
-
limit:
|
|
201
|
+
limit: parsePositiveInt(options.limit, "--limit"),
|
|
396
202
|
};
|
|
397
203
|
const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
|
|
398
204
|
outputIssues(result, options.format, options.fields, { query });
|
|
@@ -417,8 +223,14 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
417
223
|
: config.defaultPriority;
|
|
418
224
|
// --- Validation (labels, description, assignee, project, title) ---
|
|
419
225
|
// Controlled by config.validation.enabled (default: true).
|
|
420
|
-
// Bypassed by --skip-validation flag
|
|
421
|
-
|
|
226
|
+
// Bypassed by --skip-validation flag, OR by --from-template which
|
|
227
|
+
// defers field provision to Linear's server-side template
|
|
228
|
+
// instantiation. With --from-template + no overrides, the
|
|
229
|
+
// resulting issue inherits title/description/labels/etc. from the
|
|
230
|
+
// template — running the local validator against undefined fields
|
|
231
|
+
// would produce a false-negative "Missing --labels" error.
|
|
232
|
+
const hasFromTemplate = typeof options.fromTemplate === "string" && options.fromTemplate;
|
|
233
|
+
if (!options.skipValidation && !hasFromTemplate) {
|
|
422
234
|
const rawLabels = options.labels ? splitList(options.labels) : null;
|
|
423
235
|
const description = resolveDescription(options);
|
|
424
236
|
const validationResult = validateIssueCreation({
|
|
@@ -435,7 +247,7 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
435
247
|
enforceValidation(validationResult);
|
|
436
248
|
}
|
|
437
249
|
if (!effectivePriorityInput) {
|
|
438
|
-
outputWarning("Creating issue without --priority. Consider specifying it for better triage."
|
|
250
|
+
outputWarning("Creating issue without --priority. Consider specifying it for better triage.");
|
|
439
251
|
}
|
|
440
252
|
const teamInput = options.team || config.defaultTeam;
|
|
441
253
|
const teamId = resolveTeam(teamInput);
|
|
@@ -506,7 +318,7 @@ function buildDescriptionWithAttachments(baseDescription, uploadResults) {
|
|
|
506
318
|
}
|
|
507
319
|
async function handleCreateIssue(title, options, command) {
|
|
508
320
|
const rootOpts = getRootOpts(command);
|
|
509
|
-
const { teamInput, teamId, assigneeId, labelIds, status, subscriberIds, priority, } = await resolveCreateInputs(title, options, rootOpts);
|
|
321
|
+
const { teamInput, teamId, assigneeId, labelIds, status, subscriberIds, priority, } = await resolveCreateInputs(title ?? "", options, rootOpts);
|
|
510
322
|
const uploadResults = await uploadAttachmentsIfNeeded(options, rootOpts);
|
|
511
323
|
const descriptionWithAttachments = buildDescriptionWithAttachments(resolveDescription(options) || "", uploadResults);
|
|
512
324
|
// Append messageFooter (config or --footer flag) so auto-link picks up any
|
|
@@ -526,7 +338,9 @@ async function handleCreateIssue(title, options, command) {
|
|
|
526
338
|
// because the issue doesn't exist yet — pass undefined.
|
|
527
339
|
const prepared = await prepareAutoLinkedDescription(description || undefined, options, undefined, linearService, graphQLService);
|
|
528
340
|
const result = await issuesService.createIssue({
|
|
529
|
-
title,
|
|
341
|
+
// title is omitted when --from-template is set without an override,
|
|
342
|
+
// so Linear copies the template's title server-side.
|
|
343
|
+
...(title ? { title } : {}),
|
|
530
344
|
teamId,
|
|
531
345
|
teamInput,
|
|
532
346
|
description: prepared.description,
|
|
@@ -540,6 +354,12 @@ async function handleCreateIssue(title, options, command) {
|
|
|
540
354
|
cycleId: options.cycle,
|
|
541
355
|
subscriberIds,
|
|
542
356
|
dueDate: options.dueDate,
|
|
357
|
+
// Linear server-side template instantiation. When set,
|
|
358
|
+
// Linear copies the template's title/description/labels/priority
|
|
359
|
+
// onto the new issue; any explicit field above wins by override.
|
|
360
|
+
...(typeof options.fromTemplate === "string" && options.fromTemplate
|
|
361
|
+
? { templateId: options.fromTemplate }
|
|
362
|
+
: {}),
|
|
543
363
|
});
|
|
544
364
|
const relations = await createRelations(result.id, options, graphQLService, linearService);
|
|
545
365
|
// Pass the ORIGINAL description (pre-wrap) so the extractor's prose-keyword inference
|
|
@@ -593,22 +413,19 @@ async function handleHistoryIssue(issueId, _options, command) {
|
|
|
593
413
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
594
414
|
const linearService = await createLinearService(rootOpts);
|
|
595
415
|
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
596
|
-
const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, {
|
|
597
|
-
id: resolvedId,
|
|
598
|
-
});
|
|
416
|
+
const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, { id: resolvedId });
|
|
599
417
|
if (!result.issue) {
|
|
600
418
|
throw new Error(`Issue "${issueId}" not found`);
|
|
601
419
|
}
|
|
602
420
|
const issue = result.issue;
|
|
603
|
-
const
|
|
604
|
-
const spans = (stateHistory?.nodes ?? []).map((span) => ({
|
|
421
|
+
const spans = issue.stateHistory.nodes.map((span) => ({
|
|
605
422
|
state: {
|
|
606
423
|
id: span.state.id,
|
|
607
424
|
name: span.state.name,
|
|
608
425
|
type: span.state.type,
|
|
609
426
|
},
|
|
610
427
|
startedAt: span.startedAt,
|
|
611
|
-
endedAt: span.endedAt
|
|
428
|
+
endedAt: span.endedAt ?? undefined,
|
|
612
429
|
}));
|
|
613
430
|
outputSuccess({
|
|
614
431
|
id: issue.id,
|
|
@@ -632,82 +449,19 @@ async function handleRelateIssue(issueId, options, command) {
|
|
|
632
449
|
meta: { count: relations.length, source: issueId },
|
|
633
450
|
});
|
|
634
451
|
}
|
|
635
|
-
/**
|
|
636
|
-
* Invert relation type for incoming (inverse) relations so the output
|
|
637
|
-
* reads naturally from the perspective of the queried issue.
|
|
638
|
-
* e.g. if DEV-100 "blocks" DEV-200, and we query DEV-200,
|
|
639
|
-
* the inverse relation type is "blocks" but direction is incoming → "blockedBy".
|
|
640
|
-
*/
|
|
641
|
-
function normalizeInverseType(type) {
|
|
642
|
-
if (type === "blocks") {
|
|
643
|
-
return "blockedBy";
|
|
644
|
-
}
|
|
645
|
-
return type;
|
|
646
|
-
}
|
|
647
|
-
function buildRelatedIssueSummary(peer) {
|
|
648
|
-
const state = peer.state;
|
|
649
|
-
const assignee = peer.assignee;
|
|
650
|
-
const team = peer.team;
|
|
651
|
-
return {
|
|
652
|
-
id: peer.id,
|
|
653
|
-
identifier: peer.identifier,
|
|
654
|
-
title: peer.title,
|
|
655
|
-
...(state
|
|
656
|
-
? { state: { id: state.id, name: state.name } }
|
|
657
|
-
: {}),
|
|
658
|
-
...(peer.priority == null ? {} : { priority: peer.priority }),
|
|
659
|
-
...(assignee
|
|
660
|
-
? {
|
|
661
|
-
assignee: {
|
|
662
|
-
id: assignee.id,
|
|
663
|
-
name: assignee.name,
|
|
664
|
-
},
|
|
665
|
-
}
|
|
666
|
-
: {}),
|
|
667
|
-
...(team
|
|
668
|
-
? {
|
|
669
|
-
team: {
|
|
670
|
-
id: team.id,
|
|
671
|
-
key: team.key,
|
|
672
|
-
name: team.name,
|
|
673
|
-
},
|
|
674
|
-
}
|
|
675
|
-
: {}),
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
function buildRelationEntries(nodes, peerKey, direction, normalizeType) {
|
|
679
|
-
const entries = [];
|
|
680
|
-
for (const rel of nodes ?? []) {
|
|
681
|
-
const peer = rel[peerKey];
|
|
682
|
-
if (!peer) {
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
entries.push({
|
|
686
|
-
id: rel.id,
|
|
687
|
-
type: normalizeType(rel.type),
|
|
688
|
-
direction,
|
|
689
|
-
issue: buildRelatedIssueSummary(peer),
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
return entries;
|
|
693
|
-
}
|
|
694
452
|
async function handleRelatedIssues(issueId, _options, command) {
|
|
695
453
|
const rootOpts = getRootOpts(command);
|
|
696
454
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
697
455
|
const linearService = await createLinearService(rootOpts);
|
|
698
456
|
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
699
|
-
const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
|
|
700
|
-
id: resolvedId,
|
|
701
|
-
});
|
|
457
|
+
const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, { id: resolvedId });
|
|
702
458
|
if (!result.issue) {
|
|
703
459
|
throw new Error(`Issue "${issueId}" not found`);
|
|
704
460
|
}
|
|
705
461
|
const issue = result.issue;
|
|
706
|
-
const relations = issue.relations;
|
|
707
|
-
const inverseRelations = issue.inverseRelations;
|
|
708
462
|
const entries = [
|
|
709
|
-
...
|
|
710
|
-
...
|
|
463
|
+
...buildOutgoingRelationEntries(issue.relations.nodes),
|
|
464
|
+
...buildIncomingRelationEntries(issue.inverseRelations.nodes),
|
|
711
465
|
];
|
|
712
466
|
outputSuccess({
|
|
713
467
|
id: issue.id,
|
|
@@ -717,211 +471,6 @@ async function handleRelatedIssues(issueId, _options, command) {
|
|
|
717
471
|
meta: { count: entries.length },
|
|
718
472
|
});
|
|
719
473
|
}
|
|
720
|
-
async function fetchCommentBodies(issueUuid, graphQLService) {
|
|
721
|
-
const result = await graphQLService.rawRequest(LIST_COMMENTS_QUERY, {
|
|
722
|
-
issueId: issueUuid,
|
|
723
|
-
first: 250,
|
|
724
|
-
});
|
|
725
|
-
const issue = result.issue;
|
|
726
|
-
const comments = issue?.comments;
|
|
727
|
-
const nodes = comments?.nodes ?? [];
|
|
728
|
-
return nodes
|
|
729
|
-
.map((c) => c.body)
|
|
730
|
-
.filter((b) => typeof b === "string" && b.length > 0);
|
|
731
|
-
}
|
|
732
|
-
async function linkReferencesForIssue(args) {
|
|
733
|
-
const comments = args.includeComments
|
|
734
|
-
? await fetchCommentBodies(args.issueUuid, args.graphQLService)
|
|
735
|
-
: undefined;
|
|
736
|
-
return autoLinkReferences({
|
|
737
|
-
issueId: args.issueUuid,
|
|
738
|
-
identifier: args.identifier,
|
|
739
|
-
description: args.description,
|
|
740
|
-
comments,
|
|
741
|
-
dryRun: args.dryRun,
|
|
742
|
-
graphQLService: args.graphQLService,
|
|
743
|
-
linearService: args.linearService,
|
|
744
|
-
preResolved: args.preResolved,
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Prepare a description rewrite: validate refs, wrap valid IDs as markdown links,
|
|
749
|
-
* and return the rewritten text (or undefined if nothing would change).
|
|
750
|
-
*/
|
|
751
|
-
async function prepareDescriptionRewrite(description, selfIdentifier, linearService, graphQLService) {
|
|
752
|
-
if (!description) {
|
|
753
|
-
return { preResolved: undefined, wrapped: undefined };
|
|
754
|
-
}
|
|
755
|
-
const refs = extractIssueReferences(description, selfIdentifier);
|
|
756
|
-
if (refs.length === 0) {
|
|
757
|
-
return { preResolved: undefined, wrapped: undefined };
|
|
758
|
-
}
|
|
759
|
-
const preResolved = await validateReferences(refs.map((r) => r.identifier), linearService);
|
|
760
|
-
if (preResolved.size === 0) {
|
|
761
|
-
return { preResolved, wrapped: undefined };
|
|
762
|
-
}
|
|
763
|
-
const validIds = new Set(preResolved.keys());
|
|
764
|
-
const urlKey = await getWorkspaceUrlKey(graphQLService);
|
|
765
|
-
const wrapped = wrapIssueReferencesAsLinks(description, validIds, urlKey);
|
|
766
|
-
return {
|
|
767
|
-
preResolved,
|
|
768
|
-
wrapped: wrapped === description ? undefined : wrapped,
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
async function pushDescriptionUpdate(issueUuid, description, graphQLService) {
|
|
772
|
-
const updateResult = await graphQLService.rawRequest(UPDATE_ISSUE_MUTATION, {
|
|
773
|
-
id: issueUuid,
|
|
774
|
-
input: { description },
|
|
775
|
-
});
|
|
776
|
-
const mutation = updateResult.issueUpdate;
|
|
777
|
-
if (!mutation?.success) {
|
|
778
|
-
throw new Error("Failed to rewrite description");
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
async function handleLinkReferencesSingle(issueId, options, graphQLService, linearService) {
|
|
782
|
-
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
783
|
-
// GET_ISSUE_RELATIONS_QUERY also returns description, so this single call gives us everything
|
|
784
|
-
// we need for the description-only path. Comments are fetched separately on demand.
|
|
785
|
-
const issueResult = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
|
|
786
|
-
id: resolvedId,
|
|
787
|
-
});
|
|
788
|
-
if (!issueResult.issue) {
|
|
789
|
-
throw new Error(`Issue "${issueId}" not found`);
|
|
790
|
-
}
|
|
791
|
-
const issue = issueResult.issue;
|
|
792
|
-
const identifier = issue.identifier;
|
|
793
|
-
const description = issue.description ?? "";
|
|
794
|
-
const dryRun = Boolean(options.dryRun);
|
|
795
|
-
const includeComments = Boolean(options.includeComments);
|
|
796
|
-
const rewriteDescription = Boolean(options.rewriteDescription);
|
|
797
|
-
// If rewriting, validate refs once up front so we can both wrap the description and
|
|
798
|
-
// pass the resolved map down to autoLink — avoids a second resolve round trip.
|
|
799
|
-
const rewrite = rewriteDescription
|
|
800
|
-
? await prepareDescriptionRewrite(description, identifier, linearService, graphQLService)
|
|
801
|
-
: { preResolved: undefined, wrapped: undefined };
|
|
802
|
-
const autoLinked = await linkReferencesForIssue({
|
|
803
|
-
issueUuid: resolvedId,
|
|
804
|
-
identifier,
|
|
805
|
-
description,
|
|
806
|
-
includeComments,
|
|
807
|
-
dryRun,
|
|
808
|
-
graphQLService,
|
|
809
|
-
linearService,
|
|
810
|
-
preResolved: rewrite.preResolved,
|
|
811
|
-
});
|
|
812
|
-
let descriptionRewritten = false;
|
|
813
|
-
if (rewriteDescription && rewrite.wrapped && !dryRun) {
|
|
814
|
-
await pushDescriptionUpdate(resolvedId, rewrite.wrapped, graphQLService);
|
|
815
|
-
descriptionRewritten = true;
|
|
816
|
-
}
|
|
817
|
-
outputSuccess({
|
|
818
|
-
id: resolvedId,
|
|
819
|
-
identifier,
|
|
820
|
-
title: issue.title,
|
|
821
|
-
autoLinked,
|
|
822
|
-
...(rewriteDescription
|
|
823
|
-
? {
|
|
824
|
-
descriptionRewritten,
|
|
825
|
-
...(dryRun && rewrite.wrapped
|
|
826
|
-
? { descriptionPreview: rewrite.wrapped }
|
|
827
|
-
: {}),
|
|
828
|
-
}
|
|
829
|
-
: {}),
|
|
830
|
-
meta: {
|
|
831
|
-
dryRun,
|
|
832
|
-
includeComments,
|
|
833
|
-
...(rewriteDescription ? { rewriteDescription: true } : {}),
|
|
834
|
-
},
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
async function handleLinkReferencesBatch(teamInput, options, graphQLService, linearService) {
|
|
838
|
-
const teamId = resolveTeam(teamInput);
|
|
839
|
-
const limit = options.limit
|
|
840
|
-
? Number.parseInt(options.limit, 10)
|
|
841
|
-
: 100;
|
|
842
|
-
if (!Number.isFinite(limit) || limit <= 0) {
|
|
843
|
-
throw new Error(`--limit must be a positive integer, got: ${options.limit}`);
|
|
844
|
-
}
|
|
845
|
-
const dryRun = Boolean(options.dryRun);
|
|
846
|
-
const includeComments = Boolean(options.includeComments);
|
|
847
|
-
const scan = await graphQLService.rawRequest(SCAN_ISSUES_QUERY, {
|
|
848
|
-
filter: { team: { id: { eq: teamId } } },
|
|
849
|
-
first: limit,
|
|
850
|
-
});
|
|
851
|
-
const issues = scan.issues?.nodes;
|
|
852
|
-
const nodes = issues ?? [];
|
|
853
|
-
const entries = [];
|
|
854
|
-
for (const node of nodes) {
|
|
855
|
-
const issueUuid = node.id;
|
|
856
|
-
const identifier = node.identifier;
|
|
857
|
-
const description = node.description ?? "";
|
|
858
|
-
const autoLinked = await linkReferencesForIssue({
|
|
859
|
-
issueUuid,
|
|
860
|
-
identifier,
|
|
861
|
-
description,
|
|
862
|
-
includeComments,
|
|
863
|
-
dryRun,
|
|
864
|
-
graphQLService,
|
|
865
|
-
linearService,
|
|
866
|
-
});
|
|
867
|
-
entries.push({ identifier, autoLinked });
|
|
868
|
-
}
|
|
869
|
-
// Summary counts across the batch
|
|
870
|
-
const totalLinked = entries.reduce((n, e) => n + e.autoLinked.linked.length, 0);
|
|
871
|
-
const totalSkipped = entries.reduce((n, e) => n + e.autoLinked.skipped.length, 0);
|
|
872
|
-
const totalFailed = entries.reduce((n, e) => n + e.autoLinked.failed.length, 0);
|
|
873
|
-
outputSuccess({
|
|
874
|
-
data: entries,
|
|
875
|
-
meta: {
|
|
876
|
-
count: entries.length,
|
|
877
|
-
team: teamInput,
|
|
878
|
-
dryRun,
|
|
879
|
-
includeComments,
|
|
880
|
-
totals: {
|
|
881
|
-
linked: totalLinked,
|
|
882
|
-
skipped: totalSkipped,
|
|
883
|
-
failed: totalFailed,
|
|
884
|
-
},
|
|
885
|
-
},
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
async function handleLinkReferencesIssue(issueIdOrTeamFlag, options, command) {
|
|
889
|
-
const rootOpts = getRootOpts(command);
|
|
890
|
-
const graphQLService = await createGraphQLService(rootOpts);
|
|
891
|
-
const linearService = await createLinearService(rootOpts);
|
|
892
|
-
if (options.team) {
|
|
893
|
-
if (issueIdOrTeamFlag) {
|
|
894
|
-
throw new Error("Provide either an issueId positional argument OR --team, not both. With --team, omit the issueId.");
|
|
895
|
-
}
|
|
896
|
-
if (options.rewriteDescription) {
|
|
897
|
-
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.");
|
|
898
|
-
}
|
|
899
|
-
return handleLinkReferencesBatch(options.team, options, graphQLService, linearService);
|
|
900
|
-
}
|
|
901
|
-
if (!issueIdOrTeamFlag) {
|
|
902
|
-
throw new Error("issueId is required (or use --team <key> to scan a whole team)");
|
|
903
|
-
}
|
|
904
|
-
return handleLinkReferencesSingle(issueIdOrTeamFlag, options, graphQLService, linearService);
|
|
905
|
-
}
|
|
906
|
-
async function handleReadIssue(issueIds, _options, command) {
|
|
907
|
-
const rootOpts = getRootOpts(command);
|
|
908
|
-
const graphQLService = await createGraphQLService(rootOpts);
|
|
909
|
-
const linearService = await createLinearService(rootOpts);
|
|
910
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
911
|
-
const fileService = await createFileService(rootOpts);
|
|
912
|
-
if (issueIds.length === 1) {
|
|
913
|
-
const issue = await issuesService.getIssueById(issueIds[0]);
|
|
914
|
-
const resolved = await downloadLinearUploads(issue, fileService);
|
|
915
|
-
outputSuccess(resolved);
|
|
916
|
-
}
|
|
917
|
-
else {
|
|
918
|
-
const results = await Promise.all(issueIds.map(async (id) => {
|
|
919
|
-
const issue = await issuesService.getIssueById(id);
|
|
920
|
-
return downloadLinearUploads(issue, fileService);
|
|
921
|
-
}));
|
|
922
|
-
outputSuccess(results);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
474
|
async function handleUpdateIssue(issueId, options, command) {
|
|
926
475
|
// Normalize aliases
|
|
927
476
|
if (options.state && !options.status) {
|
|
@@ -940,9 +489,10 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
940
489
|
const linearService = await createLinearService(rootOpts);
|
|
941
490
|
if (options.appendDescription) {
|
|
942
491
|
const resolved = await linearService.resolveIssueId(issueId);
|
|
943
|
-
const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", {
|
|
944
|
-
|
|
945
|
-
|
|
492
|
+
const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", {
|
|
493
|
+
id: resolved,
|
|
494
|
+
});
|
|
495
|
+
const existing = current.issue?.description ?? "";
|
|
946
496
|
options.description = `${existing}\n${options.appendDescription}`;
|
|
947
497
|
}
|
|
948
498
|
if (options.title || options.description) {
|
|
@@ -985,6 +535,34 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
985
535
|
: undefined;
|
|
986
536
|
outputSuccess(autoLinked ? { ...result, autoLinked } : result);
|
|
987
537
|
}
|
|
538
|
+
async function handleArchiveIssue(issueId, _options, command) {
|
|
539
|
+
const rootOpts = getRootOpts(command);
|
|
540
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
541
|
+
const linearService = await createLinearService(rootOpts);
|
|
542
|
+
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
543
|
+
const result = await issuesService.archiveIssue(issueId);
|
|
544
|
+
outputSuccess({
|
|
545
|
+
success: true,
|
|
546
|
+
archived: true,
|
|
547
|
+
...result,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
async function handleDeleteIssue(issueId, options, command) {
|
|
551
|
+
const rootOpts = getRootOpts(command);
|
|
552
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
553
|
+
const linearService = await createLinearService(rootOpts);
|
|
554
|
+
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
555
|
+
const permanentlyDelete = Boolean(options.permanentlyDelete || options.hard);
|
|
556
|
+
const result = await issuesService.deleteIssue(issueId, {
|
|
557
|
+
permanentlyDelete,
|
|
558
|
+
});
|
|
559
|
+
outputSuccess({
|
|
560
|
+
success: true,
|
|
561
|
+
deleted: true,
|
|
562
|
+
permanentlyDeleted: permanentlyDelete,
|
|
563
|
+
...result,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
988
566
|
async function handleRetrolink(options, command) {
|
|
989
567
|
// 1. Read current branch
|
|
990
568
|
let currentBranch;
|
|
@@ -1082,7 +660,7 @@ export function setupIssuesCommands(program) {
|
|
|
1082
660
|
.option("--status <status>", "filter by status (comma-separated, e.g. Todo,Backlog)")
|
|
1083
661
|
.option("--priority <priority>", "filter by priority (comma-separated: urgent,high,medium,low,none or 0-4)")
|
|
1084
662
|
.option("--sort <field>", "sort results (priority, status, created, updated)")
|
|
1085
|
-
.option("--format <format>", "output format (json, table, md, csv)", "json")
|
|
663
|
+
.option("--format <format>", "output format (json, summary, table, md, csv)", "json")
|
|
1086
664
|
.option("--fields <fields>", "columns for table/csv (comma-separated: identifier,title,status,priority,assignee,project,team,labels,updated)")
|
|
1087
665
|
.action(handleAsyncCommand(handleListIssues));
|
|
1088
666
|
issues
|
|
@@ -1097,7 +675,7 @@ export function setupIssuesCommands(program) {
|
|
|
1097
675
|
.option("--label <labels>", "alias for --labels")
|
|
1098
676
|
.option("--priority <priority>", "filter by priority (comma-separated: urgent,high,medium,low,none or 0-4)")
|
|
1099
677
|
.option("--sort <field>", "sort results (priority, status, created, updated)")
|
|
1100
|
-
.option("--format <format>", "output format (json, table, md, csv)", "json")
|
|
678
|
+
.option("--format <format>", "output format (json, summary, table, md, csv)", "json")
|
|
1101
679
|
.option("--fields <fields>", "columns for table/csv output")
|
|
1102
680
|
.option("-l, --limit <number>", "limit results", "10")
|
|
1103
681
|
.action(handleAsyncCommand(handleSearchIssues));
|
|
@@ -1108,6 +686,7 @@ export function setupIssuesCommands(program) {
|
|
|
1108
686
|
.option("-d, --description <desc>", "issue description")
|
|
1109
687
|
.option("--description-file <path>", "read description from file (use - for stdin)")
|
|
1110
688
|
.option("--template <name>", "use a named description template from config.descriptionTemplates")
|
|
689
|
+
.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.")
|
|
1111
690
|
.option("-a, --assignee <assignee>", "assign to user (name, alias, or UUID)")
|
|
1112
691
|
.option("--no-assignee", "create unassigned even when config.defaultAssignee is set")
|
|
1113
692
|
.option("-p, --priority <priority>", "priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
|
|
@@ -1138,8 +717,12 @@ export function setupIssuesCommands(program) {
|
|
|
1138
717
|
options.labels = options.label;
|
|
1139
718
|
}
|
|
1140
719
|
const title = options.title || titleArg;
|
|
1141
|
-
|
|
1142
|
-
|
|
720
|
+
// --from-template lets Linear copy the template's title, so a
|
|
721
|
+
// local title is optional in that path. Without --from-template,
|
|
722
|
+
// the create mutation requires a title up front.
|
|
723
|
+
const fromTemplate = typeof options.fromTemplate === "string" && options.fromTemplate;
|
|
724
|
+
if (!title && !fromTemplate) {
|
|
725
|
+
throw new Error("Title is required. Provide it as a positional argument, with --title, or use --from-template to copy the template's title.");
|
|
1143
726
|
}
|
|
1144
727
|
return handleCreateIssue(title, options, command);
|
|
1145
728
|
}));
|
|
@@ -1150,7 +733,7 @@ export function setupIssuesCommands(program) {
|
|
|
1150
733
|
.alias("show")
|
|
1151
734
|
.description("Get issue details. Accepts multiple IDs for batch retrieval.")
|
|
1152
735
|
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.\nMultiple IDs: el-linear issue get DEV-123 DEV-456 DEV-789")
|
|
1153
|
-
.action(handleAsyncCommand(
|
|
736
|
+
.action(handleAsyncCommand(readIssues));
|
|
1154
737
|
issues
|
|
1155
738
|
.command("update <issueId>")
|
|
1156
739
|
.alias("edit")
|
|
@@ -1181,6 +764,23 @@ export function setupIssuesCommands(program) {
|
|
|
1181
764
|
.option("--strict", "strict brand validation")
|
|
1182
765
|
.option("--no-auto-link", "skip auto-linking issue references found in the description")
|
|
1183
766
|
.action(handleAsyncCommand(handleUpdateIssue));
|
|
767
|
+
issues
|
|
768
|
+
.command("archive <issueId>")
|
|
769
|
+
.description("Archive an issue.")
|
|
770
|
+
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
|
|
771
|
+
.action(handleAsyncCommand(handleArchiveIssue));
|
|
772
|
+
issues
|
|
773
|
+
.command("delete <issueId>")
|
|
774
|
+
.description("Delete (trash) an issue.")
|
|
775
|
+
.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.")
|
|
776
|
+
.option("--permanently-delete", "skip Linear's 30-day grace period (admin only)")
|
|
777
|
+
.option("--hard", "alias for --permanently-delete")
|
|
778
|
+
.action(handleAsyncCommand(handleDeleteIssue));
|
|
779
|
+
issues
|
|
780
|
+
.command("hard-delete <issueId>")
|
|
781
|
+
.description("Permanently delete an issue immediately (admin only).")
|
|
782
|
+
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
|
|
783
|
+
.action(handleAsyncCommand((issueId, options, command) => handleDeleteIssue(issueId, { ...options, permanentlyDelete: true }, command)));
|
|
1184
784
|
issues
|
|
1185
785
|
.command("history <issueId>")
|
|
1186
786
|
.description("Show issue state transition history (time in each status).")
|