@dabble/linear-cli 1.1.0 → 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/README.md CHANGED
@@ -50,9 +50,11 @@ linear whoami # Show current user and team
50
50
 
51
51
  ### Issues
52
52
  ```bash
53
+ linear issues # Default: backlog + todo
53
54
  linear issues --unblocked # Ready to work on
54
55
  linear issues --open # All non-completed issues
55
- linear issues --in-progress # Issues in progress
56
+ linear issues --status in-progress # Filter by status
57
+ linear issues --status todo --status in-progress # Multiple statuses
56
58
  linear issues --mine # Only your issues
57
59
  linear issues --label bug # Filter by label
58
60
  linear issue show ISSUE-1 # Full details with parent context
package/bin/linear.mjs CHANGED
@@ -288,6 +288,14 @@ function parseArgs(args, flags = {}) {
288
288
  const flagDef = flags[key];
289
289
  if (flagDef === 'boolean') {
290
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);
291
299
  } else {
292
300
  const value = args[++i];
293
301
  if (value === undefined || value.startsWith('-')) {
@@ -313,28 +321,42 @@ async function cmdIssues(args) {
313
321
  unblocked: 'boolean', u: 'boolean',
314
322
  all: 'boolean', a: 'boolean',
315
323
  open: 'boolean', o: 'boolean',
316
- backlog: 'boolean', b: 'boolean',
317
324
  mine: 'boolean', m: 'boolean',
318
- 'in-progress': 'boolean',
325
+ status: 'array', s: 'array',
319
326
  project: 'string', p: 'string',
320
327
  milestone: 'string',
321
- state: 'string', s: 'string',
322
- label: 'string', l: 'string',
328
+ label: 'array', l: 'array',
323
329
  priority: 'string',
324
330
  });
325
331
 
326
- const inProgress = opts['in-progress'];
327
332
  const unblocked = opts.unblocked || opts.u;
328
333
  const allStates = opts.all || opts.a;
329
334
  const openOnly = opts.open || opts.o;
330
- const backlogOnly = opts.backlog || opts.b;
331
335
  const mineOnly = opts.mine || opts.m;
336
+ const statusFilter = opts.status || opts.s || [];
332
337
  const projectFilter = opts.project || opts.p;
333
338
  const milestoneFilter = opts.milestone;
334
- const stateFilter = opts.state || opts.s;
335
- const labelFilter = opts.label || opts.l;
339
+ const labelFilters = opts.label || opts.l || [];
336
340
  const priorityFilter = (opts.priority || '').toLowerCase();
337
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());
359
+
338
360
  // Get current user ID for filtering/sorting
339
361
  const viewerResult = await gql('{ viewer { id } }');
340
362
  const viewerId = viewerResult.data?.viewer?.id;
@@ -411,9 +433,9 @@ async function cmdIssues(args) {
411
433
  if (mineOnly) {
412
434
  filtered = filtered.filter(i => i.assignee?.id === viewerId);
413
435
  }
414
- if (labelFilter) {
436
+ if (labelFilters.length > 0) {
415
437
  filtered = filtered.filter(i =>
416
- i.labels?.nodes?.some(l => l.name.toLowerCase() === labelFilter.toLowerCase())
438
+ labelFilters.some(lf => i.labels?.nodes?.some(l => l.name.toLowerCase() === lf.toLowerCase()))
417
439
  );
418
440
  }
419
441
  if (projectFilter) {
@@ -437,6 +459,13 @@ async function cmdIssues(args) {
437
459
  return filtered;
438
460
  };
439
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
+
440
469
  if (unblocked) {
441
470
  // Collect all blocked issue IDs
442
471
  const blocked = new Set();
@@ -453,48 +482,48 @@ async function cmdIssues(args) {
453
482
  !['completed', 'canceled'].includes(i.state.type) &&
454
483
  !blocked.has(i.identifier)
455
484
  );
485
+ if (resolvedStatusTypes.length > 0) {
486
+ filtered = filterByStatus(filtered, resolvedStatusTypes);
487
+ }
456
488
 
457
489
  filtered = applyFilters(filtered);
458
490
 
459
491
  console.log(colors.bold('Unblocked Issues:\n'));
460
492
  console.log(formatTable(filtered.map(formatRow)));
461
493
  } else if (allStates) {
462
- let filtered = applyFilters(issues);
494
+ let filtered = issues;
495
+ if (resolvedStatusTypes.length > 0) {
496
+ filtered = filterByStatus(filtered, resolvedStatusTypes);
497
+ }
498
+ filtered = applyFilters(filtered);
463
499
 
464
500
  console.log(colors.bold('All Issues:\n'));
465
501
  console.log(formatTable(filtered.map(formatRow)));
466
502
  } else if (openOnly) {
467
- // Open = everything except completed/canceled
468
503
  let filtered = issues.filter(i =>
469
504
  !['completed', 'canceled'].includes(i.state.type)
470
505
  );
506
+ if (resolvedStatusTypes.length > 0) {
507
+ filtered = filterByStatus(filtered, resolvedStatusTypes);
508
+ }
471
509
 
472
510
  filtered = applyFilters(filtered);
473
511
 
474
512
  console.log(colors.bold('Open Issues:\n'));
475
513
  console.log(formatTable(filtered.map(formatRow)));
476
- } else if (inProgress) {
477
- let filtered = issues.filter(i => i.state.type === 'started');
514
+ } else if (resolvedStatusTypes.length > 0) {
515
+ let filtered = filterByStatus(issues, resolvedStatusTypes);
478
516
  filtered = applyFilters(filtered);
479
517
 
480
- console.log(colors.bold('In Progress:\n'));
481
- console.log(formatTable(filtered.map(formatRow)));
482
- } else if (backlogOnly || stateFilter) {
483
- const targetState = stateFilter || 'backlog';
484
- let filtered = issues.filter(i =>
485
- i.state.type === targetState || i.state.name.toLowerCase() === targetState.toLowerCase()
486
- );
487
-
488
- filtered = applyFilters(filtered);
489
-
490
- console.log(colors.bold(`Issues (${targetState}):\n`));
518
+ const label = statusFilter.join(' + ');
519
+ console.log(colors.bold(`Issues (${label}):\n`));
491
520
  console.log(formatTable(filtered.map(formatRow)));
492
521
  } else {
493
- // Default: show backlog
494
- 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');
495
524
  filtered = applyFilters(filtered);
496
525
 
497
- console.log(colors.bold('Issues (backlog):\n'));
526
+ console.log(colors.bold('Issues (backlog + todo):\n'));
498
527
  console.log(formatTable(filtered.map(formatRow)));
499
528
  }
500
529
  }
@@ -674,13 +703,13 @@ async function cmdIssueCreate(args) {
674
703
  project: 'string', p: 'string',
675
704
  milestone: 'string',
676
705
  parent: 'string',
677
- state: 'string', s: 'string',
706
+ status: 'string', s: 'string',
678
707
  assign: 'boolean',
679
708
  estimate: 'string', e: 'string',
680
709
  priority: 'string',
681
- label: 'string', l: 'string',
682
- blocks: 'string',
683
- 'blocked-by': 'string',
710
+ label: 'array', l: 'array',
711
+ blocks: 'array',
712
+ 'blocked-by': 'array',
684
713
  });
685
714
 
686
715
  const title = opts.title || opts.t || opts._[0];
@@ -691,9 +720,9 @@ async function cmdIssueCreate(args) {
691
720
  const parent = opts.parent;
692
721
  const shouldAssign = opts.assign;
693
722
  const estimate = (opts.estimate || opts.e || '').toLowerCase();
694
- const labelName = opts.label || opts.l;
695
- const blocksIssue = opts.blocks;
696
- const blockedByIssue = opts['blocked-by'];
723
+ const labelNames = opts.label || opts.l || [];
724
+ const blocksIssues = opts.blocks || [];
725
+ const blockedByIssues = opts['blocked-by'] || [];
697
726
 
698
727
  if (!title) {
699
728
  console.error(colors.red('Error: Title is required'));
@@ -765,20 +794,22 @@ async function cmdIssueCreate(args) {
765
794
  }
766
795
  }
767
796
 
768
- // Look up label ID
797
+ // Look up label IDs
769
798
  let labelIds = [];
770
- if (labelName) {
799
+ if (labelNames.length > 0) {
771
800
  const labelsResult = await gql(`{
772
801
  team(id: "${TEAM_KEY}") {
773
802
  labels(first: 100) { nodes { id name } }
774
803
  }
775
804
  }`);
776
805
  const labels = labelsResult.data?.team?.labels?.nodes || [];
777
- const match = labels.find(l => l.name.toLowerCase() === labelName.toLowerCase());
778
- if (match) {
779
- labelIds.push(match.id);
780
- } else {
781
- 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
+ }
782
813
  }
783
814
  }
784
815
 
@@ -817,27 +848,25 @@ async function cmdIssueCreate(args) {
817
848
  console.log(issue.url);
818
849
 
819
850
  // Create blocking relations if specified
820
- if (blocksIssue || blockedByIssue) {
851
+ if (blocksIssues.length > 0 || blockedByIssues.length > 0) {
821
852
  const relationMutation = `
822
853
  mutation($input: IssueRelationCreateInput!) {
823
854
  issueRelationCreate(input: $input) { success }
824
855
  }
825
856
  `;
826
857
 
827
- if (blocksIssue) {
828
- // This issue blocks another issue
858
+ for (const target of blocksIssues) {
829
859
  await gql(relationMutation, {
830
- input: { issueId: issue.identifier, relatedIssueId: blocksIssue, type: 'blocks' }
860
+ input: { issueId: issue.identifier, relatedIssueId: target, type: 'blocks' }
831
861
  });
832
- console.log(colors.gray(` → blocks ${blocksIssue}`));
862
+ console.log(colors.gray(` → blocks ${target}`));
833
863
  }
834
864
 
835
- if (blockedByIssue) {
836
- // This issue is blocked by another issue
865
+ for (const target of blockedByIssues) {
837
866
  await gql(relationMutation, {
838
- input: { issueId: blockedByIssue, relatedIssueId: issue.identifier, type: 'blocks' }
867
+ input: { issueId: target, relatedIssueId: issue.identifier, type: 'blocks' }
839
868
  });
840
- console.log(colors.gray(` → blocked by ${blockedByIssue}`));
869
+ console.log(colors.gray(` → blocked by ${target}`));
841
870
  }
842
871
  }
843
872
  } else {
@@ -857,26 +886,52 @@ async function cmdIssueUpdate(args) {
857
886
  const opts = parseArgs(args.slice(1), {
858
887
  title: 'string', t: 'string',
859
888
  description: 'string', d: 'string',
860
- state: 'string', s: 'string',
889
+ status: 'string', s: 'string',
861
890
  project: 'string', p: 'string',
862
891
  milestone: 'string',
863
892
  priority: 'string',
893
+ estimate: 'string', e: 'string',
894
+ label: 'array', l: 'array',
895
+ assign: 'boolean',
896
+ parent: 'string',
864
897
  append: 'string', a: 'string',
865
898
  check: 'string',
866
899
  uncheck: 'string',
867
- blocks: 'string',
868
- 'blocked-by': 'string',
900
+ blocks: 'array',
901
+ 'blocked-by': 'array',
869
902
  });
870
903
 
871
- const blocksIssue = opts.blocks;
872
- const blockedByIssue = opts['blocked-by'];
904
+ const blocksIssues = opts.blocks || [];
905
+ const blockedByIssues = opts['blocked-by'] || [];
873
906
  const projectName = resolveAlias(opts.project || opts.p);
874
907
  const milestoneName = resolveAlias(opts.milestone);
875
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;
876
913
  const input = {};
877
914
 
878
915
  if (opts.title || opts.t) input.title = opts.title || opts.t;
879
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
+
880
935
  // Handle priority
881
936
  if (priorityName) {
882
937
  if (!PRIORITY_MAP.hasOwnProperty(priorityName)) {
@@ -960,8 +1015,8 @@ async function cmdIssueUpdate(args) {
960
1015
  }
961
1016
 
962
1017
  // Handle state
963
- if (opts.state || opts.s) {
964
- const stateName = opts.state || opts.s;
1018
+ if (opts.status || opts.s) {
1019
+ const stateName = opts.status || opts.s;
965
1020
  const statesResult = await gql(`{
966
1021
  team(id: "${TEAM_KEY}") {
967
1022
  states { nodes { id name } }
@@ -972,6 +1027,26 @@ async function cmdIssueUpdate(args) {
972
1027
  if (match) input.stateId = match.id;
973
1028
  }
974
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
+
975
1050
  // Handle project and milestone
976
1051
  if (projectName || milestoneName) {
977
1052
  const projectsResult = await gql(`{
@@ -1013,7 +1088,7 @@ async function cmdIssueUpdate(args) {
1013
1088
  }
1014
1089
 
1015
1090
  // Handle blocking relations (can be set even without other updates)
1016
- const hasRelationUpdates = blocksIssue || blockedByIssue;
1091
+ const hasRelationUpdates = blocksIssues.length > 0 || blockedByIssues.length > 0;
1017
1092
 
1018
1093
  if (Object.keys(input).length === 0 && !hasRelationUpdates) {
1019
1094
  console.error(colors.red('Error: No updates specified'));
@@ -1052,18 +1127,18 @@ async function cmdIssueUpdate(args) {
1052
1127
  }
1053
1128
  `;
1054
1129
 
1055
- if (blocksIssue) {
1130
+ for (const target of blocksIssues) {
1056
1131
  await gql(relationMutation, {
1057
- input: { issueId: issueId, relatedIssueId: blocksIssue, type: 'blocks' }
1132
+ input: { issueId: issueId, relatedIssueId: target, type: 'blocks' }
1058
1133
  });
1059
- console.log(colors.green(`${issueId} now blocks ${blocksIssue}`));
1134
+ console.log(colors.green(`${issueId} now blocks ${target}`));
1060
1135
  }
1061
1136
 
1062
- if (blockedByIssue) {
1137
+ for (const target of blockedByIssues) {
1063
1138
  await gql(relationMutation, {
1064
- input: { issueId: blockedByIssue, relatedIssueId: issueId, type: 'blocks' }
1139
+ input: { issueId: target, relatedIssueId: issueId, type: 'blocks' }
1065
1140
  });
1066
- console.log(colors.green(`${issueId} now blocked by ${blockedByIssue}`));
1141
+ console.log(colors.green(`${issueId} now blocked by ${target}`));
1067
1142
  }
1068
1143
  }
1069
1144
  }
@@ -2722,7 +2797,6 @@ async function cmdStandup(args) {
2722
2797
 
2723
2798
  const skipGitHub = opts['no-github'];
2724
2799
  const yesterday = getYesterdayDate();
2725
- const today = getTodayDate();
2726
2800
 
2727
2801
  // Get current user
2728
2802
  const viewerResult = await gql('{ viewer { id name } }');
@@ -2815,93 +2889,88 @@ async function cmdStandup(args) {
2815
2889
  }
2816
2890
  }
2817
2891
 
2818
- // GitHub activity
2892
+ // GitHub activity (cross-repo)
2819
2893
  if (!skipGitHub) {
2820
2894
  console.log('');
2821
2895
  console.log(colors.gray(`─────────────────────────────────────────\n`));
2822
2896
  console.log(colors.bold('GitHub Activity (yesterday):'));
2823
2897
 
2898
+ let hasActivity = false;
2899
+ let ghAvailable = true;
2900
+
2901
+ // Get commits across all repos
2824
2902
  try {
2825
- // Get commits from yesterday
2826
- const sinceDate = `${yesterday}T00:00:00`;
2827
- 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
+ }
2828
2918
 
2829
- // Try to get repo info
2830
- let repoOwner, repoName;
2831
- try {
2832
- const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
2833
- const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
2834
- if (match) {
2835
- repoOwner = match[1];
2836
- 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
+ }
2837
2925
  }
2838
- } catch (err) {
2839
- // Not in a git repo or no origin
2840
2926
  }
2927
+ } catch (err) {
2928
+ ghAvailable = false;
2929
+ console.log(colors.gray(' (gh CLI not available - install gh for GitHub activity)'));
2930
+ }
2841
2931
 
2842
- if (repoOwner && repoName) {
2843
- // Get git user name for author matching (may differ from Linear display name)
2844
- let gitUserName;
2845
- try {
2846
- gitUserName = execSync('git config user.name', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
2847
- } catch (err) {
2848
- gitUserName = viewer.name; // Fall back to Linear name
2849
- }
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' }));
2850
2940
 
2851
- // Get commits using git log (more reliable than gh for commits)
2852
- try {
2853
- const gitLog = execSync(
2854
- `git log --since="${sinceDate}" --until="${untilDate}" --author="${gitUserName}" --oneline`,
2855
- { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
2856
- ).trim();
2857
-
2858
- if (gitLog) {
2859
- const commits = gitLog.split('\n').filter(Boolean);
2860
- console.log(`\n Commits (${commits.length}):`);
2861
- for (const commit of commits.slice(0, 10)) {
2862
- console.log(` ${commit}`);
2863
- }
2864
- if (commits.length > 10) {
2865
- console.log(colors.gray(` ... and ${commits.length - 10} more`));
2866
- }
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);
2867
2955
  }
2868
- } catch (err) {
2869
- // No commits or git error
2870
2956
  }
2871
2957
 
2872
- // Get PRs using gh
2873
- try {
2874
- const prsJson = execSync(
2875
- `gh pr list --author @me --state all --json number,title,state,mergedAt,createdAt --limit 20`,
2876
- { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
2877
- );
2878
- const prs = JSON.parse(prsJson);
2879
-
2880
- // Filter to PRs created or merged yesterday
2881
- const relevantPrs = prs.filter(pr => {
2882
- const createdDate = pr.createdAt?.split('T')[0];
2883
- const mergedDate = pr.mergedAt?.split('T')[0];
2884
- return createdDate === yesterday || mergedDate === yesterday;
2885
- });
2886
-
2887
- if (relevantPrs.length > 0) {
2888
- console.log(`\n Pull Requests:`);
2889
- for (const pr of relevantPrs) {
2890
- const status = pr.state === 'MERGED' ? colors.green('merged') :
2891
- pr.state === 'OPEN' ? colors.yellow('open') :
2892
- colors.gray(pr.state.toLowerCase());
2893
- console.log(` #${pr.number} ${pr.title} [${status}]`);
2894
- }
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}]`);
2895
2965
  }
2896
- } catch (err) {
2897
- // gh not available or error
2898
- console.log(colors.gray(' (gh CLI not available or not authenticated)'));
2899
2966
  }
2900
- } else {
2901
- console.log(colors.gray(' (not in a GitHub repository)'));
2967
+ } catch (err) {
2968
+ // gh search error
2902
2969
  }
2903
- } catch (err) {
2904
- 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'));
2905
2974
  }
2906
2975
  }
2907
2976
 
@@ -3120,17 +3189,15 @@ PLANNING:
3120
3189
  --all, -a Include completed projects
3121
3190
 
3122
3191
  ISSUES:
3123
- issues [options] List issues (default: backlog, yours first)
3192
+ issues [options] List issues (default: backlog + todo, yours first)
3124
3193
  --unblocked, -u Show only unblocked issues
3125
3194
  --open, -o Show all non-completed/canceled issues
3126
- --backlog, -b Show only backlog issues
3195
+ --status, -s <name> Filter by status (repeatable: --status todo --status backlog)
3127
3196
  --all, -a Show all states (including completed)
3128
3197
  --mine, -m Show only issues assigned to you
3129
- --in-progress Show issues in progress
3130
3198
  --project, -p <name> Filter by project
3131
3199
  --milestone <name> Filter by milestone
3132
- --state, -s <state> Filter by state
3133
- --label, -l <name> Filter by label
3200
+ --label, -l <name> Filter by label (repeatable)
3134
3201
  --priority <level> Filter by priority (urgent/high/medium/low/none)
3135
3202
  issues reorder <ids...> Reorder issues by listing IDs in order
3136
3203
 
@@ -3145,21 +3212,25 @@ ISSUES:
3145
3212
  --assign Assign to yourself
3146
3213
  --estimate, -e <size> Estimate: XS, S, M, L, XL
3147
3214
  --priority <level> Priority: urgent, high, medium, low, none
3148
- --label, -l <name> Add label
3149
- --blocks <id> This issue blocks another
3150
- --blocked-by <id> This issue is blocked by another
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)
3151
3218
  issue update <id> [opts] Update an issue
3152
3219
  --title, -t <title> New title
3153
3220
  --description, -d <desc> New description
3154
- --state, -s <state> New state
3221
+ --status, -s <status> New status (todo, in-progress, done, backlog, etc.)
3155
3222
  --project, -p <name> Move to project
3156
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
3157
3227
  --priority <level> Set priority (urgent/high/medium/low/none)
3228
+ --label, -l <name> Set label (repeatable)
3158
3229
  --append, -a <text> Append to description
3159
3230
  --check <text> Check a checkbox item (fuzzy match)
3160
3231
  --uncheck <text> Uncheck a checkbox item (fuzzy match)
3161
- --blocks <id> Add blocking relation
3162
- --blocked-by <id> Add blocked-by relation
3232
+ --blocks <id> Add blocking relation (repeatable)
3233
+ --blocked-by <id> Add blocked-by relation (repeatable)
3163
3234
  issue close <id> Mark issue as done
3164
3235
  issue comment <id> <body> Add a comment
3165
3236
  issue move <id> Move issue in sort order
@@ -17,8 +17,8 @@ Run `linear standup` to get:
17
17
  - Issues currently in progress
18
18
  - Issues that are blocked
19
19
 
20
- **From GitHub (if in a repo):**
21
- - Commits you made yesterday
20
+ **From GitHub (across all repos):**
21
+ - Commits you made yesterday, grouped by repo
22
22
  - PRs you opened or merged yesterday
23
23
 
24
24
  ## After Running
@@ -48,8 +48,9 @@ Here's your standup summary:
48
48
  **Blocked:**
49
49
  ⊘ ISSUE-12: Waiting on API credentials
50
50
 
51
- **GitHub:**
52
- - 4 commits on beautiful-tech
51
+ **GitHub (all repos):**
52
+ - dabble/beautiful-tech: 4 commits
53
+ - dabble/linear-cli: 2 commits
53
54
  - PR #42 merged: ISSUE-5: Add caching layer
54
55
 
55
56
  [Presents options above]
@@ -59,5 +60,5 @@ Here's your standup summary:
59
60
 
60
61
  - The `linear standup` command handles all the data fetching
61
62
  - GitHub info requires the `gh` CLI to be installed and authenticated
62
- - If not in a git repo, GitHub section will be skipped
63
+ - Shows activity across all GitHub repos, not just the current one
63
64
  - Use `--no-github` flag to skip GitHub even if available
@@ -84,28 +84,36 @@ linear whoami # Show current user/team
84
84
  linear roadmap # Projects with milestones and progress
85
85
 
86
86
  # Issues
87
+ linear issues # Default: backlog + todo issues
87
88
  linear issues --unblocked # Ready to work on (no blockers)
88
89
  linear issues --open # All non-completed issues
89
- linear issues --backlog # Backlog issues only
90
- linear issues --in-progress # Issues currently in progress
90
+ linear issues --status todo # Only todo issues
91
+ linear issues --status backlog # Only backlog issues
92
+ linear issues --status in-progress # Issues currently in progress
93
+ linear issues --status todo --status in-progress # Multiple statuses
91
94
  linear issues --mine # Only your assigned issues
92
95
  linear issues --project "Name" # Issues in a project
93
96
  linear issues --milestone "M1" # Issues in a milestone
94
97
  linear issues --label bug # Filter by label
95
98
  linear issues --priority urgent # Filter by priority (urgent/high/medium/low/none)
96
- # Flags can be combined: linear issues --in-progress --mine
99
+ # Flags can be combined: linear issues --status todo --mine
97
100
  linear issue show ISSUE-1 # Full details with parent context
98
101
  linear issue start ISSUE-1 # Assign to you + set In Progress
99
102
  linear issue create --title "Fix bug" --project "Phase 1" --assign --estimate M
100
103
  linear issue create --title "Urgent bug" --priority urgent --assign
101
104
  linear issue create --title "Task" --milestone "Beta" --estimate S
102
- linear issue create --title "Blocked task" --blocked-by ISSUE-1
103
- linear issue update ISSUE-1 --state "In Progress"
105
+ linear issue create --title "Blocked task" --blocked-by ISSUE-1 --blocked-by ISSUE-2
106
+ linear issue create --title "Labeled" --label bug --label frontend # Multiple labels
107
+ linear issue update ISSUE-1 --status "In Progress"
104
108
  linear issue update ISSUE-1 --priority high # Set priority
109
+ linear issue update ISSUE-1 --estimate M # Set estimate
110
+ linear issue update ISSUE-1 --label bug --label frontend # Set labels (repeatable)
111
+ linear issue update ISSUE-1 --assign # Assign to yourself
112
+ linear issue update ISSUE-1 --parent ISSUE-2 # Set parent issue
105
113
  linear issue update ISSUE-1 --milestone "Beta"
106
114
  linear issue update ISSUE-1 --append "Notes..."
107
115
  linear issue update ISSUE-1 --check "validation" # Check off a todo item
108
- linear issue update ISSUE-1 --blocks ISSUE-2 # Add blocking relation
116
+ linear issue update ISSUE-1 --blocks ISSUE-2 --blocks ISSUE-3 # Repeatable
109
117
  linear issue close ISSUE-1
110
118
  linear issue comment ISSUE-1 "Comment text"
111
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dabble/linear-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Linear CLI with unblocked issue filtering, built for AI-assisted development",
5
5
  "type": "module",
6
6
  "bin": {