@dabble/linear-cli 1.0.5 → 1.1.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/bin/linear.mjs +441 -36
- package/claude/skills/linear-cli/SKILL.md +69 -0
- package/package.json +1 -1
package/bin/linear.mjs
CHANGED
|
@@ -14,6 +14,7 @@ const API_URL = 'https://api.linear.app/graphql';
|
|
|
14
14
|
let CONFIG_FILE = '';
|
|
15
15
|
let LINEAR_API_KEY = '';
|
|
16
16
|
let TEAM_KEY = '';
|
|
17
|
+
let ALIASES = {};
|
|
17
18
|
|
|
18
19
|
// Colors (ANSI)
|
|
19
20
|
const colors = {
|
|
@@ -43,12 +44,32 @@ function loadConfig() {
|
|
|
43
44
|
// Load from config file first (highest priority)
|
|
44
45
|
if (CONFIG_FILE) {
|
|
45
46
|
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
47
|
+
let inAliasSection = false;
|
|
48
|
+
|
|
46
49
|
for (const line of content.split('\n')) {
|
|
47
|
-
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
52
|
+
|
|
53
|
+
// Check for section header
|
|
54
|
+
if (trimmed === '[aliases]') {
|
|
55
|
+
inAliasSection = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
59
|
+
inAliasSection = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
const [key, ...rest] = line.split('=');
|
|
49
64
|
const value = rest.join('=').trim();
|
|
50
|
-
|
|
51
|
-
if (
|
|
65
|
+
|
|
66
|
+
if (inAliasSection) {
|
|
67
|
+
// Store aliases with uppercase keys
|
|
68
|
+
ALIASES[key.trim().toUpperCase()] = value;
|
|
69
|
+
} else {
|
|
70
|
+
if (key.trim() === 'api_key') LINEAR_API_KEY = value;
|
|
71
|
+
if (key.trim() === 'team') TEAM_KEY = value;
|
|
72
|
+
}
|
|
52
73
|
}
|
|
53
74
|
}
|
|
54
75
|
|
|
@@ -57,6 +78,107 @@ function loadConfig() {
|
|
|
57
78
|
if (!TEAM_KEY) TEAM_KEY = process.env.LINEAR_TEAM || '';
|
|
58
79
|
}
|
|
59
80
|
|
|
81
|
+
function resolveAlias(nameOrAlias) {
|
|
82
|
+
if (!nameOrAlias) return nameOrAlias;
|
|
83
|
+
return ALIASES[nameOrAlias.toUpperCase()] || nameOrAlias;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function saveAlias(code, name) {
|
|
87
|
+
if (!CONFIG_FILE) {
|
|
88
|
+
console.error(colors.red('Error: No config file found. Run "linear login" first.'));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
93
|
+
const lines = content.split('\n');
|
|
94
|
+
|
|
95
|
+
// Find or create [aliases] section
|
|
96
|
+
let aliasStart = -1;
|
|
97
|
+
let aliasEnd = -1;
|
|
98
|
+
let existingAliasLine = -1;
|
|
99
|
+
const upperCode = code.toUpperCase();
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
const trimmed = lines[i].trim();
|
|
103
|
+
if (trimmed === '[aliases]') {
|
|
104
|
+
aliasStart = i;
|
|
105
|
+
} else if (aliasStart !== -1 && aliasEnd === -1) {
|
|
106
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
107
|
+
aliasEnd = i;
|
|
108
|
+
} else if (trimmed && !trimmed.startsWith('#')) {
|
|
109
|
+
const [key] = trimmed.split('=');
|
|
110
|
+
if (key.trim().toUpperCase() === upperCode) {
|
|
111
|
+
existingAliasLine = i;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If no alias section end found, it goes to EOF
|
|
118
|
+
if (aliasStart !== -1 && aliasEnd === -1) {
|
|
119
|
+
aliasEnd = lines.length;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const aliasLine = `${upperCode}=${name}`;
|
|
123
|
+
|
|
124
|
+
if (existingAliasLine !== -1) {
|
|
125
|
+
// Update existing alias
|
|
126
|
+
lines[existingAliasLine] = aliasLine;
|
|
127
|
+
} else if (aliasStart !== -1) {
|
|
128
|
+
// Add to existing section
|
|
129
|
+
lines.splice(aliasStart + 1, 0, aliasLine);
|
|
130
|
+
} else {
|
|
131
|
+
// Create new section at end
|
|
132
|
+
if (lines[lines.length - 1] !== '') {
|
|
133
|
+
lines.push('');
|
|
134
|
+
}
|
|
135
|
+
lines.push('[aliases]');
|
|
136
|
+
lines.push(aliasLine);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
writeFileSync(CONFIG_FILE, lines.join('\n'));
|
|
140
|
+
ALIASES[upperCode] = name;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function removeAlias(code) {
|
|
144
|
+
if (!CONFIG_FILE) {
|
|
145
|
+
console.error(colors.red('Error: No config file found.'));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const upperCode = code.toUpperCase();
|
|
150
|
+
if (!ALIASES[upperCode]) {
|
|
151
|
+
console.error(colors.red(`Alias not found: ${code}`));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
156
|
+
const lines = content.split('\n');
|
|
157
|
+
let inAliasSection = false;
|
|
158
|
+
|
|
159
|
+
const newLines = lines.filter(line => {
|
|
160
|
+
const trimmed = line.trim();
|
|
161
|
+
if (trimmed === '[aliases]') {
|
|
162
|
+
inAliasSection = true;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
166
|
+
inAliasSection = false;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (inAliasSection && trimmed && !trimmed.startsWith('#')) {
|
|
170
|
+
const [key] = trimmed.split('=');
|
|
171
|
+
if (key.trim().toUpperCase() === upperCode) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
writeFileSync(CONFIG_FILE, newLines.join('\n'));
|
|
179
|
+
delete ALIASES[upperCode];
|
|
180
|
+
}
|
|
181
|
+
|
|
60
182
|
function checkAuth() {
|
|
61
183
|
if (!LINEAR_API_KEY) {
|
|
62
184
|
console.error(colors.red("Error: Not logged in. Run 'linear login' first."));
|
|
@@ -132,16 +254,27 @@ function openBrowser(url) {
|
|
|
132
254
|
exec(cmd);
|
|
133
255
|
}
|
|
134
256
|
|
|
257
|
+
// Strip ANSI escape codes for length calculation
|
|
258
|
+
function stripAnsi(str) {
|
|
259
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
260
|
+
}
|
|
261
|
+
|
|
135
262
|
function formatTable(rows) {
|
|
136
263
|
if (rows.length === 0) return '';
|
|
137
264
|
const colWidths = [];
|
|
138
265
|
for (const row of rows) {
|
|
139
266
|
row.forEach((cell, i) => {
|
|
140
|
-
|
|
267
|
+
// Use visible length (without ANSI codes) for width calculation
|
|
268
|
+
colWidths[i] = Math.max(colWidths[i] || 0, stripAnsi(String(cell)).length);
|
|
141
269
|
});
|
|
142
270
|
}
|
|
143
271
|
return rows.map(row =>
|
|
144
|
-
row.map((cell, i) =>
|
|
272
|
+
row.map((cell, i) => {
|
|
273
|
+
const str = String(cell);
|
|
274
|
+
const visibleLen = stripAnsi(str).length;
|
|
275
|
+
// Pad based on visible length, not string length
|
|
276
|
+
return str + ' '.repeat(Math.max(0, colWidths[i] - visibleLen));
|
|
277
|
+
}).join(' ')
|
|
145
278
|
).join('\n');
|
|
146
279
|
}
|
|
147
280
|
|
|
@@ -187,6 +320,7 @@ async function cmdIssues(args) {
|
|
|
187
320
|
milestone: 'string',
|
|
188
321
|
state: 'string', s: 'string',
|
|
189
322
|
label: 'string', l: 'string',
|
|
323
|
+
priority: 'string',
|
|
190
324
|
});
|
|
191
325
|
|
|
192
326
|
const inProgress = opts['in-progress'];
|
|
@@ -199,6 +333,7 @@ async function cmdIssues(args) {
|
|
|
199
333
|
const milestoneFilter = opts.milestone;
|
|
200
334
|
const stateFilter = opts.state || opts.s;
|
|
201
335
|
const labelFilter = opts.label || opts.l;
|
|
336
|
+
const priorityFilter = (opts.priority || '').toLowerCase();
|
|
202
337
|
|
|
203
338
|
// Get current user ID for filtering/sorting
|
|
204
339
|
const viewerResult = await gql('{ viewer { id } }');
|
|
@@ -234,26 +369,35 @@ async function cmdIssues(args) {
|
|
|
234
369
|
// Check if any issues have assignees (to decide whether to show column)
|
|
235
370
|
const hasAssignees = issues.some(i => i.assignee);
|
|
236
371
|
|
|
237
|
-
// Sort: assigned to you first, then by priority, then by sortOrder
|
|
372
|
+
// Sort: assigned to you first, then by priority (urgent first), then by sortOrder
|
|
238
373
|
issues.sort((a, b) => {
|
|
239
374
|
const aIsMine = a.assignee?.id === viewerId;
|
|
240
375
|
const bIsMine = b.assignee?.id === viewerId;
|
|
241
376
|
if (aIsMine && !bIsMine) return -1;
|
|
242
377
|
if (!aIsMine && bIsMine) return 1;
|
|
243
|
-
// Then by priority (
|
|
244
|
-
|
|
378
|
+
// Then by priority (lower number = more urgent, but 0 means no priority so sort last)
|
|
379
|
+
const aPri = a.priority || 5; // No priority (0) sorts after Low (4)
|
|
380
|
+
const bPri = b.priority || 5;
|
|
381
|
+
if (aPri !== bPri) return aPri - bPri;
|
|
245
382
|
// Then by sortOrder
|
|
246
383
|
return (b.sortOrder || 0) - (a.sortOrder || 0);
|
|
247
384
|
});
|
|
248
385
|
|
|
386
|
+
// Check if any issues have priority set
|
|
387
|
+
const hasPriority = issues.some(i => i.priority > 0);
|
|
388
|
+
|
|
249
389
|
// Helper to format issue row
|
|
250
390
|
const formatRow = (i) => {
|
|
251
391
|
const row = [
|
|
252
392
|
i.identifier,
|
|
253
393
|
i.title,
|
|
254
394
|
i.state.name,
|
|
255
|
-
i.project?.name || '-'
|
|
256
395
|
];
|
|
396
|
+
if (hasPriority) {
|
|
397
|
+
const pri = PRIORITY_LABELS[i.priority] || '';
|
|
398
|
+
row.push(pri ? colors.bold(pri) : '-');
|
|
399
|
+
}
|
|
400
|
+
row.push(i.project?.name || '-');
|
|
257
401
|
if (hasAssignees) {
|
|
258
402
|
const assignee = i.assignee?.id === viewerId ? 'you' : (i.assignee?.name || '-');
|
|
259
403
|
row.push(assignee);
|
|
@@ -273,15 +417,23 @@ async function cmdIssues(args) {
|
|
|
273
417
|
);
|
|
274
418
|
}
|
|
275
419
|
if (projectFilter) {
|
|
420
|
+
const resolvedProject = resolveAlias(projectFilter);
|
|
276
421
|
filtered = filtered.filter(i =>
|
|
277
|
-
i.project?.name?.toLowerCase().includes(
|
|
422
|
+
i.project?.name?.toLowerCase().includes(resolvedProject.toLowerCase())
|
|
278
423
|
);
|
|
279
424
|
}
|
|
280
425
|
if (milestoneFilter) {
|
|
426
|
+
const resolvedMilestone = resolveAlias(milestoneFilter);
|
|
281
427
|
filtered = filtered.filter(i =>
|
|
282
|
-
i.projectMilestone?.name?.toLowerCase().includes(
|
|
428
|
+
i.projectMilestone?.name?.toLowerCase().includes(resolvedMilestone.toLowerCase())
|
|
283
429
|
);
|
|
284
430
|
}
|
|
431
|
+
if (priorityFilter) {
|
|
432
|
+
const targetPriority = PRIORITY_MAP[priorityFilter];
|
|
433
|
+
if (targetPriority !== undefined) {
|
|
434
|
+
filtered = filtered.filter(i => i.priority === targetPriority);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
285
437
|
return filtered;
|
|
286
438
|
};
|
|
287
439
|
|
|
@@ -497,6 +649,24 @@ const ESTIMATE_MAP = {
|
|
|
497
649
|
'xl': 5,
|
|
498
650
|
};
|
|
499
651
|
|
|
652
|
+
// Linear priority values (lower number = higher priority)
|
|
653
|
+
// 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low
|
|
654
|
+
const PRIORITY_LABELS = {
|
|
655
|
+
0: '',
|
|
656
|
+
1: 'Urgent',
|
|
657
|
+
2: 'High',
|
|
658
|
+
3: 'Medium',
|
|
659
|
+
4: 'Low',
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const PRIORITY_MAP = {
|
|
663
|
+
'urgent': 1,
|
|
664
|
+
'high': 2,
|
|
665
|
+
'medium': 3,
|
|
666
|
+
'low': 4,
|
|
667
|
+
'none': 0,
|
|
668
|
+
};
|
|
669
|
+
|
|
500
670
|
async function cmdIssueCreate(args) {
|
|
501
671
|
const opts = parseArgs(args, {
|
|
502
672
|
title: 'string', t: 'string',
|
|
@@ -507,6 +677,7 @@ async function cmdIssueCreate(args) {
|
|
|
507
677
|
state: 'string', s: 'string',
|
|
508
678
|
assign: 'boolean',
|
|
509
679
|
estimate: 'string', e: 'string',
|
|
680
|
+
priority: 'string',
|
|
510
681
|
label: 'string', l: 'string',
|
|
511
682
|
blocks: 'string',
|
|
512
683
|
'blocked-by': 'string',
|
|
@@ -514,8 +685,9 @@ async function cmdIssueCreate(args) {
|
|
|
514
685
|
|
|
515
686
|
const title = opts.title || opts.t || opts._[0];
|
|
516
687
|
const description = opts.description || opts.d || '';
|
|
517
|
-
const project = opts.project || opts.p;
|
|
518
|
-
const
|
|
688
|
+
const project = resolveAlias(opts.project || opts.p);
|
|
689
|
+
const priority = (opts.priority || '').toLowerCase();
|
|
690
|
+
const milestone = resolveAlias(opts.milestone);
|
|
519
691
|
const parent = opts.parent;
|
|
520
692
|
const shouldAssign = opts.assign;
|
|
521
693
|
const estimate = (opts.estimate || opts.e || '').toLowerCase();
|
|
@@ -525,7 +697,7 @@ async function cmdIssueCreate(args) {
|
|
|
525
697
|
|
|
526
698
|
if (!title) {
|
|
527
699
|
console.error(colors.red('Error: Title is required'));
|
|
528
|
-
console.error('Usage: linear issue create --title "Issue title" [--project "..."] [--milestone "..."] [--parent ISSUE-X] [--estimate M] [--assign] [--label bug] [--blocks ISSUE-X] [--blocked-by ISSUE-X]');
|
|
700
|
+
console.error('Usage: linear issue create --title "Issue title" [--project "..."] [--milestone "..."] [--parent ISSUE-X] [--estimate M] [--priority urgent] [--assign] [--label bug] [--blocks ISSUE-X] [--blocked-by ISSUE-X]');
|
|
529
701
|
process.exit(1);
|
|
530
702
|
}
|
|
531
703
|
|
|
@@ -535,6 +707,12 @@ async function cmdIssueCreate(args) {
|
|
|
535
707
|
process.exit(1);
|
|
536
708
|
}
|
|
537
709
|
|
|
710
|
+
// Validate priority
|
|
711
|
+
if (priority && !PRIORITY_MAP.hasOwnProperty(priority)) {
|
|
712
|
+
console.error(colors.red(`Error: Invalid priority "${priority}". Use: urgent, high, medium, low, or none`));
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
|
|
538
716
|
// Get team UUID (required for mutations)
|
|
539
717
|
const teamResult = await gql(`{ team(id: "${TEAM_KEY}") { id } }`);
|
|
540
718
|
const teamId = teamResult.data?.team?.id;
|
|
@@ -626,6 +804,7 @@ async function cmdIssueCreate(args) {
|
|
|
626
804
|
if (parent) input.parentId = parent;
|
|
627
805
|
if (assigneeId) input.assigneeId = assigneeId;
|
|
628
806
|
if (estimate) input.estimate = ESTIMATE_MAP[estimate];
|
|
807
|
+
if (priority) input.priority = PRIORITY_MAP[priority];
|
|
629
808
|
if (labelIds.length > 0) input.labelIds = labelIds;
|
|
630
809
|
|
|
631
810
|
const result = await gql(mutation, { input });
|
|
@@ -633,7 +812,8 @@ async function cmdIssueCreate(args) {
|
|
|
633
812
|
if (result.data?.issueCreate?.success) {
|
|
634
813
|
const issue = result.data.issueCreate.issue;
|
|
635
814
|
const estLabel = estimate ? ` [${estimate.toUpperCase()}]` : '';
|
|
636
|
-
|
|
815
|
+
const priLabel = priority && priority !== 'none' ? ` [${priority.charAt(0).toUpperCase() + priority.slice(1)}]` : '';
|
|
816
|
+
console.log(colors.green(`Created: ${issue.identifier}${estLabel}${priLabel}`));
|
|
637
817
|
console.log(issue.url);
|
|
638
818
|
|
|
639
819
|
// Create blocking relations if specified
|
|
@@ -680,19 +860,32 @@ async function cmdIssueUpdate(args) {
|
|
|
680
860
|
state: 'string', s: 'string',
|
|
681
861
|
project: 'string', p: 'string',
|
|
682
862
|
milestone: 'string',
|
|
863
|
+
priority: 'string',
|
|
683
864
|
append: 'string', a: 'string',
|
|
865
|
+
check: 'string',
|
|
866
|
+
uncheck: 'string',
|
|
684
867
|
blocks: 'string',
|
|
685
868
|
'blocked-by': 'string',
|
|
686
869
|
});
|
|
687
870
|
|
|
688
871
|
const blocksIssue = opts.blocks;
|
|
689
872
|
const blockedByIssue = opts['blocked-by'];
|
|
690
|
-
const projectName = opts.project || opts.p;
|
|
691
|
-
const milestoneName = opts.milestone;
|
|
873
|
+
const projectName = resolveAlias(opts.project || opts.p);
|
|
874
|
+
const milestoneName = resolveAlias(opts.milestone);
|
|
875
|
+
const priorityName = (opts.priority || '').toLowerCase();
|
|
692
876
|
const input = {};
|
|
693
877
|
|
|
694
878
|
if (opts.title || opts.t) input.title = opts.title || opts.t;
|
|
695
879
|
|
|
880
|
+
// Handle priority
|
|
881
|
+
if (priorityName) {
|
|
882
|
+
if (!PRIORITY_MAP.hasOwnProperty(priorityName)) {
|
|
883
|
+
console.error(colors.red(`Error: Invalid priority "${priorityName}". Use: urgent, high, medium, low, or none`));
|
|
884
|
+
process.exit(1);
|
|
885
|
+
}
|
|
886
|
+
input.priority = PRIORITY_MAP[priorityName];
|
|
887
|
+
}
|
|
888
|
+
|
|
696
889
|
// Handle append
|
|
697
890
|
if (opts.append || opts.a) {
|
|
698
891
|
const currentResult = await gql(`{ issue(id: "${issueId}") { description } }`);
|
|
@@ -702,6 +895,70 @@ async function cmdIssueUpdate(args) {
|
|
|
702
895
|
input.description = opts.description || opts.d;
|
|
703
896
|
}
|
|
704
897
|
|
|
898
|
+
// Handle check/uncheck
|
|
899
|
+
const checkText = opts.check;
|
|
900
|
+
const uncheckText = opts.uncheck;
|
|
901
|
+
if (checkText || uncheckText) {
|
|
902
|
+
const isCheck = !!checkText;
|
|
903
|
+
const query = checkText || uncheckText;
|
|
904
|
+
const fromPattern = isCheck ? /- \[ \] / : /- \[x\] /i;
|
|
905
|
+
const toMark = isCheck ? '- [x] ' : '- [ ] ';
|
|
906
|
+
const verb = isCheck ? 'Checked' : 'Unchecked';
|
|
907
|
+
|
|
908
|
+
// Fetch current description if we haven't already
|
|
909
|
+
let desc = input.description;
|
|
910
|
+
if (!desc) {
|
|
911
|
+
const currentResult = await gql(`{ issue(id: "${issueId}") { description } }`);
|
|
912
|
+
desc = currentResult.data?.issue?.description || '';
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const lines = desc.split('\n');
|
|
916
|
+
const checkboxLines = lines
|
|
917
|
+
.map((line, i) => ({ line, index: i }))
|
|
918
|
+
.filter(({ line }) => fromPattern.test(line));
|
|
919
|
+
|
|
920
|
+
if (checkboxLines.length === 0) {
|
|
921
|
+
console.error(colors.red(`Error: No ${isCheck ? 'unchecked' : 'checked'} items found in description`));
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Find best match: score each checkbox line by similarity to query
|
|
926
|
+
const queryLower = query.toLowerCase();
|
|
927
|
+
let bestMatch = null;
|
|
928
|
+
let bestScore = 0;
|
|
929
|
+
|
|
930
|
+
for (const { line, index } of checkboxLines) {
|
|
931
|
+
const text = line.replace(/- \[[ x]\] /i, '').toLowerCase();
|
|
932
|
+
// Exact match
|
|
933
|
+
if (text === queryLower) { bestMatch = { line, index }; bestScore = Infinity; break; }
|
|
934
|
+
// Substring match
|
|
935
|
+
if (text.includes(queryLower) || queryLower.includes(text)) {
|
|
936
|
+
const score = queryLower.length / Math.max(text.length, queryLower.length);
|
|
937
|
+
if (score > bestScore) { bestScore = score; bestMatch = { line, index }; }
|
|
938
|
+
} else {
|
|
939
|
+
// Word overlap scoring
|
|
940
|
+
const queryWords = queryLower.split(/\s+/);
|
|
941
|
+
const textWords = text.split(/\s+/);
|
|
942
|
+
const overlap = queryWords.filter(w => textWords.some(tw => tw.includes(w) || w.includes(tw))).length;
|
|
943
|
+
const score = overlap / Math.max(queryWords.length, textWords.length);
|
|
944
|
+
if (score > bestScore) { bestScore = score; bestMatch = { line, index }; }
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (!bestMatch || bestScore < 0.3) {
|
|
949
|
+
console.error(colors.red(`Error: No checkbox matching "${query}"`));
|
|
950
|
+
console.error('Available items:');
|
|
951
|
+
checkboxLines.forEach(({ line }) => console.error(' ' + line.trim()));
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
lines[bestMatch.index] = bestMatch.line.replace(fromPattern, toMark);
|
|
956
|
+
input.description = lines.join('\n');
|
|
957
|
+
|
|
958
|
+
const itemText = bestMatch.line.replace(/- \[[ x]\] /i, '').trim();
|
|
959
|
+
console.log(colors.green(`${verb}: ${itemText}`));
|
|
960
|
+
}
|
|
961
|
+
|
|
705
962
|
// Handle state
|
|
706
963
|
if (opts.state || opts.s) {
|
|
707
964
|
const stateName = opts.state || opts.s;
|
|
@@ -953,21 +1210,38 @@ async function cmdProjects(args) {
|
|
|
953
1210
|
projects = projects.filter(p => !['completed', 'canceled'].includes(p.state));
|
|
954
1211
|
}
|
|
955
1212
|
|
|
1213
|
+
// Find alias for a project (name must start with alias target)
|
|
1214
|
+
const findAliasFor = (name) => {
|
|
1215
|
+
const lowerName = name.toLowerCase();
|
|
1216
|
+
let bestMatch = null;
|
|
1217
|
+
let bestLength = 0;
|
|
1218
|
+
for (const [code, aliasName] of Object.entries(ALIASES)) {
|
|
1219
|
+
const lowerAlias = aliasName.toLowerCase();
|
|
1220
|
+
// Name must start with the alias target, and prefer longer matches
|
|
1221
|
+
if (lowerName.startsWith(lowerAlias) && lowerAlias.length > bestLength) {
|
|
1222
|
+
bestMatch = code;
|
|
1223
|
+
bestLength = lowerAlias.length;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return bestMatch;
|
|
1227
|
+
};
|
|
1228
|
+
|
|
956
1229
|
console.log(colors.bold('Projects:\n'));
|
|
957
|
-
const rows = projects.map(p =>
|
|
958
|
-
p.name
|
|
959
|
-
p.
|
|
960
|
-
`${Math.floor(p.progress * 100)}%`
|
|
961
|
-
|
|
1230
|
+
const rows = projects.map(p => {
|
|
1231
|
+
const alias = findAliasFor(p.name);
|
|
1232
|
+
const nameCol = alias ? `${colors.bold(`[${alias}]`)} ${p.name}` : p.name;
|
|
1233
|
+
return [nameCol, p.state, `${Math.floor(p.progress * 100)}%`];
|
|
1234
|
+
});
|
|
962
1235
|
console.log(formatTable(rows));
|
|
963
1236
|
}
|
|
964
1237
|
|
|
965
1238
|
async function cmdProjectShow(args) {
|
|
966
|
-
const
|
|
967
|
-
if (!
|
|
1239
|
+
const projectNameArg = args[0];
|
|
1240
|
+
if (!projectNameArg) {
|
|
968
1241
|
console.error(colors.red('Error: Project name required'));
|
|
969
1242
|
process.exit(1);
|
|
970
1243
|
}
|
|
1244
|
+
const projectName = resolveAlias(projectNameArg);
|
|
971
1245
|
|
|
972
1246
|
const query = `{
|
|
973
1247
|
team(id: "${TEAM_KEY}") {
|
|
@@ -982,7 +1256,7 @@ async function cmdProjectShow(args) {
|
|
|
982
1256
|
|
|
983
1257
|
const result = await gql(query);
|
|
984
1258
|
const projects = result.data?.team?.projects?.nodes || [];
|
|
985
|
-
const project = projects.find(p => p.name.includes(projectName));
|
|
1259
|
+
const project = projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
|
|
986
1260
|
|
|
987
1261
|
if (!project) {
|
|
988
1262
|
console.error(colors.red(`Project not found: ${projectName}`));
|
|
@@ -1129,34 +1403,58 @@ async function cmdMilestones(args) {
|
|
|
1129
1403
|
projects = projects.filter(p => !['completed', 'canceled'].includes(p.state));
|
|
1130
1404
|
}
|
|
1131
1405
|
|
|
1132
|
-
// Filter by project name if specified
|
|
1406
|
+
// Filter by project name if specified (resolve alias first)
|
|
1133
1407
|
if (projectFilter) {
|
|
1134
|
-
|
|
1408
|
+
const resolvedFilter = resolveAlias(projectFilter);
|
|
1409
|
+
projects = projects.filter(p => p.name.toLowerCase().includes(resolvedFilter.toLowerCase()));
|
|
1135
1410
|
}
|
|
1136
1411
|
|
|
1412
|
+
// Find alias for a name (name must start with alias target)
|
|
1413
|
+
const findAliasFor = (name) => {
|
|
1414
|
+
const lowerName = name.toLowerCase();
|
|
1415
|
+
let bestMatch = null;
|
|
1416
|
+
let bestLength = 0;
|
|
1417
|
+
for (const [code, aliasName] of Object.entries(ALIASES)) {
|
|
1418
|
+
const lowerAlias = aliasName.toLowerCase();
|
|
1419
|
+
// Name must start with the alias target, and prefer longer matches
|
|
1420
|
+
if (lowerName.startsWith(lowerAlias) && lowerAlias.length > bestLength) {
|
|
1421
|
+
bestMatch = code;
|
|
1422
|
+
bestLength = lowerAlias.length;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
return bestMatch;
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1137
1428
|
console.log(colors.bold('Milestones:\n'));
|
|
1138
1429
|
for (const project of projects) {
|
|
1139
1430
|
const milestones = project.projectMilestones?.nodes || [];
|
|
1140
1431
|
if (milestones.length === 0) continue;
|
|
1141
1432
|
|
|
1142
|
-
|
|
1433
|
+
const projectAlias = findAliasFor(project.name);
|
|
1434
|
+
const projectHeader = projectAlias
|
|
1435
|
+
? `${colors.bold(`[${projectAlias}]`)} ${colors.bold(project.name)}`
|
|
1436
|
+
: colors.bold(project.name);
|
|
1437
|
+
console.log(projectHeader);
|
|
1143
1438
|
for (const m of milestones) {
|
|
1439
|
+
const milestoneAlias = findAliasFor(m.name);
|
|
1440
|
+
const namePrefix = milestoneAlias ? `${colors.bold(`[${milestoneAlias}]`)} ` : '';
|
|
1144
1441
|
const date = m.targetDate ? ` (${m.targetDate})` : '';
|
|
1145
1442
|
const status = m.status !== 'planned' ? ` [${m.status}]` : '';
|
|
1146
|
-
console.log(` ${m.name}${date}${status}`);
|
|
1443
|
+
console.log(` ${namePrefix}${m.name}${date}${status}`);
|
|
1147
1444
|
}
|
|
1148
1445
|
console.log('');
|
|
1149
1446
|
}
|
|
1150
1447
|
}
|
|
1151
1448
|
|
|
1152
1449
|
async function cmdMilestoneShow(args) {
|
|
1153
|
-
const
|
|
1154
|
-
if (!
|
|
1450
|
+
const milestoneNameArg = args[0];
|
|
1451
|
+
if (!milestoneNameArg) {
|
|
1155
1452
|
console.error(colors.red('Error: Milestone name required'));
|
|
1156
1453
|
process.exit(1);
|
|
1157
1454
|
}
|
|
1455
|
+
const milestoneName = resolveAlias(milestoneNameArg);
|
|
1158
1456
|
|
|
1159
|
-
const
|
|
1457
|
+
const projectsQuery = `{
|
|
1160
1458
|
team(id: "${TEAM_KEY}") {
|
|
1161
1459
|
projects(first: 50) {
|
|
1162
1460
|
nodes {
|
|
@@ -1164,7 +1462,6 @@ async function cmdMilestoneShow(args) {
|
|
|
1164
1462
|
projectMilestones {
|
|
1165
1463
|
nodes {
|
|
1166
1464
|
id name description targetDate status sortOrder
|
|
1167
|
-
issues { nodes { identifier title state { name type } } }
|
|
1168
1465
|
}
|
|
1169
1466
|
}
|
|
1170
1467
|
}
|
|
@@ -1172,8 +1469,23 @@ async function cmdMilestoneShow(args) {
|
|
|
1172
1469
|
}
|
|
1173
1470
|
}`;
|
|
1174
1471
|
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1472
|
+
const issuesQuery = `{
|
|
1473
|
+
team(id: "${TEAM_KEY}") {
|
|
1474
|
+
issues(first: 200) {
|
|
1475
|
+
nodes {
|
|
1476
|
+
identifier title state { name type }
|
|
1477
|
+
projectMilestone { id }
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}`;
|
|
1482
|
+
|
|
1483
|
+
const [projectsResult, issuesResult] = await Promise.all([
|
|
1484
|
+
gql(projectsQuery),
|
|
1485
|
+
gql(issuesQuery)
|
|
1486
|
+
]);
|
|
1487
|
+
const projects = projectsResult.data?.team?.projects?.nodes || [];
|
|
1488
|
+
const allIssues = issuesResult.data?.team?.issues?.nodes || [];
|
|
1177
1489
|
|
|
1178
1490
|
let milestone = null;
|
|
1179
1491
|
let projectName = '';
|
|
@@ -1199,7 +1511,7 @@ async function cmdMilestoneShow(args) {
|
|
|
1199
1511
|
if (milestone.targetDate) console.log(`Target: ${milestone.targetDate}`);
|
|
1200
1512
|
if (milestone.description) console.log(`\n## Description\n${milestone.description}`);
|
|
1201
1513
|
|
|
1202
|
-
const issues =
|
|
1514
|
+
const issues = allIssues.filter(i => i.projectMilestone?.id === milestone.id);
|
|
1203
1515
|
if (issues.length > 0) {
|
|
1204
1516
|
// Group by state type
|
|
1205
1517
|
const done = issues.filter(i => i.state.type === 'completed');
|
|
@@ -1910,6 +2222,77 @@ async function cmdLabelCreate(args) {
|
|
|
1910
2222
|
}
|
|
1911
2223
|
}
|
|
1912
2224
|
|
|
2225
|
+
// ============================================================================
|
|
2226
|
+
// ALIASES
|
|
2227
|
+
// ============================================================================
|
|
2228
|
+
|
|
2229
|
+
async function cmdAlias(args) {
|
|
2230
|
+
const opts = parseArgs(args, {
|
|
2231
|
+
list: 'boolean', l: 'boolean',
|
|
2232
|
+
remove: 'string', r: 'string',
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
const showList = opts.list || opts.l;
|
|
2236
|
+
const removeCode = opts.remove || opts.r;
|
|
2237
|
+
const code = opts._[0];
|
|
2238
|
+
const name = opts._[1];
|
|
2239
|
+
|
|
2240
|
+
// List aliases
|
|
2241
|
+
if (showList || (Object.keys(opts).length === 1 && opts._.length === 0)) {
|
|
2242
|
+
const aliases = Object.entries(ALIASES);
|
|
2243
|
+
if (aliases.length === 0) {
|
|
2244
|
+
console.log('No aliases defined.');
|
|
2245
|
+
console.log('Usage: linear alias CODE "Project or Milestone Name"');
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// Fetch projects to determine type (project vs milestone)
|
|
2250
|
+
const query = `{
|
|
2251
|
+
team(id: "${TEAM_KEY}") {
|
|
2252
|
+
projects(first: 50) {
|
|
2253
|
+
nodes { name }
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}`;
|
|
2257
|
+
|
|
2258
|
+
const result = await gql(query);
|
|
2259
|
+
const projects = result.data?.team?.projects?.nodes || [];
|
|
2260
|
+
|
|
2261
|
+
// Check if alias target matches a project (using partial match)
|
|
2262
|
+
const matchesProject = (target) => {
|
|
2263
|
+
const lowerTarget = target.toLowerCase();
|
|
2264
|
+
return projects.some(p => p.name.toLowerCase().includes(lowerTarget));
|
|
2265
|
+
};
|
|
2266
|
+
|
|
2267
|
+
console.log(colors.bold('Aliases:\n'));
|
|
2268
|
+
for (const [code, target] of aliases) {
|
|
2269
|
+
const isProject = matchesProject(target);
|
|
2270
|
+
const type = isProject ? colors.blue('project') : colors.yellow('milestone');
|
|
2271
|
+
console.log(` ${colors.bold(code)} -> ${target} (${type})`);
|
|
2272
|
+
}
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// Remove alias
|
|
2277
|
+
if (removeCode) {
|
|
2278
|
+
removeAlias(removeCode);
|
|
2279
|
+
console.log(colors.green(`Removed alias: ${removeCode.toUpperCase()}`));
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// Create/update alias
|
|
2284
|
+
if (!code || !name) {
|
|
2285
|
+
console.error(colors.red('Error: Code and name required'));
|
|
2286
|
+
console.error('Usage: linear alias CODE "Project or Milestone Name"');
|
|
2287
|
+
console.error(' linear alias --list');
|
|
2288
|
+
console.error(' linear alias --remove CODE');
|
|
2289
|
+
process.exit(1);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
saveAlias(code, name);
|
|
2293
|
+
console.log(colors.green(`Alias set: ${code.toUpperCase()} -> ${name}`));
|
|
2294
|
+
}
|
|
2295
|
+
|
|
1913
2296
|
// ============================================================================
|
|
1914
2297
|
// GIT INTEGRATION
|
|
1915
2298
|
// ============================================================================
|
|
@@ -2748,6 +3131,7 @@ ISSUES:
|
|
|
2748
3131
|
--milestone <name> Filter by milestone
|
|
2749
3132
|
--state, -s <state> Filter by state
|
|
2750
3133
|
--label, -l <name> Filter by label
|
|
3134
|
+
--priority <level> Filter by priority (urgent/high/medium/low/none)
|
|
2751
3135
|
issues reorder <ids...> Reorder issues by listing IDs in order
|
|
2752
3136
|
|
|
2753
3137
|
issue show <id> Show issue details with parent context
|
|
@@ -2760,6 +3144,7 @@ ISSUES:
|
|
|
2760
3144
|
--parent <id> Parent issue (for sub-issues)
|
|
2761
3145
|
--assign Assign to yourself
|
|
2762
3146
|
--estimate, -e <size> Estimate: XS, S, M, L, XL
|
|
3147
|
+
--priority <level> Priority: urgent, high, medium, low, none
|
|
2763
3148
|
--label, -l <name> Add label
|
|
2764
3149
|
--blocks <id> This issue blocks another
|
|
2765
3150
|
--blocked-by <id> This issue is blocked by another
|
|
@@ -2769,7 +3154,10 @@ ISSUES:
|
|
|
2769
3154
|
--state, -s <state> New state
|
|
2770
3155
|
--project, -p <name> Move to project
|
|
2771
3156
|
--milestone <name> Move to milestone
|
|
3157
|
+
--priority <level> Set priority (urgent/high/medium/low/none)
|
|
2772
3158
|
--append, -a <text> Append to description
|
|
3159
|
+
--check <text> Check a checkbox item (fuzzy match)
|
|
3160
|
+
--uncheck <text> Uncheck a checkbox item (fuzzy match)
|
|
2773
3161
|
--blocks <id> Add blocking relation
|
|
2774
3162
|
--blocked-by <id> Add blocked-by relation
|
|
2775
3163
|
issue close <id> Mark issue as done
|
|
@@ -2816,6 +3204,15 @@ LABELS:
|
|
|
2816
3204
|
--description, -d <desc> Label description
|
|
2817
3205
|
--color, -c <hex> Label color (e.g., #FF0000)
|
|
2818
3206
|
|
|
3207
|
+
ALIASES:
|
|
3208
|
+
alias <CODE> "<name>" Create alias for project/milestone
|
|
3209
|
+
alias --list List all aliases
|
|
3210
|
+
alias --remove <CODE> Remove an alias
|
|
3211
|
+
|
|
3212
|
+
Aliases can be used anywhere a project or milestone name is accepted:
|
|
3213
|
+
linear issues --project LWW
|
|
3214
|
+
linear issue create --milestone MVP "New feature"
|
|
3215
|
+
|
|
2819
3216
|
GIT:
|
|
2820
3217
|
branch <id> Create git branch from issue (ISSUE-5-issue-title)
|
|
2821
3218
|
|
|
@@ -2838,6 +3235,10 @@ CONFIGURATION:
|
|
|
2838
3235
|
api_key=lin_api_xxx
|
|
2839
3236
|
team=ISSUE
|
|
2840
3237
|
|
|
3238
|
+
[aliases]
|
|
3239
|
+
LWW=Last-Write-Wins Support
|
|
3240
|
+
MVP=MVP Release
|
|
3241
|
+
|
|
2841
3242
|
EXAMPLES:
|
|
2842
3243
|
linear roadmap # See all projects and milestones
|
|
2843
3244
|
linear issues --unblocked # Find workable issues
|
|
@@ -2971,6 +3372,10 @@ async function main() {
|
|
|
2971
3372
|
}
|
|
2972
3373
|
break;
|
|
2973
3374
|
}
|
|
3375
|
+
case 'alias':
|
|
3376
|
+
checkAuth();
|
|
3377
|
+
await cmdAlias(args.slice(1));
|
|
3378
|
+
break;
|
|
2974
3379
|
case 'branch':
|
|
2975
3380
|
checkAuth();
|
|
2976
3381
|
await cmdBranch(args.slice(1));
|
|
@@ -31,6 +31,45 @@ Config is loaded in order: `./.linear` → `~/.linear` → env vars
|
|
|
31
31
|
# .linear file format
|
|
32
32
|
api_key=lin_api_xxx
|
|
33
33
|
team=ISSUE
|
|
34
|
+
|
|
35
|
+
[aliases]
|
|
36
|
+
V2=Version 2.0 Release
|
|
37
|
+
MVP=MVP Milestone
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Aliases
|
|
41
|
+
|
|
42
|
+
Create short codes for projects and milestones to use in commands:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Create aliases
|
|
46
|
+
linear alias V2 "Version 2.0" # For a project
|
|
47
|
+
linear alias MVP "MVP Milestone" # For a milestone
|
|
48
|
+
|
|
49
|
+
# List all aliases
|
|
50
|
+
linear alias --list
|
|
51
|
+
|
|
52
|
+
# Remove an alias
|
|
53
|
+
linear alias --remove MVP
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use aliases anywhere a project or milestone name is accepted:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
linear issues --project V2
|
|
60
|
+
linear issues --milestone MVP
|
|
61
|
+
linear issue create --project V2 --milestone MVP "New feature"
|
|
62
|
+
linear milestones --project V2
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Aliases are shown in `linear projects` and `linear milestones` output:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Projects:
|
|
69
|
+
[V2] Version 2.0 Release started 58%
|
|
70
|
+
|
|
71
|
+
Milestones:
|
|
72
|
+
[MVP] MVP Milestone [next]
|
|
34
73
|
```
|
|
35
74
|
|
|
36
75
|
## Quick Reference
|
|
@@ -53,15 +92,19 @@ linear issues --mine # Only your assigned issues
|
|
|
53
92
|
linear issues --project "Name" # Issues in a project
|
|
54
93
|
linear issues --milestone "M1" # Issues in a milestone
|
|
55
94
|
linear issues --label bug # Filter by label
|
|
95
|
+
linear issues --priority urgent # Filter by priority (urgent/high/medium/low/none)
|
|
56
96
|
# Flags can be combined: linear issues --in-progress --mine
|
|
57
97
|
linear issue show ISSUE-1 # Full details with parent context
|
|
58
98
|
linear issue start ISSUE-1 # Assign to you + set In Progress
|
|
59
99
|
linear issue create --title "Fix bug" --project "Phase 1" --assign --estimate M
|
|
100
|
+
linear issue create --title "Urgent bug" --priority urgent --assign
|
|
60
101
|
linear issue create --title "Task" --milestone "Beta" --estimate S
|
|
61
102
|
linear issue create --title "Blocked task" --blocked-by ISSUE-1
|
|
62
103
|
linear issue update ISSUE-1 --state "In Progress"
|
|
104
|
+
linear issue update ISSUE-1 --priority high # Set priority
|
|
63
105
|
linear issue update ISSUE-1 --milestone "Beta"
|
|
64
106
|
linear issue update ISSUE-1 --append "Notes..."
|
|
107
|
+
linear issue update ISSUE-1 --check "validation" # Check off a todo item
|
|
65
108
|
linear issue update ISSUE-1 --blocks ISSUE-2 # Add blocking relation
|
|
66
109
|
linear issue close ISSUE-1
|
|
67
110
|
linear issue comment ISSUE-1 "Comment text"
|
|
@@ -90,6 +133,12 @@ linear issue move ISSUE-5 --before ISSUE-1 # Move single issue
|
|
|
90
133
|
linear labels # List all labels
|
|
91
134
|
linear label create "bug" --color "#FF0000"
|
|
92
135
|
|
|
136
|
+
# Aliases
|
|
137
|
+
linear alias V2 "Version 2.0" # Create alias for project/milestone
|
|
138
|
+
linear alias --list # List all aliases
|
|
139
|
+
linear alias --remove V2 # Remove alias
|
|
140
|
+
# Then use: linear issues --project V2
|
|
141
|
+
|
|
93
142
|
# Git
|
|
94
143
|
linear branch ISSUE-1 # Create branch: ISSUE-1-issue-title
|
|
95
144
|
```
|
|
@@ -174,6 +223,26 @@ linear issue create --title "Step 3: Add tests" --parent ISSUE-5 --estimate S
|
|
|
174
223
|
linear issue start ISSUE-6
|
|
175
224
|
```
|
|
176
225
|
|
|
226
|
+
### Checklists vs. sub-issues
|
|
227
|
+
Use description checklists for lightweight steps within a single issue. Use sub-issues when items need their own status, assignee, or estimate.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Checklist — quick implementation steps, a punch list, acceptance criteria
|
|
231
|
+
linear issue update ISSUE-5 --append "## TODO\n- [ ] Add validation\n- [ ] Update tests\n- [ ] Check edge cases"
|
|
232
|
+
|
|
233
|
+
# Check off completed items (fuzzy matches the item text)
|
|
234
|
+
linear issue update ISSUE-5 --check "validation"
|
|
235
|
+
linear issue update ISSUE-5 --check "tests"
|
|
236
|
+
|
|
237
|
+
# Uncheck if needed
|
|
238
|
+
linear issue update ISSUE-5 --uncheck "validation"
|
|
239
|
+
|
|
240
|
+
# Sub-issues — substantial, independently trackable work
|
|
241
|
+
linear issue create --title "Add login endpoint" --parent ISSUE-5 --estimate S
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Prefer checklists when the items are small and don't need independent tracking. Prefer sub-issues when you'd want to assign, estimate, or block on them individually. Use `--check` to mark items complete as you finish them.
|
|
245
|
+
|
|
177
246
|
### Completing work
|
|
178
247
|
After finishing implementation, ask the developer if they want to close the issue:
|
|
179
248
|
|