@enrichlayer/el-linear 1.5.0 → 1.7.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 +32 -4
- package/dist/auth/oauth-app-config.d.ts +21 -0
- package/dist/auth/oauth-app-config.js +82 -0
- package/dist/commands/attachments.js +11 -12
- package/dist/commands/batch.js +15 -14
- package/dist/commands/comments.js +22 -10
- package/dist/commands/cycles.js +5 -4
- package/dist/commands/documents.js +18 -15
- package/dist/commands/embeds.js +4 -6
- package/dist/commands/graphql.js +5 -4
- package/dist/commands/init/aliases.js +0 -14
- package/dist/commands/init/defaults.d.ts +18 -1
- package/dist/commands/init/defaults.js +83 -8
- package/dist/commands/init/index.js +7 -13
- package/dist/commands/init/oauth.d.ts +4 -4
- package/dist/commands/init/oauth.js +24 -6
- package/dist/commands/init/shared.js +0 -1
- package/dist/commands/init/token.js +0 -7
- package/dist/commands/init/workspace.js +0 -2
- package/dist/commands/issue-id.js +3 -1
- package/dist/commands/issues.js +130 -54
- package/dist/commands/labels.js +23 -9
- package/dist/commands/profile/migrate-legacy.js +0 -1
- package/dist/commands/project-milestones.js +13 -12
- package/dist/commands/projects.js +28 -15
- package/dist/commands/read-shortcut.js +8 -7
- package/dist/commands/refs.js +4 -3
- package/dist/commands/releases.js +9 -8
- package/dist/commands/search.js +3 -2
- package/dist/commands/teams.js +14 -3
- package/dist/commands/templates.js +5 -4
- package/dist/commands/users.js +3 -2
- package/dist/config/config.d.ts +37 -0
- package/dist/config/issue-validation.js +1 -1
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/resolver.js +1 -1
- package/dist/main.js +3 -2
- package/dist/utils/disk-cache.d.ts +51 -0
- package/dist/utils/disk-cache.js +178 -0
- package/dist/utils/download-uploads.d.ts +2 -1
- package/dist/utils/download-uploads.js +2 -4
- package/dist/utils/file-service.d.ts +26 -2
- package/dist/utils/file-service.js +27 -5
- package/dist/utils/footer.d.ts +19 -0
- package/dist/utils/footer.js +27 -0
- package/dist/utils/gdoc-parser.js +1 -1
- package/dist/utils/graphql-attachments-service.d.ts +1 -1
- package/dist/utils/graphql-attachments-service.js +2 -2
- package/dist/utils/graphql-documents-service.d.ts +1 -1
- package/dist/utils/graphql-documents-service.js +2 -2
- package/dist/utils/graphql-service.d.ts +2 -2
- package/dist/utils/graphql-service.js +7 -4
- package/dist/utils/linear-service.d.ts +18 -3
- package/dist/utils/linear-service.js +21 -6
- package/dist/utils/markdown-prosemirror.js +16 -9
- package/dist/utils/root-opts.d.ts +11 -0
- package/dist/utils/root-opts.js +13 -0
- package/dist/utils/validators.d.ts +7 -0
- package/dist/utils/validators.js +14 -14
- package/package.json +1 -1
package/dist/commands/issues.js
CHANGED
|
@@ -7,10 +7,10 @@ import { resolveDefaultStatus } from "../config/status-defaults.js";
|
|
|
7
7
|
import { enforceTerms } from "../config/term-enforcer.js";
|
|
8
8
|
import { LIST_COMMENTS_QUERY } from "../queries/comments.js";
|
|
9
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 { getApiToken } from "../utils/auth.js";
|
|
11
10
|
import { autoLinkReferences, } from "../utils/auto-link-references.js";
|
|
12
11
|
import { downloadLinearUploads } from "../utils/download-uploads.js";
|
|
13
|
-
import {
|
|
12
|
+
import { createFileService } from "../utils/file-service.js";
|
|
13
|
+
import { applyFooter } from "../utils/footer.js";
|
|
14
14
|
import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
|
|
15
15
|
import { GraphQLIssuesService } from "../utils/graphql-issues-service.js";
|
|
16
16
|
import { createGraphQLService, } from "../utils/graphql-service.js";
|
|
@@ -19,6 +19,7 @@ import { wrapIssueReferencesAsLinks } from "../utils/issue-reference-wrapper.js"
|
|
|
19
19
|
import { createLinearService, } from "../utils/linear-service.js";
|
|
20
20
|
import { logger } from "../utils/logger.js";
|
|
21
21
|
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
22
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
22
23
|
import { formatCsv, formatMarkdown, formatTable, } from "../utils/table-formatter.js";
|
|
23
24
|
import { validateReferences } from "../utils/validate-references.js";
|
|
24
25
|
import { parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
|
|
@@ -78,14 +79,43 @@ function readDescriptionFile(filePath) {
|
|
|
78
79
|
return fs.readFileSync(filePath, "utf8").trim();
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
|
-
* Resolve the description from --description, --description-file, or
|
|
82
|
-
* --
|
|
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.
|
|
83
91
|
*/
|
|
84
92
|
function resolveDescription(options) {
|
|
85
|
-
|
|
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) {
|
|
86
101
|
return readDescriptionFile(options.descriptionFile);
|
|
87
102
|
}
|
|
88
|
-
|
|
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;
|
|
89
119
|
}
|
|
90
120
|
function isImageFile(filename) {
|
|
91
121
|
const ext = filename.lastIndexOf(".");
|
|
@@ -233,12 +263,20 @@ function buildUpdateArgs(issueId, options, assigneeId) {
|
|
|
233
263
|
else if (options.labels) {
|
|
234
264
|
labelIds = splitList(options.labels);
|
|
235
265
|
}
|
|
266
|
+
// On update, fall back to config.defaultPriority only when --priority
|
|
267
|
+
// wasn't passed. This keeps every update setting a priority for users who
|
|
268
|
+
// want a workspace-wide baseline (e.g. all unassigned triage tickets bump
|
|
269
|
+
// to "medium"). To leave priority untouched, omit defaultPriority from
|
|
270
|
+
// config — the field is opt-in.
|
|
271
|
+
const priorityInput = typeof options.priority === "string"
|
|
272
|
+
? options.priority
|
|
273
|
+
: loadConfig().defaultPriority;
|
|
236
274
|
return {
|
|
237
275
|
id: issueId,
|
|
238
276
|
title: options.title,
|
|
239
277
|
description: options.description,
|
|
240
278
|
statusId: options.status,
|
|
241
|
-
priority:
|
|
279
|
+
priority: priorityInput ? validatePriority(priorityInput) : undefined,
|
|
242
280
|
assigneeId,
|
|
243
281
|
projectId: options.project,
|
|
244
282
|
labelIds,
|
|
@@ -296,9 +334,9 @@ async function handleListIssues(options, command) {
|
|
|
296
334
|
if (options.label && !options.labels) {
|
|
297
335
|
options.labels = options.label;
|
|
298
336
|
}
|
|
299
|
-
const rootOpts = command
|
|
300
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
301
|
-
const linearService = createLinearService(rootOpts);
|
|
337
|
+
const rootOpts = getRootOpts(command);
|
|
338
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
339
|
+
const linearService = await createLinearService(rootOpts);
|
|
302
340
|
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
303
341
|
const hasFilters = options.team ||
|
|
304
342
|
options.labels ||
|
|
@@ -337,9 +375,9 @@ async function handleSearchIssues(query, options, command) {
|
|
|
337
375
|
if (options.label && !options.labels) {
|
|
338
376
|
options.labels = options.label;
|
|
339
377
|
}
|
|
340
|
-
const rootOpts = command
|
|
341
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
342
|
-
const linearService = createLinearService(rootOpts);
|
|
378
|
+
const rootOpts = getRootOpts(command);
|
|
379
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
380
|
+
const linearService = await createLinearService(rootOpts);
|
|
343
381
|
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
344
382
|
const searchArgs = {
|
|
345
383
|
query,
|
|
@@ -362,6 +400,21 @@ async function handleSearchIssues(query, options, command) {
|
|
|
362
400
|
async function resolveCreateInputs(title, options, rootOpts) {
|
|
363
401
|
const config = loadConfig();
|
|
364
402
|
enforceTerms([title, options.description], { strict: options.strict });
|
|
403
|
+
// Effective assignee: explicit --assignee wins; --no-assignee (commander
|
|
404
|
+
// parses as `assignee === false`) skips both flag and config; otherwise
|
|
405
|
+
// fall back to config.defaultAssignee. Computed BEFORE validation so the
|
|
406
|
+
// "assignee required" rule sees the resolved value, not undefined.
|
|
407
|
+
const noAssignee = options.assignee === false;
|
|
408
|
+
const explicitAssignee = typeof options.assignee === "string" ? options.assignee : undefined;
|
|
409
|
+
const effectiveAssignee = noAssignee
|
|
410
|
+
? undefined
|
|
411
|
+
: (explicitAssignee ?? config.defaultAssignee);
|
|
412
|
+
// Effective priority: explicit --priority wins; otherwise fall back to
|
|
413
|
+
// config.defaultPriority. Both go through validatePriority so a bad config
|
|
414
|
+
// value fails fast with a useful error.
|
|
415
|
+
const effectivePriorityInput = typeof options.priority === "string"
|
|
416
|
+
? options.priority
|
|
417
|
+
: config.defaultPriority;
|
|
365
418
|
// --- Validation (labels, description, assignee, project, title) ---
|
|
366
419
|
// Controlled by config.validation.enabled (default: true).
|
|
367
420
|
// Bypassed by --skip-validation flag.
|
|
@@ -372,7 +425,7 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
372
425
|
labels: rawLabels,
|
|
373
426
|
description: description || undefined,
|
|
374
427
|
title,
|
|
375
|
-
assignee:
|
|
428
|
+
assignee: effectiveAssignee,
|
|
376
429
|
project: options.project,
|
|
377
430
|
});
|
|
378
431
|
// Apply normalized labels back so resolution uses the canonical names
|
|
@@ -381,13 +434,13 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
381
434
|
}
|
|
382
435
|
enforceValidation(validationResult);
|
|
383
436
|
}
|
|
384
|
-
if (!
|
|
437
|
+
if (!effectivePriorityInput) {
|
|
385
438
|
outputWarning("Creating issue without --priority. Consider specifying it for better triage.", "missing_fields");
|
|
386
439
|
}
|
|
387
440
|
const teamInput = options.team || config.defaultTeam;
|
|
388
441
|
const teamId = resolveTeam(teamInput);
|
|
389
|
-
const assigneeId =
|
|
390
|
-
? await resolveAssignee(
|
|
442
|
+
const assigneeId = effectiveAssignee
|
|
443
|
+
? await resolveAssignee(effectiveAssignee, rootOpts)
|
|
391
444
|
: undefined;
|
|
392
445
|
let labelIds = [];
|
|
393
446
|
if (options.labels) {
|
|
@@ -408,7 +461,18 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
408
461
|
if (options.subscriber) {
|
|
409
462
|
subscriberIds = splitList(options.subscriber).map((s) => resolveMember(s));
|
|
410
463
|
}
|
|
411
|
-
|
|
464
|
+
const priority = effectivePriorityInput
|
|
465
|
+
? validatePriority(effectivePriorityInput)
|
|
466
|
+
: undefined;
|
|
467
|
+
return {
|
|
468
|
+
teamInput,
|
|
469
|
+
teamId,
|
|
470
|
+
assigneeId,
|
|
471
|
+
labelIds,
|
|
472
|
+
status,
|
|
473
|
+
subscriberIds,
|
|
474
|
+
priority,
|
|
475
|
+
};
|
|
412
476
|
}
|
|
413
477
|
async function uploadAttachmentsIfNeeded(options, rootOpts) {
|
|
414
478
|
if (!options.attachment) {
|
|
@@ -417,8 +481,7 @@ async function uploadAttachmentsIfNeeded(options, rootOpts) {
|
|
|
417
481
|
const paths = Array.isArray(options.attachment)
|
|
418
482
|
? options.attachment
|
|
419
483
|
: [options.attachment];
|
|
420
|
-
const
|
|
421
|
-
const fileService = new FileService(apiToken);
|
|
484
|
+
const fileService = await createFileService(rootOpts);
|
|
422
485
|
const results = [];
|
|
423
486
|
for (const filePath of paths) {
|
|
424
487
|
const result = await fileService.uploadFile(filePath);
|
|
@@ -442,12 +505,21 @@ function buildDescriptionWithAttachments(baseDescription, uploadResults) {
|
|
|
442
505
|
return description;
|
|
443
506
|
}
|
|
444
507
|
async function handleCreateIssue(title, options, command) {
|
|
445
|
-
const rootOpts = command
|
|
446
|
-
const { teamInput, teamId, assigneeId, labelIds, status, subscriberIds } = await resolveCreateInputs(title, options, rootOpts);
|
|
508
|
+
const rootOpts = getRootOpts(command);
|
|
509
|
+
const { teamInput, teamId, assigneeId, labelIds, status, subscriberIds, priority, } = await resolveCreateInputs(title, options, rootOpts);
|
|
447
510
|
const uploadResults = await uploadAttachmentsIfNeeded(options, rootOpts);
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
511
|
+
const descriptionWithAttachments = buildDescriptionWithAttachments(resolveDescription(options) || "", uploadResults);
|
|
512
|
+
// Append messageFooter (config or --footer flag) so auto-link picks up any
|
|
513
|
+
// issue refs in the footer too. --no-footer skips both flag and config.
|
|
514
|
+
// Commander parses --no-footer as `options.footer === false`.
|
|
515
|
+
const noFooter = options.footer === false;
|
|
516
|
+
const explicitFooter = typeof options.footer === "string" ? options.footer : undefined;
|
|
517
|
+
const description = applyFooter(descriptionWithAttachments, {
|
|
518
|
+
footer: explicitFooter,
|
|
519
|
+
noFooter,
|
|
520
|
+
}) ?? "";
|
|
521
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
522
|
+
const linearService = await createLinearService(rootOpts);
|
|
451
523
|
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
452
524
|
// Wrap valid issue identifiers as markdown links before creating, so the description
|
|
453
525
|
// saved on Linear has clickable refs from the start. Self-reference can't apply here
|
|
@@ -459,7 +531,7 @@ async function handleCreateIssue(title, options, command) {
|
|
|
459
531
|
teamInput,
|
|
460
532
|
description: prepared.description,
|
|
461
533
|
assigneeId,
|
|
462
|
-
priority
|
|
534
|
+
priority,
|
|
463
535
|
projectId: options.project,
|
|
464
536
|
statusId: status,
|
|
465
537
|
labelIds: labelIds.length > 0 ? labelIds : undefined,
|
|
@@ -486,7 +558,7 @@ async function handleCreateIssue(title, options, command) {
|
|
|
486
558
|
// so only create separate attachment records for non-image files.
|
|
487
559
|
const attachments = [];
|
|
488
560
|
if (uploadResults.length > 0) {
|
|
489
|
-
const attachmentsService = createGraphQLAttachmentsService(rootOpts);
|
|
561
|
+
const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
|
|
490
562
|
for (const uploadResult of uploadResults) {
|
|
491
563
|
if (!isImageFile(uploadResult.filename)) {
|
|
492
564
|
const attachment = await attachmentsService.createAttachment({
|
|
@@ -517,9 +589,9 @@ async function handleCreateIssue(title, options, command) {
|
|
|
517
589
|
outputSuccess(output);
|
|
518
590
|
}
|
|
519
591
|
async function handleHistoryIssue(issueId, _options, command) {
|
|
520
|
-
const rootOpts = command
|
|
521
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
522
|
-
const linearService = createLinearService(rootOpts);
|
|
592
|
+
const rootOpts = getRootOpts(command);
|
|
593
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
594
|
+
const linearService = await createLinearService(rootOpts);
|
|
523
595
|
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
524
596
|
const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, {
|
|
525
597
|
id: resolvedId,
|
|
@@ -547,9 +619,9 @@ async function handleHistoryIssue(issueId, _options, command) {
|
|
|
547
619
|
});
|
|
548
620
|
}
|
|
549
621
|
async function handleRelateIssue(issueId, options, command) {
|
|
550
|
-
const rootOpts = command
|
|
551
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
552
|
-
const linearService = createLinearService(rootOpts);
|
|
622
|
+
const rootOpts = getRootOpts(command);
|
|
623
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
624
|
+
const linearService = await createLinearService(rootOpts);
|
|
553
625
|
const sourceId = await linearService.resolveIssueId(issueId);
|
|
554
626
|
const relations = await createRelations(sourceId, options, graphQLService, linearService);
|
|
555
627
|
if (relations.length === 0) {
|
|
@@ -620,9 +692,9 @@ function buildRelationEntries(nodes, peerKey, direction, normalizeType) {
|
|
|
620
692
|
return entries;
|
|
621
693
|
}
|
|
622
694
|
async function handleRelatedIssues(issueId, _options, command) {
|
|
623
|
-
const rootOpts = command
|
|
624
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
625
|
-
const linearService = createLinearService(rootOpts);
|
|
695
|
+
const rootOpts = getRootOpts(command);
|
|
696
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
697
|
+
const linearService = await createLinearService(rootOpts);
|
|
626
698
|
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
627
699
|
const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
|
|
628
700
|
id: resolvedId,
|
|
@@ -813,10 +885,10 @@ async function handleLinkReferencesBatch(teamInput, options, graphQLService, lin
|
|
|
813
885
|
},
|
|
814
886
|
});
|
|
815
887
|
}
|
|
816
|
-
function handleLinkReferencesIssue(issueIdOrTeamFlag, options, command) {
|
|
817
|
-
const rootOpts = command
|
|
818
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
819
|
-
const linearService = createLinearService(rootOpts);
|
|
888
|
+
async function handleLinkReferencesIssue(issueIdOrTeamFlag, options, command) {
|
|
889
|
+
const rootOpts = getRootOpts(command);
|
|
890
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
891
|
+
const linearService = await createLinearService(rootOpts);
|
|
820
892
|
if (options.team) {
|
|
821
893
|
if (issueIdOrTeamFlag) {
|
|
822
894
|
throw new Error("Provide either an issueId positional argument OR --team, not both. With --team, omit the issueId.");
|
|
@@ -832,20 +904,20 @@ function handleLinkReferencesIssue(issueIdOrTeamFlag, options, command) {
|
|
|
832
904
|
return handleLinkReferencesSingle(issueIdOrTeamFlag, options, graphQLService, linearService);
|
|
833
905
|
}
|
|
834
906
|
async function handleReadIssue(issueIds, _options, command) {
|
|
835
|
-
const rootOpts = command
|
|
836
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
837
|
-
const linearService = createLinearService(rootOpts);
|
|
907
|
+
const rootOpts = getRootOpts(command);
|
|
908
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
909
|
+
const linearService = await createLinearService(rootOpts);
|
|
838
910
|
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
839
|
-
const
|
|
911
|
+
const fileService = await createFileService(rootOpts);
|
|
840
912
|
if (issueIds.length === 1) {
|
|
841
913
|
const issue = await issuesService.getIssueById(issueIds[0]);
|
|
842
|
-
const resolved = await downloadLinearUploads(issue,
|
|
914
|
+
const resolved = await downloadLinearUploads(issue, fileService);
|
|
843
915
|
outputSuccess(resolved);
|
|
844
916
|
}
|
|
845
917
|
else {
|
|
846
918
|
const results = await Promise.all(issueIds.map(async (id) => {
|
|
847
919
|
const issue = await issuesService.getIssueById(id);
|
|
848
|
-
return downloadLinearUploads(issue,
|
|
920
|
+
return downloadLinearUploads(issue, fileService);
|
|
849
921
|
}));
|
|
850
922
|
outputSuccess(results);
|
|
851
923
|
}
|
|
@@ -863,9 +935,9 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
863
935
|
options.description = readDescriptionFile(options.descriptionFile);
|
|
864
936
|
}
|
|
865
937
|
validateUpdateOptions(options);
|
|
866
|
-
const rootOpts = command
|
|
867
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
868
|
-
const linearService = createLinearService(rootOpts);
|
|
938
|
+
const rootOpts = getRootOpts(command);
|
|
939
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
940
|
+
const linearService = await createLinearService(rootOpts);
|
|
869
941
|
if (options.appendDescription) {
|
|
870
942
|
const resolved = await linearService.resolveIssueId(issueId);
|
|
871
943
|
const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", { id: resolved });
|
|
@@ -952,9 +1024,9 @@ async function handleRetrolink(options, command) {
|
|
|
952
1024
|
const description = commits.length > 0
|
|
953
1025
|
? commits.map((msg) => `- ${msg}`).join("\n")
|
|
954
1026
|
: `Retrolinked from branch: ${currentBranch}`;
|
|
955
|
-
const rootOpts = command
|
|
956
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
957
|
-
const linearService = createLinearService(rootOpts);
|
|
1027
|
+
const rootOpts = getRootOpts(command);
|
|
1028
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
1029
|
+
const linearService = await createLinearService(rootOpts);
|
|
958
1030
|
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
959
1031
|
let result;
|
|
960
1032
|
if (options.issue) {
|
|
@@ -1035,8 +1107,10 @@ export function setupIssuesCommands(program) {
|
|
|
1035
1107
|
.option("-t, --title <title>", "issue title (alternative to positional argument)")
|
|
1036
1108
|
.option("-d, --description <desc>", "issue description")
|
|
1037
1109
|
.option("--description-file <path>", "read description from file (use - for stdin)")
|
|
1110
|
+
.option("--template <name>", "use a named description template from config.descriptionTemplates")
|
|
1038
1111
|
.option("-a, --assignee <assignee>", "assign to user (name, alias, or UUID)")
|
|
1039
|
-
.option("-
|
|
1112
|
+
.option("--no-assignee", "create unassigned even when config.defaultAssignee is set")
|
|
1113
|
+
.option("-p, --priority <priority>", "priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
|
|
1040
1114
|
.option("--project <project>", "add to project (name or ID)")
|
|
1041
1115
|
.option("--team <team>", "team key or name (default: from config)")
|
|
1042
1116
|
.option("--labels <labels>", "labels (comma-separated names, auto-resolved per team)")
|
|
@@ -1056,6 +1130,8 @@ export function setupIssuesCommands(program) {
|
|
|
1056
1130
|
.option("--checkout", "create and checkout a git branch named after the issue")
|
|
1057
1131
|
.option("--skip-validation", "skip all validation (labels, description, assignee, project)")
|
|
1058
1132
|
.option("--no-auto-link", "skip auto-linking issue references found in the description")
|
|
1133
|
+
.option("--footer <text>", "text appended to the description (overrides config.messageFooter)")
|
|
1134
|
+
.option("--no-footer", "skip the configured messageFooter for this issue")
|
|
1059
1135
|
.action(handleAsyncCommand((titleArg, options, command) => {
|
|
1060
1136
|
// Normalize --label alias to --labels
|
|
1061
1137
|
if (options.label && !options.labels) {
|
|
@@ -1087,7 +1163,7 @@ export function setupIssuesCommands(program) {
|
|
|
1087
1163
|
.option("--append-description <text>", "append text to the existing description")
|
|
1088
1164
|
.option("-s, --status <status>", "new status name or ID")
|
|
1089
1165
|
.option("--state <status>", "alias for --status (new status name or ID)")
|
|
1090
|
-
.option("-p, --priority <priority>", "new priority: name (urgent/high/medium/low) or number (
|
|
1166
|
+
.option("-p, --priority <priority>", "new priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
|
|
1091
1167
|
.option("--assignee <assignee>", "new assignee (name, alias, or UUID)")
|
|
1092
1168
|
.option("--project <project>", "new project (name or ID)")
|
|
1093
1169
|
.option("--labels <labels>", "labels (comma-separated names or IDs)")
|
package/dist/commands/labels.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { loadConfig } from "../config/config.js";
|
|
1
2
|
import { resolveTeam } from "../config/resolver.js";
|
|
2
3
|
import { CREATE_LABEL_MUTATION, FIND_PARENT_LABEL_QUERY, RESTORE_LABEL_MUTATION, RETIRE_LABEL_MUTATION, } from "../queries/labels.js";
|
|
4
|
+
import { cached, resolveCacheTTL } from "../utils/disk-cache.js";
|
|
3
5
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
4
6
|
import { createLinearService } from "../utils/linear-service.js";
|
|
5
7
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
8
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
6
9
|
import { validateHexColor } from "../utils/validators.js";
|
|
7
10
|
async function handleCreateLabel(name, options, command) {
|
|
8
|
-
const rootOpts = command
|
|
11
|
+
const rootOpts = getRootOpts(command);
|
|
9
12
|
const teamId = resolveTeam(options.team);
|
|
10
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
13
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
11
14
|
const input = { name, teamId };
|
|
12
15
|
if (options.color) {
|
|
13
16
|
input.color = validateHexColor(options.color);
|
|
@@ -37,8 +40,8 @@ async function handleCreateLabel(name, options, command) {
|
|
|
37
40
|
outputSuccess(createResult.issueLabel);
|
|
38
41
|
}
|
|
39
42
|
async function handleRetireLabel(labelId, _options, command) {
|
|
40
|
-
const rootOpts = command
|
|
41
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
43
|
+
const rootOpts = getRootOpts(command);
|
|
44
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
42
45
|
const result = await graphQLService.rawRequest(RETIRE_LABEL_MUTATION, {
|
|
43
46
|
id: labelId,
|
|
44
47
|
});
|
|
@@ -49,8 +52,8 @@ async function handleRetireLabel(labelId, _options, command) {
|
|
|
49
52
|
outputSuccess(retireResult.issueLabel);
|
|
50
53
|
}
|
|
51
54
|
async function handleRestoreLabel(labelId, _options, command) {
|
|
52
|
-
const rootOpts = command
|
|
53
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
55
|
+
const rootOpts = getRootOpts(command);
|
|
56
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
54
57
|
const result = await graphQLService.rawRequest(RESTORE_LABEL_MUTATION, {
|
|
55
58
|
id: labelId,
|
|
56
59
|
});
|
|
@@ -72,10 +75,21 @@ export function setupLabelsCommands(program) {
|
|
|
72
75
|
.option("--team <team>", "filter by team key, name, or ID")
|
|
73
76
|
.option("-l, --limit <number>", "limit results", "100")
|
|
74
77
|
.action(handleAsyncCommand(async (options, command) => {
|
|
75
|
-
const rootOpts = command
|
|
78
|
+
const rootOpts = getRootOpts(command);
|
|
76
79
|
const teamFilter = options.team ? resolveTeam(options.team) : undefined;
|
|
77
|
-
const
|
|
78
|
-
const
|
|
80
|
+
const limit = Number.parseInt(options.limit, 10);
|
|
81
|
+
const ttl = resolveCacheTTL({
|
|
82
|
+
configTTL: loadConfig().cacheTTLSeconds,
|
|
83
|
+
noCacheFlag: rootOpts.cache === false,
|
|
84
|
+
});
|
|
85
|
+
// Cache key includes the team filter so list-with-team and list-
|
|
86
|
+
// without-team don't collide. Same `limit` participates because
|
|
87
|
+
// a smaller list isn't a valid cached answer for a larger ask.
|
|
88
|
+
const cacheKey = `labels-list-team:${teamFilter ?? "_all"}-limit:${limit}`;
|
|
89
|
+
const result = await cached(cacheKey, ttl, async () => {
|
|
90
|
+
const service = await createLinearService(rootOpts);
|
|
91
|
+
return service.getLabels(teamFilter, limit);
|
|
92
|
+
});
|
|
79
93
|
outputSuccess({
|
|
80
94
|
data: result.labels,
|
|
81
95
|
meta: { count: result.labels.length },
|
|
@@ -157,7 +157,6 @@ async function resolveAndValidateToken(options, prompts, validate) {
|
|
|
157
157
|
}
|
|
158
158
|
catch (err) {
|
|
159
159
|
const raw = err instanceof Error ? err.message : String(err);
|
|
160
|
-
// biome-ignore lint/suspicious/noConsole: interactive prompt feedback
|
|
161
160
|
console.log(` ✗ ${sanitizeForLog(raw)}`);
|
|
162
161
|
}
|
|
163
162
|
}
|
|
@@ -3,6 +3,7 @@ import { multipleMatchesError, notFoundError, } from "../utils/error-messages.js
|
|
|
3
3
|
import { createGraphQLService, } from "../utils/graphql-service.js";
|
|
4
4
|
import { createLinearService, } from "../utils/linear-service.js";
|
|
5
5
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
6
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
6
7
|
import { isUuid } from "../utils/uuid.js";
|
|
7
8
|
import { validateIsoDate } from "../utils/validators.js";
|
|
8
9
|
async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearService, projectNameOrId) {
|
|
@@ -37,9 +38,9 @@ async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearServi
|
|
|
37
38
|
return nodes[0].id;
|
|
38
39
|
}
|
|
39
40
|
async function handleListMilestones(options, command) {
|
|
40
|
-
const rootOpts = command
|
|
41
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
42
|
-
const linearService = createLinearService(rootOpts);
|
|
41
|
+
const rootOpts = getRootOpts(command);
|
|
42
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
43
|
+
const linearService = await createLinearService(rootOpts);
|
|
43
44
|
const projectId = await linearService.resolveProjectId(options.project);
|
|
44
45
|
const result = await graphQLService.rawRequest(LIST_PROJECT_MILESTONES_QUERY, {
|
|
45
46
|
projectId,
|
|
@@ -51,9 +52,9 @@ async function handleListMilestones(options, command) {
|
|
|
51
52
|
outputSuccess({ data: nodes, meta: { count: nodes.length } });
|
|
52
53
|
}
|
|
53
54
|
async function handleReadMilestone(milestoneIdOrName, options, command) {
|
|
54
|
-
const rootOpts = command
|
|
55
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
56
|
-
const linearService = createLinearService(rootOpts);
|
|
55
|
+
const rootOpts = getRootOpts(command);
|
|
56
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
57
|
+
const linearService = await createLinearService(rootOpts);
|
|
57
58
|
const milestoneId = await resolveMilestoneId(milestoneIdOrName, graphQLService, linearService, options.project);
|
|
58
59
|
const result = await graphQLService.rawRequest(GET_PROJECT_MILESTONE_BY_ID_QUERY, {
|
|
59
60
|
id: milestoneId,
|
|
@@ -62,9 +63,9 @@ async function handleReadMilestone(milestoneIdOrName, options, command) {
|
|
|
62
63
|
outputSuccess(result.projectMilestone);
|
|
63
64
|
}
|
|
64
65
|
async function handleCreateMilestone(name, options, command) {
|
|
65
|
-
const rootOpts = command
|
|
66
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
67
|
-
const linearService = createLinearService(rootOpts);
|
|
66
|
+
const rootOpts = getRootOpts(command);
|
|
67
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
68
|
+
const linearService = await createLinearService(rootOpts);
|
|
68
69
|
const projectId = await linearService.resolveProjectId(options.project);
|
|
69
70
|
if (options.targetDate) {
|
|
70
71
|
validateIsoDate(options.targetDate);
|
|
@@ -82,9 +83,9 @@ async function handleCreateMilestone(name, options, command) {
|
|
|
82
83
|
outputSuccess(createData.projectMilestone);
|
|
83
84
|
}
|
|
84
85
|
async function handleUpdateMilestone(milestoneIdOrName, options, command) {
|
|
85
|
-
const rootOpts = command
|
|
86
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
87
|
-
const linearService = createLinearService(rootOpts);
|
|
86
|
+
const rootOpts = getRootOpts(command);
|
|
87
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
88
|
+
const linearService = await createLinearService(rootOpts);
|
|
88
89
|
const milestoneId = await resolveMilestoneId(milestoneIdOrName, graphQLService, linearService, options.project);
|
|
89
90
|
const updateVars = { id: milestoneId };
|
|
90
91
|
if (options.name !== undefined) {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { loadConfig } from "../config/config.js";
|
|
1
2
|
import { resolveTeam } from "../config/resolver.js";
|
|
2
3
|
import { CREATE_PROJECT_MUTATION, GET_PROJECT_QUERY, GET_PROJECT_TEAM_ISSUES_QUERY, PROJECT_BY_ID_QUERY, SEARCH_PROJECTS_BY_NAME_QUERY, UPDATE_PROJECT_MUTATION, } from "../queries/projects.js";
|
|
4
|
+
import { cached, resolveCacheTTL } from "../utils/disk-cache.js";
|
|
3
5
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
4
6
|
import { createLinearService } from "../utils/linear-service.js";
|
|
5
7
|
import { logger } from "../utils/logger.js";
|
|
6
8
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
9
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
7
10
|
import { isUuid } from "../utils/uuid.js";
|
|
8
11
|
import { splitList } from "../utils/validators.js";
|
|
9
12
|
function formatProjectsOutput(projects, format, fieldNames) {
|
|
@@ -126,9 +129,10 @@ function formatTeamsOutput(projectUpdate) {
|
|
|
126
129
|
};
|
|
127
130
|
}
|
|
128
131
|
async function handleAddTeam(projectNameOrId, teamInput, _options, command) {
|
|
129
|
-
const rootOpts = command
|
|
130
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
131
|
-
const
|
|
132
|
+
const rootOpts = getRootOpts(command);
|
|
133
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
134
|
+
const linearService = await createLinearService(rootOpts);
|
|
135
|
+
const finalTeamId = await linearService.resolveTeamId(resolveTeam(teamInput));
|
|
132
136
|
const { projectId, currentTeams } = await resolveProjectWithTeams(graphQLService, projectNameOrId);
|
|
133
137
|
const currentTeamIds = currentTeams.map((t) => t.id);
|
|
134
138
|
if (currentTeamIds.includes(finalTeamId)) {
|
|
@@ -149,9 +153,10 @@ async function handleAddTeam(projectNameOrId, teamInput, _options, command) {
|
|
|
149
153
|
outputSuccess(formatTeamsOutput(projectUpdate));
|
|
150
154
|
}
|
|
151
155
|
async function handleAddTeams(projectNameOrId, teamInputs, _options, command) {
|
|
152
|
-
const rootOpts = command
|
|
153
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
154
|
-
const
|
|
156
|
+
const rootOpts = getRootOpts(command);
|
|
157
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
158
|
+
const linearService = await createLinearService(rootOpts);
|
|
159
|
+
const resolvedTeamIds = await Promise.all(teamInputs.map((t) => linearService.resolveTeamId(resolveTeam(t))));
|
|
155
160
|
const { projectId, currentTeams } = await resolveProjectWithTeams(graphQLService, projectNameOrId);
|
|
156
161
|
const currentTeamIds = currentTeams.map((t) => t.id);
|
|
157
162
|
const newTeamIds = resolvedTeamIds.filter((id) => !currentTeamIds.includes(id));
|
|
@@ -181,9 +186,10 @@ async function handleAddTeams(projectNameOrId, teamInputs, _options, command) {
|
|
|
181
186
|
});
|
|
182
187
|
}
|
|
183
188
|
async function handleRemoveTeam(projectNameOrId, teamInput, options, command) {
|
|
184
|
-
const rootOpts = command
|
|
185
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
186
|
-
const
|
|
189
|
+
const rootOpts = getRootOpts(command);
|
|
190
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
191
|
+
const linearService = await createLinearService(rootOpts);
|
|
192
|
+
const finalTeamId = await linearService.resolveTeamId(resolveTeam(teamInput));
|
|
187
193
|
const { projectId, projectName, currentTeams } = await resolveProjectWithTeams(graphQLService, projectNameOrId);
|
|
188
194
|
const currentTeamIds = currentTeams.map((t) => t.id);
|
|
189
195
|
if (!currentTeamIds.includes(finalTeamId)) {
|
|
@@ -220,8 +226,8 @@ async function handleRemoveTeam(projectNameOrId, teamInput, options, command) {
|
|
|
220
226
|
});
|
|
221
227
|
}
|
|
222
228
|
async function handleCreateProject(name, options, command) {
|
|
223
|
-
const rootOpts = command
|
|
224
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
229
|
+
const rootOpts = getRootOpts(command);
|
|
230
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
225
231
|
// Step 1: Check for duplicate projects (case-insensitive)
|
|
226
232
|
const searchResult = await graphQLService.rawRequest(SEARCH_PROJECTS_BY_NAME_QUERY, { name });
|
|
227
233
|
const existing = (searchResult.projects
|
|
@@ -236,7 +242,7 @@ async function handleCreateProject(name, options, command) {
|
|
|
236
242
|
`Use --force to create anyway, or use the existing project.`);
|
|
237
243
|
}
|
|
238
244
|
// Step 2: Resolve team IDs
|
|
239
|
-
const service = createLinearService(rootOpts);
|
|
245
|
+
const service = await createLinearService(rootOpts);
|
|
240
246
|
const teamKeys = options.team ? splitList(options.team) : [];
|
|
241
247
|
const teamIds = [];
|
|
242
248
|
for (const key of teamKeys) {
|
|
@@ -300,9 +306,16 @@ export function setupProjectsCommands(program) {
|
|
|
300
306
|
.option("--format <format>", "output format (json, table, md, csv)", "json")
|
|
301
307
|
.option("--fields <fields>", "columns for table/csv (comma-separated: name,state,progress,teams,lead,targetDate)")
|
|
302
308
|
.action(handleAsyncCommand(async (options, command) => {
|
|
303
|
-
const rootOpts = command
|
|
304
|
-
const
|
|
305
|
-
const
|
|
309
|
+
const rootOpts = getRootOpts(command);
|
|
310
|
+
const limit = Number.parseInt(options.limit, 10);
|
|
311
|
+
const ttl = resolveCacheTTL({
|
|
312
|
+
configTTL: loadConfig().cacheTTLSeconds,
|
|
313
|
+
noCacheFlag: rootOpts.cache === false,
|
|
314
|
+
});
|
|
315
|
+
const result = await cached(`projects-list-limit:${limit}`, ttl, async () => {
|
|
316
|
+
const service = await createLinearService(rootOpts);
|
|
317
|
+
return service.getProjects(limit);
|
|
318
|
+
});
|
|
306
319
|
const format = options.format;
|
|
307
320
|
if (format === "table" ||
|
|
308
321
|
format === "md" ||
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { getApiToken } from "../utils/auth.js";
|
|
2
1
|
import { downloadLinearUploads } from "../utils/download-uploads.js";
|
|
2
|
+
import { createFileService } from "../utils/file-service.js";
|
|
3
3
|
import { GraphQLIssuesService } from "../utils/graphql-issues-service.js";
|
|
4
4
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
5
5
|
import { createLinearService } from "../utils/linear-service.js";
|
|
6
6
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
7
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
7
8
|
/**
|
|
8
9
|
* Issue ID pattern: 1-5 uppercase letters, dash, 1+ digits (e.g. ADM-652, DEV-12).
|
|
9
10
|
*/
|
|
@@ -49,20 +50,20 @@ export function setupReadShortcut(program) {
|
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
async function readIssues(issueIds, _options, command) {
|
|
52
|
-
const rootOpts = command
|
|
53
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
54
|
-
const linearService = createLinearService(rootOpts);
|
|
53
|
+
const rootOpts = getRootOpts(command);
|
|
54
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
55
|
+
const linearService = await createLinearService(rootOpts);
|
|
55
56
|
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
56
|
-
const
|
|
57
|
+
const fileService = await createFileService(rootOpts);
|
|
57
58
|
if (issueIds.length === 1) {
|
|
58
59
|
const issue = await issuesService.getIssueById(issueIds[0]);
|
|
59
|
-
const resolved = await downloadLinearUploads(issue,
|
|
60
|
+
const resolved = await downloadLinearUploads(issue, fileService);
|
|
60
61
|
outputSuccess(resolved);
|
|
61
62
|
}
|
|
62
63
|
else {
|
|
63
64
|
const results = await Promise.all(issueIds.map(async (id) => {
|
|
64
65
|
const issue = await issuesService.getIssueById(id);
|
|
65
|
-
return downloadLinearUploads(issue,
|
|
66
|
+
return downloadLinearUploads(issue, fileService);
|
|
66
67
|
}));
|
|
67
68
|
outputSuccess(results);
|
|
68
69
|
}
|