@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 +3 -1
- package/bin/linear.mjs +218 -147
- package/claude/commands/standup.md +6 -5
- package/claude/skills/linear-cli/SKILL.md +14 -6
- package/package.json +1 -1
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
|
|
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
|
-
'
|
|
325
|
+
status: 'array', s: 'array',
|
|
319
326
|
project: 'string', p: 'string',
|
|
320
327
|
milestone: 'string',
|
|
321
|
-
|
|
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
|
|
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 (
|
|
436
|
+
if (labelFilters.length > 0) {
|
|
415
437
|
filtered = filtered.filter(i =>
|
|
416
|
-
i.labels?.nodes?.some(l => l.name.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 =
|
|
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 (
|
|
477
|
-
let filtered = issues
|
|
514
|
+
} else if (resolvedStatusTypes.length > 0) {
|
|
515
|
+
let filtered = filterByStatus(issues, resolvedStatusTypes);
|
|
478
516
|
filtered = applyFilters(filtered);
|
|
479
517
|
|
|
480
|
-
|
|
481
|
-
console.log(
|
|
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
|
-
|
|
706
|
+
status: 'string', s: 'string',
|
|
678
707
|
assign: 'boolean',
|
|
679
708
|
estimate: 'string', e: 'string',
|
|
680
709
|
priority: 'string',
|
|
681
|
-
label: '
|
|
682
|
-
blocks: '
|
|
683
|
-
'blocked-by': '
|
|
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
|
|
695
|
-
const
|
|
696
|
-
const
|
|
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
|
|
797
|
+
// Look up label IDs
|
|
769
798
|
let labelIds = [];
|
|
770
|
-
if (
|
|
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
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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 (
|
|
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
|
-
|
|
828
|
-
// This issue blocks another issue
|
|
858
|
+
for (const target of blocksIssues) {
|
|
829
859
|
await gql(relationMutation, {
|
|
830
|
-
input: { issueId: issue.identifier, relatedIssueId:
|
|
860
|
+
input: { issueId: issue.identifier, relatedIssueId: target, type: 'blocks' }
|
|
831
861
|
});
|
|
832
|
-
console.log(colors.gray(` → blocks ${
|
|
862
|
+
console.log(colors.gray(` → blocks ${target}`));
|
|
833
863
|
}
|
|
834
864
|
|
|
835
|
-
|
|
836
|
-
// This issue is blocked by another issue
|
|
865
|
+
for (const target of blockedByIssues) {
|
|
837
866
|
await gql(relationMutation, {
|
|
838
|
-
input: { issueId:
|
|
867
|
+
input: { issueId: target, relatedIssueId: issue.identifier, type: 'blocks' }
|
|
839
868
|
});
|
|
840
|
-
console.log(colors.gray(` → blocked by ${
|
|
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
|
-
|
|
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: '
|
|
868
|
-
'blocked-by': '
|
|
900
|
+
blocks: 'array',
|
|
901
|
+
'blocked-by': 'array',
|
|
869
902
|
});
|
|
870
903
|
|
|
871
|
-
const
|
|
872
|
-
const
|
|
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.
|
|
964
|
-
const stateName = opts.
|
|
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 =
|
|
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
|
-
|
|
1130
|
+
for (const target of blocksIssues) {
|
|
1056
1131
|
await gql(relationMutation, {
|
|
1057
|
-
input: { issueId: issueId, relatedIssueId:
|
|
1132
|
+
input: { issueId: issueId, relatedIssueId: target, type: 'blocks' }
|
|
1058
1133
|
});
|
|
1059
|
-
console.log(colors.green(`${issueId} now blocks ${
|
|
1134
|
+
console.log(colors.green(`${issueId} now blocks ${target}`));
|
|
1060
1135
|
}
|
|
1061
1136
|
|
|
1062
|
-
|
|
1137
|
+
for (const target of blockedByIssues) {
|
|
1063
1138
|
await gql(relationMutation, {
|
|
1064
|
-
input: { issueId:
|
|
1139
|
+
input: { issueId: target, relatedIssueId: issueId, type: 'blocks' }
|
|
1065
1140
|
});
|
|
1066
|
-
console.log(colors.green(`${issueId} now blocked by ${
|
|
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
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
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
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
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
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
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
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
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
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
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
|
-
}
|
|
2901
|
-
|
|
2967
|
+
} catch (err) {
|
|
2968
|
+
// gh search error
|
|
2902
2969
|
}
|
|
2903
|
-
}
|
|
2904
|
-
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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 (
|
|
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
|
|
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
|
-
-
|
|
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 --
|
|
90
|
-
linear issues --
|
|
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 --
|
|
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
|
|
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 #
|
|
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
|
|