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