@dabble/linear-cli 1.1.0 → 1.1.2

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
@@ -12,9 +12,12 @@ import { exec, execSync } from 'child_process';
12
12
 
13
13
  const API_URL = 'https://api.linear.app/graphql';
14
14
  let CONFIG_FILE = '';
15
+ let AUTH_CONFIG_FILE = '';
15
16
  let LINEAR_API_KEY = '';
16
17
  let TEAM_KEY = '';
17
18
  let ALIASES = {};
19
+ let DEFAULT_PROJECT = '';
20
+ let DEFAULT_MILESTONE = '';
18
21
 
19
22
  // Colors (ANSI)
20
23
  const colors = {
@@ -34,16 +37,16 @@ function loadConfig() {
34
37
  const localPath = join(process.cwd(), '.linear');
35
38
  const globalPath = join(homedir(), '.linear');
36
39
 
37
- // Priority: ./.linear > ~/.linear > env vars
38
- if (existsSync(localPath)) {
39
- CONFIG_FILE = localPath;
40
- } else if (existsSync(globalPath)) {
41
- CONFIG_FILE = globalPath;
42
- }
40
+ // Layer: read global first, then local on top (local values override global)
41
+ const filesToLoad = [];
42
+ if (existsSync(globalPath)) filesToLoad.push(globalPath);
43
+ if (existsSync(localPath)) filesToLoad.push(localPath);
43
44
 
44
- // Load from config file first (highest priority)
45
- if (CONFIG_FILE) {
46
- const content = readFileSync(CONFIG_FILE, 'utf-8');
45
+ // CONFIG_FILE is used for local/project writes (open/close) — always local
46
+ CONFIG_FILE = localPath;
47
+
48
+ for (const filePath of filesToLoad) {
49
+ const content = readFileSync(filePath, 'utf-8');
47
50
  let inAliasSection = false;
48
51
 
49
52
  for (const line of content.split('\n')) {
@@ -64,11 +67,17 @@ function loadConfig() {
64
67
  const value = rest.join('=').trim();
65
68
 
66
69
  if (inAliasSection) {
67
- // Store aliases with uppercase keys
70
+ // Store aliases with uppercase keys (later files override)
68
71
  ALIASES[key.trim().toUpperCase()] = value;
69
72
  } else {
70
- if (key.trim() === 'api_key') LINEAR_API_KEY = value;
71
- if (key.trim() === 'team') TEAM_KEY = value;
73
+ const k = key.trim();
74
+ if (k === 'api_key') {
75
+ LINEAR_API_KEY = value;
76
+ AUTH_CONFIG_FILE = filePath;
77
+ }
78
+ if (k === 'team') TEAM_KEY = value;
79
+ if (k === 'project') DEFAULT_PROJECT = value;
80
+ if (k === 'milestone') DEFAULT_MILESTONE = value;
72
81
  }
73
82
  }
74
83
  }
@@ -76,6 +85,8 @@ function loadConfig() {
76
85
  // Fall back to env vars if not set by config file
77
86
  if (!LINEAR_API_KEY) LINEAR_API_KEY = process.env.LINEAR_API_KEY || '';
78
87
  if (!TEAM_KEY) TEAM_KEY = process.env.LINEAR_TEAM || '';
88
+ if (!DEFAULT_PROJECT) DEFAULT_PROJECT = process.env.LINEAR_PROJECT || '';
89
+ if (!DEFAULT_MILESTONE) DEFAULT_MILESTONE = process.env.LINEAR_MILESTONE || '';
79
90
  }
80
91
 
81
92
  function resolveAlias(nameOrAlias) {
@@ -83,13 +94,39 @@ function resolveAlias(nameOrAlias) {
83
94
  return ALIASES[nameOrAlias.toUpperCase()] || nameOrAlias;
84
95
  }
85
96
 
86
- function saveAlias(code, name) {
87
- if (!CONFIG_FILE) {
88
- console.error(colors.red('Error: No config file found. Run "linear login" first.'));
97
+ async function ensureAuthConfig() {
98
+ if (AUTH_CONFIG_FILE) return;
99
+
100
+ console.log('No config file found. Where should aliases be saved?\n');
101
+ console.log(' 1. This project only (./.linear)');
102
+ console.log(' 2. Global, for all projects (~/.linear)');
103
+ console.log('');
104
+
105
+ const choice = await prompt('Enter number: ');
106
+ if (choice !== '1' && choice !== '2') {
107
+ console.error(colors.red('Error: Please enter 1 or 2'));
89
108
  process.exit(1);
90
109
  }
91
110
 
92
- const content = readFileSync(CONFIG_FILE, 'utf-8');
111
+ if (choice === '1') {
112
+ AUTH_CONFIG_FILE = join(process.cwd(), '.linear');
113
+ if (!existsSync(AUTH_CONFIG_FILE)) {
114
+ writeFileSync(AUTH_CONFIG_FILE, '');
115
+ ensureGitignore();
116
+ }
117
+ } else {
118
+ AUTH_CONFIG_FILE = join(homedir(), '.linear');
119
+ if (!existsSync(AUTH_CONFIG_FILE)) {
120
+ writeFileSync(AUTH_CONFIG_FILE, '');
121
+ }
122
+ }
123
+ console.log('');
124
+ }
125
+
126
+ async function saveAlias(code, name) {
127
+ await ensureAuthConfig();
128
+
129
+ const content = readFileSync(AUTH_CONFIG_FILE, 'utf-8');
93
130
  const lines = content.split('\n');
94
131
 
95
132
  // Find or create [aliases] section
@@ -136,12 +173,12 @@ function saveAlias(code, name) {
136
173
  lines.push(aliasLine);
137
174
  }
138
175
 
139
- writeFileSync(CONFIG_FILE, lines.join('\n'));
176
+ writeFileSync(AUTH_CONFIG_FILE, lines.join('\n'));
140
177
  ALIASES[upperCode] = name;
141
178
  }
142
179
 
143
- function removeAlias(code) {
144
- if (!CONFIG_FILE) {
180
+ async function removeAlias(code) {
181
+ if (!AUTH_CONFIG_FILE) {
145
182
  console.error(colors.red('Error: No config file found.'));
146
183
  process.exit(1);
147
184
  }
@@ -152,7 +189,7 @@ function removeAlias(code) {
152
189
  process.exit(1);
153
190
  }
154
191
 
155
- const content = readFileSync(CONFIG_FILE, 'utf-8');
192
+ const content = readFileSync(AUTH_CONFIG_FILE, 'utf-8');
156
193
  const lines = content.split('\n');
157
194
  let inAliasSection = false;
158
195
 
@@ -175,10 +212,100 @@ function removeAlias(code) {
175
212
  return true;
176
213
  });
177
214
 
178
- writeFileSync(CONFIG_FILE, newLines.join('\n'));
215
+ writeFileSync(AUTH_CONFIG_FILE, newLines.join('\n'));
179
216
  delete ALIASES[upperCode];
180
217
  }
181
218
 
219
+ function ensureLocalConfig() {
220
+ // CONFIG_FILE always points to local ./.linear
221
+ if (existsSync(CONFIG_FILE)) return;
222
+
223
+ // Create local config file
224
+ writeFileSync(CONFIG_FILE, '');
225
+
226
+ // Add .linear to .gitignore
227
+ ensureGitignore();
228
+ }
229
+
230
+ function ensureGitignore() {
231
+ const gitignorePath = join(process.cwd(), '.gitignore');
232
+ try {
233
+ let gitignore = '';
234
+ if (existsSync(gitignorePath)) {
235
+ gitignore = readFileSync(gitignorePath, 'utf-8');
236
+ }
237
+
238
+ const lines = gitignore.split('\n').map(l => l.trim());
239
+ if (!lines.includes('.linear')) {
240
+ const newline = gitignore.endsWith('\n') || gitignore === '' ? '' : '\n';
241
+ writeFileSync(gitignorePath, gitignore + newline + '.linear\n');
242
+ console.log(colors.green(`Added .linear to .gitignore`));
243
+ }
244
+ } catch (err) {
245
+ // Silently ignore if we can't update .gitignore
246
+ }
247
+ }
248
+
249
+ function writeConfigValue(key, value) {
250
+ ensureLocalConfig();
251
+
252
+ const content = readFileSync(CONFIG_FILE, 'utf-8');
253
+ const lines = content.split('\n');
254
+ let found = false;
255
+ let insertBefore = -1;
256
+
257
+ for (let i = 0; i < lines.length; i++) {
258
+ const trimmed = lines[i].trim();
259
+ // Stop before section headers
260
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
261
+ if (insertBefore === -1) insertBefore = i;
262
+ break;
263
+ }
264
+ if (!trimmed || trimmed.startsWith('#')) continue;
265
+ const [k] = trimmed.split('=');
266
+ if (k.trim() === key) {
267
+ lines[i] = `${key}=${value}`;
268
+ found = true;
269
+ break;
270
+ }
271
+ }
272
+
273
+ if (!found) {
274
+ const newLine = `${key}=${value}`;
275
+ if (insertBefore !== -1) {
276
+ lines.splice(insertBefore, 0, newLine);
277
+ } else {
278
+ lines.push(newLine);
279
+ }
280
+ }
281
+
282
+ writeFileSync(CONFIG_FILE, lines.join('\n'));
283
+ }
284
+
285
+ function removeConfigValue(key) {
286
+ if (!existsSync(CONFIG_FILE)) return null;
287
+
288
+ const content = readFileSync(CONFIG_FILE, 'utf-8');
289
+ const lines = content.split('\n');
290
+ let removed = null;
291
+
292
+ const newLines = lines.filter(line => {
293
+ const trimmed = line.trim();
294
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('[')) return true;
295
+ const [k, ...rest] = trimmed.split('=');
296
+ if (k.trim() === key) {
297
+ removed = rest.join('=').trim();
298
+ return false;
299
+ }
300
+ return true;
301
+ });
302
+
303
+ if (removed !== null) {
304
+ writeFileSync(CONFIG_FILE, newLines.join('\n'));
305
+ }
306
+ return removed;
307
+ }
308
+
182
309
  function checkAuth() {
183
310
  if (!LINEAR_API_KEY) {
184
311
  console.error(colors.red("Error: Not logged in. Run 'linear login' first."));
@@ -288,6 +415,14 @@ function parseArgs(args, flags = {}) {
288
415
  const flagDef = flags[key];
289
416
  if (flagDef === 'boolean') {
290
417
  result[key] = true;
418
+ } else if (flagDef === 'array') {
419
+ const value = args[++i];
420
+ if (value === undefined || value.startsWith('-')) {
421
+ console.error(colors.red(`Error: --${key} requires a value`));
422
+ process.exit(1);
423
+ }
424
+ result[key] = result[key] || [];
425
+ result[key].push(value);
291
426
  } else {
292
427
  const value = args[++i];
293
428
  if (value === undefined || value.startsWith('-')) {
@@ -313,28 +448,46 @@ async function cmdIssues(args) {
313
448
  unblocked: 'boolean', u: 'boolean',
314
449
  all: 'boolean', a: 'boolean',
315
450
  open: 'boolean', o: 'boolean',
316
- backlog: 'boolean', b: 'boolean',
317
451
  mine: 'boolean', m: 'boolean',
318
- 'in-progress': 'boolean',
452
+ status: 'array', s: 'array',
319
453
  project: 'string', p: 'string',
320
454
  milestone: 'string',
321
- state: 'string', s: 'string',
322
- label: 'string', l: 'string',
455
+ 'no-project': 'boolean',
456
+ 'no-milestone': 'boolean',
457
+ label: 'array', l: 'array',
323
458
  priority: 'string',
324
459
  });
325
460
 
326
- const inProgress = opts['in-progress'];
327
461
  const unblocked = opts.unblocked || opts.u;
328
462
  const allStates = opts.all || opts.a;
329
463
  const openOnly = opts.open || opts.o;
330
- const backlogOnly = opts.backlog || opts.b;
331
464
  const mineOnly = opts.mine || opts.m;
332
- const projectFilter = opts.project || opts.p;
333
- const milestoneFilter = opts.milestone;
334
- const stateFilter = opts.state || opts.s;
335
- const labelFilter = opts.label || opts.l;
465
+ const statusFilter = opts.status || opts.s || [];
466
+ const noProject = opts['no-project'];
467
+ const noMilestone = opts['no-milestone'];
468
+ const projectFilter = noProject ? '' : (opts.project || opts.p || DEFAULT_PROJECT);
469
+ const milestoneFilter = noMilestone ? '' : (opts.milestone || DEFAULT_MILESTONE);
470
+ const labelFilters = opts.label || opts.l || [];
336
471
  const priorityFilter = (opts.priority || '').toLowerCase();
337
472
 
473
+ // Map user-friendly status names to Linear's internal state types
474
+ const STATUS_TYPE_MAP = {
475
+ 'backlog': 'backlog',
476
+ 'todo': 'unstarted',
477
+ 'in-progress': 'started',
478
+ 'inprogress': 'started',
479
+ 'in_progress': 'started',
480
+ 'started': 'started',
481
+ 'done': 'completed',
482
+ 'completed': 'completed',
483
+ 'canceled': 'canceled',
484
+ 'cancelled': 'canceled',
485
+ 'triage': 'triage',
486
+ };
487
+
488
+ // Resolve status filters to state types (match by type map or by state name)
489
+ const resolvedStatusTypes = statusFilter.map(s => STATUS_TYPE_MAP[s.toLowerCase()] || s.toLowerCase());
490
+
338
491
  // Get current user ID for filtering/sorting
339
492
  const viewerResult = await gql('{ viewer { id } }');
340
493
  const viewerId = viewerResult.data?.viewer?.id;
@@ -386,6 +539,15 @@ async function cmdIssues(args) {
386
539
  // Check if any issues have priority set
387
540
  const hasPriority = issues.some(i => i.priority > 0);
388
541
 
542
+ // When filtering to a single project, drop the project column
543
+ const showProjectCol = !projectFilter;
544
+
545
+ // Build context string for header (e.g. "[My Project > Sprint 3]")
546
+ const contextParts = [];
547
+ if (projectFilter) contextParts.push(resolveAlias(projectFilter));
548
+ if (milestoneFilter) contextParts.push(resolveAlias(milestoneFilter));
549
+ const contextStr = contextParts.length > 0 ? ` [${contextParts.join(' > ')}]` : '';
550
+
389
551
  // Helper to format issue row
390
552
  const formatRow = (i) => {
391
553
  const row = [
@@ -397,7 +559,9 @@ async function cmdIssues(args) {
397
559
  const pri = PRIORITY_LABELS[i.priority] || '';
398
560
  row.push(pri ? colors.bold(pri) : '-');
399
561
  }
400
- row.push(i.project?.name || '-');
562
+ if (showProjectCol) {
563
+ row.push(i.project?.name || '-');
564
+ }
401
565
  if (hasAssignees) {
402
566
  const assignee = i.assignee?.id === viewerId ? 'you' : (i.assignee?.name || '-');
403
567
  row.push(assignee);
@@ -411,9 +575,9 @@ async function cmdIssues(args) {
411
575
  if (mineOnly) {
412
576
  filtered = filtered.filter(i => i.assignee?.id === viewerId);
413
577
  }
414
- if (labelFilter) {
578
+ if (labelFilters.length > 0) {
415
579
  filtered = filtered.filter(i =>
416
- i.labels?.nodes?.some(l => l.name.toLowerCase() === labelFilter.toLowerCase())
580
+ labelFilters.some(lf => i.labels?.nodes?.some(l => l.name.toLowerCase() === lf.toLowerCase()))
417
581
  );
418
582
  }
419
583
  if (projectFilter) {
@@ -437,6 +601,13 @@ async function cmdIssues(args) {
437
601
  return filtered;
438
602
  };
439
603
 
604
+ // Apply status filter to issues
605
+ const filterByStatus = (list, types) => {
606
+ return list.filter(i =>
607
+ types.includes(i.state.type) || types.includes(i.state.name.toLowerCase())
608
+ );
609
+ };
610
+
440
611
  if (unblocked) {
441
612
  // Collect all blocked issue IDs
442
613
  const blocked = new Set();
@@ -453,48 +624,48 @@ async function cmdIssues(args) {
453
624
  !['completed', 'canceled'].includes(i.state.type) &&
454
625
  !blocked.has(i.identifier)
455
626
  );
627
+ if (resolvedStatusTypes.length > 0) {
628
+ filtered = filterByStatus(filtered, resolvedStatusTypes);
629
+ }
456
630
 
457
631
  filtered = applyFilters(filtered);
458
632
 
459
- console.log(colors.bold('Unblocked Issues:\n'));
633
+ console.log(colors.bold(`Unblocked Issues${contextStr}:\n`));
460
634
  console.log(formatTable(filtered.map(formatRow)));
461
635
  } else if (allStates) {
462
- let filtered = applyFilters(issues);
636
+ let filtered = issues;
637
+ if (resolvedStatusTypes.length > 0) {
638
+ filtered = filterByStatus(filtered, resolvedStatusTypes);
639
+ }
640
+ filtered = applyFilters(filtered);
463
641
 
464
- console.log(colors.bold('All Issues:\n'));
642
+ console.log(colors.bold(`All Issues${contextStr}:\n`));
465
643
  console.log(formatTable(filtered.map(formatRow)));
466
644
  } else if (openOnly) {
467
- // Open = everything except completed/canceled
468
645
  let filtered = issues.filter(i =>
469
646
  !['completed', 'canceled'].includes(i.state.type)
470
647
  );
648
+ if (resolvedStatusTypes.length > 0) {
649
+ filtered = filterByStatus(filtered, resolvedStatusTypes);
650
+ }
471
651
 
472
652
  filtered = applyFilters(filtered);
473
653
 
474
- console.log(colors.bold('Open Issues:\n'));
654
+ console.log(colors.bold(`Open Issues${contextStr}:\n`));
475
655
  console.log(formatTable(filtered.map(formatRow)));
476
- } else if (inProgress) {
477
- let filtered = issues.filter(i => i.state.type === 'started');
478
- filtered = applyFilters(filtered);
479
-
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
-
656
+ } else if (resolvedStatusTypes.length > 0) {
657
+ let filtered = filterByStatus(issues, resolvedStatusTypes);
488
658
  filtered = applyFilters(filtered);
489
659
 
490
- console.log(colors.bold(`Issues (${targetState}):\n`));
660
+ const label = statusFilter.join(' + ');
661
+ console.log(colors.bold(`Issues${contextStr} (${label}):\n`));
491
662
  console.log(formatTable(filtered.map(formatRow)));
492
663
  } else {
493
- // Default: show backlog
494
- let filtered = issues.filter(i => i.state.type === 'backlog');
664
+ // Default: show backlog + todo
665
+ let filtered = issues.filter(i => i.state.type === 'backlog' || i.state.type === 'unstarted');
495
666
  filtered = applyFilters(filtered);
496
667
 
497
- console.log(colors.bold('Issues (backlog):\n'));
668
+ console.log(colors.bold(`Issues${contextStr} (backlog + todo):\n`));
498
669
  console.log(formatTable(filtered.map(formatRow)));
499
670
  }
500
671
  }
@@ -674,26 +845,26 @@ async function cmdIssueCreate(args) {
674
845
  project: 'string', p: 'string',
675
846
  milestone: 'string',
676
847
  parent: 'string',
677
- state: 'string', s: 'string',
848
+ status: 'string', s: 'string',
678
849
  assign: 'boolean',
679
850
  estimate: 'string', e: 'string',
680
851
  priority: 'string',
681
- label: 'string', l: 'string',
682
- blocks: 'string',
683
- 'blocked-by': 'string',
852
+ label: 'array', l: 'array',
853
+ blocks: 'array',
854
+ 'blocked-by': 'array',
684
855
  });
685
856
 
686
857
  const title = opts.title || opts.t || opts._[0];
687
858
  const description = opts.description || opts.d || '';
688
- const project = resolveAlias(opts.project || opts.p);
859
+ const project = resolveAlias(opts.project || opts.p) || DEFAULT_PROJECT;
689
860
  const priority = (opts.priority || '').toLowerCase();
690
- const milestone = resolveAlias(opts.milestone);
861
+ const milestone = resolveAlias(opts.milestone) || DEFAULT_MILESTONE;
691
862
  const parent = opts.parent;
692
863
  const shouldAssign = opts.assign;
693
864
  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'];
865
+ const labelNames = opts.label || opts.l || [];
866
+ const blocksIssues = opts.blocks || [];
867
+ const blockedByIssues = opts['blocked-by'] || [];
697
868
 
698
869
  if (!title) {
699
870
  console.error(colors.red('Error: Title is required'));
@@ -765,20 +936,22 @@ async function cmdIssueCreate(args) {
765
936
  }
766
937
  }
767
938
 
768
- // Look up label ID
939
+ // Look up label IDs
769
940
  let labelIds = [];
770
- if (labelName) {
941
+ if (labelNames.length > 0) {
771
942
  const labelsResult = await gql(`{
772
943
  team(id: "${TEAM_KEY}") {
773
944
  labels(first: 100) { nodes { id name } }
774
945
  }
775
946
  }`);
776
947
  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.`));
948
+ for (const labelName of labelNames) {
949
+ const match = labels.find(l => l.name.toLowerCase() === labelName.toLowerCase());
950
+ if (match) {
951
+ labelIds.push(match.id);
952
+ } else {
953
+ console.error(colors.yellow(`Warning: Label "${labelName}" not found.`));
954
+ }
782
955
  }
783
956
  }
784
957
 
@@ -817,27 +990,25 @@ async function cmdIssueCreate(args) {
817
990
  console.log(issue.url);
818
991
 
819
992
  // Create blocking relations if specified
820
- if (blocksIssue || blockedByIssue) {
993
+ if (blocksIssues.length > 0 || blockedByIssues.length > 0) {
821
994
  const relationMutation = `
822
995
  mutation($input: IssueRelationCreateInput!) {
823
996
  issueRelationCreate(input: $input) { success }
824
997
  }
825
998
  `;
826
999
 
827
- if (blocksIssue) {
828
- // This issue blocks another issue
1000
+ for (const target of blocksIssues) {
829
1001
  await gql(relationMutation, {
830
- input: { issueId: issue.identifier, relatedIssueId: blocksIssue, type: 'blocks' }
1002
+ input: { issueId: issue.identifier, relatedIssueId: target, type: 'blocks' }
831
1003
  });
832
- console.log(colors.gray(` → blocks ${blocksIssue}`));
1004
+ console.log(colors.gray(` → blocks ${target}`));
833
1005
  }
834
1006
 
835
- if (blockedByIssue) {
836
- // This issue is blocked by another issue
1007
+ for (const target of blockedByIssues) {
837
1008
  await gql(relationMutation, {
838
- input: { issueId: blockedByIssue, relatedIssueId: issue.identifier, type: 'blocks' }
1009
+ input: { issueId: target, relatedIssueId: issue.identifier, type: 'blocks' }
839
1010
  });
840
- console.log(colors.gray(` → blocked by ${blockedByIssue}`));
1011
+ console.log(colors.gray(` → blocked by ${target}`));
841
1012
  }
842
1013
  }
843
1014
  } else {
@@ -857,26 +1028,52 @@ async function cmdIssueUpdate(args) {
857
1028
  const opts = parseArgs(args.slice(1), {
858
1029
  title: 'string', t: 'string',
859
1030
  description: 'string', d: 'string',
860
- state: 'string', s: 'string',
1031
+ status: 'string', s: 'string',
861
1032
  project: 'string', p: 'string',
862
1033
  milestone: 'string',
863
1034
  priority: 'string',
1035
+ estimate: 'string', e: 'string',
1036
+ label: 'array', l: 'array',
1037
+ assign: 'boolean',
1038
+ parent: 'string',
864
1039
  append: 'string', a: 'string',
865
1040
  check: 'string',
866
1041
  uncheck: 'string',
867
- blocks: 'string',
868
- 'blocked-by': 'string',
1042
+ blocks: 'array',
1043
+ 'blocked-by': 'array',
869
1044
  });
870
1045
 
871
- const blocksIssue = opts.blocks;
872
- const blockedByIssue = opts['blocked-by'];
1046
+ const blocksIssues = opts.blocks || [];
1047
+ const blockedByIssues = opts['blocked-by'] || [];
873
1048
  const projectName = resolveAlias(opts.project || opts.p);
874
1049
  const milestoneName = resolveAlias(opts.milestone);
875
1050
  const priorityName = (opts.priority || '').toLowerCase();
1051
+ const estimate = (opts.estimate || opts.e || '').toLowerCase();
1052
+ const labelNames = opts.label || opts.l || [];
1053
+ const shouldAssign = opts.assign;
1054
+ const parent = opts.parent;
876
1055
  const input = {};
877
1056
 
878
1057
  if (opts.title || opts.t) input.title = opts.title || opts.t;
879
1058
 
1059
+ // Handle estimate
1060
+ if (estimate) {
1061
+ if (!ESTIMATE_MAP.hasOwnProperty(estimate)) {
1062
+ console.error(colors.red(`Error: Invalid estimate "${estimate}". Use: XS, S, M, L, or XL`));
1063
+ process.exit(1);
1064
+ }
1065
+ input.estimate = ESTIMATE_MAP[estimate];
1066
+ }
1067
+
1068
+ // Handle parent
1069
+ if (parent) input.parentId = parent;
1070
+
1071
+ // Handle assign
1072
+ if (shouldAssign) {
1073
+ const viewerResult = await gql('{ viewer { id } }');
1074
+ input.assigneeId = viewerResult.data?.viewer?.id;
1075
+ }
1076
+
880
1077
  // Handle priority
881
1078
  if (priorityName) {
882
1079
  if (!PRIORITY_MAP.hasOwnProperty(priorityName)) {
@@ -960,8 +1157,8 @@ async function cmdIssueUpdate(args) {
960
1157
  }
961
1158
 
962
1159
  // Handle state
963
- if (opts.state || opts.s) {
964
- const stateName = opts.state || opts.s;
1160
+ if (opts.status || opts.s) {
1161
+ const stateName = opts.status || opts.s;
965
1162
  const statesResult = await gql(`{
966
1163
  team(id: "${TEAM_KEY}") {
967
1164
  states { nodes { id name } }
@@ -972,6 +1169,26 @@ async function cmdIssueUpdate(args) {
972
1169
  if (match) input.stateId = match.id;
973
1170
  }
974
1171
 
1172
+ // Handle labels
1173
+ if (labelNames.length > 0) {
1174
+ const labelsResult = await gql(`{
1175
+ team(id: "${TEAM_KEY}") {
1176
+ labels(first: 100) { nodes { id name } }
1177
+ }
1178
+ }`);
1179
+ const labels = labelsResult.data?.team?.labels?.nodes || [];
1180
+ const labelIds = [];
1181
+ for (const labelName of labelNames) {
1182
+ const match = labels.find(l => l.name.toLowerCase() === labelName.toLowerCase());
1183
+ if (match) {
1184
+ labelIds.push(match.id);
1185
+ } else {
1186
+ console.error(colors.yellow(`Warning: Label "${labelName}" not found.`));
1187
+ }
1188
+ }
1189
+ if (labelIds.length > 0) input.labelIds = labelIds;
1190
+ }
1191
+
975
1192
  // Handle project and milestone
976
1193
  if (projectName || milestoneName) {
977
1194
  const projectsResult = await gql(`{
@@ -1013,7 +1230,7 @@ async function cmdIssueUpdate(args) {
1013
1230
  }
1014
1231
 
1015
1232
  // Handle blocking relations (can be set even without other updates)
1016
- const hasRelationUpdates = blocksIssue || blockedByIssue;
1233
+ const hasRelationUpdates = blocksIssues.length > 0 || blockedByIssues.length > 0;
1017
1234
 
1018
1235
  if (Object.keys(input).length === 0 && !hasRelationUpdates) {
1019
1236
  console.error(colors.red('Error: No updates specified'));
@@ -1052,18 +1269,18 @@ async function cmdIssueUpdate(args) {
1052
1269
  }
1053
1270
  `;
1054
1271
 
1055
- if (blocksIssue) {
1272
+ for (const target of blocksIssues) {
1056
1273
  await gql(relationMutation, {
1057
- input: { issueId: issueId, relatedIssueId: blocksIssue, type: 'blocks' }
1274
+ input: { issueId: issueId, relatedIssueId: target, type: 'blocks' }
1058
1275
  });
1059
- console.log(colors.green(`${issueId} now blocks ${blocksIssue}`));
1276
+ console.log(colors.green(`${issueId} now blocks ${target}`));
1060
1277
  }
1061
1278
 
1062
- if (blockedByIssue) {
1279
+ for (const target of blockedByIssues) {
1063
1280
  await gql(relationMutation, {
1064
- input: { issueId: blockedByIssue, relatedIssueId: issueId, type: 'blocks' }
1281
+ input: { issueId: target, relatedIssueId: issueId, type: 'blocks' }
1065
1282
  });
1066
- console.log(colors.green(`${issueId} now blocked by ${blockedByIssue}`));
1283
+ console.log(colors.green(`${issueId} now blocked by ${target}`));
1067
1284
  }
1068
1285
  }
1069
1286
  }
@@ -2275,7 +2492,7 @@ async function cmdAlias(args) {
2275
2492
 
2276
2493
  // Remove alias
2277
2494
  if (removeCode) {
2278
- removeAlias(removeCode);
2495
+ await removeAlias(removeCode);
2279
2496
  console.log(colors.green(`Removed alias: ${removeCode.toUpperCase()}`));
2280
2497
  return;
2281
2498
  }
@@ -2289,7 +2506,7 @@ async function cmdAlias(args) {
2289
2506
  process.exit(1);
2290
2507
  }
2291
2508
 
2292
- saveAlias(code, name);
2509
+ await saveAlias(code, name);
2293
2510
  console.log(colors.green(`Alias set: ${code.toUpperCase()} -> ${name}`));
2294
2511
  }
2295
2512
 
@@ -2722,7 +2939,6 @@ async function cmdStandup(args) {
2722
2939
 
2723
2940
  const skipGitHub = opts['no-github'];
2724
2941
  const yesterday = getYesterdayDate();
2725
- const today = getTodayDate();
2726
2942
 
2727
2943
  // Get current user
2728
2944
  const viewerResult = await gql('{ viewer { id name } }');
@@ -2815,93 +3031,88 @@ async function cmdStandup(args) {
2815
3031
  }
2816
3032
  }
2817
3033
 
2818
- // GitHub activity
3034
+ // GitHub activity (cross-repo)
2819
3035
  if (!skipGitHub) {
2820
3036
  console.log('');
2821
3037
  console.log(colors.gray(`─────────────────────────────────────────\n`));
2822
3038
  console.log(colors.bold('GitHub Activity (yesterday):'));
2823
3039
 
3040
+ let hasActivity = false;
3041
+ let ghAvailable = true;
3042
+
3043
+ // Get commits across all repos
2824
3044
  try {
2825
- // Get commits from yesterday
2826
- const sinceDate = `${yesterday}T00:00:00`;
2827
- const untilDate = `${today}T00:00:00`;
3045
+ const commitsJson = execSync(
3046
+ `gh search commits --author=@me --committer-date=${yesterday} --json sha,commit,repository --limit 50`,
3047
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
3048
+ );
3049
+ const commits = JSON.parse(commitsJson);
3050
+
3051
+ if (commits.length > 0) {
3052
+ hasActivity = true;
3053
+ const byRepo = {};
3054
+ for (const c of commits) {
3055
+ const repo = c.repository?.fullName || 'unknown';
3056
+ if (!byRepo[repo]) byRepo[repo] = [];
3057
+ const msg = c.commit?.message?.split('\n')[0] || c.sha.slice(0, 7);
3058
+ byRepo[repo].push(`${c.sha.slice(0, 7)} ${msg}`);
3059
+ }
2828
3060
 
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];
3061
+ console.log(`\n Commits (${commits.length}):`);
3062
+ for (const [repo, repoCommits] of Object.entries(byRepo)) {
3063
+ console.log(` ${colors.bold(repo)} (${repoCommits.length}):`);
3064
+ for (const commit of repoCommits) {
3065
+ console.log(` ${commit}`);
3066
+ }
2837
3067
  }
2838
- } catch (err) {
2839
- // Not in a git repo or no origin
2840
3068
  }
3069
+ } catch (err) {
3070
+ ghAvailable = false;
3071
+ console.log(colors.gray(' (gh CLI not available - install gh for GitHub activity)'));
3072
+ }
2841
3073
 
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
- }
3074
+ // Get PRs across all repos
3075
+ if (ghAvailable) {
3076
+ try {
3077
+ const mergedJson = execSync(
3078
+ `gh search prs --author=@me --merged-at=${yesterday} --json number,title,repository --limit 20`,
3079
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
3080
+ );
3081
+ const mergedPrs = JSON.parse(mergedJson).map(pr => ({ ...pr, prStatus: 'merged' }));
2850
3082
 
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
- }
3083
+ const createdJson = execSync(
3084
+ `gh search prs --author=@me --created=${yesterday} --state=open --json number,title,repository --limit 20`,
3085
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
3086
+ );
3087
+ const createdPrs = JSON.parse(createdJson).map(pr => ({ ...pr, prStatus: 'open' }));
3088
+
3089
+ // Deduplicate (a PR created and merged same day appears in both)
3090
+ const seen = new Set();
3091
+ const allPrs = [];
3092
+ for (const pr of [...mergedPrs, ...createdPrs]) {
3093
+ const key = `${pr.repository?.fullName}#${pr.number}`;
3094
+ if (!seen.has(key)) {
3095
+ seen.add(key);
3096
+ allPrs.push(pr);
2867
3097
  }
2868
- } catch (err) {
2869
- // No commits or git error
2870
3098
  }
2871
3099
 
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
- }
3100
+ if (allPrs.length > 0) {
3101
+ hasActivity = true;
3102
+ console.log(`\n Pull Requests:`);
3103
+ for (const pr of allPrs) {
3104
+ const repo = pr.repository?.name || '';
3105
+ const status = pr.prStatus === 'merged' ? colors.green('merged') : colors.yellow('open');
3106
+ console.log(` ${colors.gray(repo + '#')}${pr.number} ${pr.title} [${status}]`);
2895
3107
  }
2896
- } catch (err) {
2897
- // gh not available or error
2898
- console.log(colors.gray(' (gh CLI not available or not authenticated)'));
2899
3108
  }
2900
- } else {
2901
- console.log(colors.gray(' (not in a GitHub repository)'));
3109
+ } catch (err) {
3110
+ // gh search error
2902
3111
  }
2903
- } catch (err) {
2904
- console.log(colors.gray(` Error fetching GitHub data: ${err.message}`));
3112
+ }
3113
+
3114
+ if (!hasActivity && ghAvailable) {
3115
+ console.log(colors.gray(' No GitHub activity yesterday'));
2905
3116
  }
2906
3117
  }
2907
3118
 
@@ -3024,25 +3235,7 @@ team=${selectedKey}
3024
3235
 
3025
3236
  // Add .linear to .gitignore if saving locally
3026
3237
  if (!saveGlobal) {
3027
- const gitignorePath = join(process.cwd(), '.gitignore');
3028
- try {
3029
- let gitignore = '';
3030
- if (existsSync(gitignorePath)) {
3031
- gitignore = readFileSync(gitignorePath, 'utf-8');
3032
- }
3033
-
3034
- // Check if .linear is already in .gitignore
3035
- const lines = gitignore.split('\n').map(l => l.trim());
3036
- if (!lines.includes('.linear')) {
3037
- // Add .linear to .gitignore
3038
- const newline = gitignore.endsWith('\n') || gitignore === '' ? '' : '\n';
3039
- const content = gitignore + newline + '.linear\n';
3040
- writeFileSync(gitignorePath, content);
3041
- console.log(colors.green(`Added .linear to .gitignore`));
3042
- }
3043
- } catch (err) {
3044
- // Silently ignore if we can't update .gitignore
3045
- }
3238
+ ensureGitignore();
3046
3239
 
3047
3240
  // Add .linear to .worktreeinclude for worktree support
3048
3241
  const worktreeIncludePath = join(process.cwd(), '.worktreeinclude');
@@ -3120,17 +3313,17 @@ PLANNING:
3120
3313
  --all, -a Include completed projects
3121
3314
 
3122
3315
  ISSUES:
3123
- issues [options] List issues (default: backlog, yours first)
3316
+ issues [options] List issues (default: backlog + todo, yours first)
3124
3317
  --unblocked, -u Show only unblocked issues
3125
3318
  --open, -o Show all non-completed/canceled issues
3126
- --backlog, -b Show only backlog issues
3319
+ --status, -s <name> Filter by status (repeatable: --status todo --status backlog)
3127
3320
  --all, -a Show all states (including completed)
3128
3321
  --mine, -m Show only issues assigned to you
3129
- --in-progress Show issues in progress
3130
- --project, -p <name> Filter by project
3131
- --milestone <name> Filter by milestone
3132
- --state, -s <state> Filter by state
3133
- --label, -l <name> Filter by label
3322
+ --project, -p <name> Filter by project (default: open project)
3323
+ --milestone <name> Filter by milestone (default: open milestone)
3324
+ --no-project Bypass default project filter
3325
+ --no-milestone Bypass default milestone filter
3326
+ --label, -l <name> Filter by label (repeatable)
3134
3327
  --priority <level> Filter by priority (urgent/high/medium/low/none)
3135
3328
  issues reorder <ids...> Reorder issues by listing IDs in order
3136
3329
 
@@ -3145,21 +3338,25 @@ ISSUES:
3145
3338
  --assign Assign to yourself
3146
3339
  --estimate, -e <size> Estimate: XS, S, M, L, XL
3147
3340
  --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
3341
+ --label, -l <name> Add label (repeatable)
3342
+ --blocks <id> This issue blocks another (repeatable)
3343
+ --blocked-by <id> This issue is blocked by another (repeatable)
3151
3344
  issue update <id> [opts] Update an issue
3152
3345
  --title, -t <title> New title
3153
3346
  --description, -d <desc> New description
3154
- --state, -s <state> New state
3347
+ --status, -s <status> New status (todo, in-progress, done, backlog, etc.)
3155
3348
  --project, -p <name> Move to project
3156
3349
  --milestone <name> Move to milestone
3350
+ --parent <id> Set parent issue
3351
+ --assign Assign to yourself
3352
+ --estimate, -e <size> Set estimate: XS, S, M, L, XL
3157
3353
  --priority <level> Set priority (urgent/high/medium/low/none)
3354
+ --label, -l <name> Set label (repeatable)
3158
3355
  --append, -a <text> Append to description
3159
3356
  --check <text> Check a checkbox item (fuzzy match)
3160
3357
  --uncheck <text> Uncheck a checkbox item (fuzzy match)
3161
- --blocks <id> Add blocking relation
3162
- --blocked-by <id> Add blocked-by relation
3358
+ --blocks <id> Add blocking relation (repeatable)
3359
+ --blocked-by <id> Add blocked-by relation (repeatable)
3163
3360
  issue close <id> Mark issue as done
3164
3361
  issue comment <id> <body> Add a comment
3165
3362
  issue move <id> Move issue in sort order
@@ -3176,6 +3373,8 @@ PROJECTS:
3176
3373
  --name, -n <name> Project name (required)
3177
3374
  --description, -d <desc> Project description
3178
3375
  project complete <name> Mark project as completed
3376
+ project open <name> Set default project for issues/create
3377
+ project close Clear default project
3179
3378
  project move <name> Move project in sort order
3180
3379
  --before <name> Move before this project
3181
3380
  --after <name> Move after this project
@@ -3193,6 +3392,8 @@ MILESTONES:
3193
3392
  --project, -p <name> Project (required)
3194
3393
  --description, -d <desc> Milestone description
3195
3394
  --target-date <date> Target date (YYYY-MM-DD)
3395
+ milestone open <name> Set default milestone for issues/create
3396
+ milestone close Clear default milestone
3196
3397
  milestone move <name> Move milestone in sort order
3197
3398
  --before <name> Move before this milestone
3198
3399
  --after <name> Move after this milestone
@@ -3229,17 +3430,27 @@ WORKFLOW:
3229
3430
  lnext() { eval "$(linear next "$@")"; }
3230
3431
 
3231
3432
  CONFIGURATION:
3232
- Config is loaded from ./.linear first, then ~/.linear, then env vars.
3433
+ Config is layered: ~/.linear (global) then ./.linear (local override).
3434
+ Local values override global; unset local values inherit from global.
3435
+ Env vars (LINEAR_API_KEY, LINEAR_TEAM, LINEAR_PROJECT, LINEAR_MILESTONE)
3436
+ are used as fallbacks when not set in any config file.
3233
3437
 
3234
3438
  File format:
3235
3439
  api_key=lin_api_xxx
3236
3440
  team=ISSUE
3441
+ project=My Project
3442
+ milestone=Sprint 3
3237
3443
 
3238
3444
  [aliases]
3239
3445
  LWW=Last-Write-Wins Support
3240
3446
  MVP=MVP Release
3241
3447
 
3242
3448
  EXAMPLES:
3449
+ linear project open "Phase 1" # Set default project context
3450
+ linear milestone open "Sprint 3" # Set default milestone context
3451
+ linear issues # Filtered to Phase 1 > Sprint 3
3452
+ linear issues --no-project # Bypass default, show all projects
3453
+ linear project close # Clear default project
3243
3454
  linear roadmap # See all projects and milestones
3244
3455
  linear issues --unblocked # Find workable issues
3245
3456
  linear issues --project "Phase 1" # Issues in a project
@@ -3322,6 +3533,29 @@ async function main() {
3322
3533
  case 'create': await cmdProjectCreate(subargs); break;
3323
3534
  case 'complete': await cmdProjectComplete(subargs); break;
3324
3535
  case 'move': await cmdProjectMove(subargs); break;
3536
+ case 'open': {
3537
+ const name = resolveAlias(subargs[0]);
3538
+ if (!name) {
3539
+ console.error(colors.red('Error: Project name required'));
3540
+ console.error('Usage: linear project open "Project Name"');
3541
+ process.exit(1);
3542
+ }
3543
+ writeConfigValue('project', name);
3544
+ DEFAULT_PROJECT = name;
3545
+ console.log(colors.green(`Opened project: ${name}`));
3546
+ console.log(colors.gray(`Saved to ${CONFIG_FILE}`));
3547
+ break;
3548
+ }
3549
+ case 'close': {
3550
+ const removed = removeConfigValue('project');
3551
+ if (removed) {
3552
+ console.log(colors.green(`Closed project: ${removed}`));
3553
+ DEFAULT_PROJECT = '';
3554
+ } else {
3555
+ console.log(colors.gray('No project was open'));
3556
+ }
3557
+ break;
3558
+ }
3325
3559
  default:
3326
3560
  console.error(`Unknown project command: ${subcmd}`);
3327
3561
  process.exit(1);
@@ -3346,6 +3580,29 @@ async function main() {
3346
3580
  case 'show': await cmdMilestoneShow(subargs); break;
3347
3581
  case 'create': await cmdMilestoneCreate(subargs); break;
3348
3582
  case 'move': await cmdMilestoneMove(subargs); break;
3583
+ case 'open': {
3584
+ const name = resolveAlias(subargs[0]);
3585
+ if (!name) {
3586
+ console.error(colors.red('Error: Milestone name required'));
3587
+ console.error('Usage: linear milestone open "Milestone Name"');
3588
+ process.exit(1);
3589
+ }
3590
+ writeConfigValue('milestone', name);
3591
+ DEFAULT_MILESTONE = name;
3592
+ console.log(colors.green(`Opened milestone: ${name}`));
3593
+ console.log(colors.gray(`Saved to ${CONFIG_FILE}`));
3594
+ break;
3595
+ }
3596
+ case 'close': {
3597
+ const removed = removeConfigValue('milestone');
3598
+ if (removed) {
3599
+ console.log(colors.green(`Closed milestone: ${removed}`));
3600
+ DEFAULT_MILESTONE = '';
3601
+ } else {
3602
+ console.log(colors.gray('No milestone was open'));
3603
+ }
3604
+ break;
3605
+ }
3349
3606
  default:
3350
3607
  console.error(`Unknown milestone command: ${subcmd}`);
3351
3608
  process.exit(1);
@@ -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
@@ -25,12 +25,14 @@ This will:
25
25
 
26
26
  ## Configuration
27
27
 
28
- Config is loaded in order: `./.linear` `~/.linear` env vars
28
+ Config is layered: `~/.linear` (global) is loaded first, then `./.linear` (local) overrides on top. Local values override global; unset local values inherit from global. Env vars (`LINEAR_API_KEY`, `LINEAR_TEAM`, `LINEAR_PROJECT`, `LINEAR_MILESTONE`) are used as fallbacks.
29
29
 
30
30
  ```
31
31
  # .linear file format
32
32
  api_key=lin_api_xxx
33
33
  team=ISSUE
34
+ project=My Project
35
+ milestone=Sprint 3
34
36
 
35
37
  [aliases]
36
38
  V2=Version 2.0 Release
@@ -83,29 +85,45 @@ linear whoami # Show current user/team
83
85
  # Roadmap (overview)
84
86
  linear roadmap # Projects with milestones and progress
85
87
 
88
+ # Default context (sets project/milestone for issues & create)
89
+ linear project open "Phase 1" # Set default project
90
+ linear milestone open "Sprint 3" # Set default milestone
91
+ linear project close # Clear default project
92
+ linear milestone close # Clear default milestone
93
+
86
94
  # Issues
95
+ linear issues # Default: backlog + todo issues (filtered by open project/milestone)
96
+ linear issues --no-project # Bypass default project filter
97
+ linear issues --no-milestone # Bypass default milestone filter
87
98
  linear issues --unblocked # Ready to work on (no blockers)
88
99
  linear issues --open # All non-completed issues
89
- linear issues --backlog # Backlog issues only
90
- linear issues --in-progress # Issues currently in progress
100
+ linear issues --status todo # Only todo issues
101
+ linear issues --status backlog # Only backlog issues
102
+ linear issues --status in-progress # Issues currently in progress
103
+ linear issues --status todo --status in-progress # Multiple statuses
91
104
  linear issues --mine # Only your assigned issues
92
105
  linear issues --project "Name" # Issues in a project
93
106
  linear issues --milestone "M1" # Issues in a milestone
94
107
  linear issues --label bug # Filter by label
95
108
  linear issues --priority urgent # Filter by priority (urgent/high/medium/low/none)
96
- # Flags can be combined: linear issues --in-progress --mine
109
+ # Flags can be combined: linear issues --status todo --mine
97
110
  linear issue show ISSUE-1 # Full details with parent context
98
111
  linear issue start ISSUE-1 # Assign to you + set In Progress
99
112
  linear issue create --title "Fix bug" --project "Phase 1" --assign --estimate M
100
113
  linear issue create --title "Urgent bug" --priority urgent --assign
101
114
  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"
115
+ linear issue create --title "Blocked task" --blocked-by ISSUE-1 --blocked-by ISSUE-2
116
+ linear issue create --title "Labeled" --label bug --label frontend # Multiple labels
117
+ linear issue update ISSUE-1 --status "In Progress"
104
118
  linear issue update ISSUE-1 --priority high # Set priority
119
+ linear issue update ISSUE-1 --estimate M # Set estimate
120
+ linear issue update ISSUE-1 --label bug --label frontend # Set labels (repeatable)
121
+ linear issue update ISSUE-1 --assign # Assign to yourself
122
+ linear issue update ISSUE-1 --parent ISSUE-2 # Set parent issue
105
123
  linear issue update ISSUE-1 --milestone "Beta"
106
124
  linear issue update ISSUE-1 --append "Notes..."
107
125
  linear issue update ISSUE-1 --check "validation" # Check off a todo item
108
- linear issue update ISSUE-1 --blocks ISSUE-2 # Add blocking relation
126
+ linear issue update ISSUE-1 --blocks ISSUE-2 --blocks ISSUE-3 # Repeatable
109
127
  linear issue close ISSUE-1
110
128
  linear issue comment ISSUE-1 "Comment text"
111
129
 
@@ -115,11 +133,15 @@ linear projects --all # Include completed
115
133
  linear project show "Phase 1" # Details with issues
116
134
  linear project create "Name" --description "..."
117
135
  linear project complete "Phase 1"
136
+ linear project open "Phase 1" # Set as default project
137
+ linear project close # Clear default project
118
138
 
119
139
  # Milestones
120
140
  linear milestones --project "P1" # Milestones in a project
121
141
  linear milestone show "Beta" # Details with issues
122
142
  linear milestone create "Beta" --project "P1" --target-date 2024-03-01
143
+ linear milestone open "Beta" # Set as default milestone
144
+ linear milestone close # Clear default milestone
123
145
 
124
146
  # Reordering (drag-drop equivalent)
125
147
  linear projects reorder "P1" "P2" "P3" # Set project order
@@ -182,6 +204,16 @@ gh pr create --title "ISSUE-5: Add caching layer"
182
204
 
183
205
  ## Workflow Guidelines
184
206
 
207
+ ### Setting context
208
+ When working on a specific project/milestone, set it as default to avoid repeating flags:
209
+ ```bash
210
+ linear project open "Phase 1" # All commands now default to Phase 1
211
+ linear milestone open "Sprint 3" # And to Sprint 3 milestone
212
+ linear issues # Shows Phase 1 > Sprint 3 issues only
213
+ linear issue create --title "Fix" # Created in Phase 1, Sprint 3
214
+ linear project close # Done? Clear the context
215
+ ```
216
+
185
217
  ### Getting oriented
186
218
  ```bash
187
219
  linear roadmap # See all projects, milestones, progress
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.2",
4
4
  "description": "Linear CLI with unblocked issue filtering, built for AI-assisted development",
5
5
  "type": "module",
6
6
  "bin": {