@dabble/linear-cli 1.1.1 → 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/bin/linear.mjs +241 -55
- package/claude/skills/linear-cli/SKILL.md +26 -2
- package/package.json +1 -1
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."));
|
|
@@ -325,6 +452,8 @@ async function cmdIssues(args) {
|
|
|
325
452
|
status: 'array', s: 'array',
|
|
326
453
|
project: 'string', p: 'string',
|
|
327
454
|
milestone: 'string',
|
|
455
|
+
'no-project': 'boolean',
|
|
456
|
+
'no-milestone': 'boolean',
|
|
328
457
|
label: 'array', l: 'array',
|
|
329
458
|
priority: 'string',
|
|
330
459
|
});
|
|
@@ -334,8 +463,10 @@ async function cmdIssues(args) {
|
|
|
334
463
|
const openOnly = opts.open || opts.o;
|
|
335
464
|
const mineOnly = opts.mine || opts.m;
|
|
336
465
|
const statusFilter = opts.status || opts.s || [];
|
|
337
|
-
const
|
|
338
|
-
const
|
|
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);
|
|
339
470
|
const labelFilters = opts.label || opts.l || [];
|
|
340
471
|
const priorityFilter = (opts.priority || '').toLowerCase();
|
|
341
472
|
|
|
@@ -408,6 +539,15 @@ async function cmdIssues(args) {
|
|
|
408
539
|
// Check if any issues have priority set
|
|
409
540
|
const hasPriority = issues.some(i => i.priority > 0);
|
|
410
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
|
+
|
|
411
551
|
// Helper to format issue row
|
|
412
552
|
const formatRow = (i) => {
|
|
413
553
|
const row = [
|
|
@@ -419,7 +559,9 @@ async function cmdIssues(args) {
|
|
|
419
559
|
const pri = PRIORITY_LABELS[i.priority] || '';
|
|
420
560
|
row.push(pri ? colors.bold(pri) : '-');
|
|
421
561
|
}
|
|
422
|
-
|
|
562
|
+
if (showProjectCol) {
|
|
563
|
+
row.push(i.project?.name || '-');
|
|
564
|
+
}
|
|
423
565
|
if (hasAssignees) {
|
|
424
566
|
const assignee = i.assignee?.id === viewerId ? 'you' : (i.assignee?.name || '-');
|
|
425
567
|
row.push(assignee);
|
|
@@ -488,7 +630,7 @@ async function cmdIssues(args) {
|
|
|
488
630
|
|
|
489
631
|
filtered = applyFilters(filtered);
|
|
490
632
|
|
|
491
|
-
console.log(colors.bold(
|
|
633
|
+
console.log(colors.bold(`Unblocked Issues${contextStr}:\n`));
|
|
492
634
|
console.log(formatTable(filtered.map(formatRow)));
|
|
493
635
|
} else if (allStates) {
|
|
494
636
|
let filtered = issues;
|
|
@@ -497,7 +639,7 @@ async function cmdIssues(args) {
|
|
|
497
639
|
}
|
|
498
640
|
filtered = applyFilters(filtered);
|
|
499
641
|
|
|
500
|
-
console.log(colors.bold(
|
|
642
|
+
console.log(colors.bold(`All Issues${contextStr}:\n`));
|
|
501
643
|
console.log(formatTable(filtered.map(formatRow)));
|
|
502
644
|
} else if (openOnly) {
|
|
503
645
|
let filtered = issues.filter(i =>
|
|
@@ -509,21 +651,21 @@ async function cmdIssues(args) {
|
|
|
509
651
|
|
|
510
652
|
filtered = applyFilters(filtered);
|
|
511
653
|
|
|
512
|
-
console.log(colors.bold(
|
|
654
|
+
console.log(colors.bold(`Open Issues${contextStr}:\n`));
|
|
513
655
|
console.log(formatTable(filtered.map(formatRow)));
|
|
514
656
|
} else if (resolvedStatusTypes.length > 0) {
|
|
515
657
|
let filtered = filterByStatus(issues, resolvedStatusTypes);
|
|
516
658
|
filtered = applyFilters(filtered);
|
|
517
659
|
|
|
518
660
|
const label = statusFilter.join(' + ');
|
|
519
|
-
console.log(colors.bold(`Issues (${label}):\n`));
|
|
661
|
+
console.log(colors.bold(`Issues${contextStr} (${label}):\n`));
|
|
520
662
|
console.log(formatTable(filtered.map(formatRow)));
|
|
521
663
|
} else {
|
|
522
664
|
// Default: show backlog + todo
|
|
523
665
|
let filtered = issues.filter(i => i.state.type === 'backlog' || i.state.type === 'unstarted');
|
|
524
666
|
filtered = applyFilters(filtered);
|
|
525
667
|
|
|
526
|
-
console.log(colors.bold(
|
|
668
|
+
console.log(colors.bold(`Issues${contextStr} (backlog + todo):\n`));
|
|
527
669
|
console.log(formatTable(filtered.map(formatRow)));
|
|
528
670
|
}
|
|
529
671
|
}
|
|
@@ -714,9 +856,9 @@ async function cmdIssueCreate(args) {
|
|
|
714
856
|
|
|
715
857
|
const title = opts.title || opts.t || opts._[0];
|
|
716
858
|
const description = opts.description || opts.d || '';
|
|
717
|
-
const project = resolveAlias(opts.project || opts.p);
|
|
859
|
+
const project = resolveAlias(opts.project || opts.p) || DEFAULT_PROJECT;
|
|
718
860
|
const priority = (opts.priority || '').toLowerCase();
|
|
719
|
-
const milestone = resolveAlias(opts.milestone);
|
|
861
|
+
const milestone = resolveAlias(opts.milestone) || DEFAULT_MILESTONE;
|
|
720
862
|
const parent = opts.parent;
|
|
721
863
|
const shouldAssign = opts.assign;
|
|
722
864
|
const estimate = (opts.estimate || opts.e || '').toLowerCase();
|
|
@@ -2350,7 +2492,7 @@ async function cmdAlias(args) {
|
|
|
2350
2492
|
|
|
2351
2493
|
// Remove alias
|
|
2352
2494
|
if (removeCode) {
|
|
2353
|
-
removeAlias(removeCode);
|
|
2495
|
+
await removeAlias(removeCode);
|
|
2354
2496
|
console.log(colors.green(`Removed alias: ${removeCode.toUpperCase()}`));
|
|
2355
2497
|
return;
|
|
2356
2498
|
}
|
|
@@ -2364,7 +2506,7 @@ async function cmdAlias(args) {
|
|
|
2364
2506
|
process.exit(1);
|
|
2365
2507
|
}
|
|
2366
2508
|
|
|
2367
|
-
saveAlias(code, name);
|
|
2509
|
+
await saveAlias(code, name);
|
|
2368
2510
|
console.log(colors.green(`Alias set: ${code.toUpperCase()} -> ${name}`));
|
|
2369
2511
|
}
|
|
2370
2512
|
|
|
@@ -3093,25 +3235,7 @@ team=${selectedKey}
|
|
|
3093
3235
|
|
|
3094
3236
|
// Add .linear to .gitignore if saving locally
|
|
3095
3237
|
if (!saveGlobal) {
|
|
3096
|
-
|
|
3097
|
-
try {
|
|
3098
|
-
let gitignore = '';
|
|
3099
|
-
if (existsSync(gitignorePath)) {
|
|
3100
|
-
gitignore = readFileSync(gitignorePath, 'utf-8');
|
|
3101
|
-
}
|
|
3102
|
-
|
|
3103
|
-
// Check if .linear is already in .gitignore
|
|
3104
|
-
const lines = gitignore.split('\n').map(l => l.trim());
|
|
3105
|
-
if (!lines.includes('.linear')) {
|
|
3106
|
-
// Add .linear to .gitignore
|
|
3107
|
-
const newline = gitignore.endsWith('\n') || gitignore === '' ? '' : '\n';
|
|
3108
|
-
const content = gitignore + newline + '.linear\n';
|
|
3109
|
-
writeFileSync(gitignorePath, content);
|
|
3110
|
-
console.log(colors.green(`Added .linear to .gitignore`));
|
|
3111
|
-
}
|
|
3112
|
-
} catch (err) {
|
|
3113
|
-
// Silently ignore if we can't update .gitignore
|
|
3114
|
-
}
|
|
3238
|
+
ensureGitignore();
|
|
3115
3239
|
|
|
3116
3240
|
// Add .linear to .worktreeinclude for worktree support
|
|
3117
3241
|
const worktreeIncludePath = join(process.cwd(), '.worktreeinclude');
|
|
@@ -3195,8 +3319,10 @@ ISSUES:
|
|
|
3195
3319
|
--status, -s <name> Filter by status (repeatable: --status todo --status backlog)
|
|
3196
3320
|
--all, -a Show all states (including completed)
|
|
3197
3321
|
--mine, -m Show only issues assigned to you
|
|
3198
|
-
--project, -p <name> Filter by project
|
|
3199
|
-
--milestone <name> Filter by milestone
|
|
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
|
|
3200
3326
|
--label, -l <name> Filter by label (repeatable)
|
|
3201
3327
|
--priority <level> Filter by priority (urgent/high/medium/low/none)
|
|
3202
3328
|
issues reorder <ids...> Reorder issues by listing IDs in order
|
|
@@ -3247,6 +3373,8 @@ PROJECTS:
|
|
|
3247
3373
|
--name, -n <name> Project name (required)
|
|
3248
3374
|
--description, -d <desc> Project description
|
|
3249
3375
|
project complete <name> Mark project as completed
|
|
3376
|
+
project open <name> Set default project for issues/create
|
|
3377
|
+
project close Clear default project
|
|
3250
3378
|
project move <name> Move project in sort order
|
|
3251
3379
|
--before <name> Move before this project
|
|
3252
3380
|
--after <name> Move after this project
|
|
@@ -3264,6 +3392,8 @@ MILESTONES:
|
|
|
3264
3392
|
--project, -p <name> Project (required)
|
|
3265
3393
|
--description, -d <desc> Milestone description
|
|
3266
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
|
|
3267
3397
|
milestone move <name> Move milestone in sort order
|
|
3268
3398
|
--before <name> Move before this milestone
|
|
3269
3399
|
--after <name> Move after this milestone
|
|
@@ -3300,17 +3430,27 @@ WORKFLOW:
|
|
|
3300
3430
|
lnext() { eval "$(linear next "$@")"; }
|
|
3301
3431
|
|
|
3302
3432
|
CONFIGURATION:
|
|
3303
|
-
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.
|
|
3304
3437
|
|
|
3305
3438
|
File format:
|
|
3306
3439
|
api_key=lin_api_xxx
|
|
3307
3440
|
team=ISSUE
|
|
3441
|
+
project=My Project
|
|
3442
|
+
milestone=Sprint 3
|
|
3308
3443
|
|
|
3309
3444
|
[aliases]
|
|
3310
3445
|
LWW=Last-Write-Wins Support
|
|
3311
3446
|
MVP=MVP Release
|
|
3312
3447
|
|
|
3313
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
|
|
3314
3454
|
linear roadmap # See all projects and milestones
|
|
3315
3455
|
linear issues --unblocked # Find workable issues
|
|
3316
3456
|
linear issues --project "Phase 1" # Issues in a project
|
|
@@ -3393,6 +3533,29 @@ async function main() {
|
|
|
3393
3533
|
case 'create': await cmdProjectCreate(subargs); break;
|
|
3394
3534
|
case 'complete': await cmdProjectComplete(subargs); break;
|
|
3395
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
|
+
}
|
|
3396
3559
|
default:
|
|
3397
3560
|
console.error(`Unknown project command: ${subcmd}`);
|
|
3398
3561
|
process.exit(1);
|
|
@@ -3417,6 +3580,29 @@ async function main() {
|
|
|
3417
3580
|
case 'show': await cmdMilestoneShow(subargs); break;
|
|
3418
3581
|
case 'create': await cmdMilestoneCreate(subargs); break;
|
|
3419
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
|
+
}
|
|
3420
3606
|
default:
|
|
3421
3607
|
console.error(`Unknown milestone command: ${subcmd}`);
|
|
3422
3608
|
process.exit(1);
|
|
@@ -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,8 +85,16 @@ 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
|
|
87
|
-
linear issues # Default: backlog + todo 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
|
|
88
98
|
linear issues --unblocked # Ready to work on (no blockers)
|
|
89
99
|
linear issues --open # All non-completed issues
|
|
90
100
|
linear issues --status todo # Only todo issues
|
|
@@ -123,11 +133,15 @@ linear projects --all # Include completed
|
|
|
123
133
|
linear project show "Phase 1" # Details with issues
|
|
124
134
|
linear project create "Name" --description "..."
|
|
125
135
|
linear project complete "Phase 1"
|
|
136
|
+
linear project open "Phase 1" # Set as default project
|
|
137
|
+
linear project close # Clear default project
|
|
126
138
|
|
|
127
139
|
# Milestones
|
|
128
140
|
linear milestones --project "P1" # Milestones in a project
|
|
129
141
|
linear milestone show "Beta" # Details with issues
|
|
130
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
|
|
131
145
|
|
|
132
146
|
# Reordering (drag-drop equivalent)
|
|
133
147
|
linear projects reorder "P1" "P2" "P3" # Set project order
|
|
@@ -190,6 +204,16 @@ gh pr create --title "ISSUE-5: Add caching layer"
|
|
|
190
204
|
|
|
191
205
|
## Workflow Guidelines
|
|
192
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
|
+
|
|
193
217
|
### Getting oriented
|
|
194
218
|
```bash
|
|
195
219
|
linear roadmap # See all projects, milestones, progress
|