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 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. **Discover capabilities**: Use `linear schema` to understand available operations
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. **Error codes**: Programmatic error handling via error codes
384
- 4. **Raw queries**: Use `linear query` for complex operations not covered by built-in commands
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. Discover what operations are available
390
- linear schema
453
+ # 1. Get complete CLI documentation in one command
454
+ linear info
391
455
 
392
- # 2. Get details about issues
393
- linear schema issues
456
+ # 2. Or get compact version for limited context
457
+ linear info --compact
394
458
 
395
- # 3. List issues assigned to current user
396
- linear issues list --assignee me
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
- # 4. Create a new issue
399
- linear issues create --input '{"title":"From LLM","teamId":"xxx"}'
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
+ }
@@ -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.5.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.5.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',
@@ -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
  */
@@ -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
  */