linear-cli-agents 0.1.1 → 0.2.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 +50 -1
- package/bin/dev.js +0 -0
- package/dist/commands/auth/__tests__/status.test.d.ts +1 -0
- package/dist/commands/auth/__tests__/status.test.js +23 -0
- package/dist/commands/auth/login.d.ts +1 -0
- package/dist/commands/auth/login.js +39 -3
- package/dist/commands/issues/delete.js +1 -3
- package/dist/commands/issues/get.d.ts +3 -0
- package/dist/commands/issues/get.js +39 -6
- package/dist/commands/issues/list.d.ts +1 -0
- package/dist/commands/issues/list.js +42 -5
- package/dist/commands/me.d.ts +10 -0
- package/dist/commands/me.js +76 -0
- package/dist/commands/open.d.ts +15 -0
- package/dist/commands/open.js +100 -0
- package/dist/commands/teams/list.d.ts +11 -0
- package/dist/commands/teams/list.js +99 -0
- package/dist/lib/__tests__/errors.test.d.ts +1 -0
- package/dist/lib/__tests__/errors.test.js +98 -0
- package/dist/lib/__tests__/output.test.d.ts +1 -0
- package/dist/lib/__tests__/output.test.js +112 -0
- package/dist/lib/formatter.d.ts +59 -0
- package/dist/lib/formatter.js +192 -0
- package/dist/lib/output.d.ts +17 -1
- package/dist/lib/output.js +43 -0
- package/dist/lib/types.d.ts +7 -0
- package/oclif.manifest.json +221 -4
- package/package.json +35 -15
package/README.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
# linear-cli
|
|
1
|
+
# linear-cli-agents
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/linear-cli-agents)
|
|
4
|
+
[](https://github.com/nchgn/linear-cli/actions/workflows/ci.yml)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
2
6
|
|
|
3
7
|
A CLI for interacting with [Linear](https://linear.app), designed for LLMs and agents.
|
|
4
8
|
|
|
@@ -163,6 +167,51 @@ pnpm build
|
|
|
163
167
|
pnpm test
|
|
164
168
|
```
|
|
165
169
|
|
|
170
|
+
## Troubleshooting
|
|
171
|
+
|
|
172
|
+
### Authentication Issues
|
|
173
|
+
|
|
174
|
+
**"Not authenticated" error:**
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Check current auth status
|
|
178
|
+
linear auth status
|
|
179
|
+
|
|
180
|
+
# Re-authenticate
|
|
181
|
+
linear auth login --key lin_api_xxxxx
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Using environment variable:**
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
export LINEAR_API_KEY=lin_api_xxxxx
|
|
188
|
+
linear auth status # Should show source: environment
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Common Errors
|
|
192
|
+
|
|
193
|
+
| Error Code | Cause | Solution |
|
|
194
|
+
| ------------------- | -------------------------- | ----------------------------------------------- |
|
|
195
|
+
| `NOT_AUTHENTICATED` | No API key configured | Run `linear auth login` or set `LINEAR_API_KEY` |
|
|
196
|
+
| `INVALID_API_KEY` | API key expired or invalid | Generate a new key in Linear settings |
|
|
197
|
+
| `NOT_FOUND` | Resource doesn't exist | Check the issue/team identifier |
|
|
198
|
+
| `RATE_LIMITED` | Too many requests | Wait before retrying |
|
|
199
|
+
|
|
200
|
+
### Getting Help
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# See all commands
|
|
204
|
+
linear --help
|
|
205
|
+
|
|
206
|
+
# Get help for a specific command
|
|
207
|
+
linear issues --help
|
|
208
|
+
linear issues create --help
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Contributing
|
|
212
|
+
|
|
213
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
214
|
+
|
|
166
215
|
## License
|
|
167
216
|
|
|
168
217
|
MIT
|
package/bin/dev.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
/**
|
|
3
|
+
* Note: Full integration tests for auth commands require mocking
|
|
4
|
+
* the Linear API client, which is complex with oclif's command runner.
|
|
5
|
+
*
|
|
6
|
+
* For now, we test the output format contracts and defer
|
|
7
|
+
* integration testing to manual verification or E2E tests.
|
|
8
|
+
*
|
|
9
|
+
* The underlying utilities (output.ts, errors.ts) are fully tested.
|
|
10
|
+
*/
|
|
11
|
+
describe('auth status command', () => {
|
|
12
|
+
it('should have proper command structure', async () => {
|
|
13
|
+
// Dynamic import to verify module loads correctly
|
|
14
|
+
const { default: AuthStatus } = await import('../status.js');
|
|
15
|
+
expect(AuthStatus.description).toBe('Check authentication status');
|
|
16
|
+
expect(AuthStatus.examples).toHaveLength(1);
|
|
17
|
+
});
|
|
18
|
+
it('should export as oclif Command', async () => {
|
|
19
|
+
const { default: AuthStatus } = await import('../status.js');
|
|
20
|
+
const { Command } = await import('@oclif/core');
|
|
21
|
+
expect(AuthStatus.prototype).toBeInstanceOf(Command.prototype.constructor);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -4,6 +4,7 @@ export default class AuthLogin extends Command {
|
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
6
|
key: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
browser: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
8
|
};
|
|
8
9
|
static args: {
|
|
9
10
|
key: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import open from 'open';
|
|
2
3
|
import { createClient } from '../../lib/client.js';
|
|
3
4
|
import { saveApiKey, getConfigPath } from '../../lib/config.js';
|
|
4
|
-
import { success, print } from '../../lib/output.js';
|
|
5
|
-
import { handleError
|
|
5
|
+
import { success, error, print } from '../../lib/output.js';
|
|
6
|
+
import { handleError } from '../../lib/errors.js';
|
|
7
|
+
import { colors } from '../../lib/formatter.js';
|
|
8
|
+
const LINEAR_API_SETTINGS_URL = 'https://linear.app/settings/api';
|
|
6
9
|
export default class AuthLogin extends Command {
|
|
7
10
|
static description = 'Authenticate with Linear using an API key';
|
|
8
11
|
static examples = [
|
|
9
12
|
'<%= config.bin %> auth login --key lin_api_xxxxx',
|
|
13
|
+
'<%= config.bin %> auth login --browser',
|
|
10
14
|
'LINEAR_API_KEY=lin_api_xxxxx <%= config.bin %> auth login',
|
|
11
15
|
];
|
|
12
16
|
static flags = {
|
|
@@ -15,6 +19,11 @@ export default class AuthLogin extends Command {
|
|
|
15
19
|
description: 'Linear API key (or set LINEAR_API_KEY env var)',
|
|
16
20
|
env: 'LINEAR_API_KEY',
|
|
17
21
|
}),
|
|
22
|
+
browser: Flags.boolean({
|
|
23
|
+
char: 'b',
|
|
24
|
+
description: 'Open Linear API settings in browser to create a key',
|
|
25
|
+
default: false,
|
|
26
|
+
}),
|
|
18
27
|
};
|
|
19
28
|
static args = {
|
|
20
29
|
key: Args.string({
|
|
@@ -26,8 +35,35 @@ export default class AuthLogin extends Command {
|
|
|
26
35
|
try {
|
|
27
36
|
const { args, flags } = await this.parse(AuthLogin);
|
|
28
37
|
const apiKey = flags.key || args.key;
|
|
38
|
+
// If --browser flag, open Linear API settings
|
|
39
|
+
if (flags.browser) {
|
|
40
|
+
await open(LINEAR_API_SETTINGS_URL);
|
|
41
|
+
print(success({
|
|
42
|
+
message: 'Opened Linear API settings in browser',
|
|
43
|
+
url: LINEAR_API_SETTINGS_URL,
|
|
44
|
+
instructions: [
|
|
45
|
+
'1. Click "Create key" or "New API key"',
|
|
46
|
+
'2. Give it a label (e.g., "CLI")',
|
|
47
|
+
'3. Copy the generated key (starts with lin_api_)',
|
|
48
|
+
'4. Run: linear auth login --key YOUR_KEY',
|
|
49
|
+
],
|
|
50
|
+
}));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// If no API key provided, show helpful instructions
|
|
29
54
|
if (!apiKey) {
|
|
30
|
-
|
|
55
|
+
console.log(colors.bold('\nTo authenticate with Linear CLI, you need an API key.\n'));
|
|
56
|
+
console.log(colors.cyan('Option 1: Open browser to create a key'));
|
|
57
|
+
console.log(' linear auth login --browser\n');
|
|
58
|
+
console.log(colors.cyan('Option 2: If you already have a key'));
|
|
59
|
+
console.log(' linear auth login --key lin_api_xxxxx\n');
|
|
60
|
+
console.log(colors.cyan('Option 3: Set environment variable'));
|
|
61
|
+
console.log(' export LINEAR_API_KEY=lin_api_xxxxx\n');
|
|
62
|
+
console.log(colors.dim(`Manual URL: ${LINEAR_API_SETTINGS_URL}`));
|
|
63
|
+
console.log('');
|
|
64
|
+
print(error('MISSING_API_KEY', 'No API key provided. Use --browser to create one or --key to provide an existing key.', { url: LINEAR_API_SETTINGS_URL }));
|
|
65
|
+
this.exit(1);
|
|
66
|
+
return;
|
|
31
67
|
}
|
|
32
68
|
// Validate the API key by making a test request
|
|
33
69
|
const client = createClient(apiKey);
|
|
@@ -48,9 +48,7 @@ export default class IssuesDelete extends Command {
|
|
|
48
48
|
identifier,
|
|
49
49
|
deleted: true,
|
|
50
50
|
permanent: flags.permanent,
|
|
51
|
-
message: flags.permanent
|
|
52
|
-
? `Issue ${identifier} permanently deleted`
|
|
53
|
-
: `Issue ${identifier} moved to trash`,
|
|
51
|
+
message: flags.permanent ? `Issue ${identifier} permanently deleted` : `Issue ${identifier} moved to trash`,
|
|
54
52
|
}));
|
|
55
53
|
}
|
|
56
54
|
catch (err) {
|
|
@@ -5,5 +5,8 @@ export default class IssuesGet extends Command {
|
|
|
5
5
|
static args: {
|
|
6
6
|
id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
7
|
};
|
|
8
|
+
static flags: {
|
|
9
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
8
11
|
run(): Promise<void>;
|
|
9
12
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { Args, Command } from '@oclif/core';
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { getClient } from '../../lib/client.js';
|
|
3
|
-
import { success, print } from '../../lib/output.js';
|
|
3
|
+
import { success, print, printItem } from '../../lib/output.js';
|
|
4
4
|
import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
|
|
5
5
|
import { resolveIssueId } from '../../lib/issue-utils.js';
|
|
6
6
|
export default class IssuesGet extends Command {
|
|
7
7
|
static description = 'Get a single issue by ID or identifier';
|
|
8
8
|
static examples = [
|
|
9
|
-
'<%= config.bin %> issues get abc123',
|
|
10
9
|
'<%= config.bin %> issues get ENG-123',
|
|
10
|
+
'<%= config.bin %> issues get ENG-123 --format table',
|
|
11
|
+
'<%= config.bin %> issues get abc123',
|
|
11
12
|
];
|
|
12
13
|
static args = {
|
|
13
14
|
id: Args.string({
|
|
@@ -15,9 +16,18 @@ export default class IssuesGet extends Command {
|
|
|
15
16
|
required: true,
|
|
16
17
|
}),
|
|
17
18
|
};
|
|
19
|
+
static flags = {
|
|
20
|
+
format: Flags.string({
|
|
21
|
+
char: 'F',
|
|
22
|
+
description: 'Output format',
|
|
23
|
+
options: ['json', 'table', 'plain'],
|
|
24
|
+
default: 'json',
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
18
27
|
async run() {
|
|
19
28
|
try {
|
|
20
|
-
const { args } = await this.parse(IssuesGet);
|
|
29
|
+
const { args, flags } = await this.parse(IssuesGet);
|
|
30
|
+
const format = flags.format;
|
|
21
31
|
const client = getClient();
|
|
22
32
|
const issueId = await resolveIssueId(client, args.id);
|
|
23
33
|
const issue = await client.issue(issueId);
|
|
@@ -32,7 +42,7 @@ export default class IssuesGet extends Command {
|
|
|
32
42
|
issue.labels(),
|
|
33
43
|
issue.comments(),
|
|
34
44
|
]);
|
|
35
|
-
|
|
45
|
+
const data = {
|
|
36
46
|
id: issue.id,
|
|
37
47
|
identifier: issue.identifier,
|
|
38
48
|
title: issue.title,
|
|
@@ -71,7 +81,30 @@ export default class IssuesGet extends Command {
|
|
|
71
81
|
color: label.color,
|
|
72
82
|
})),
|
|
73
83
|
commentsCount: comments.nodes.length,
|
|
74
|
-
}
|
|
84
|
+
};
|
|
85
|
+
if (format === 'json') {
|
|
86
|
+
print(success(data));
|
|
87
|
+
}
|
|
88
|
+
else if (format === 'table') {
|
|
89
|
+
printItem({
|
|
90
|
+
identifier: data.identifier,
|
|
91
|
+
title: data.title,
|
|
92
|
+
state: data.state?.name ?? 'N/A',
|
|
93
|
+
priority: data.priorityLabel,
|
|
94
|
+
team: data.team?.key ?? 'N/A',
|
|
95
|
+
assignee: data.assignee?.name ?? 'Unassigned',
|
|
96
|
+
labels: data.labels.map((l) => l.name).join(', ') || 'None',
|
|
97
|
+
estimate: data.estimate ?? 'None',
|
|
98
|
+
comments: data.commentsCount,
|
|
99
|
+
url: data.url,
|
|
100
|
+
createdAt: data.createdAt,
|
|
101
|
+
updatedAt: data.updatedAt,
|
|
102
|
+
}, format);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// plain: just the identifier
|
|
106
|
+
console.log(data.identifier);
|
|
107
|
+
}
|
|
75
108
|
}
|
|
76
109
|
catch (err) {
|
|
77
110
|
handleError(err);
|
|
@@ -3,6 +3,7 @@ export default class IssuesList extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
team: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
assignee: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
state: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import { getClient } from '../../lib/client.js';
|
|
3
|
-
import { successList, print } from '../../lib/output.js';
|
|
3
|
+
import { successList, print, printList } from '../../lib/output.js';
|
|
4
4
|
import { handleError } from '../../lib/errors.js';
|
|
5
|
+
import { colors, truncate, formatPriority } from '../../lib/formatter.js';
|
|
6
|
+
const COLUMNS = [
|
|
7
|
+
{
|
|
8
|
+
key: 'identifier',
|
|
9
|
+
header: 'ID',
|
|
10
|
+
format: (value) => colors.cyan(String(value)),
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
key: 'priority',
|
|
14
|
+
header: 'PRI',
|
|
15
|
+
format: (value) => formatPriority(Number(value)),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: 'title',
|
|
19
|
+
header: 'TITLE',
|
|
20
|
+
format: (value) => truncate(String(value), 50),
|
|
21
|
+
},
|
|
22
|
+
];
|
|
5
23
|
export default class IssuesList extends Command {
|
|
6
24
|
static description = 'List issues with optional filtering';
|
|
7
25
|
static examples = [
|
|
8
26
|
'<%= config.bin %> issues list',
|
|
27
|
+
'<%= config.bin %> issues list --format table',
|
|
9
28
|
'<%= config.bin %> issues list --team ENG',
|
|
10
29
|
'<%= config.bin %> issues list --assignee me',
|
|
11
30
|
'<%= config.bin %> issues list --filter \'{"state":{"name":{"eq":"In Progress"}}}\'',
|
|
12
31
|
'<%= config.bin %> issues list --first 50 --after cursor123',
|
|
13
32
|
];
|
|
14
33
|
static flags = {
|
|
34
|
+
format: Flags.string({
|
|
35
|
+
char: 'F',
|
|
36
|
+
description: 'Output format',
|
|
37
|
+
options: ['json', 'table', 'plain'],
|
|
38
|
+
default: 'json',
|
|
39
|
+
}),
|
|
15
40
|
team: Flags.string({
|
|
16
41
|
char: 't',
|
|
17
42
|
description: 'Filter by team key (e.g., ENG)',
|
|
@@ -39,6 +64,7 @@ export default class IssuesList extends Command {
|
|
|
39
64
|
async run() {
|
|
40
65
|
try {
|
|
41
66
|
const { flags } = await this.parse(IssuesList);
|
|
67
|
+
const format = flags.format;
|
|
42
68
|
const client = getClient();
|
|
43
69
|
// Build filter
|
|
44
70
|
let filter = {};
|
|
@@ -78,20 +104,31 @@ export default class IssuesList extends Command {
|
|
|
78
104
|
id: issue.id,
|
|
79
105
|
identifier: issue.identifier,
|
|
80
106
|
title: issue.title,
|
|
81
|
-
description: issue.description,
|
|
107
|
+
description: issue.description ?? undefined,
|
|
82
108
|
priority: issue.priority,
|
|
83
109
|
priorityLabel: issue.priorityLabel,
|
|
84
|
-
estimate: issue.estimate,
|
|
110
|
+
estimate: issue.estimate ?? undefined,
|
|
85
111
|
url: issue.url,
|
|
86
112
|
createdAt: issue.createdAt,
|
|
87
113
|
updatedAt: issue.updatedAt,
|
|
88
114
|
}));
|
|
89
|
-
|
|
115
|
+
const pageInfo = {
|
|
90
116
|
hasNextPage: issues.pageInfo.hasNextPage,
|
|
91
117
|
hasPreviousPage: issues.pageInfo.hasPreviousPage,
|
|
92
118
|
startCursor: issues.pageInfo.startCursor,
|
|
93
119
|
endCursor: issues.pageInfo.endCursor,
|
|
94
|
-
}
|
|
120
|
+
};
|
|
121
|
+
if (format === 'json') {
|
|
122
|
+
print(successList(data, pageInfo));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
printList(data, format, {
|
|
126
|
+
columns: COLUMNS,
|
|
127
|
+
primaryKey: 'identifier',
|
|
128
|
+
secondaryKey: 'title',
|
|
129
|
+
pageInfo,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
95
132
|
}
|
|
96
133
|
catch (err) {
|
|
97
134
|
handleError(err);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Me extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static aliases: string[];
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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 } from '../lib/errors.js';
|
|
5
|
+
export default class Me extends Command {
|
|
6
|
+
static description = 'Show current user information';
|
|
7
|
+
static aliases = ['whoami'];
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> me',
|
|
10
|
+
'<%= config.bin %> me --format table',
|
|
11
|
+
'<%= config.bin %> whoami',
|
|
12
|
+
];
|
|
13
|
+
static flags = {
|
|
14
|
+
format: Flags.string({
|
|
15
|
+
char: 'F',
|
|
16
|
+
description: 'Output format',
|
|
17
|
+
options: ['json', 'table', 'plain'],
|
|
18
|
+
default: 'json',
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
async run() {
|
|
22
|
+
try {
|
|
23
|
+
const { flags } = await this.parse(Me);
|
|
24
|
+
const format = flags.format;
|
|
25
|
+
const client = getClient();
|
|
26
|
+
const viewer = await client.viewer;
|
|
27
|
+
const [teams, organization] = await Promise.all([viewer.teams(), viewer.organization]);
|
|
28
|
+
const data = {
|
|
29
|
+
id: viewer.id,
|
|
30
|
+
name: viewer.name,
|
|
31
|
+
email: viewer.email,
|
|
32
|
+
displayName: viewer.displayName,
|
|
33
|
+
active: viewer.active,
|
|
34
|
+
admin: viewer.admin,
|
|
35
|
+
timezone: viewer.timezone,
|
|
36
|
+
createdAt: viewer.createdAt,
|
|
37
|
+
organization: organization
|
|
38
|
+
? {
|
|
39
|
+
id: organization.id,
|
|
40
|
+
name: organization.name,
|
|
41
|
+
urlKey: organization.urlKey,
|
|
42
|
+
}
|
|
43
|
+
: null,
|
|
44
|
+
teams: teams.nodes.map((team) => ({
|
|
45
|
+
id: team.id,
|
|
46
|
+
key: team.key,
|
|
47
|
+
name: team.name,
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
if (format === 'json') {
|
|
51
|
+
print(success(data));
|
|
52
|
+
}
|
|
53
|
+
else if (format === 'table') {
|
|
54
|
+
printItem({
|
|
55
|
+
id: data.id,
|
|
56
|
+
name: data.name,
|
|
57
|
+
email: data.email,
|
|
58
|
+
displayName: data.displayName,
|
|
59
|
+
active: data.active ? 'Yes' : 'No',
|
|
60
|
+
admin: data.admin ? 'Yes' : 'No',
|
|
61
|
+
timezone: data.timezone,
|
|
62
|
+
organization: data.organization?.name ?? 'N/A',
|
|
63
|
+
teams: data.teams.map((t) => t.key).join(', '),
|
|
64
|
+
}, format);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// plain: just the email
|
|
68
|
+
console.log(data.email);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
handleError(err);
|
|
73
|
+
this.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Open extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
issue: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
team: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
inbox: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
settings: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
'my-issues': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import open from 'open';
|
|
3
|
+
import { getClient } from '../lib/client.js';
|
|
4
|
+
import { success, print } from '../lib/output.js';
|
|
5
|
+
import { handleError } from '../lib/errors.js';
|
|
6
|
+
import { parseIdentifier, isUUID, resolveIssueId } from '../lib/issue-utils.js';
|
|
7
|
+
export default class Open extends Command {
|
|
8
|
+
static description = 'Open Linear resources in browser';
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> open ENG-123',
|
|
11
|
+
'<%= config.bin %> open --team ENG',
|
|
12
|
+
'<%= config.bin %> open --inbox',
|
|
13
|
+
'<%= config.bin %> open --settings',
|
|
14
|
+
'<%= config.bin %> open --my-issues',
|
|
15
|
+
];
|
|
16
|
+
static args = {
|
|
17
|
+
issue: Args.string({
|
|
18
|
+
description: 'Issue identifier (e.g., ENG-123) or ID to open',
|
|
19
|
+
required: false,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
static flags = {
|
|
23
|
+
team: Flags.string({
|
|
24
|
+
char: 't',
|
|
25
|
+
description: 'Open team page by key (e.g., ENG)',
|
|
26
|
+
exclusive: ['inbox', 'settings', 'my-issues'],
|
|
27
|
+
}),
|
|
28
|
+
inbox: Flags.boolean({
|
|
29
|
+
description: 'Open inbox',
|
|
30
|
+
exclusive: ['team', 'settings', 'my-issues'],
|
|
31
|
+
}),
|
|
32
|
+
settings: Flags.boolean({
|
|
33
|
+
description: 'Open workspace settings',
|
|
34
|
+
exclusive: ['team', 'inbox', 'my-issues'],
|
|
35
|
+
}),
|
|
36
|
+
'my-issues': Flags.boolean({
|
|
37
|
+
description: 'Open my issues view',
|
|
38
|
+
exclusive: ['team', 'inbox', 'settings'],
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
async run() {
|
|
42
|
+
try {
|
|
43
|
+
const { args, flags } = await this.parse(Open);
|
|
44
|
+
const client = getClient();
|
|
45
|
+
// Get organization for URL building
|
|
46
|
+
const viewer = await client.viewer;
|
|
47
|
+
const organization = await viewer.organization;
|
|
48
|
+
if (!organization) {
|
|
49
|
+
throw new Error('Could not determine organization');
|
|
50
|
+
}
|
|
51
|
+
const orgKey = organization.urlKey;
|
|
52
|
+
let url;
|
|
53
|
+
if (args.issue) {
|
|
54
|
+
// Open specific issue
|
|
55
|
+
const parsed = parseIdentifier(args.issue);
|
|
56
|
+
if (parsed) {
|
|
57
|
+
url = `https://linear.app/${orgKey}/issue/${args.issue}`;
|
|
58
|
+
}
|
|
59
|
+
else if (isUUID(args.issue)) {
|
|
60
|
+
// Need to fetch issue to get identifier for URL
|
|
61
|
+
const issueId = await resolveIssueId(client, args.issue);
|
|
62
|
+
const issue = await client.issue(issueId);
|
|
63
|
+
url = issue.url;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
throw new Error(`Invalid issue identifier: ${args.issue}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (flags.team) {
|
|
70
|
+
// Open team page
|
|
71
|
+
url = `https://linear.app/${orgKey}/team/${flags.team}`;
|
|
72
|
+
}
|
|
73
|
+
else if (flags.inbox) {
|
|
74
|
+
// Open inbox
|
|
75
|
+
url = `https://linear.app/${orgKey}/inbox`;
|
|
76
|
+
}
|
|
77
|
+
else if (flags.settings) {
|
|
78
|
+
// Open settings
|
|
79
|
+
url = `https://linear.app/${orgKey}/settings`;
|
|
80
|
+
}
|
|
81
|
+
else if (flags['my-issues']) {
|
|
82
|
+
// Open my issues
|
|
83
|
+
url = `https://linear.app/${orgKey}/my-issues`;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Default: open workspace home
|
|
87
|
+
url = `https://linear.app/${orgKey}`;
|
|
88
|
+
}
|
|
89
|
+
await open(url);
|
|
90
|
+
print(success({
|
|
91
|
+
opened: true,
|
|
92
|
+
url,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
handleError(err);
|
|
97
|
+
this.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class TeamsList 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
|
+
first: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
after: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|