@dabble/linear-cli 1.0.5 → 1.1.1

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 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
- if (!line || line.startsWith('#')) continue;
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
- if (key === 'api_key') LINEAR_API_KEY = value;
51
- if (key === 'team') TEAM_KEY = value;
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
- colWidths[i] = Math.max(colWidths[i] || 0, String(cell).length);
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) => String(cell).padEnd(colWidths[i])).join(' ')
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
 
@@ -155,6 +288,14 @@ function parseArgs(args, flags = {}) {
155
288
  const flagDef = flags[key];
156
289
  if (flagDef === 'boolean') {
157
290
  result[key] = true;
291
+ } else if (flagDef === 'array') {
292
+ const value = args[++i];
293
+ if (value === undefined || value.startsWith('-')) {
294
+ console.error(colors.red(`Error: --${key} requires a value`));
295
+ process.exit(1);
296
+ }
297
+ result[key] = result[key] || [];
298
+ result[key].push(value);
158
299
  } else {
159
300
  const value = args[++i];
160
301
  if (value === undefined || value.startsWith('-')) {
@@ -180,25 +321,41 @@ async function cmdIssues(args) {
180
321
  unblocked: 'boolean', u: 'boolean',
181
322
  all: 'boolean', a: 'boolean',
182
323
  open: 'boolean', o: 'boolean',
183
- backlog: 'boolean', b: 'boolean',
184
324
  mine: 'boolean', m: 'boolean',
185
- 'in-progress': 'boolean',
325
+ status: 'array', s: 'array',
186
326
  project: 'string', p: 'string',
187
327
  milestone: 'string',
188
- state: 'string', s: 'string',
189
- label: 'string', l: 'string',
328
+ label: 'array', l: 'array',
329
+ priority: 'string',
190
330
  });
191
331
 
192
- const inProgress = opts['in-progress'];
193
332
  const unblocked = opts.unblocked || opts.u;
194
333
  const allStates = opts.all || opts.a;
195
334
  const openOnly = opts.open || opts.o;
196
- const backlogOnly = opts.backlog || opts.b;
197
335
  const mineOnly = opts.mine || opts.m;
336
+ const statusFilter = opts.status || opts.s || [];
198
337
  const projectFilter = opts.project || opts.p;
199
338
  const milestoneFilter = opts.milestone;
200
- const stateFilter = opts.state || opts.s;
201
- const labelFilter = opts.label || opts.l;
339
+ const labelFilters = opts.label || opts.l || [];
340
+ const priorityFilter = (opts.priority || '').toLowerCase();
341
+
342
+ // Map user-friendly status names to Linear's internal state types
343
+ const STATUS_TYPE_MAP = {
344
+ 'backlog': 'backlog',
345
+ 'todo': 'unstarted',
346
+ 'in-progress': 'started',
347
+ 'inprogress': 'started',
348
+ 'in_progress': 'started',
349
+ 'started': 'started',
350
+ 'done': 'completed',
351
+ 'completed': 'completed',
352
+ 'canceled': 'canceled',
353
+ 'cancelled': 'canceled',
354
+ 'triage': 'triage',
355
+ };
356
+
357
+ // Resolve status filters to state types (match by type map or by state name)
358
+ const resolvedStatusTypes = statusFilter.map(s => STATUS_TYPE_MAP[s.toLowerCase()] || s.toLowerCase());
202
359
 
203
360
  // Get current user ID for filtering/sorting
204
361
  const viewerResult = await gql('{ viewer { id } }');
@@ -234,26 +391,35 @@ async function cmdIssues(args) {
234
391
  // Check if any issues have assignees (to decide whether to show column)
235
392
  const hasAssignees = issues.some(i => i.assignee);
236
393
 
237
- // Sort: assigned to you first, then by priority, then by sortOrder
394
+ // Sort: assigned to you first, then by priority (urgent first), then by sortOrder
238
395
  issues.sort((a, b) => {
239
396
  const aIsMine = a.assignee?.id === viewerId;
240
397
  const bIsMine = b.assignee?.id === viewerId;
241
398
  if (aIsMine && !bIsMine) return -1;
242
399
  if (!aIsMine && bIsMine) return 1;
243
- // Then by priority (higher = more urgent)
244
- if ((b.priority || 0) !== (a.priority || 0)) return (b.priority || 0) - (a.priority || 0);
400
+ // Then by priority (lower number = more urgent, but 0 means no priority so sort last)
401
+ const aPri = a.priority || 5; // No priority (0) sorts after Low (4)
402
+ const bPri = b.priority || 5;
403
+ if (aPri !== bPri) return aPri - bPri;
245
404
  // Then by sortOrder
246
405
  return (b.sortOrder || 0) - (a.sortOrder || 0);
247
406
  });
248
407
 
408
+ // Check if any issues have priority set
409
+ const hasPriority = issues.some(i => i.priority > 0);
410
+
249
411
  // Helper to format issue row
250
412
  const formatRow = (i) => {
251
413
  const row = [
252
414
  i.identifier,
253
415
  i.title,
254
416
  i.state.name,
255
- i.project?.name || '-'
256
417
  ];
418
+ if (hasPriority) {
419
+ const pri = PRIORITY_LABELS[i.priority] || '';
420
+ row.push(pri ? colors.bold(pri) : '-');
421
+ }
422
+ row.push(i.project?.name || '-');
257
423
  if (hasAssignees) {
258
424
  const assignee = i.assignee?.id === viewerId ? 'you' : (i.assignee?.name || '-');
259
425
  row.push(assignee);
@@ -267,24 +433,39 @@ async function cmdIssues(args) {
267
433
  if (mineOnly) {
268
434
  filtered = filtered.filter(i => i.assignee?.id === viewerId);
269
435
  }
270
- if (labelFilter) {
436
+ if (labelFilters.length > 0) {
271
437
  filtered = filtered.filter(i =>
272
- i.labels?.nodes?.some(l => l.name.toLowerCase() === labelFilter.toLowerCase())
438
+ labelFilters.some(lf => i.labels?.nodes?.some(l => l.name.toLowerCase() === lf.toLowerCase()))
273
439
  );
274
440
  }
275
441
  if (projectFilter) {
442
+ const resolvedProject = resolveAlias(projectFilter);
276
443
  filtered = filtered.filter(i =>
277
- i.project?.name?.toLowerCase().includes(projectFilter.toLowerCase())
444
+ i.project?.name?.toLowerCase().includes(resolvedProject.toLowerCase())
278
445
  );
279
446
  }
280
447
  if (milestoneFilter) {
448
+ const resolvedMilestone = resolveAlias(milestoneFilter);
281
449
  filtered = filtered.filter(i =>
282
- i.projectMilestone?.name?.toLowerCase().includes(milestoneFilter.toLowerCase())
450
+ i.projectMilestone?.name?.toLowerCase().includes(resolvedMilestone.toLowerCase())
283
451
  );
284
452
  }
453
+ if (priorityFilter) {
454
+ const targetPriority = PRIORITY_MAP[priorityFilter];
455
+ if (targetPriority !== undefined) {
456
+ filtered = filtered.filter(i => i.priority === targetPriority);
457
+ }
458
+ }
285
459
  return filtered;
286
460
  };
287
461
 
462
+ // Apply status filter to issues
463
+ const filterByStatus = (list, types) => {
464
+ return list.filter(i =>
465
+ types.includes(i.state.type) || types.includes(i.state.name.toLowerCase())
466
+ );
467
+ };
468
+
288
469
  if (unblocked) {
289
470
  // Collect all blocked issue IDs
290
471
  const blocked = new Set();
@@ -301,48 +482,48 @@ async function cmdIssues(args) {
301
482
  !['completed', 'canceled'].includes(i.state.type) &&
302
483
  !blocked.has(i.identifier)
303
484
  );
485
+ if (resolvedStatusTypes.length > 0) {
486
+ filtered = filterByStatus(filtered, resolvedStatusTypes);
487
+ }
304
488
 
305
489
  filtered = applyFilters(filtered);
306
490
 
307
491
  console.log(colors.bold('Unblocked Issues:\n'));
308
492
  console.log(formatTable(filtered.map(formatRow)));
309
493
  } else if (allStates) {
310
- let filtered = applyFilters(issues);
494
+ let filtered = issues;
495
+ if (resolvedStatusTypes.length > 0) {
496
+ filtered = filterByStatus(filtered, resolvedStatusTypes);
497
+ }
498
+ filtered = applyFilters(filtered);
311
499
 
312
500
  console.log(colors.bold('All Issues:\n'));
313
501
  console.log(formatTable(filtered.map(formatRow)));
314
502
  } else if (openOnly) {
315
- // Open = everything except completed/canceled
316
503
  let filtered = issues.filter(i =>
317
504
  !['completed', 'canceled'].includes(i.state.type)
318
505
  );
506
+ if (resolvedStatusTypes.length > 0) {
507
+ filtered = filterByStatus(filtered, resolvedStatusTypes);
508
+ }
319
509
 
320
510
  filtered = applyFilters(filtered);
321
511
 
322
512
  console.log(colors.bold('Open Issues:\n'));
323
513
  console.log(formatTable(filtered.map(formatRow)));
324
- } else if (inProgress) {
325
- let filtered = issues.filter(i => i.state.type === 'started');
326
- filtered = applyFilters(filtered);
327
-
328
- console.log(colors.bold('In Progress:\n'));
329
- console.log(formatTable(filtered.map(formatRow)));
330
- } else if (backlogOnly || stateFilter) {
331
- const targetState = stateFilter || 'backlog';
332
- let filtered = issues.filter(i =>
333
- i.state.type === targetState || i.state.name.toLowerCase() === targetState.toLowerCase()
334
- );
335
-
514
+ } else if (resolvedStatusTypes.length > 0) {
515
+ let filtered = filterByStatus(issues, resolvedStatusTypes);
336
516
  filtered = applyFilters(filtered);
337
517
 
338
- console.log(colors.bold(`Issues (${targetState}):\n`));
518
+ const label = statusFilter.join(' + ');
519
+ console.log(colors.bold(`Issues (${label}):\n`));
339
520
  console.log(formatTable(filtered.map(formatRow)));
340
521
  } else {
341
- // Default: show backlog
342
- let filtered = issues.filter(i => i.state.type === 'backlog');
522
+ // Default: show backlog + todo
523
+ let filtered = issues.filter(i => i.state.type === 'backlog' || i.state.type === 'unstarted');
343
524
  filtered = applyFilters(filtered);
344
525
 
345
- console.log(colors.bold('Issues (backlog):\n'));
526
+ console.log(colors.bold('Issues (backlog + todo):\n'));
346
527
  console.log(formatTable(filtered.map(formatRow)));
347
528
  }
348
529
  }
@@ -497,6 +678,24 @@ const ESTIMATE_MAP = {
497
678
  'xl': 5,
498
679
  };
499
680
 
681
+ // Linear priority values (lower number = higher priority)
682
+ // 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low
683
+ const PRIORITY_LABELS = {
684
+ 0: '',
685
+ 1: 'Urgent',
686
+ 2: 'High',
687
+ 3: 'Medium',
688
+ 4: 'Low',
689
+ };
690
+
691
+ const PRIORITY_MAP = {
692
+ 'urgent': 1,
693
+ 'high': 2,
694
+ 'medium': 3,
695
+ 'low': 4,
696
+ 'none': 0,
697
+ };
698
+
500
699
  async function cmdIssueCreate(args) {
501
700
  const opts = parseArgs(args, {
502
701
  title: 'string', t: 'string',
@@ -504,28 +703,30 @@ async function cmdIssueCreate(args) {
504
703
  project: 'string', p: 'string',
505
704
  milestone: 'string',
506
705
  parent: 'string',
507
- state: 'string', s: 'string',
706
+ status: 'string', s: 'string',
508
707
  assign: 'boolean',
509
708
  estimate: 'string', e: 'string',
510
- label: 'string', l: 'string',
511
- blocks: 'string',
512
- 'blocked-by': 'string',
709
+ priority: 'string',
710
+ label: 'array', l: 'array',
711
+ blocks: 'array',
712
+ 'blocked-by': 'array',
513
713
  });
514
714
 
515
715
  const title = opts.title || opts.t || opts._[0];
516
716
  const description = opts.description || opts.d || '';
517
- const project = opts.project || opts.p;
518
- const milestone = opts.milestone;
717
+ const project = resolveAlias(opts.project || opts.p);
718
+ const priority = (opts.priority || '').toLowerCase();
719
+ const milestone = resolveAlias(opts.milestone);
519
720
  const parent = opts.parent;
520
721
  const shouldAssign = opts.assign;
521
722
  const estimate = (opts.estimate || opts.e || '').toLowerCase();
522
- const labelName = opts.label || opts.l;
523
- const blocksIssue = opts.blocks;
524
- const blockedByIssue = opts['blocked-by'];
723
+ const labelNames = opts.label || opts.l || [];
724
+ const blocksIssues = opts.blocks || [];
725
+ const blockedByIssues = opts['blocked-by'] || [];
525
726
 
526
727
  if (!title) {
527
728
  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]');
729
+ 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
730
  process.exit(1);
530
731
  }
531
732
 
@@ -535,6 +736,12 @@ async function cmdIssueCreate(args) {
535
736
  process.exit(1);
536
737
  }
537
738
 
739
+ // Validate priority
740
+ if (priority && !PRIORITY_MAP.hasOwnProperty(priority)) {
741
+ console.error(colors.red(`Error: Invalid priority "${priority}". Use: urgent, high, medium, low, or none`));
742
+ process.exit(1);
743
+ }
744
+
538
745
  // Get team UUID (required for mutations)
539
746
  const teamResult = await gql(`{ team(id: "${TEAM_KEY}") { id } }`);
540
747
  const teamId = teamResult.data?.team?.id;
@@ -587,20 +794,22 @@ async function cmdIssueCreate(args) {
587
794
  }
588
795
  }
589
796
 
590
- // Look up label ID
797
+ // Look up label IDs
591
798
  let labelIds = [];
592
- if (labelName) {
799
+ if (labelNames.length > 0) {
593
800
  const labelsResult = await gql(`{
594
801
  team(id: "${TEAM_KEY}") {
595
802
  labels(first: 100) { nodes { id name } }
596
803
  }
597
804
  }`);
598
805
  const labels = labelsResult.data?.team?.labels?.nodes || [];
599
- const match = labels.find(l => l.name.toLowerCase() === labelName.toLowerCase());
600
- if (match) {
601
- labelIds.push(match.id);
602
- } else {
603
- console.error(colors.yellow(`Warning: Label "${labelName}" not found. Creating issue without label.`));
806
+ for (const labelName of labelNames) {
807
+ const match = labels.find(l => l.name.toLowerCase() === labelName.toLowerCase());
808
+ if (match) {
809
+ labelIds.push(match.id);
810
+ } else {
811
+ console.error(colors.yellow(`Warning: Label "${labelName}" not found.`));
812
+ }
604
813
  }
605
814
  }
606
815
 
@@ -626,6 +835,7 @@ async function cmdIssueCreate(args) {
626
835
  if (parent) input.parentId = parent;
627
836
  if (assigneeId) input.assigneeId = assigneeId;
628
837
  if (estimate) input.estimate = ESTIMATE_MAP[estimate];
838
+ if (priority) input.priority = PRIORITY_MAP[priority];
629
839
  if (labelIds.length > 0) input.labelIds = labelIds;
630
840
 
631
841
  const result = await gql(mutation, { input });
@@ -633,31 +843,30 @@ async function cmdIssueCreate(args) {
633
843
  if (result.data?.issueCreate?.success) {
634
844
  const issue = result.data.issueCreate.issue;
635
845
  const estLabel = estimate ? ` [${estimate.toUpperCase()}]` : '';
636
- console.log(colors.green(`Created: ${issue.identifier}${estLabel}`));
846
+ const priLabel = priority && priority !== 'none' ? ` [${priority.charAt(0).toUpperCase() + priority.slice(1)}]` : '';
847
+ console.log(colors.green(`Created: ${issue.identifier}${estLabel}${priLabel}`));
637
848
  console.log(issue.url);
638
849
 
639
850
  // Create blocking relations if specified
640
- if (blocksIssue || blockedByIssue) {
851
+ if (blocksIssues.length > 0 || blockedByIssues.length > 0) {
641
852
  const relationMutation = `
642
853
  mutation($input: IssueRelationCreateInput!) {
643
854
  issueRelationCreate(input: $input) { success }
644
855
  }
645
856
  `;
646
857
 
647
- if (blocksIssue) {
648
- // This issue blocks another issue
858
+ for (const target of blocksIssues) {
649
859
  await gql(relationMutation, {
650
- input: { issueId: issue.identifier, relatedIssueId: blocksIssue, type: 'blocks' }
860
+ input: { issueId: issue.identifier, relatedIssueId: target, type: 'blocks' }
651
861
  });
652
- console.log(colors.gray(` → blocks ${blocksIssue}`));
862
+ console.log(colors.gray(` → blocks ${target}`));
653
863
  }
654
864
 
655
- if (blockedByIssue) {
656
- // This issue is blocked by another issue
865
+ for (const target of blockedByIssues) {
657
866
  await gql(relationMutation, {
658
- input: { issueId: blockedByIssue, relatedIssueId: issue.identifier, type: 'blocks' }
867
+ input: { issueId: target, relatedIssueId: issue.identifier, type: 'blocks' }
659
868
  });
660
- console.log(colors.gray(` → blocked by ${blockedByIssue}`));
869
+ console.log(colors.gray(` → blocked by ${target}`));
661
870
  }
662
871
  }
663
872
  } else {
@@ -677,22 +886,61 @@ async function cmdIssueUpdate(args) {
677
886
  const opts = parseArgs(args.slice(1), {
678
887
  title: 'string', t: 'string',
679
888
  description: 'string', d: 'string',
680
- state: 'string', s: 'string',
889
+ status: 'string', s: 'string',
681
890
  project: 'string', p: 'string',
682
891
  milestone: 'string',
892
+ priority: 'string',
893
+ estimate: 'string', e: 'string',
894
+ label: 'array', l: 'array',
895
+ assign: 'boolean',
896
+ parent: 'string',
683
897
  append: 'string', a: 'string',
684
- blocks: 'string',
685
- 'blocked-by': 'string',
898
+ check: 'string',
899
+ uncheck: 'string',
900
+ blocks: 'array',
901
+ 'blocked-by': 'array',
686
902
  });
687
903
 
688
- const blocksIssue = opts.blocks;
689
- const blockedByIssue = opts['blocked-by'];
690
- const projectName = opts.project || opts.p;
691
- const milestoneName = opts.milestone;
904
+ const blocksIssues = opts.blocks || [];
905
+ const blockedByIssues = opts['blocked-by'] || [];
906
+ const projectName = resolveAlias(opts.project || opts.p);
907
+ const milestoneName = resolveAlias(opts.milestone);
908
+ const priorityName = (opts.priority || '').toLowerCase();
909
+ const estimate = (opts.estimate || opts.e || '').toLowerCase();
910
+ const labelNames = opts.label || opts.l || [];
911
+ const shouldAssign = opts.assign;
912
+ const parent = opts.parent;
692
913
  const input = {};
693
914
 
694
915
  if (opts.title || opts.t) input.title = opts.title || opts.t;
695
916
 
917
+ // Handle estimate
918
+ if (estimate) {
919
+ if (!ESTIMATE_MAP.hasOwnProperty(estimate)) {
920
+ console.error(colors.red(`Error: Invalid estimate "${estimate}". Use: XS, S, M, L, or XL`));
921
+ process.exit(1);
922
+ }
923
+ input.estimate = ESTIMATE_MAP[estimate];
924
+ }
925
+
926
+ // Handle parent
927
+ if (parent) input.parentId = parent;
928
+
929
+ // Handle assign
930
+ if (shouldAssign) {
931
+ const viewerResult = await gql('{ viewer { id } }');
932
+ input.assigneeId = viewerResult.data?.viewer?.id;
933
+ }
934
+
935
+ // Handle priority
936
+ if (priorityName) {
937
+ if (!PRIORITY_MAP.hasOwnProperty(priorityName)) {
938
+ console.error(colors.red(`Error: Invalid priority "${priorityName}". Use: urgent, high, medium, low, or none`));
939
+ process.exit(1);
940
+ }
941
+ input.priority = PRIORITY_MAP[priorityName];
942
+ }
943
+
696
944
  // Handle append
697
945
  if (opts.append || opts.a) {
698
946
  const currentResult = await gql(`{ issue(id: "${issueId}") { description } }`);
@@ -702,9 +950,73 @@ async function cmdIssueUpdate(args) {
702
950
  input.description = opts.description || opts.d;
703
951
  }
704
952
 
953
+ // Handle check/uncheck
954
+ const checkText = opts.check;
955
+ const uncheckText = opts.uncheck;
956
+ if (checkText || uncheckText) {
957
+ const isCheck = !!checkText;
958
+ const query = checkText || uncheckText;
959
+ const fromPattern = isCheck ? /- \[ \] / : /- \[x\] /i;
960
+ const toMark = isCheck ? '- [x] ' : '- [ ] ';
961
+ const verb = isCheck ? 'Checked' : 'Unchecked';
962
+
963
+ // Fetch current description if we haven't already
964
+ let desc = input.description;
965
+ if (!desc) {
966
+ const currentResult = await gql(`{ issue(id: "${issueId}") { description } }`);
967
+ desc = currentResult.data?.issue?.description || '';
968
+ }
969
+
970
+ const lines = desc.split('\n');
971
+ const checkboxLines = lines
972
+ .map((line, i) => ({ line, index: i }))
973
+ .filter(({ line }) => fromPattern.test(line));
974
+
975
+ if (checkboxLines.length === 0) {
976
+ console.error(colors.red(`Error: No ${isCheck ? 'unchecked' : 'checked'} items found in description`));
977
+ process.exit(1);
978
+ }
979
+
980
+ // Find best match: score each checkbox line by similarity to query
981
+ const queryLower = query.toLowerCase();
982
+ let bestMatch = null;
983
+ let bestScore = 0;
984
+
985
+ for (const { line, index } of checkboxLines) {
986
+ const text = line.replace(/- \[[ x]\] /i, '').toLowerCase();
987
+ // Exact match
988
+ if (text === queryLower) { bestMatch = { line, index }; bestScore = Infinity; break; }
989
+ // Substring match
990
+ if (text.includes(queryLower) || queryLower.includes(text)) {
991
+ const score = queryLower.length / Math.max(text.length, queryLower.length);
992
+ if (score > bestScore) { bestScore = score; bestMatch = { line, index }; }
993
+ } else {
994
+ // Word overlap scoring
995
+ const queryWords = queryLower.split(/\s+/);
996
+ const textWords = text.split(/\s+/);
997
+ const overlap = queryWords.filter(w => textWords.some(tw => tw.includes(w) || w.includes(tw))).length;
998
+ const score = overlap / Math.max(queryWords.length, textWords.length);
999
+ if (score > bestScore) { bestScore = score; bestMatch = { line, index }; }
1000
+ }
1001
+ }
1002
+
1003
+ if (!bestMatch || bestScore < 0.3) {
1004
+ console.error(colors.red(`Error: No checkbox matching "${query}"`));
1005
+ console.error('Available items:');
1006
+ checkboxLines.forEach(({ line }) => console.error(' ' + line.trim()));
1007
+ process.exit(1);
1008
+ }
1009
+
1010
+ lines[bestMatch.index] = bestMatch.line.replace(fromPattern, toMark);
1011
+ input.description = lines.join('\n');
1012
+
1013
+ const itemText = bestMatch.line.replace(/- \[[ x]\] /i, '').trim();
1014
+ console.log(colors.green(`${verb}: ${itemText}`));
1015
+ }
1016
+
705
1017
  // Handle state
706
- if (opts.state || opts.s) {
707
- const stateName = opts.state || opts.s;
1018
+ if (opts.status || opts.s) {
1019
+ const stateName = opts.status || opts.s;
708
1020
  const statesResult = await gql(`{
709
1021
  team(id: "${TEAM_KEY}") {
710
1022
  states { nodes { id name } }
@@ -715,6 +1027,26 @@ async function cmdIssueUpdate(args) {
715
1027
  if (match) input.stateId = match.id;
716
1028
  }
717
1029
 
1030
+ // Handle labels
1031
+ if (labelNames.length > 0) {
1032
+ const labelsResult = await gql(`{
1033
+ team(id: "${TEAM_KEY}") {
1034
+ labels(first: 100) { nodes { id name } }
1035
+ }
1036
+ }`);
1037
+ const labels = labelsResult.data?.team?.labels?.nodes || [];
1038
+ const labelIds = [];
1039
+ for (const labelName of labelNames) {
1040
+ const match = labels.find(l => l.name.toLowerCase() === labelName.toLowerCase());
1041
+ if (match) {
1042
+ labelIds.push(match.id);
1043
+ } else {
1044
+ console.error(colors.yellow(`Warning: Label "${labelName}" not found.`));
1045
+ }
1046
+ }
1047
+ if (labelIds.length > 0) input.labelIds = labelIds;
1048
+ }
1049
+
718
1050
  // Handle project and milestone
719
1051
  if (projectName || milestoneName) {
720
1052
  const projectsResult = await gql(`{
@@ -756,7 +1088,7 @@ async function cmdIssueUpdate(args) {
756
1088
  }
757
1089
 
758
1090
  // Handle blocking relations (can be set even without other updates)
759
- const hasRelationUpdates = blocksIssue || blockedByIssue;
1091
+ const hasRelationUpdates = blocksIssues.length > 0 || blockedByIssues.length > 0;
760
1092
 
761
1093
  if (Object.keys(input).length === 0 && !hasRelationUpdates) {
762
1094
  console.error(colors.red('Error: No updates specified'));
@@ -795,18 +1127,18 @@ async function cmdIssueUpdate(args) {
795
1127
  }
796
1128
  `;
797
1129
 
798
- if (blocksIssue) {
1130
+ for (const target of blocksIssues) {
799
1131
  await gql(relationMutation, {
800
- input: { issueId: issueId, relatedIssueId: blocksIssue, type: 'blocks' }
1132
+ input: { issueId: issueId, relatedIssueId: target, type: 'blocks' }
801
1133
  });
802
- console.log(colors.green(`${issueId} now blocks ${blocksIssue}`));
1134
+ console.log(colors.green(`${issueId} now blocks ${target}`));
803
1135
  }
804
1136
 
805
- if (blockedByIssue) {
1137
+ for (const target of blockedByIssues) {
806
1138
  await gql(relationMutation, {
807
- input: { issueId: blockedByIssue, relatedIssueId: issueId, type: 'blocks' }
1139
+ input: { issueId: target, relatedIssueId: issueId, type: 'blocks' }
808
1140
  });
809
- console.log(colors.green(`${issueId} now blocked by ${blockedByIssue}`));
1141
+ console.log(colors.green(`${issueId} now blocked by ${target}`));
810
1142
  }
811
1143
  }
812
1144
  }
@@ -953,21 +1285,38 @@ async function cmdProjects(args) {
953
1285
  projects = projects.filter(p => !['completed', 'canceled'].includes(p.state));
954
1286
  }
955
1287
 
1288
+ // Find alias for a project (name must start with alias target)
1289
+ const findAliasFor = (name) => {
1290
+ const lowerName = name.toLowerCase();
1291
+ let bestMatch = null;
1292
+ let bestLength = 0;
1293
+ for (const [code, aliasName] of Object.entries(ALIASES)) {
1294
+ const lowerAlias = aliasName.toLowerCase();
1295
+ // Name must start with the alias target, and prefer longer matches
1296
+ if (lowerName.startsWith(lowerAlias) && lowerAlias.length > bestLength) {
1297
+ bestMatch = code;
1298
+ bestLength = lowerAlias.length;
1299
+ }
1300
+ }
1301
+ return bestMatch;
1302
+ };
1303
+
956
1304
  console.log(colors.bold('Projects:\n'));
957
- const rows = projects.map(p => [
958
- p.name,
959
- p.state,
960
- `${Math.floor(p.progress * 100)}%`
961
- ]);
1305
+ const rows = projects.map(p => {
1306
+ const alias = findAliasFor(p.name);
1307
+ const nameCol = alias ? `${colors.bold(`[${alias}]`)} ${p.name}` : p.name;
1308
+ return [nameCol, p.state, `${Math.floor(p.progress * 100)}%`];
1309
+ });
962
1310
  console.log(formatTable(rows));
963
1311
  }
964
1312
 
965
1313
  async function cmdProjectShow(args) {
966
- const projectName = args[0];
967
- if (!projectName) {
1314
+ const projectNameArg = args[0];
1315
+ if (!projectNameArg) {
968
1316
  console.error(colors.red('Error: Project name required'));
969
1317
  process.exit(1);
970
1318
  }
1319
+ const projectName = resolveAlias(projectNameArg);
971
1320
 
972
1321
  const query = `{
973
1322
  team(id: "${TEAM_KEY}") {
@@ -982,7 +1331,7 @@ async function cmdProjectShow(args) {
982
1331
 
983
1332
  const result = await gql(query);
984
1333
  const projects = result.data?.team?.projects?.nodes || [];
985
- const project = projects.find(p => p.name.includes(projectName));
1334
+ const project = projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
986
1335
 
987
1336
  if (!project) {
988
1337
  console.error(colors.red(`Project not found: ${projectName}`));
@@ -1129,34 +1478,58 @@ async function cmdMilestones(args) {
1129
1478
  projects = projects.filter(p => !['completed', 'canceled'].includes(p.state));
1130
1479
  }
1131
1480
 
1132
- // Filter by project name if specified
1481
+ // Filter by project name if specified (resolve alias first)
1133
1482
  if (projectFilter) {
1134
- projects = projects.filter(p => p.name.toLowerCase().includes(projectFilter.toLowerCase()));
1483
+ const resolvedFilter = resolveAlias(projectFilter);
1484
+ projects = projects.filter(p => p.name.toLowerCase().includes(resolvedFilter.toLowerCase()));
1135
1485
  }
1136
1486
 
1487
+ // Find alias for a name (name must start with alias target)
1488
+ const findAliasFor = (name) => {
1489
+ const lowerName = name.toLowerCase();
1490
+ let bestMatch = null;
1491
+ let bestLength = 0;
1492
+ for (const [code, aliasName] of Object.entries(ALIASES)) {
1493
+ const lowerAlias = aliasName.toLowerCase();
1494
+ // Name must start with the alias target, and prefer longer matches
1495
+ if (lowerName.startsWith(lowerAlias) && lowerAlias.length > bestLength) {
1496
+ bestMatch = code;
1497
+ bestLength = lowerAlias.length;
1498
+ }
1499
+ }
1500
+ return bestMatch;
1501
+ };
1502
+
1137
1503
  console.log(colors.bold('Milestones:\n'));
1138
1504
  for (const project of projects) {
1139
1505
  const milestones = project.projectMilestones?.nodes || [];
1140
1506
  if (milestones.length === 0) continue;
1141
1507
 
1142
- console.log(colors.bold(project.name));
1508
+ const projectAlias = findAliasFor(project.name);
1509
+ const projectHeader = projectAlias
1510
+ ? `${colors.bold(`[${projectAlias}]`)} ${colors.bold(project.name)}`
1511
+ : colors.bold(project.name);
1512
+ console.log(projectHeader);
1143
1513
  for (const m of milestones) {
1514
+ const milestoneAlias = findAliasFor(m.name);
1515
+ const namePrefix = milestoneAlias ? `${colors.bold(`[${milestoneAlias}]`)} ` : '';
1144
1516
  const date = m.targetDate ? ` (${m.targetDate})` : '';
1145
1517
  const status = m.status !== 'planned' ? ` [${m.status}]` : '';
1146
- console.log(` ${m.name}${date}${status}`);
1518
+ console.log(` ${namePrefix}${m.name}${date}${status}`);
1147
1519
  }
1148
1520
  console.log('');
1149
1521
  }
1150
1522
  }
1151
1523
 
1152
1524
  async function cmdMilestoneShow(args) {
1153
- const milestoneName = args[0];
1154
- if (!milestoneName) {
1525
+ const milestoneNameArg = args[0];
1526
+ if (!milestoneNameArg) {
1155
1527
  console.error(colors.red('Error: Milestone name required'));
1156
1528
  process.exit(1);
1157
1529
  }
1530
+ const milestoneName = resolveAlias(milestoneNameArg);
1158
1531
 
1159
- const query = `{
1532
+ const projectsQuery = `{
1160
1533
  team(id: "${TEAM_KEY}") {
1161
1534
  projects(first: 50) {
1162
1535
  nodes {
@@ -1164,7 +1537,6 @@ async function cmdMilestoneShow(args) {
1164
1537
  projectMilestones {
1165
1538
  nodes {
1166
1539
  id name description targetDate status sortOrder
1167
- issues { nodes { identifier title state { name type } } }
1168
1540
  }
1169
1541
  }
1170
1542
  }
@@ -1172,8 +1544,23 @@ async function cmdMilestoneShow(args) {
1172
1544
  }
1173
1545
  }`;
1174
1546
 
1175
- const result = await gql(query);
1176
- const projects = result.data?.team?.projects?.nodes || [];
1547
+ const issuesQuery = `{
1548
+ team(id: "${TEAM_KEY}") {
1549
+ issues(first: 200) {
1550
+ nodes {
1551
+ identifier title state { name type }
1552
+ projectMilestone { id }
1553
+ }
1554
+ }
1555
+ }
1556
+ }`;
1557
+
1558
+ const [projectsResult, issuesResult] = await Promise.all([
1559
+ gql(projectsQuery),
1560
+ gql(issuesQuery)
1561
+ ]);
1562
+ const projects = projectsResult.data?.team?.projects?.nodes || [];
1563
+ const allIssues = issuesResult.data?.team?.issues?.nodes || [];
1177
1564
 
1178
1565
  let milestone = null;
1179
1566
  let projectName = '';
@@ -1199,7 +1586,7 @@ async function cmdMilestoneShow(args) {
1199
1586
  if (milestone.targetDate) console.log(`Target: ${milestone.targetDate}`);
1200
1587
  if (milestone.description) console.log(`\n## Description\n${milestone.description}`);
1201
1588
 
1202
- const issues = milestone.issues?.nodes || [];
1589
+ const issues = allIssues.filter(i => i.projectMilestone?.id === milestone.id);
1203
1590
  if (issues.length > 0) {
1204
1591
  // Group by state type
1205
1592
  const done = issues.filter(i => i.state.type === 'completed');
@@ -1910,6 +2297,77 @@ async function cmdLabelCreate(args) {
1910
2297
  }
1911
2298
  }
1912
2299
 
2300
+ // ============================================================================
2301
+ // ALIASES
2302
+ // ============================================================================
2303
+
2304
+ async function cmdAlias(args) {
2305
+ const opts = parseArgs(args, {
2306
+ list: 'boolean', l: 'boolean',
2307
+ remove: 'string', r: 'string',
2308
+ });
2309
+
2310
+ const showList = opts.list || opts.l;
2311
+ const removeCode = opts.remove || opts.r;
2312
+ const code = opts._[0];
2313
+ const name = opts._[1];
2314
+
2315
+ // List aliases
2316
+ if (showList || (Object.keys(opts).length === 1 && opts._.length === 0)) {
2317
+ const aliases = Object.entries(ALIASES);
2318
+ if (aliases.length === 0) {
2319
+ console.log('No aliases defined.');
2320
+ console.log('Usage: linear alias CODE "Project or Milestone Name"');
2321
+ return;
2322
+ }
2323
+
2324
+ // Fetch projects to determine type (project vs milestone)
2325
+ const query = `{
2326
+ team(id: "${TEAM_KEY}") {
2327
+ projects(first: 50) {
2328
+ nodes { name }
2329
+ }
2330
+ }
2331
+ }`;
2332
+
2333
+ const result = await gql(query);
2334
+ const projects = result.data?.team?.projects?.nodes || [];
2335
+
2336
+ // Check if alias target matches a project (using partial match)
2337
+ const matchesProject = (target) => {
2338
+ const lowerTarget = target.toLowerCase();
2339
+ return projects.some(p => p.name.toLowerCase().includes(lowerTarget));
2340
+ };
2341
+
2342
+ console.log(colors.bold('Aliases:\n'));
2343
+ for (const [code, target] of aliases) {
2344
+ const isProject = matchesProject(target);
2345
+ const type = isProject ? colors.blue('project') : colors.yellow('milestone');
2346
+ console.log(` ${colors.bold(code)} -> ${target} (${type})`);
2347
+ }
2348
+ return;
2349
+ }
2350
+
2351
+ // Remove alias
2352
+ if (removeCode) {
2353
+ removeAlias(removeCode);
2354
+ console.log(colors.green(`Removed alias: ${removeCode.toUpperCase()}`));
2355
+ return;
2356
+ }
2357
+
2358
+ // Create/update alias
2359
+ if (!code || !name) {
2360
+ console.error(colors.red('Error: Code and name required'));
2361
+ console.error('Usage: linear alias CODE "Project or Milestone Name"');
2362
+ console.error(' linear alias --list');
2363
+ console.error(' linear alias --remove CODE');
2364
+ process.exit(1);
2365
+ }
2366
+
2367
+ saveAlias(code, name);
2368
+ console.log(colors.green(`Alias set: ${code.toUpperCase()} -> ${name}`));
2369
+ }
2370
+
1913
2371
  // ============================================================================
1914
2372
  // GIT INTEGRATION
1915
2373
  // ============================================================================
@@ -2339,7 +2797,6 @@ async function cmdStandup(args) {
2339
2797
 
2340
2798
  const skipGitHub = opts['no-github'];
2341
2799
  const yesterday = getYesterdayDate();
2342
- const today = getTodayDate();
2343
2800
 
2344
2801
  // Get current user
2345
2802
  const viewerResult = await gql('{ viewer { id name } }');
@@ -2432,93 +2889,88 @@ async function cmdStandup(args) {
2432
2889
  }
2433
2890
  }
2434
2891
 
2435
- // GitHub activity
2892
+ // GitHub activity (cross-repo)
2436
2893
  if (!skipGitHub) {
2437
2894
  console.log('');
2438
2895
  console.log(colors.gray(`─────────────────────────────────────────\n`));
2439
2896
  console.log(colors.bold('GitHub Activity (yesterday):'));
2440
2897
 
2898
+ let hasActivity = false;
2899
+ let ghAvailable = true;
2900
+
2901
+ // Get commits across all repos
2441
2902
  try {
2442
- // Get commits from yesterday
2443
- const sinceDate = `${yesterday}T00:00:00`;
2444
- const untilDate = `${today}T00:00:00`;
2903
+ const commitsJson = execSync(
2904
+ `gh search commits --author=@me --committer-date=${yesterday} --json sha,commit,repository --limit 50`,
2905
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
2906
+ );
2907
+ const commits = JSON.parse(commitsJson);
2908
+
2909
+ if (commits.length > 0) {
2910
+ hasActivity = true;
2911
+ const byRepo = {};
2912
+ for (const c of commits) {
2913
+ const repo = c.repository?.fullName || 'unknown';
2914
+ if (!byRepo[repo]) byRepo[repo] = [];
2915
+ const msg = c.commit?.message?.split('\n')[0] || c.sha.slice(0, 7);
2916
+ byRepo[repo].push(`${c.sha.slice(0, 7)} ${msg}`);
2917
+ }
2445
2918
 
2446
- // Try to get repo info
2447
- let repoOwner, repoName;
2448
- try {
2449
- const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
2450
- const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
2451
- if (match) {
2452
- repoOwner = match[1];
2453
- repoName = match[2];
2919
+ console.log(`\n Commits (${commits.length}):`);
2920
+ for (const [repo, repoCommits] of Object.entries(byRepo)) {
2921
+ console.log(` ${colors.bold(repo)} (${repoCommits.length}):`);
2922
+ for (const commit of repoCommits) {
2923
+ console.log(` ${commit}`);
2924
+ }
2454
2925
  }
2455
- } catch (err) {
2456
- // Not in a git repo or no origin
2457
2926
  }
2927
+ } catch (err) {
2928
+ ghAvailable = false;
2929
+ console.log(colors.gray(' (gh CLI not available - install gh for GitHub activity)'));
2930
+ }
2458
2931
 
2459
- if (repoOwner && repoName) {
2460
- // Get git user name for author matching (may differ from Linear display name)
2461
- let gitUserName;
2462
- try {
2463
- gitUserName = execSync('git config user.name', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
2464
- } catch (err) {
2465
- gitUserName = viewer.name; // Fall back to Linear name
2466
- }
2932
+ // Get PRs across all repos
2933
+ if (ghAvailable) {
2934
+ try {
2935
+ const mergedJson = execSync(
2936
+ `gh search prs --author=@me --merged-at=${yesterday} --json number,title,repository --limit 20`,
2937
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
2938
+ );
2939
+ const mergedPrs = JSON.parse(mergedJson).map(pr => ({ ...pr, prStatus: 'merged' }));
2467
2940
 
2468
- // Get commits using git log (more reliable than gh for commits)
2469
- try {
2470
- const gitLog = execSync(
2471
- `git log --since="${sinceDate}" --until="${untilDate}" --author="${gitUserName}" --oneline`,
2472
- { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
2473
- ).trim();
2474
-
2475
- if (gitLog) {
2476
- const commits = gitLog.split('\n').filter(Boolean);
2477
- console.log(`\n Commits (${commits.length}):`);
2478
- for (const commit of commits.slice(0, 10)) {
2479
- console.log(` ${commit}`);
2480
- }
2481
- if (commits.length > 10) {
2482
- console.log(colors.gray(` ... and ${commits.length - 10} more`));
2483
- }
2941
+ const createdJson = execSync(
2942
+ `gh search prs --author=@me --created=${yesterday} --state=open --json number,title,repository --limit 20`,
2943
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
2944
+ );
2945
+ const createdPrs = JSON.parse(createdJson).map(pr => ({ ...pr, prStatus: 'open' }));
2946
+
2947
+ // Deduplicate (a PR created and merged same day appears in both)
2948
+ const seen = new Set();
2949
+ const allPrs = [];
2950
+ for (const pr of [...mergedPrs, ...createdPrs]) {
2951
+ const key = `${pr.repository?.fullName}#${pr.number}`;
2952
+ if (!seen.has(key)) {
2953
+ seen.add(key);
2954
+ allPrs.push(pr);
2484
2955
  }
2485
- } catch (err) {
2486
- // No commits or git error
2487
2956
  }
2488
2957
 
2489
- // Get PRs using gh
2490
- try {
2491
- const prsJson = execSync(
2492
- `gh pr list --author @me --state all --json number,title,state,mergedAt,createdAt --limit 20`,
2493
- { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
2494
- );
2495
- const prs = JSON.parse(prsJson);
2496
-
2497
- // Filter to PRs created or merged yesterday
2498
- const relevantPrs = prs.filter(pr => {
2499
- const createdDate = pr.createdAt?.split('T')[0];
2500
- const mergedDate = pr.mergedAt?.split('T')[0];
2501
- return createdDate === yesterday || mergedDate === yesterday;
2502
- });
2503
-
2504
- if (relevantPrs.length > 0) {
2505
- console.log(`\n Pull Requests:`);
2506
- for (const pr of relevantPrs) {
2507
- const status = pr.state === 'MERGED' ? colors.green('merged') :
2508
- pr.state === 'OPEN' ? colors.yellow('open') :
2509
- colors.gray(pr.state.toLowerCase());
2510
- console.log(` #${pr.number} ${pr.title} [${status}]`);
2511
- }
2958
+ if (allPrs.length > 0) {
2959
+ hasActivity = true;
2960
+ console.log(`\n Pull Requests:`);
2961
+ for (const pr of allPrs) {
2962
+ const repo = pr.repository?.name || '';
2963
+ const status = pr.prStatus === 'merged' ? colors.green('merged') : colors.yellow('open');
2964
+ console.log(` ${colors.gray(repo + '#')}${pr.number} ${pr.title} [${status}]`);
2512
2965
  }
2513
- } catch (err) {
2514
- // gh not available or error
2515
- console.log(colors.gray(' (gh CLI not available or not authenticated)'));
2516
2966
  }
2517
- } else {
2518
- console.log(colors.gray(' (not in a GitHub repository)'));
2967
+ } catch (err) {
2968
+ // gh search error
2519
2969
  }
2520
- } catch (err) {
2521
- console.log(colors.gray(` Error fetching GitHub data: ${err.message}`));
2970
+ }
2971
+
2972
+ if (!hasActivity && ghAvailable) {
2973
+ console.log(colors.gray(' No GitHub activity yesterday'));
2522
2974
  }
2523
2975
  }
2524
2976
 
@@ -2737,17 +3189,16 @@ PLANNING:
2737
3189
  --all, -a Include completed projects
2738
3190
 
2739
3191
  ISSUES:
2740
- issues [options] List issues (default: backlog, yours first)
3192
+ issues [options] List issues (default: backlog + todo, yours first)
2741
3193
  --unblocked, -u Show only unblocked issues
2742
3194
  --open, -o Show all non-completed/canceled issues
2743
- --backlog, -b Show only backlog issues
3195
+ --status, -s <name> Filter by status (repeatable: --status todo --status backlog)
2744
3196
  --all, -a Show all states (including completed)
2745
3197
  --mine, -m Show only issues assigned to you
2746
- --in-progress Show issues in progress
2747
3198
  --project, -p <name> Filter by project
2748
3199
  --milestone <name> Filter by milestone
2749
- --state, -s <state> Filter by state
2750
- --label, -l <name> Filter by label
3200
+ --label, -l <name> Filter by label (repeatable)
3201
+ --priority <level> Filter by priority (urgent/high/medium/low/none)
2751
3202
  issues reorder <ids...> Reorder issues by listing IDs in order
2752
3203
 
2753
3204
  issue show <id> Show issue details with parent context
@@ -2760,18 +3211,26 @@ ISSUES:
2760
3211
  --parent <id> Parent issue (for sub-issues)
2761
3212
  --assign Assign to yourself
2762
3213
  --estimate, -e <size> Estimate: XS, S, M, L, XL
2763
- --label, -l <name> Add label
2764
- --blocks <id> This issue blocks another
2765
- --blocked-by <id> This issue is blocked by another
3214
+ --priority <level> Priority: urgent, high, medium, low, none
3215
+ --label, -l <name> Add label (repeatable)
3216
+ --blocks <id> This issue blocks another (repeatable)
3217
+ --blocked-by <id> This issue is blocked by another (repeatable)
2766
3218
  issue update <id> [opts] Update an issue
2767
3219
  --title, -t <title> New title
2768
3220
  --description, -d <desc> New description
2769
- --state, -s <state> New state
3221
+ --status, -s <status> New status (todo, in-progress, done, backlog, etc.)
2770
3222
  --project, -p <name> Move to project
2771
3223
  --milestone <name> Move to milestone
3224
+ --parent <id> Set parent issue
3225
+ --assign Assign to yourself
3226
+ --estimate, -e <size> Set estimate: XS, S, M, L, XL
3227
+ --priority <level> Set priority (urgent/high/medium/low/none)
3228
+ --label, -l <name> Set label (repeatable)
2772
3229
  --append, -a <text> Append to description
2773
- --blocks <id> Add blocking relation
2774
- --blocked-by <id> Add blocked-by relation
3230
+ --check <text> Check a checkbox item (fuzzy match)
3231
+ --uncheck <text> Uncheck a checkbox item (fuzzy match)
3232
+ --blocks <id> Add blocking relation (repeatable)
3233
+ --blocked-by <id> Add blocked-by relation (repeatable)
2775
3234
  issue close <id> Mark issue as done
2776
3235
  issue comment <id> <body> Add a comment
2777
3236
  issue move <id> Move issue in sort order
@@ -2816,6 +3275,15 @@ LABELS:
2816
3275
  --description, -d <desc> Label description
2817
3276
  --color, -c <hex> Label color (e.g., #FF0000)
2818
3277
 
3278
+ ALIASES:
3279
+ alias <CODE> "<name>" Create alias for project/milestone
3280
+ alias --list List all aliases
3281
+ alias --remove <CODE> Remove an alias
3282
+
3283
+ Aliases can be used anywhere a project or milestone name is accepted:
3284
+ linear issues --project LWW
3285
+ linear issue create --milestone MVP "New feature"
3286
+
2819
3287
  GIT:
2820
3288
  branch <id> Create git branch from issue (ISSUE-5-issue-title)
2821
3289
 
@@ -2838,6 +3306,10 @@ CONFIGURATION:
2838
3306
  api_key=lin_api_xxx
2839
3307
  team=ISSUE
2840
3308
 
3309
+ [aliases]
3310
+ LWW=Last-Write-Wins Support
3311
+ MVP=MVP Release
3312
+
2841
3313
  EXAMPLES:
2842
3314
  linear roadmap # See all projects and milestones
2843
3315
  linear issues --unblocked # Find workable issues
@@ -2971,6 +3443,10 @@ async function main() {
2971
3443
  }
2972
3444
  break;
2973
3445
  }
3446
+ case 'alias':
3447
+ checkAuth();
3448
+ await cmdAlias(args.slice(1));
3449
+ break;
2974
3450
  case 'branch':
2975
3451
  checkAuth();
2976
3452
  await cmdBranch(args.slice(1));