linear-cli-agents 0.5.0 → 0.6.0
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 +85 -11
- package/dist/commands/cycles/current.d.ts +11 -0
- package/dist/commands/cycles/current.js +104 -0
- package/dist/commands/cycles/get.d.ts +12 -0
- package/dist/commands/cycles/get.js +86 -0
- package/dist/commands/cycles/list.d.ts +16 -0
- package/dist/commands/cycles/list.js +147 -0
- package/dist/commands/info.js +50 -2
- package/dist/lib/formatter.d.ts +4 -0
- package/dist/lib/formatter.js +15 -0
- package/oclif.manifest.json +696 -499
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -10,6 +10,9 @@ A CLI for interacting with [Linear](https://linear.app), designed for LLMs and a
|
|
|
10
10
|
|
|
11
11
|
- **JSON output**: All commands return structured JSON, perfect for parsing by LLMs
|
|
12
12
|
- **Multiple formats**: JSON (default), table (colored), or plain text output
|
|
13
|
+
- **Comprehensive docs**: `linear info` returns full CLI documentation in one command
|
|
14
|
+
- **Configurable defaults**: Set default team to skip `--team-id` on every command
|
|
15
|
+
- **Bulk operations**: Update multiple issues at once with `bulk-update` and `bulk-label`
|
|
13
16
|
- **Schema introspection**: Discover available operations programmatically
|
|
14
17
|
- **Full CRUD**: Issues, projects, labels, comments, templates, milestones
|
|
15
18
|
- **Issue relations**: Manage blocks, duplicates, and related issues
|
|
@@ -27,6 +30,25 @@ npm install -g linear-cli-agents
|
|
|
27
30
|
pnpm add -g linear-cli-agents
|
|
28
31
|
```
|
|
29
32
|
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Install
|
|
37
|
+
npm install -g linear-cli-agents
|
|
38
|
+
|
|
39
|
+
# Authenticate
|
|
40
|
+
linear auth login
|
|
41
|
+
|
|
42
|
+
# Get full CLI documentation (recommended for LLMs)
|
|
43
|
+
linear info
|
|
44
|
+
|
|
45
|
+
# Configure default team (optional, skips --team-id on every command)
|
|
46
|
+
linear config set default-team-id YOUR_TEAM_ID
|
|
47
|
+
|
|
48
|
+
# Add CLI instructions to your CLAUDE.md (optional)
|
|
49
|
+
linear setup
|
|
50
|
+
```
|
|
51
|
+
|
|
30
52
|
## Authentication
|
|
31
53
|
|
|
32
54
|
```bash
|
|
@@ -49,6 +71,20 @@ linear me
|
|
|
49
71
|
linear auth logout
|
|
50
72
|
```
|
|
51
73
|
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Set default team (skips --team-id on create commands)
|
|
78
|
+
linear config set default-team-id YOUR_TEAM_UUID
|
|
79
|
+
linear config set default-team-key TEAM_KEY
|
|
80
|
+
|
|
81
|
+
# Get a config value
|
|
82
|
+
linear config get default-team-id
|
|
83
|
+
|
|
84
|
+
# List all config
|
|
85
|
+
linear config list
|
|
86
|
+
```
|
|
87
|
+
|
|
52
88
|
## Usage
|
|
53
89
|
|
|
54
90
|
### Issues
|
|
@@ -89,6 +125,14 @@ linear issues archive ENG-123 --unarchive
|
|
|
89
125
|
# Manage labels on issues
|
|
90
126
|
linear issues add-labels ENG-123 --label-ids LABEL_ID1,LABEL_ID2
|
|
91
127
|
linear issues remove-labels ENG-123 --label-ids LABEL_ID1
|
|
128
|
+
|
|
129
|
+
# Bulk update multiple issues at once
|
|
130
|
+
linear issues bulk-update --ids ENG-1,ENG-2,ENG-3 --state-id STATE_ID
|
|
131
|
+
linear issues bulk-update --ids ENG-1,ENG-2 --priority 2 --assignee-id USER_ID
|
|
132
|
+
|
|
133
|
+
# Bulk add/remove labels from multiple issues
|
|
134
|
+
linear issues bulk-label --ids ENG-1,ENG-2,ENG-3 --add-labels LABEL1,LABEL2
|
|
135
|
+
linear issues bulk-label --ids ENG-1,ENG-2 --remove-labels LABEL1
|
|
92
136
|
```
|
|
93
137
|
|
|
94
138
|
### Projects
|
|
@@ -225,6 +269,25 @@ linear states list
|
|
|
225
269
|
linear states list --team ENG
|
|
226
270
|
```
|
|
227
271
|
|
|
272
|
+
### Cycles (Sprints)
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# List all cycles
|
|
276
|
+
linear cycles list
|
|
277
|
+
linear cycles list --team ENG
|
|
278
|
+
|
|
279
|
+
# Filter by status
|
|
280
|
+
linear cycles list --active # Currently running
|
|
281
|
+
linear cycles list --upcoming # Future cycles
|
|
282
|
+
linear cycles list --completed # Past cycles
|
|
283
|
+
|
|
284
|
+
# Get current cycle for a team
|
|
285
|
+
linear cycles current --team ENG
|
|
286
|
+
|
|
287
|
+
# Get cycle details
|
|
288
|
+
linear cycles get CYCLE_ID
|
|
289
|
+
```
|
|
290
|
+
|
|
228
291
|
### Users
|
|
229
292
|
|
|
230
293
|
```bash
|
|
@@ -378,25 +441,36 @@ linear issues list --format table --no-color
|
|
|
378
441
|
|
|
379
442
|
The CLI is designed to be easily used by LLMs and AI agents:
|
|
380
443
|
|
|
381
|
-
1. **
|
|
444
|
+
1. **Single discovery command**: Use `linear info` to get complete documentation in one JSON response
|
|
382
445
|
2. **Structured output**: All responses are JSON with consistent format
|
|
383
|
-
3. **
|
|
384
|
-
4. **
|
|
446
|
+
3. **Configurable defaults**: Set default team to reduce command complexity
|
|
447
|
+
4. **Bulk operations**: Update multiple issues efficiently
|
|
448
|
+
5. **Error codes**: Programmatic error handling via error codes
|
|
385
449
|
|
|
386
450
|
### Example LLM Workflow
|
|
387
451
|
|
|
388
452
|
```bash
|
|
389
|
-
# 1.
|
|
390
|
-
linear
|
|
453
|
+
# 1. Get complete CLI documentation in one command
|
|
454
|
+
linear info
|
|
391
455
|
|
|
392
|
-
# 2.
|
|
393
|
-
linear
|
|
456
|
+
# 2. Or get compact version for limited context
|
|
457
|
+
linear info --compact
|
|
394
458
|
|
|
395
|
-
# 3.
|
|
396
|
-
linear issues
|
|
459
|
+
# 3. Create issues (uses default team if configured)
|
|
460
|
+
linear issues create --title "From LLM"
|
|
461
|
+
|
|
462
|
+
# 4. Bulk update multiple issues
|
|
463
|
+
linear issues bulk-update --ids ENG-1,ENG-2,ENG-3 --state-id STATE_ID
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Claude Code Integration
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
# Add CLI instructions to CLAUDE.md
|
|
470
|
+
linear setup
|
|
397
471
|
|
|
398
|
-
#
|
|
399
|
-
linear
|
|
472
|
+
# Remove instructions
|
|
473
|
+
linear setup --remove
|
|
400
474
|
```
|
|
401
475
|
|
|
402
476
|
## Development
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class CyclesCurrent extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'team-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
team: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { getClient } from '../../lib/client.js';
|
|
3
|
+
import { success, print, printItem } from '../../lib/output.js';
|
|
4
|
+
import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
|
|
5
|
+
export default class CyclesCurrent extends Command {
|
|
6
|
+
static description = 'Get the current active cycle for a team';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> cycles current --team ENG',
|
|
9
|
+
'<%= config.bin %> cycles current --team-id TEAM_ID',
|
|
10
|
+
'<%= config.bin %> cycles current --team ENG --format table',
|
|
11
|
+
];
|
|
12
|
+
static flags = {
|
|
13
|
+
format: Flags.string({
|
|
14
|
+
char: 'F',
|
|
15
|
+
description: 'Output format',
|
|
16
|
+
options: ['json', 'table', 'plain'],
|
|
17
|
+
default: 'json',
|
|
18
|
+
}),
|
|
19
|
+
'team-id': Flags.string({
|
|
20
|
+
description: 'Team ID',
|
|
21
|
+
exclusive: ['team'],
|
|
22
|
+
}),
|
|
23
|
+
team: Flags.string({
|
|
24
|
+
description: 'Team key (e.g., ENG)',
|
|
25
|
+
exclusive: ['team-id'],
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
async run() {
|
|
29
|
+
try {
|
|
30
|
+
const { flags } = await this.parse(CyclesCurrent);
|
|
31
|
+
const format = flags.format;
|
|
32
|
+
const client = getClient();
|
|
33
|
+
if (!flags['team-id'] && !flags.team) {
|
|
34
|
+
throw new CliError(ErrorCodes.MISSING_REQUIRED_FIELD, 'Team is required. Use --team or --team-id');
|
|
35
|
+
}
|
|
36
|
+
// Build filter for active cycle
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const filter = {
|
|
39
|
+
startsAt: { lte: now },
|
|
40
|
+
endsAt: { gte: now },
|
|
41
|
+
};
|
|
42
|
+
if (flags['team-id']) {
|
|
43
|
+
filter.team = { id: { eq: flags['team-id'] } };
|
|
44
|
+
}
|
|
45
|
+
else if (flags.team) {
|
|
46
|
+
filter.team = { key: { eq: flags.team } };
|
|
47
|
+
}
|
|
48
|
+
const cycles = await client.cycles({
|
|
49
|
+
first: 1,
|
|
50
|
+
filter,
|
|
51
|
+
});
|
|
52
|
+
if (cycles.nodes.length === 0) {
|
|
53
|
+
throw new CliError(ErrorCodes.NOT_FOUND, 'No active cycle found for this team');
|
|
54
|
+
}
|
|
55
|
+
const cycle = cycles.nodes[0];
|
|
56
|
+
const [team, issues] = await Promise.all([cycle.team, cycle.issues()]);
|
|
57
|
+
const issuesSummary = {
|
|
58
|
+
total: issues.nodes.length,
|
|
59
|
+
completed: issues.nodes.filter((i) => i.completedAt).length,
|
|
60
|
+
};
|
|
61
|
+
const data = {
|
|
62
|
+
id: cycle.id,
|
|
63
|
+
number: cycle.number,
|
|
64
|
+
name: cycle.name ?? null,
|
|
65
|
+
description: cycle.description ?? null,
|
|
66
|
+
startsAt: cycle.startsAt,
|
|
67
|
+
endsAt: cycle.endsAt,
|
|
68
|
+
progress: cycle.progress,
|
|
69
|
+
team: team
|
|
70
|
+
? {
|
|
71
|
+
id: team.id,
|
|
72
|
+
key: team.key,
|
|
73
|
+
name: team.name,
|
|
74
|
+
}
|
|
75
|
+
: null,
|
|
76
|
+
issues: issuesSummary,
|
|
77
|
+
daysRemaining: Math.ceil((new Date(cycle.endsAt).getTime() - now.getTime()) / (1000 * 60 * 60 * 24)),
|
|
78
|
+
};
|
|
79
|
+
if (format === 'json') {
|
|
80
|
+
print(success(data));
|
|
81
|
+
}
|
|
82
|
+
else if (format === 'table') {
|
|
83
|
+
printItem({
|
|
84
|
+
id: data.id,
|
|
85
|
+
number: data.number,
|
|
86
|
+
name: data.name ?? 'Unnamed',
|
|
87
|
+
team: data.team?.key ?? 'N/A',
|
|
88
|
+
startsAt: data.startsAt,
|
|
89
|
+
endsAt: data.endsAt,
|
|
90
|
+
progress: `${Math.round(data.progress * 100)}%`,
|
|
91
|
+
issues: `${issuesSummary.completed}/${issuesSummary.total} completed`,
|
|
92
|
+
daysRemaining: `${data.daysRemaining} days`,
|
|
93
|
+
}, format);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(data.id);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
handleError(err);
|
|
101
|
+
this.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class CyclesGet extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { getClient } from '../../lib/client.js';
|
|
3
|
+
import { success, print, printItem } from '../../lib/output.js';
|
|
4
|
+
import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
|
|
5
|
+
export default class CyclesGet extends Command {
|
|
6
|
+
static description = 'Get cycle (sprint) details';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> cycles get CYCLE_ID',
|
|
9
|
+
'<%= config.bin %> cycles get CYCLE_ID --format table',
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
id: Args.string({
|
|
13
|
+
description: 'Cycle ID',
|
|
14
|
+
required: true,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
static flags = {
|
|
18
|
+
format: Flags.string({
|
|
19
|
+
char: 'F',
|
|
20
|
+
description: 'Output format',
|
|
21
|
+
options: ['json', 'table', 'plain'],
|
|
22
|
+
default: 'json',
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
async run() {
|
|
26
|
+
try {
|
|
27
|
+
const { args, flags } = await this.parse(CyclesGet);
|
|
28
|
+
const format = flags.format;
|
|
29
|
+
const client = getClient();
|
|
30
|
+
const cycle = await client.cycle(args.id);
|
|
31
|
+
if (!cycle) {
|
|
32
|
+
throw new CliError(ErrorCodes.NOT_FOUND, `Cycle ${args.id} not found`);
|
|
33
|
+
}
|
|
34
|
+
const [team, issues] = await Promise.all([cycle.team, cycle.issues()]);
|
|
35
|
+
const issuesSummary = {
|
|
36
|
+
total: issues.nodes.length,
|
|
37
|
+
completed: issues.nodes.filter((i) => i.completedAt).length,
|
|
38
|
+
};
|
|
39
|
+
const data = {
|
|
40
|
+
id: cycle.id,
|
|
41
|
+
number: cycle.number,
|
|
42
|
+
name: cycle.name ?? null,
|
|
43
|
+
description: cycle.description ?? null,
|
|
44
|
+
startsAt: cycle.startsAt,
|
|
45
|
+
endsAt: cycle.endsAt,
|
|
46
|
+
completedAt: cycle.completedAt ?? null,
|
|
47
|
+
progress: cycle.progress,
|
|
48
|
+
scopeHistory: cycle.scopeHistory,
|
|
49
|
+
completedScopeHistory: cycle.completedScopeHistory,
|
|
50
|
+
team: team
|
|
51
|
+
? {
|
|
52
|
+
id: team.id,
|
|
53
|
+
key: team.key,
|
|
54
|
+
name: team.name,
|
|
55
|
+
}
|
|
56
|
+
: null,
|
|
57
|
+
issues: issuesSummary,
|
|
58
|
+
createdAt: cycle.createdAt,
|
|
59
|
+
updatedAt: cycle.updatedAt,
|
|
60
|
+
};
|
|
61
|
+
if (format === 'json') {
|
|
62
|
+
print(success(data));
|
|
63
|
+
}
|
|
64
|
+
else if (format === 'table') {
|
|
65
|
+
printItem({
|
|
66
|
+
id: data.id,
|
|
67
|
+
number: data.number,
|
|
68
|
+
name: data.name ?? 'Unnamed',
|
|
69
|
+
team: data.team?.key ?? 'N/A',
|
|
70
|
+
startsAt: data.startsAt,
|
|
71
|
+
endsAt: data.endsAt,
|
|
72
|
+
completedAt: data.completedAt ?? 'In progress',
|
|
73
|
+
progress: `${Math.round(data.progress * 100)}%`,
|
|
74
|
+
issues: `${issuesSummary.completed}/${issuesSummary.total} completed`,
|
|
75
|
+
}, format);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log(data.id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
handleError(err);
|
|
83
|
+
this.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class CyclesList extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'team-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
team: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
active: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
upcoming: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
completed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
first: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
after: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
};
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { getClient } from '../../lib/client.js';
|
|
3
|
+
import { successList, print, printList } from '../../lib/output.js';
|
|
4
|
+
import { handleError } from '../../lib/errors.js';
|
|
5
|
+
import { colors, truncate, formatProgress } from '../../lib/formatter.js';
|
|
6
|
+
const COLUMNS = [
|
|
7
|
+
{
|
|
8
|
+
key: 'number',
|
|
9
|
+
header: '#',
|
|
10
|
+
format: (value) => colors.dim(String(value)),
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
key: 'name',
|
|
14
|
+
header: 'NAME',
|
|
15
|
+
format: (value) => (value ? colors.bold(truncate(String(value), 25)) : colors.gray('Unnamed')),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: 'teamKey',
|
|
19
|
+
header: 'TEAM',
|
|
20
|
+
format: (value) => colors.cyan(String(value)),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'startsAt',
|
|
24
|
+
header: 'START',
|
|
25
|
+
format: (value) => colors.dim(new Date(value).toISOString().split('T')[0]),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: 'endsAt',
|
|
29
|
+
header: 'END',
|
|
30
|
+
format: (value) => colors.dim(new Date(value).toISOString().split('T')[0]),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: 'progress',
|
|
34
|
+
header: 'PROGRESS',
|
|
35
|
+
format: (value) => formatProgress(Number(value)),
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
export default class CyclesList extends Command {
|
|
39
|
+
static description = 'List cycles (sprints)';
|
|
40
|
+
static examples = [
|
|
41
|
+
'<%= config.bin %> cycles list',
|
|
42
|
+
'<%= config.bin %> cycles list --team-id TEAM_ID',
|
|
43
|
+
'<%= config.bin %> cycles list --team ENG',
|
|
44
|
+
'<%= config.bin %> cycles list --format table',
|
|
45
|
+
'<%= config.bin %> cycles list --active',
|
|
46
|
+
];
|
|
47
|
+
static flags = {
|
|
48
|
+
format: Flags.string({
|
|
49
|
+
char: 'F',
|
|
50
|
+
description: 'Output format',
|
|
51
|
+
options: ['json', 'table', 'plain'],
|
|
52
|
+
default: 'json',
|
|
53
|
+
}),
|
|
54
|
+
'team-id': Flags.string({
|
|
55
|
+
description: 'Filter by team ID',
|
|
56
|
+
}),
|
|
57
|
+
team: Flags.string({
|
|
58
|
+
description: 'Filter by team key (e.g., ENG)',
|
|
59
|
+
}),
|
|
60
|
+
active: Flags.boolean({
|
|
61
|
+
description: 'Show only active cycles',
|
|
62
|
+
default: false,
|
|
63
|
+
}),
|
|
64
|
+
upcoming: Flags.boolean({
|
|
65
|
+
description: 'Show only upcoming cycles',
|
|
66
|
+
default: false,
|
|
67
|
+
}),
|
|
68
|
+
completed: Flags.boolean({
|
|
69
|
+
description: 'Show only completed cycles',
|
|
70
|
+
default: false,
|
|
71
|
+
}),
|
|
72
|
+
first: Flags.integer({
|
|
73
|
+
description: 'Number of cycles to fetch (default: 50)',
|
|
74
|
+
default: 50,
|
|
75
|
+
}),
|
|
76
|
+
after: Flags.string({
|
|
77
|
+
description: 'Cursor for pagination',
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
async run() {
|
|
81
|
+
try {
|
|
82
|
+
const { flags } = await this.parse(CyclesList);
|
|
83
|
+
const format = flags.format;
|
|
84
|
+
const client = getClient();
|
|
85
|
+
// Build filter
|
|
86
|
+
const filter = {};
|
|
87
|
+
if (flags['team-id']) {
|
|
88
|
+
filter.team = { id: { eq: flags['team-id'] } };
|
|
89
|
+
}
|
|
90
|
+
else if (flags.team) {
|
|
91
|
+
filter.team = { key: { eq: flags.team } };
|
|
92
|
+
}
|
|
93
|
+
const now = new Date();
|
|
94
|
+
if (flags.active) {
|
|
95
|
+
filter.startsAt = { lte: now };
|
|
96
|
+
filter.endsAt = { gte: now };
|
|
97
|
+
}
|
|
98
|
+
else if (flags.upcoming) {
|
|
99
|
+
filter.startsAt = { gt: now };
|
|
100
|
+
}
|
|
101
|
+
else if (flags.completed) {
|
|
102
|
+
filter.completedAt = { neq: null };
|
|
103
|
+
}
|
|
104
|
+
const cycles = await client.cycles({
|
|
105
|
+
first: flags.first,
|
|
106
|
+
after: flags.after,
|
|
107
|
+
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
|
108
|
+
});
|
|
109
|
+
const data = await Promise.all(cycles.nodes.map(async (cycle) => {
|
|
110
|
+
const team = await cycle.team;
|
|
111
|
+
return {
|
|
112
|
+
id: cycle.id,
|
|
113
|
+
number: cycle.number,
|
|
114
|
+
name: cycle.name ?? null,
|
|
115
|
+
startsAt: cycle.startsAt,
|
|
116
|
+
endsAt: cycle.endsAt,
|
|
117
|
+
completedAt: cycle.completedAt ?? null,
|
|
118
|
+
progress: cycle.progress,
|
|
119
|
+
teamId: team?.id ?? '',
|
|
120
|
+
teamKey: team?.key ?? '',
|
|
121
|
+
teamName: team?.name ?? '',
|
|
122
|
+
};
|
|
123
|
+
}));
|
|
124
|
+
const pageInfo = {
|
|
125
|
+
hasNextPage: cycles.pageInfo.hasNextPage,
|
|
126
|
+
hasPreviousPage: cycles.pageInfo.hasPreviousPage,
|
|
127
|
+
startCursor: cycles.pageInfo.startCursor,
|
|
128
|
+
endCursor: cycles.pageInfo.endCursor,
|
|
129
|
+
};
|
|
130
|
+
if (format === 'json') {
|
|
131
|
+
print(successList(data, pageInfo));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
printList(data, format, {
|
|
135
|
+
columns: COLUMNS,
|
|
136
|
+
primaryKey: 'name',
|
|
137
|
+
secondaryKey: 'number',
|
|
138
|
+
pageInfo,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
handleError(err);
|
|
144
|
+
this.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
package/dist/commands/info.js
CHANGED
|
@@ -435,6 +435,40 @@ const COMMANDS = {
|
|
|
435
435
|
flags: { format: { type: 'string', options: ['json', 'table', 'plain'], default: 'json' } },
|
|
436
436
|
examples: ['linear me'],
|
|
437
437
|
},
|
|
438
|
+
// Cycles
|
|
439
|
+
'cycles list': {
|
|
440
|
+
description: 'List cycles (sprints)',
|
|
441
|
+
flags: {
|
|
442
|
+
format: { type: 'string', options: ['json', 'table', 'plain'], default: 'json' },
|
|
443
|
+
'team-id': { type: 'string', description: 'Filter by team ID' },
|
|
444
|
+
team: { type: 'string', description: 'Filter by team key (e.g., ENG)' },
|
|
445
|
+
active: { type: 'boolean', description: 'Show only active cycles' },
|
|
446
|
+
upcoming: { type: 'boolean', description: 'Show only upcoming cycles' },
|
|
447
|
+
completed: { type: 'boolean', description: 'Show only completed cycles' },
|
|
448
|
+
first: { type: 'number', description: 'Number of results' },
|
|
449
|
+
},
|
|
450
|
+
examples: [
|
|
451
|
+
'linear cycles list',
|
|
452
|
+
'linear cycles list --team ENG',
|
|
453
|
+
'linear cycles list --active',
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
'cycles get': {
|
|
457
|
+
description: 'Get cycle (sprint) details',
|
|
458
|
+
args: { id: { description: 'Cycle ID', required: true } },
|
|
459
|
+
flags: { format: { type: 'string', options: ['json', 'table', 'plain'], default: 'json' } },
|
|
460
|
+
examples: ['linear cycles get CYCLE_ID'],
|
|
461
|
+
},
|
|
462
|
+
'cycles current': {
|
|
463
|
+
description: 'Get the current active cycle for a team',
|
|
464
|
+
flags: {
|
|
465
|
+
format: { type: 'string', options: ['json', 'table', 'plain'], default: 'json' },
|
|
466
|
+
'team-id': { type: 'string', description: 'Team ID' },
|
|
467
|
+
team: { type: 'string', description: 'Team key (e.g., ENG)' },
|
|
468
|
+
},
|
|
469
|
+
examples: ['linear cycles current --team ENG'],
|
|
470
|
+
},
|
|
471
|
+
// Other
|
|
438
472
|
search: {
|
|
439
473
|
description: 'Search for issues',
|
|
440
474
|
args: { query: { description: 'Search query', required: true } },
|
|
@@ -516,6 +550,20 @@ const ENTITY_SCHEMAS = {
|
|
|
516
550
|
teams: 'Associated teams',
|
|
517
551
|
},
|
|
518
552
|
},
|
|
553
|
+
cycles: {
|
|
554
|
+
entity: 'cycles',
|
|
555
|
+
operations: ['list', 'get', 'current'],
|
|
556
|
+
description: 'Time-boxed iterations (sprints)',
|
|
557
|
+
fields: {
|
|
558
|
+
id: 'Unique identifier',
|
|
559
|
+
number: 'Cycle number',
|
|
560
|
+
name: 'Cycle name (optional)',
|
|
561
|
+
startsAt: 'Start date',
|
|
562
|
+
endsAt: 'End date',
|
|
563
|
+
progress: 'Completion progress (0-1)',
|
|
564
|
+
team: 'Associated team',
|
|
565
|
+
},
|
|
566
|
+
},
|
|
519
567
|
teams: {
|
|
520
568
|
entity: 'teams',
|
|
521
569
|
operations: ['list'],
|
|
@@ -644,7 +692,7 @@ export default class Info extends Command {
|
|
|
644
692
|
return acc;
|
|
645
693
|
}, {});
|
|
646
694
|
print(success({
|
|
647
|
-
version: '0.
|
|
695
|
+
version: '0.6.0',
|
|
648
696
|
commands: compactCommands,
|
|
649
697
|
configKeys: CONFIG_KEYS,
|
|
650
698
|
note: 'Use "linear info" for full documentation with examples and workflows',
|
|
@@ -653,7 +701,7 @@ export default class Info extends Command {
|
|
|
653
701
|
}
|
|
654
702
|
// Full documentation
|
|
655
703
|
print(success({
|
|
656
|
-
version: '0.
|
|
704
|
+
version: '0.6.0',
|
|
657
705
|
overview: {
|
|
658
706
|
description: 'CLI for interacting with Linear, designed for LLMs and agents',
|
|
659
707
|
authentication: 'Run "linear auth login" or set LINEAR_API_KEY environment variable',
|
package/dist/lib/formatter.d.ts
CHANGED
|
@@ -49,6 +49,10 @@ export declare const formatPriority: (priority: number) => string;
|
|
|
49
49
|
* Truncate string to max length with ellipsis.
|
|
50
50
|
*/
|
|
51
51
|
export declare const truncate: (str: string | undefined | null, maxLength: number) => string;
|
|
52
|
+
/**
|
|
53
|
+
* Format progress as a percentage with color.
|
|
54
|
+
*/
|
|
55
|
+
export declare const formatProgress: (progress: number) => string;
|
|
52
56
|
/**
|
|
53
57
|
* Generic formatter that outputs data in the specified format.
|
|
54
58
|
*/
|
package/dist/lib/formatter.js
CHANGED
|
@@ -166,6 +166,21 @@ export const truncate = (str, maxLength) => {
|
|
|
166
166
|
return str;
|
|
167
167
|
return str.slice(0, maxLength - 1) + '\u2026';
|
|
168
168
|
};
|
|
169
|
+
/**
|
|
170
|
+
* Format progress as a percentage with color.
|
|
171
|
+
*/
|
|
172
|
+
export const formatProgress = (progress) => {
|
|
173
|
+
const percent = Math.round(progress * 100);
|
|
174
|
+
if (percent >= 100)
|
|
175
|
+
return colors.green(`${percent}%`);
|
|
176
|
+
if (percent >= 75)
|
|
177
|
+
return colors.cyan(`${percent}%`);
|
|
178
|
+
if (percent >= 50)
|
|
179
|
+
return colors.blue(`${percent}%`);
|
|
180
|
+
if (percent >= 25)
|
|
181
|
+
return colors.yellow(`${percent}%`);
|
|
182
|
+
return colors.gray(`${percent}%`);
|
|
183
|
+
};
|
|
169
184
|
/**
|
|
170
185
|
* Generic formatter that outputs data in the specified format.
|
|
171
186
|
*/
|