@bdsqqq/lnr-core 0.1.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/package.json +32 -0
- package/src/client.ts +33 -0
- package/src/config.test.ts +44 -0
- package/src/config.ts +65 -0
- package/src/cycles.test.ts +45 -0
- package/src/cycles.ts +93 -0
- package/src/index.ts +73 -0
- package/src/issues.test.ts +66 -0
- package/src/issues.ts +182 -0
- package/src/me.test.ts +49 -0
- package/src/me.ts +61 -0
- package/src/projects.test.ts +42 -0
- package/src/projects.ts +147 -0
- package/src/search.test.ts +38 -0
- package/src/search.ts +33 -0
- package/src/teams.test.ts +60 -0
- package/src/teams.ts +92 -0
- package/src/types.ts +87 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bdsqqq/lnr-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "core business logic for linear-cli",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"types": "src/index.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/bdsqqq/lnr.git",
|
|
15
|
+
"directory": "packages/core"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "bdsqqq",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"check": "tsc --noEmit",
|
|
21
|
+
"test": "bun test"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@linear/sdk": "^68.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/bun": "^1.1.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"typescript": "^5"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LinearClient } from "@linear/sdk";
|
|
2
|
+
import { getApiKey } from "./config";
|
|
3
|
+
|
|
4
|
+
let clientInstance: LinearClient | null = null;
|
|
5
|
+
|
|
6
|
+
export class NotAuthenticatedError extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super("not authenticated");
|
|
9
|
+
this.name = "NotAuthenticatedError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getClient(): LinearClient {
|
|
14
|
+
if (clientInstance) {
|
|
15
|
+
return clientInstance;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const apiKey = getApiKey();
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
throw new NotAuthenticatedError();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
clientInstance = new LinearClient({ apiKey });
|
|
24
|
+
return clientInstance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createClientWithKey(apiKey: string): LinearClient {
|
|
28
|
+
return new LinearClient({ apiKey });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resetClient(): void {
|
|
32
|
+
clientInstance = null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, test, expect, afterEach, beforeAll } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
loadConfig,
|
|
4
|
+
saveConfig,
|
|
5
|
+
getConfigValue,
|
|
6
|
+
setConfigValue,
|
|
7
|
+
type Config,
|
|
8
|
+
} from "./config";
|
|
9
|
+
|
|
10
|
+
describe("config core", () => {
|
|
11
|
+
let originalConfig: Config;
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
originalConfig = loadConfig();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
saveConfig(originalConfig);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("loadConfig returns object", () => {
|
|
22
|
+
const config = loadConfig();
|
|
23
|
+
expect(typeof config).toBe("object");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("setConfigValue and getConfigValue work", () => {
|
|
27
|
+
const testValue = "TEST_TEAM_VALUE";
|
|
28
|
+
setConfigValue("default_team", testValue);
|
|
29
|
+
expect(getConfigValue("default_team")).toBe(testValue);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("saveConfig and loadConfig roundtrip", () => {
|
|
33
|
+
const testConfig: Config = {
|
|
34
|
+
api_key: "test_key",
|
|
35
|
+
default_team: "TEST",
|
|
36
|
+
output_format: "json",
|
|
37
|
+
};
|
|
38
|
+
saveConfig(testConfig);
|
|
39
|
+
const loaded = loadConfig();
|
|
40
|
+
expect(loaded.api_key).toBe(testConfig.api_key);
|
|
41
|
+
expect(loaded.default_team).toBe(testConfig.default_team);
|
|
42
|
+
expect(loaded.output_format).toBe(testConfig.output_format);
|
|
43
|
+
});
|
|
44
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
|
|
5
|
+
export interface Config {
|
|
6
|
+
api_key?: string;
|
|
7
|
+
default_team?: string;
|
|
8
|
+
output_format?: "table" | "json" | "quiet";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = join(homedir(), ".linear-cli");
|
|
12
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
13
|
+
|
|
14
|
+
export function ensureConfigDir(): void {
|
|
15
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
16
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function loadConfig(): Config {
|
|
21
|
+
ensureConfigDir();
|
|
22
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
27
|
+
return JSON.parse(raw);
|
|
28
|
+
} catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function saveConfig(config: Config): void {
|
|
34
|
+
ensureConfigDir();
|
|
35
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getApiKey(): string | undefined {
|
|
39
|
+
return loadConfig().api_key;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function setApiKey(key: string): void {
|
|
43
|
+
const config = loadConfig();
|
|
44
|
+
config.api_key = key;
|
|
45
|
+
saveConfig(config);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function clearApiKey(): void {
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
delete config.api_key;
|
|
51
|
+
saveConfig(config);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getConfigValue<K extends keyof Config>(key: K): Config[K] {
|
|
55
|
+
return loadConfig()[key];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function setConfigValue<K extends keyof Config>(
|
|
59
|
+
key: K,
|
|
60
|
+
value: Config[K]
|
|
61
|
+
): void {
|
|
62
|
+
const config = loadConfig();
|
|
63
|
+
config[key] = value;
|
|
64
|
+
saveConfig(config);
|
|
65
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "bun:test";
|
|
2
|
+
import { LinearClient } from "@linear/sdk";
|
|
3
|
+
import { getApiKey } from "./config";
|
|
4
|
+
import { listCycles, getCurrentCycle } from "./cycles";
|
|
5
|
+
import { getAvailableTeamKeys } from "./teams";
|
|
6
|
+
|
|
7
|
+
function getTestClient(): LinearClient {
|
|
8
|
+
const apiKey = getApiKey();
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
throw new Error("no api key configured - run: li auth <api-key>");
|
|
11
|
+
}
|
|
12
|
+
return new LinearClient({ apiKey });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("cycles core", () => {
|
|
16
|
+
let client: LinearClient;
|
|
17
|
+
let teamKey: string;
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
client = getTestClient();
|
|
21
|
+
const keys = await getAvailableTeamKeys(client);
|
|
22
|
+
teamKey = keys.includes("BDSQ") ? "BDSQ" : keys[0] ?? "UNKNOWN";
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("listCycles returns cycle array", async () => {
|
|
26
|
+
const cycles = await listCycles(client, teamKey);
|
|
27
|
+
expect(Array.isArray(cycles)).toBe(true);
|
|
28
|
+
if (cycles.length > 0) {
|
|
29
|
+
expect(cycles[0]).toHaveProperty("id");
|
|
30
|
+
expect(cycles[0]).toHaveProperty("number");
|
|
31
|
+
expect(cycles[0]).toHaveProperty("startsAt");
|
|
32
|
+
expect(cycles[0]).toHaveProperty("endsAt");
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("listCycles returns empty for invalid team", async () => {
|
|
37
|
+
const cycles = await listCycles(client, "INVALID_KEY_XYZ");
|
|
38
|
+
expect(cycles).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("getCurrentCycle returns null for invalid team", async () => {
|
|
42
|
+
const cycle = await getCurrentCycle(client, "INVALID_KEY_XYZ");
|
|
43
|
+
expect(cycle).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
package/src/cycles.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { LinearClient } from "@linear/sdk";
|
|
2
|
+
import type { Cycle, Issue } from "./types";
|
|
3
|
+
|
|
4
|
+
export async function listCycles(
|
|
5
|
+
client: LinearClient,
|
|
6
|
+
teamKey: string
|
|
7
|
+
): Promise<Cycle[]> {
|
|
8
|
+
try {
|
|
9
|
+
const team = await client.team(teamKey);
|
|
10
|
+
|
|
11
|
+
if (!team) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const cyclesConnection = await team.cycles();
|
|
16
|
+
return cyclesConnection.nodes.map((c) => ({
|
|
17
|
+
id: c.id,
|
|
18
|
+
number: c.number,
|
|
19
|
+
name: c.name,
|
|
20
|
+
startsAt: c.startsAt,
|
|
21
|
+
endsAt: c.endsAt,
|
|
22
|
+
}));
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getCurrentCycle(
|
|
29
|
+
client: LinearClient,
|
|
30
|
+
teamKey: string
|
|
31
|
+
): Promise<Cycle | null> {
|
|
32
|
+
try {
|
|
33
|
+
const team = await client.team(teamKey);
|
|
34
|
+
|
|
35
|
+
if (!team) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const activeCycle = await team.activeCycle;
|
|
40
|
+
|
|
41
|
+
if (!activeCycle) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id: activeCycle.id,
|
|
47
|
+
number: activeCycle.number,
|
|
48
|
+
name: activeCycle.name,
|
|
49
|
+
startsAt: activeCycle.startsAt,
|
|
50
|
+
endsAt: activeCycle.endsAt,
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function getCycleIssues(
|
|
58
|
+
client: LinearClient,
|
|
59
|
+
teamKey: string
|
|
60
|
+
): Promise<Issue[]> {
|
|
61
|
+
try {
|
|
62
|
+
const team = await client.team(teamKey);
|
|
63
|
+
|
|
64
|
+
if (!team) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const activeCycle = await team.activeCycle;
|
|
69
|
+
|
|
70
|
+
if (!activeCycle) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const issuesConnection = await activeCycle.issues();
|
|
75
|
+
|
|
76
|
+
return Promise.all(
|
|
77
|
+
issuesConnection.nodes.map(async (i) => ({
|
|
78
|
+
id: i.id,
|
|
79
|
+
identifier: i.identifier,
|
|
80
|
+
title: i.title,
|
|
81
|
+
description: i.description,
|
|
82
|
+
state: (await i.state)?.name ?? null,
|
|
83
|
+
assignee: (await i.assignee)?.name ?? null,
|
|
84
|
+
priority: i.priority,
|
|
85
|
+
createdAt: i.createdAt,
|
|
86
|
+
updatedAt: i.updatedAt,
|
|
87
|
+
url: i.url,
|
|
88
|
+
}))
|
|
89
|
+
);
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// types
|
|
2
|
+
export type {
|
|
3
|
+
Issue,
|
|
4
|
+
Project,
|
|
5
|
+
Team,
|
|
6
|
+
TeamMember,
|
|
7
|
+
Cycle,
|
|
8
|
+
User,
|
|
9
|
+
ListIssuesFilter,
|
|
10
|
+
CreateIssueInput,
|
|
11
|
+
UpdateIssueInput,
|
|
12
|
+
CreateProjectInput,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
// client
|
|
16
|
+
export {
|
|
17
|
+
getClient,
|
|
18
|
+
createClientWithKey,
|
|
19
|
+
resetClient,
|
|
20
|
+
NotAuthenticatedError,
|
|
21
|
+
} from "./client";
|
|
22
|
+
|
|
23
|
+
// config
|
|
24
|
+
export {
|
|
25
|
+
loadConfig,
|
|
26
|
+
saveConfig,
|
|
27
|
+
getApiKey,
|
|
28
|
+
setApiKey,
|
|
29
|
+
clearApiKey,
|
|
30
|
+
getConfigValue,
|
|
31
|
+
setConfigValue,
|
|
32
|
+
ensureConfigDir,
|
|
33
|
+
type Config,
|
|
34
|
+
} from "./config";
|
|
35
|
+
|
|
36
|
+
// issues
|
|
37
|
+
export {
|
|
38
|
+
listIssues,
|
|
39
|
+
getIssue,
|
|
40
|
+
createIssue,
|
|
41
|
+
updateIssue,
|
|
42
|
+
addComment,
|
|
43
|
+
priorityFromString,
|
|
44
|
+
getTeamStates,
|
|
45
|
+
getTeamLabels,
|
|
46
|
+
} from "./issues";
|
|
47
|
+
|
|
48
|
+
// projects
|
|
49
|
+
export {
|
|
50
|
+
listProjects,
|
|
51
|
+
getProject,
|
|
52
|
+
getProjectIssues,
|
|
53
|
+
createProject,
|
|
54
|
+
deleteProject,
|
|
55
|
+
} from "./projects";
|
|
56
|
+
|
|
57
|
+
// teams
|
|
58
|
+
export {
|
|
59
|
+
listTeams,
|
|
60
|
+
getTeam,
|
|
61
|
+
getTeamMembers,
|
|
62
|
+
findTeamByKeyOrName,
|
|
63
|
+
getAvailableTeamKeys,
|
|
64
|
+
} from "./teams";
|
|
65
|
+
|
|
66
|
+
// cycles
|
|
67
|
+
export { listCycles, getCurrentCycle, getCycleIssues } from "./cycles";
|
|
68
|
+
|
|
69
|
+
// me
|
|
70
|
+
export { getViewer, getMyIssues, getMyCreatedIssues } from "./me";
|
|
71
|
+
|
|
72
|
+
// search
|
|
73
|
+
export { searchIssues } from "./search";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "bun:test";
|
|
2
|
+
import { LinearClient } from "@linear/sdk";
|
|
3
|
+
import { getApiKey } from "./config";
|
|
4
|
+
import { listIssues, getIssue, createIssue, priorityFromString } from "./issues";
|
|
5
|
+
|
|
6
|
+
const TEST_PREFIX = "[TEST-CLI]";
|
|
7
|
+
|
|
8
|
+
function getTestClient(): LinearClient {
|
|
9
|
+
const apiKey = getApiKey();
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
throw new Error("no api key configured - run: li auth <api-key>");
|
|
12
|
+
}
|
|
13
|
+
return new LinearClient({ apiKey });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function getFirstTeamKey(): Promise<string> {
|
|
17
|
+
const client = getTestClient();
|
|
18
|
+
const teams = await client.teams();
|
|
19
|
+
const bdsq = teams.nodes.find((t) => t.key === "BDSQ");
|
|
20
|
+
if (bdsq) return bdsq.key;
|
|
21
|
+
const first = teams.nodes[0];
|
|
22
|
+
if (!first) throw new Error("no teams found");
|
|
23
|
+
return first.key;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("issues core", () => {
|
|
27
|
+
let client: LinearClient;
|
|
28
|
+
let teamKey: string;
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
client = getTestClient();
|
|
32
|
+
teamKey = await getFirstTeamKey();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("priorityFromString converts priority names", () => {
|
|
36
|
+
expect(priorityFromString("urgent")).toBe(1);
|
|
37
|
+
expect(priorityFromString("high")).toBe(2);
|
|
38
|
+
expect(priorityFromString("medium")).toBe(3);
|
|
39
|
+
expect(priorityFromString("low")).toBe(4);
|
|
40
|
+
expect(priorityFromString("none")).toBe(0);
|
|
41
|
+
expect(priorityFromString("unknown")).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("listIssues returns issue array", async () => {
|
|
45
|
+
const issues = await listIssues(client);
|
|
46
|
+
expect(Array.isArray(issues)).toBe(true);
|
|
47
|
+
if (issues.length > 0) {
|
|
48
|
+
expect(issues[0]).toHaveProperty("id");
|
|
49
|
+
expect(issues[0]).toHaveProperty("identifier");
|
|
50
|
+
expect(issues[0]).toHaveProperty("title");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("listIssues filters by team", async () => {
|
|
55
|
+
const issues = await listIssues(client, { team: teamKey });
|
|
56
|
+
expect(Array.isArray(issues)).toBe(true);
|
|
57
|
+
for (const issue of issues) {
|
|
58
|
+
expect(issue.identifier.startsWith(teamKey + "-")).toBe(true);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("getIssue returns null for nonexistent issue", async () => {
|
|
63
|
+
const issue = await getIssue(client, "NONEXISTENT-99999");
|
|
64
|
+
expect(issue).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
});
|
package/src/issues.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { LinearClient } from "@linear/sdk";
|
|
2
|
+
import type { Issue, ListIssuesFilter, CreateIssueInput, UpdateIssueInput } from "./types";
|
|
3
|
+
|
|
4
|
+
export function priorityFromString(priority: string): number {
|
|
5
|
+
switch (priority.toLowerCase()) {
|
|
6
|
+
case "urgent":
|
|
7
|
+
return 1;
|
|
8
|
+
case "high":
|
|
9
|
+
return 2;
|
|
10
|
+
case "medium":
|
|
11
|
+
return 3;
|
|
12
|
+
case "low":
|
|
13
|
+
return 4;
|
|
14
|
+
case "none":
|
|
15
|
+
return 0;
|
|
16
|
+
default:
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function listIssues(
|
|
22
|
+
client: LinearClient,
|
|
23
|
+
filter: ListIssuesFilter = {}
|
|
24
|
+
): Promise<Issue[]> {
|
|
25
|
+
const apiFilter: Record<string, unknown> = {};
|
|
26
|
+
|
|
27
|
+
if (filter.team) {
|
|
28
|
+
apiFilter.team = { key: { eq: filter.team } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (filter.state) {
|
|
32
|
+
apiFilter.state = { name: { eqIgnoreCase: filter.state } };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (filter.assignee) {
|
|
36
|
+
if (filter.assignee === "@me") {
|
|
37
|
+
const viewer = await client.viewer;
|
|
38
|
+
apiFilter.assignee = { id: { eq: viewer.id } };
|
|
39
|
+
} else {
|
|
40
|
+
apiFilter.assignee = { email: { eq: filter.assignee } };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (filter.label) {
|
|
45
|
+
apiFilter.labels = { some: { name: { eqIgnoreCase: filter.label } } };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (filter.project) {
|
|
49
|
+
apiFilter.project = { name: { eqIgnoreCase: filter.project } };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const issues = await client.issues({ filter: apiFilter });
|
|
53
|
+
const nodes = issues.nodes;
|
|
54
|
+
|
|
55
|
+
return Promise.all(
|
|
56
|
+
nodes.map(async (n) => ({
|
|
57
|
+
id: n.id,
|
|
58
|
+
identifier: n.identifier,
|
|
59
|
+
title: n.title,
|
|
60
|
+
description: n.description,
|
|
61
|
+
state: (await n.state)?.name ?? null,
|
|
62
|
+
assignee: (await n.assignee)?.name ?? null,
|
|
63
|
+
priority: n.priority,
|
|
64
|
+
createdAt: n.createdAt,
|
|
65
|
+
updatedAt: n.updatedAt,
|
|
66
|
+
url: n.url,
|
|
67
|
+
}))
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getIssue(
|
|
72
|
+
client: LinearClient,
|
|
73
|
+
identifier: string
|
|
74
|
+
): Promise<Issue | null> {
|
|
75
|
+
try {
|
|
76
|
+
const issue = await client.issue(identifier);
|
|
77
|
+
|
|
78
|
+
if (!issue) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const state = await issue.state;
|
|
83
|
+
const assignee = await issue.assignee;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
id: issue.id,
|
|
87
|
+
identifier: issue.identifier,
|
|
88
|
+
title: issue.title,
|
|
89
|
+
description: issue.description,
|
|
90
|
+
state: state?.name ?? null,
|
|
91
|
+
assignee: assignee?.name ?? null,
|
|
92
|
+
priority: issue.priority,
|
|
93
|
+
createdAt: issue.createdAt,
|
|
94
|
+
updatedAt: issue.updatedAt,
|
|
95
|
+
url: issue.url,
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function createIssue(
|
|
103
|
+
client: LinearClient,
|
|
104
|
+
input: CreateIssueInput
|
|
105
|
+
): Promise<Issue | null> {
|
|
106
|
+
const result = await client.createIssue(input);
|
|
107
|
+
|
|
108
|
+
if (!result.success) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const issueData = (result as unknown as { _issue?: { id: string } })._issue;
|
|
113
|
+
if (!issueData?.id) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
118
|
+
try {
|
|
119
|
+
const createdIssue = await client.issue(issueData.id);
|
|
120
|
+
if (createdIssue) {
|
|
121
|
+
const state = await createdIssue.state;
|
|
122
|
+
const assignee = await createdIssue.assignee;
|
|
123
|
+
return {
|
|
124
|
+
id: createdIssue.id,
|
|
125
|
+
identifier: createdIssue.identifier,
|
|
126
|
+
title: createdIssue.title,
|
|
127
|
+
description: createdIssue.description,
|
|
128
|
+
state: state?.name ?? null,
|
|
129
|
+
assignee: assignee?.name ?? null,
|
|
130
|
+
priority: createdIssue.priority,
|
|
131
|
+
createdAt: createdIssue.createdAt,
|
|
132
|
+
updatedAt: createdIssue.updatedAt,
|
|
133
|
+
url: createdIssue.url,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
if (attempt < 2) await new Promise((r) => setTimeout(r, 300));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function updateIssue(
|
|
145
|
+
client: LinearClient,
|
|
146
|
+
issueId: string,
|
|
147
|
+
input: UpdateIssueInput
|
|
148
|
+
): Promise<boolean> {
|
|
149
|
+
const result = await client.updateIssue(issueId, input);
|
|
150
|
+
return result.success;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function addComment(
|
|
154
|
+
client: LinearClient,
|
|
155
|
+
issueId: string,
|
|
156
|
+
body: string
|
|
157
|
+
): Promise<boolean> {
|
|
158
|
+
const result = await client.createComment({ issueId, body });
|
|
159
|
+
return result.success;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function getTeamStates(
|
|
163
|
+
client: LinearClient,
|
|
164
|
+
teamId: string
|
|
165
|
+
): Promise<Array<{ id: string; name: string }>> {
|
|
166
|
+
const team = await client.team(teamId);
|
|
167
|
+
if (!team) return [];
|
|
168
|
+
|
|
169
|
+
const states = await team.states();
|
|
170
|
+
return states.nodes.map((s) => ({ id: s.id, name: s.name }));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function getTeamLabels(
|
|
174
|
+
client: LinearClient,
|
|
175
|
+
teamId: string
|
|
176
|
+
): Promise<Array<{ id: string; name: string }>> {
|
|
177
|
+
const team = await client.team(teamId);
|
|
178
|
+
if (!team) return [];
|
|
179
|
+
|
|
180
|
+
const labels = await team.labels();
|
|
181
|
+
return labels.nodes.map((l) => ({ id: l.id, name: l.name }));
|
|
182
|
+
}
|
package/src/me.test.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "bun:test";
|
|
2
|
+
import { LinearClient } from "@linear/sdk";
|
|
3
|
+
import { getApiKey } from "./config";
|
|
4
|
+
import { getViewer, getMyIssues, getMyCreatedIssues } from "./me";
|
|
5
|
+
|
|
6
|
+
function getTestClient(): LinearClient {
|
|
7
|
+
const apiKey = getApiKey();
|
|
8
|
+
if (!apiKey) {
|
|
9
|
+
throw new Error("no api key configured - run: li auth <api-key>");
|
|
10
|
+
}
|
|
11
|
+
return new LinearClient({ apiKey });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("me core", () => {
|
|
15
|
+
let client: LinearClient;
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
client = getTestClient();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("getViewer returns current user", async () => {
|
|
22
|
+
const viewer = await getViewer(client);
|
|
23
|
+
expect(viewer).toHaveProperty("id");
|
|
24
|
+
expect(viewer).toHaveProperty("name");
|
|
25
|
+
expect(viewer).toHaveProperty("email");
|
|
26
|
+
expect(viewer).toHaveProperty("active");
|
|
27
|
+
expect(viewer).toHaveProperty("admin");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("getMyIssues returns issue array", async () => {
|
|
31
|
+
const issues = await getMyIssues(client);
|
|
32
|
+
expect(Array.isArray(issues)).toBe(true);
|
|
33
|
+
if (issues.length > 0) {
|
|
34
|
+
expect(issues[0]).toHaveProperty("id");
|
|
35
|
+
expect(issues[0]).toHaveProperty("identifier");
|
|
36
|
+
expect(issues[0]).toHaveProperty("title");
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("getMyCreatedIssues returns issue array", async () => {
|
|
41
|
+
const issues = await getMyCreatedIssues(client);
|
|
42
|
+
expect(Array.isArray(issues)).toBe(true);
|
|
43
|
+
if (issues.length > 0) {
|
|
44
|
+
expect(issues[0]).toHaveProperty("id");
|
|
45
|
+
expect(issues[0]).toHaveProperty("identifier");
|
|
46
|
+
expect(issues[0]).toHaveProperty("title");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
package/src/me.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { LinearClient } from "@linear/sdk";
|
|
2
|
+
import type { User, Issue } from "./types";
|
|
3
|
+
|
|
4
|
+
export async function getViewer(client: LinearClient): Promise<User> {
|
|
5
|
+
const viewer = await client.viewer;
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
id: viewer.id,
|
|
9
|
+
name: viewer.name,
|
|
10
|
+
email: viewer.email,
|
|
11
|
+
displayName: viewer.displayName,
|
|
12
|
+
active: viewer.active,
|
|
13
|
+
admin: viewer.admin,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function getMyIssues(client: LinearClient): Promise<Issue[]> {
|
|
18
|
+
const viewer = await client.viewer;
|
|
19
|
+
const issuesConnection = await viewer.assignedIssues({
|
|
20
|
+
filter: { state: { type: { nin: ["completed", "canceled"] } } },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return Promise.all(
|
|
24
|
+
issuesConnection.nodes.map(async (i) => ({
|
|
25
|
+
id: i.id,
|
|
26
|
+
identifier: i.identifier,
|
|
27
|
+
title: i.title,
|
|
28
|
+
description: i.description,
|
|
29
|
+
state: (await i.state)?.name ?? null,
|
|
30
|
+
assignee: (await i.assignee)?.name ?? null,
|
|
31
|
+
priority: i.priority,
|
|
32
|
+
createdAt: i.createdAt,
|
|
33
|
+
updatedAt: i.updatedAt,
|
|
34
|
+
url: i.url,
|
|
35
|
+
}))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getMyCreatedIssues(
|
|
40
|
+
client: LinearClient
|
|
41
|
+
): Promise<Issue[]> {
|
|
42
|
+
const viewer = await client.viewer;
|
|
43
|
+
const issuesConnection = await viewer.createdIssues({
|
|
44
|
+
filter: { state: { type: { nin: ["completed", "canceled"] } } },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return Promise.all(
|
|
48
|
+
issuesConnection.nodes.map(async (i) => ({
|
|
49
|
+
id: i.id,
|
|
50
|
+
identifier: i.identifier,
|
|
51
|
+
title: i.title,
|
|
52
|
+
description: i.description,
|
|
53
|
+
state: (await i.state)?.name ?? null,
|
|
54
|
+
assignee: (await i.assignee)?.name ?? null,
|
|
55
|
+
priority: i.priority,
|
|
56
|
+
createdAt: i.createdAt,
|
|
57
|
+
updatedAt: i.updatedAt,
|
|
58
|
+
url: i.url,
|
|
59
|
+
}))
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "bun:test";
|
|
2
|
+
import { LinearClient } from "@linear/sdk";
|
|
3
|
+
import { getApiKey } from "./config";
|
|
4
|
+
import { listProjects, getProject, createProject, deleteProject } from "./projects";
|
|
5
|
+
|
|
6
|
+
const TEST_PREFIX = "[TEST-CLI]";
|
|
7
|
+
|
|
8
|
+
function getTestClient(): LinearClient {
|
|
9
|
+
const apiKey = getApiKey();
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
throw new Error("no api key configured - run: li auth <api-key>");
|
|
12
|
+
}
|
|
13
|
+
return new LinearClient({ apiKey });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("projects core", () => {
|
|
17
|
+
let client: LinearClient;
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
client = getTestClient();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("listProjects returns project array", async () => {
|
|
24
|
+
const projects = await listProjects(client);
|
|
25
|
+
expect(Array.isArray(projects)).toBe(true);
|
|
26
|
+
if (projects.length > 0) {
|
|
27
|
+
expect(projects[0]).toHaveProperty("id");
|
|
28
|
+
expect(projects[0]).toHaveProperty("name");
|
|
29
|
+
expect(projects[0]).toHaveProperty("state");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("getProject returns null for nonexistent project", async () => {
|
|
34
|
+
const project = await getProject(client, "NONEXISTENT_PROJECT_XYZ_123");
|
|
35
|
+
expect(project).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("deleteProject returns false for nonexistent project", async () => {
|
|
39
|
+
const result = await deleteProject(client, "NONEXISTENT_PROJECT_XYZ_123");
|
|
40
|
+
expect(result).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/projects.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { LinearClient } from "@linear/sdk";
|
|
2
|
+
import type { Issue, Project, CreateProjectInput } from "./types";
|
|
3
|
+
|
|
4
|
+
export async function listProjects(
|
|
5
|
+
client: LinearClient,
|
|
6
|
+
options: { team?: string; status?: string } = {}
|
|
7
|
+
): Promise<Project[]> {
|
|
8
|
+
let projectsConnection = await client.projects();
|
|
9
|
+
|
|
10
|
+
if (options.team) {
|
|
11
|
+
const teams = await client.teams();
|
|
12
|
+
const team = teams.nodes.find(
|
|
13
|
+
(t) =>
|
|
14
|
+
t.key.toLowerCase() === options.team!.toLowerCase() ||
|
|
15
|
+
t.name.toLowerCase() === options.team!.toLowerCase()
|
|
16
|
+
);
|
|
17
|
+
if (!team) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
projectsConnection = await team.projects();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let nodes = projectsConnection.nodes;
|
|
24
|
+
|
|
25
|
+
if (options.status) {
|
|
26
|
+
const statusLower = options.status.toLowerCase();
|
|
27
|
+
nodes = nodes.filter((p) => {
|
|
28
|
+
const state = p.state?.toLowerCase() ?? "";
|
|
29
|
+
return state === statusLower || state.includes(statusLower);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return nodes.map((p) => ({
|
|
34
|
+
id: p.id,
|
|
35
|
+
name: p.name,
|
|
36
|
+
description: p.description,
|
|
37
|
+
state: p.state,
|
|
38
|
+
progress: p.progress,
|
|
39
|
+
targetDate: p.targetDate,
|
|
40
|
+
startDate: p.startDate,
|
|
41
|
+
createdAt: p.createdAt,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function getProject(
|
|
46
|
+
client: LinearClient,
|
|
47
|
+
nameOrId: string
|
|
48
|
+
): Promise<Project | null> {
|
|
49
|
+
const projects = await client.projects();
|
|
50
|
+
const project = projects.nodes.find(
|
|
51
|
+
(p) => p.name.toLowerCase() === nameOrId.toLowerCase() || p.id === nameOrId
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (!project) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
id: project.id,
|
|
60
|
+
name: project.name,
|
|
61
|
+
description: project.description,
|
|
62
|
+
state: project.state,
|
|
63
|
+
progress: project.progress,
|
|
64
|
+
targetDate: project.targetDate,
|
|
65
|
+
startDate: project.startDate,
|
|
66
|
+
createdAt: project.createdAt,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function getProjectIssues(
|
|
71
|
+
client: LinearClient,
|
|
72
|
+
nameOrId: string
|
|
73
|
+
): Promise<Issue[]> {
|
|
74
|
+
const projects = await client.projects();
|
|
75
|
+
const project = projects.nodes.find(
|
|
76
|
+
(p) => p.name.toLowerCase() === nameOrId.toLowerCase() || p.id === nameOrId
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (!project) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const issues = await project.issues();
|
|
84
|
+
|
|
85
|
+
return Promise.all(
|
|
86
|
+
issues.nodes.map(async (i) => ({
|
|
87
|
+
id: i.id,
|
|
88
|
+
identifier: i.identifier,
|
|
89
|
+
title: i.title,
|
|
90
|
+
description: i.description,
|
|
91
|
+
state: (await i.state)?.name ?? null,
|
|
92
|
+
assignee: (await i.assignee)?.name ?? null,
|
|
93
|
+
priority: i.priority,
|
|
94
|
+
createdAt: i.createdAt,
|
|
95
|
+
updatedAt: i.updatedAt,
|
|
96
|
+
url: i.url,
|
|
97
|
+
}))
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function createProject(
|
|
102
|
+
client: LinearClient,
|
|
103
|
+
input: CreateProjectInput
|
|
104
|
+
): Promise<Project | null> {
|
|
105
|
+
const result = await client.createProject({
|
|
106
|
+
name: input.name,
|
|
107
|
+
description: input.description,
|
|
108
|
+
teamIds: input.teamIds ?? [],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!result.success) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const projectData = await result.project;
|
|
116
|
+
if (!projectData) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
id: projectData.id,
|
|
122
|
+
name: projectData.name,
|
|
123
|
+
description: projectData.description,
|
|
124
|
+
state: projectData.state,
|
|
125
|
+
progress: projectData.progress,
|
|
126
|
+
targetDate: projectData.targetDate,
|
|
127
|
+
startDate: projectData.startDate,
|
|
128
|
+
createdAt: projectData.createdAt,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function deleteProject(
|
|
133
|
+
client: LinearClient,
|
|
134
|
+
nameOrId: string
|
|
135
|
+
): Promise<boolean> {
|
|
136
|
+
const projects = await client.projects();
|
|
137
|
+
const project = projects.nodes.find(
|
|
138
|
+
(p) => p.name.toLowerCase() === nameOrId.toLowerCase() || p.id === nameOrId
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (!project) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = await client.deleteProject(project.id);
|
|
146
|
+
return result.success;
|
|
147
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "bun:test";
|
|
2
|
+
import { LinearClient } from "@linear/sdk";
|
|
3
|
+
import { getApiKey } from "./config";
|
|
4
|
+
import { searchIssues } from "./search";
|
|
5
|
+
|
|
6
|
+
function getTestClient(): LinearClient {
|
|
7
|
+
const apiKey = getApiKey();
|
|
8
|
+
if (!apiKey) {
|
|
9
|
+
throw new Error("no api key configured - run: li auth <api-key>");
|
|
10
|
+
}
|
|
11
|
+
return new LinearClient({ apiKey });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("search core", () => {
|
|
15
|
+
let client: LinearClient;
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
client = getTestClient();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("searchIssues returns issue array", async () => {
|
|
22
|
+
const issues = await searchIssues(client, "issue");
|
|
23
|
+
expect(Array.isArray(issues)).toBe(true);
|
|
24
|
+
if (issues.length > 0) {
|
|
25
|
+
expect(issues[0]).toHaveProperty("id");
|
|
26
|
+
expect(issues[0]).toHaveProperty("identifier");
|
|
27
|
+
expect(issues[0]).toHaveProperty("title");
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("searchIssues filters by team", async () => {
|
|
32
|
+
const issues = await searchIssues(client, "issue", { team: "BDSQ" });
|
|
33
|
+
expect(Array.isArray(issues)).toBe(true);
|
|
34
|
+
for (const issue of issues) {
|
|
35
|
+
expect(issue.identifier.startsWith("BDSQ-")).toBe(true);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
package/src/search.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { LinearClient } from "@linear/sdk";
|
|
2
|
+
import type { Issue } from "./types";
|
|
3
|
+
|
|
4
|
+
export async function searchIssues(
|
|
5
|
+
client: LinearClient,
|
|
6
|
+
query: string,
|
|
7
|
+
options: { team?: string } = {}
|
|
8
|
+
): Promise<Issue[]> {
|
|
9
|
+
const searchResults = await client.searchIssues(query);
|
|
10
|
+
let issues = searchResults.nodes;
|
|
11
|
+
|
|
12
|
+
if (options.team) {
|
|
13
|
+
const teamKey = options.team.toUpperCase();
|
|
14
|
+
issues = issues.filter((issue) =>
|
|
15
|
+
issue.identifier.startsWith(teamKey + "-")
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return Promise.all(
|
|
20
|
+
issues.map(async (i) => ({
|
|
21
|
+
id: i.id,
|
|
22
|
+
identifier: i.identifier,
|
|
23
|
+
title: i.title,
|
|
24
|
+
description: i.description,
|
|
25
|
+
state: (await i.state)?.name ?? null,
|
|
26
|
+
assignee: (await i.assignee)?.name ?? null,
|
|
27
|
+
priority: i.priority,
|
|
28
|
+
createdAt: i.createdAt,
|
|
29
|
+
updatedAt: i.updatedAt,
|
|
30
|
+
url: i.url,
|
|
31
|
+
}))
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "bun:test";
|
|
2
|
+
import { LinearClient } from "@linear/sdk";
|
|
3
|
+
import { getApiKey } from "./config";
|
|
4
|
+
import { listTeams, getTeam, getTeamMembers, getAvailableTeamKeys } from "./teams";
|
|
5
|
+
|
|
6
|
+
function getTestClient(): LinearClient {
|
|
7
|
+
const apiKey = getApiKey();
|
|
8
|
+
if (!apiKey) {
|
|
9
|
+
throw new Error("no api key configured - run: li auth <api-key>");
|
|
10
|
+
}
|
|
11
|
+
return new LinearClient({ apiKey });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("teams core", () => {
|
|
15
|
+
let client: LinearClient;
|
|
16
|
+
let teamKey: string;
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
client = getTestClient();
|
|
20
|
+
const keys = await getAvailableTeamKeys(client);
|
|
21
|
+
teamKey = keys.includes("BDSQ") ? "BDSQ" : keys[0] ?? "UNKNOWN";
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("listTeams returns team array", async () => {
|
|
25
|
+
const teams = await listTeams(client);
|
|
26
|
+
expect(Array.isArray(teams)).toBe(true);
|
|
27
|
+
expect(teams.length).toBeGreaterThan(0);
|
|
28
|
+
expect(teams[0]).toHaveProperty("id");
|
|
29
|
+
expect(teams[0]).toHaveProperty("key");
|
|
30
|
+
expect(teams[0]).toHaveProperty("name");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("getTeam returns team by key", async () => {
|
|
34
|
+
const team = await getTeam(client, teamKey);
|
|
35
|
+
expect(team).not.toBeNull();
|
|
36
|
+
expect(team?.key).toBe(teamKey);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("getTeam returns null for invalid key", async () => {
|
|
40
|
+
const team = await getTeam(client, "INVALID_KEY_XYZ");
|
|
41
|
+
expect(team).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("getTeamMembers returns members array", async () => {
|
|
45
|
+
const members = await getTeamMembers(client, teamKey);
|
|
46
|
+
expect(Array.isArray(members)).toBe(true);
|
|
47
|
+
if (members.length > 0) {
|
|
48
|
+
expect(members[0]).toHaveProperty("id");
|
|
49
|
+
expect(members[0]).toHaveProperty("name");
|
|
50
|
+
expect(members[0]).toHaveProperty("email");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("getAvailableTeamKeys returns string array", async () => {
|
|
55
|
+
const keys = await getAvailableTeamKeys(client);
|
|
56
|
+
expect(Array.isArray(keys)).toBe(true);
|
|
57
|
+
expect(keys.length).toBeGreaterThan(0);
|
|
58
|
+
expect(typeof keys[0]).toBe("string");
|
|
59
|
+
});
|
|
60
|
+
});
|
package/src/teams.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { LinearClient } from "@linear/sdk";
|
|
2
|
+
import type { Team, TeamMember } from "./types";
|
|
3
|
+
|
|
4
|
+
export async function listTeams(client: LinearClient): Promise<Team[]> {
|
|
5
|
+
const teamsConnection = await client.teams();
|
|
6
|
+
return teamsConnection.nodes.map((t) => ({
|
|
7
|
+
id: t.id,
|
|
8
|
+
key: t.key,
|
|
9
|
+
name: t.name,
|
|
10
|
+
description: t.description,
|
|
11
|
+
private: t.private,
|
|
12
|
+
timezone: t.timezone,
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function getTeam(
|
|
17
|
+
client: LinearClient,
|
|
18
|
+
key: string
|
|
19
|
+
): Promise<Team | null> {
|
|
20
|
+
const teamsConnection = await client.teams({
|
|
21
|
+
filter: { key: { eq: key.toUpperCase() } },
|
|
22
|
+
});
|
|
23
|
+
const team = teamsConnection.nodes[0];
|
|
24
|
+
|
|
25
|
+
if (!team) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
id: team.id,
|
|
31
|
+
key: team.key,
|
|
32
|
+
name: team.name,
|
|
33
|
+
description: team.description,
|
|
34
|
+
private: team.private,
|
|
35
|
+
timezone: team.timezone,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getTeamMembers(
|
|
40
|
+
client: LinearClient,
|
|
41
|
+
key: string
|
|
42
|
+
): Promise<TeamMember[]> {
|
|
43
|
+
const teamsConnection = await client.teams({
|
|
44
|
+
filter: { key: { eq: key.toUpperCase() } },
|
|
45
|
+
});
|
|
46
|
+
const team = teamsConnection.nodes[0];
|
|
47
|
+
|
|
48
|
+
if (!team) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const membersConnection = await team.members();
|
|
53
|
+
return membersConnection.nodes.map((m) => ({
|
|
54
|
+
id: m.id,
|
|
55
|
+
name: m.name,
|
|
56
|
+
email: m.email,
|
|
57
|
+
displayName: m.displayName,
|
|
58
|
+
active: m.active,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function findTeamByKeyOrName(
|
|
63
|
+
client: LinearClient,
|
|
64
|
+
keyOrName: string
|
|
65
|
+
): Promise<Team | null> {
|
|
66
|
+
const teams = await client.teams();
|
|
67
|
+
const team = teams.nodes.find(
|
|
68
|
+
(t) =>
|
|
69
|
+
t.key.toLowerCase() === keyOrName.toLowerCase() ||
|
|
70
|
+
t.name.toLowerCase() === keyOrName.toLowerCase()
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (!team) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
id: team.id,
|
|
79
|
+
key: team.key,
|
|
80
|
+
name: team.name,
|
|
81
|
+
description: team.description,
|
|
82
|
+
private: team.private,
|
|
83
|
+
timezone: team.timezone,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function getAvailableTeamKeys(
|
|
88
|
+
client: LinearClient
|
|
89
|
+
): Promise<string[]> {
|
|
90
|
+
const teams = await client.teams();
|
|
91
|
+
return teams.nodes.map((t) => t.key);
|
|
92
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export interface Issue {
|
|
2
|
+
id: string;
|
|
3
|
+
identifier: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string | null;
|
|
6
|
+
state?: string | null;
|
|
7
|
+
assignee?: string | null;
|
|
8
|
+
priority?: number;
|
|
9
|
+
createdAt: Date;
|
|
10
|
+
updatedAt: Date;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Project {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string | null;
|
|
18
|
+
state?: string | null;
|
|
19
|
+
progress?: number | null;
|
|
20
|
+
targetDate?: Date | null;
|
|
21
|
+
startDate?: Date | null;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Team {
|
|
26
|
+
id: string;
|
|
27
|
+
key: string;
|
|
28
|
+
name: string;
|
|
29
|
+
description?: string | null;
|
|
30
|
+
private: boolean;
|
|
31
|
+
timezone?: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TeamMember {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
email?: string | null;
|
|
38
|
+
displayName?: string | null;
|
|
39
|
+
active: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Cycle {
|
|
43
|
+
id: string;
|
|
44
|
+
number: number;
|
|
45
|
+
name?: string | null;
|
|
46
|
+
startsAt: Date;
|
|
47
|
+
endsAt: Date;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface User {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
email?: string | null;
|
|
54
|
+
displayName?: string | null;
|
|
55
|
+
active: boolean;
|
|
56
|
+
admin: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ListIssuesFilter {
|
|
60
|
+
team?: string;
|
|
61
|
+
state?: string;
|
|
62
|
+
assignee?: string;
|
|
63
|
+
label?: string;
|
|
64
|
+
project?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface CreateIssueInput {
|
|
68
|
+
teamId: string;
|
|
69
|
+
title: string;
|
|
70
|
+
description?: string;
|
|
71
|
+
assigneeId?: string;
|
|
72
|
+
priority?: number;
|
|
73
|
+
labelIds?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface UpdateIssueInput {
|
|
77
|
+
stateId?: string;
|
|
78
|
+
assigneeId?: string;
|
|
79
|
+
priority?: number;
|
|
80
|
+
labelIds?: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CreateProjectInput {
|
|
84
|
+
name: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
teamIds?: string[];
|
|
87
|
+
}
|