@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 +3 -1
- package/bin/linear.mjs +457 -200
- package/claude/commands/standup.md +6 -5
- package/claude/skills/linear-cli/SKILL.md +39 -7
- 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
|
@@ -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
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
71
|
-
if (
|
|
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
|
|
87
|
-
if (
|
|
88
|
-
|
|
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
|
-
|
|
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(
|
|
176
|
+
writeFileSync(AUTH_CONFIG_FILE, lines.join('\n'));
|
|
140
177
|
ALIASES[upperCode] = name;
|
|
141
178
|
}
|
|
142
179
|
|
|
143
|
-
function removeAlias(code) {
|
|
144
|
-
if (!
|
|
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(
|
|
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(
|
|
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
|
-
'
|
|
452
|
+
status: 'array', s: 'array',
|
|
319
453
|
project: 'string', p: 'string',
|
|
320
454
|
milestone: 'string',
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
const
|
|
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
|
-
|
|
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 (
|
|
578
|
+
if (labelFilters.length > 0) {
|
|
415
579
|
filtered = filtered.filter(i =>
|
|
416
|
-
i.labels?.nodes?.some(l => l.name.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(
|
|
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 =
|
|
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(
|
|
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(
|
|
654
|
+
console.log(colors.bold(`Open Issues${contextStr}:\n`));
|
|
475
655
|
console.log(formatTable(filtered.map(formatRow)));
|
|
476
|
-
} else if (
|
|
477
|
-
let filtered = issues
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
848
|
+
status: 'string', s: 'string',
|
|
678
849
|
assign: 'boolean',
|
|
679
850
|
estimate: 'string', e: 'string',
|
|
680
851
|
priority: 'string',
|
|
681
|
-
label: '
|
|
682
|
-
blocks: '
|
|
683
|
-
'blocked-by': '
|
|
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
|
|
695
|
-
const
|
|
696
|
-
const
|
|
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
|
|
939
|
+
// Look up label IDs
|
|
769
940
|
let labelIds = [];
|
|
770
|
-
if (
|
|
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
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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 (
|
|
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
|
-
|
|
828
|
-
// This issue blocks another issue
|
|
1000
|
+
for (const target of blocksIssues) {
|
|
829
1001
|
await gql(relationMutation, {
|
|
830
|
-
input: { issueId: issue.identifier, relatedIssueId:
|
|
1002
|
+
input: { issueId: issue.identifier, relatedIssueId: target, type: 'blocks' }
|
|
831
1003
|
});
|
|
832
|
-
console.log(colors.gray(` → blocks ${
|
|
1004
|
+
console.log(colors.gray(` → blocks ${target}`));
|
|
833
1005
|
}
|
|
834
1006
|
|
|
835
|
-
|
|
836
|
-
// This issue is blocked by another issue
|
|
1007
|
+
for (const target of blockedByIssues) {
|
|
837
1008
|
await gql(relationMutation, {
|
|
838
|
-
input: { issueId:
|
|
1009
|
+
input: { issueId: target, relatedIssueId: issue.identifier, type: 'blocks' }
|
|
839
1010
|
});
|
|
840
|
-
console.log(colors.gray(` → blocked by ${
|
|
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
|
-
|
|
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: '
|
|
868
|
-
'blocked-by': '
|
|
1042
|
+
blocks: 'array',
|
|
1043
|
+
'blocked-by': 'array',
|
|
869
1044
|
});
|
|
870
1045
|
|
|
871
|
-
const
|
|
872
|
-
const
|
|
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.
|
|
964
|
-
const stateName = opts.
|
|
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 =
|
|
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
|
-
|
|
1272
|
+
for (const target of blocksIssues) {
|
|
1056
1273
|
await gql(relationMutation, {
|
|
1057
|
-
input: { issueId: issueId, relatedIssueId:
|
|
1274
|
+
input: { issueId: issueId, relatedIssueId: target, type: 'blocks' }
|
|
1058
1275
|
});
|
|
1059
|
-
console.log(colors.green(`${issueId} now blocks ${
|
|
1276
|
+
console.log(colors.green(`${issueId} now blocks ${target}`));
|
|
1060
1277
|
}
|
|
1061
1278
|
|
|
1062
|
-
|
|
1279
|
+
for (const target of blockedByIssues) {
|
|
1063
1280
|
await gql(relationMutation, {
|
|
1064
|
-
input: { issueId:
|
|
1281
|
+
input: { issueId: target, relatedIssueId: issueId, type: 'blocks' }
|
|
1065
1282
|
});
|
|
1066
|
-
console.log(colors.green(`${issueId} now blocked by ${
|
|
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
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
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
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
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
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
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
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
}
|
|
2901
|
-
|
|
3109
|
+
} catch (err) {
|
|
3110
|
+
// gh search error
|
|
2902
3111
|
}
|
|
2903
|
-
}
|
|
2904
|
-
|
|
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
|
-
|
|
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
|
-
--
|
|
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
|
-
--
|
|
3130
|
-
--
|
|
3131
|
-
--
|
|
3132
|
-
--
|
|
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
|
-
--
|
|
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
|
|
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 (
|
|
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
|
|
@@ -25,12 +25,14 @@ This will:
|
|
|
25
25
|
|
|
26
26
|
## Configuration
|
|
27
27
|
|
|
28
|
-
Config is loaded
|
|
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 --
|
|
90
|
-
linear issues --
|
|
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 --
|
|
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
|
|
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 #
|
|
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
|