Skip to content

Commit 9b339d1

Browse files
committed
feat(cli): add Linear documents CRUD support
Add documents entity support in linear-core and CLI command registration, including gateway mappers, sdk type aliases, help/docs updates, and tests.
1 parent 172efe4 commit 9b339d1

9 files changed

Lines changed: 161 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ linear issues list --limit 10
5656
linear issues browse
5757
linear issues create --input '{"title":"Investigate bug","teamId":"<team-id>"}'
5858
linear projects list
59+
linear documents list
5960

6061
# TUI
6162
linear tui --screen issues

packages/cli/src/help/root-help.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Examples:
1010
linear issues browse
1111
linear issues create --template "Bug Report" --input '{"teamId":"<team-id>"}'
1212
linear initiatives list
13+
linear documents list
1314
linear templates list
1415
linear skills install issue-triage
1516
linear docs --open

packages/cli/src/index.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type {
88
SdkCommentUpdateInput,
99
SdkCycleInput,
1010
SdkCycleUpdateInput,
11+
SdkDocumentInput,
12+
SdkDocumentUpdateInput,
1113
SdkInitiativeInput,
1214
SdkInitiativeUpdateInput,
1315
SdkIssueInput,
@@ -107,6 +109,14 @@ function isProjectUpdateInput(value: unknown): value is SdkProjectUpdateInput {
107109
return isRecord(value) && Object.keys(value).length > 0;
108110
}
109111

112+
function isDocumentCreateInput(value: unknown): value is SdkDocumentInput {
113+
return isRecord(value) && hasString(value, "title");
114+
}
115+
116+
function isDocumentUpdateInput(value: unknown): value is SdkDocumentUpdateInput {
117+
return isRecord(value) && Object.keys(value).length > 0;
118+
}
119+
110120
function isCycleCreateInput(value: unknown): value is SdkCycleInput {
111121
return isRecord(value) && hasString(value, "teamId");
112122
}
@@ -561,6 +571,37 @@ export function createProgram(authManager = new AuthManager()): Command {
561571
authManager,
562572
);
563573

574+
registerResourceCommand(
575+
program,
576+
"documents",
577+
"Document commands",
578+
{
579+
list: async (_manager, cmd) => {
580+
const globals = getGlobalOptions(cmd);
581+
return (await sessionGateway(cmd)).listDocuments({
582+
limit: globals.limit,
583+
cursor: globals.cursor,
584+
});
585+
},
586+
get: async (_manager, id, cmd) => (await sessionGateway(cmd)).getDocument(id),
587+
create: async (_manager, payload, cmd) =>
588+
(await sessionGateway(cmd)).createDocument(
589+
ensurePayload(payload, isDocumentCreateInput, "Document create payload requires title."),
590+
),
591+
update: async (_manager, id, payload, cmd) =>
592+
(await sessionGateway(cmd)).updateDocument(
593+
id,
594+
ensurePayload(
595+
payload,
596+
isDocumentUpdateInput,
597+
"Document update payload must be a non-empty object.",
598+
),
599+
),
600+
delete: async (_manager, id, cmd) => (await sessionGateway(cmd)).deleteDocument(id),
601+
},
602+
authManager,
603+
);
604+
564605
registerResourceCommand(
565606
program,
566607
"cycles",
@@ -809,9 +850,7 @@ export function createProgram(authManager = new AuthManager()): Command {
809850
try {
810851
const session = await authManager.openSession({ profile: globals.profile });
811852
const defaultScreen =
812-
opts.screen === "projects" ||
813-
opts.screen === "initiatives" ||
814-
opts.screen === "cycles"
853+
opts.screen === "projects" || opts.screen === "initiatives" || opts.screen === "cycles"
815854
? opts.screen
816855
: "issues";
817856
await runLinearTui({

packages/cli/tests/help.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ describe("help output", () => {
99
program.commands.find((command) => command.name() === "issues")?.helpInformation() ?? "";
1010
const initiativesHelp =
1111
program.commands.find((command) => command.name() === "initiatives")?.helpInformation() ?? "";
12+
const documentsHelp =
13+
program.commands.find((command) => command.name() === "documents")?.helpInformation() ?? "";
1214
const templatesHelp =
1315
program.commands.find((command) => command.name() === "templates")?.helpInformation() ?? "";
1416

@@ -17,9 +19,11 @@ describe("help output", () => {
1719
expect(help).toContain("skills");
1820
expect(help).toContain("issues");
1921
expect(help).toContain("initiatives");
22+
expect(help).toContain("documents");
2023
expect(help).toContain("templates");
2124
expect(issuesHelp).toContain("browse");
2225
expect(initiativesHelp).toContain("create");
26+
expect(documentsHelp).toContain("list");
2327
expect(templatesHelp).toContain("list");
2428
});
2529
});

packages/linear-core/src/entities/linear-gateway.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
AttachmentRecord,
55
CommentRecord,
66
CycleRecord,
7+
DocumentRecord,
78
InitiativeRecord,
89
IssueRecord,
910
LabelRecord,
@@ -22,6 +23,9 @@ import type {
2223
SdkCycleInput,
2324
SdkCycleLike,
2425
SdkCycleUpdateInput,
26+
SdkDocumentInput,
27+
SdkDocumentLike,
28+
SdkDocumentUpdateInput,
2529
SdkInitiativeInput,
2630
SdkInitiativeLike,
2731
SdkInitiativeUpdateInput,
@@ -98,6 +102,19 @@ function toProject(record: SdkProjectLike): ProjectRecord {
98102
};
99103
}
100104

105+
function toDocument(record: SdkDocumentLike): DocumentRecord {
106+
return {
107+
id: record.id,
108+
title: record.title,
109+
content: record.content ?? undefined,
110+
url: record.url,
111+
projectId: record.projectId,
112+
initiativeId: record.initiativeId,
113+
createdAt: toDateString(record.createdAt),
114+
updatedAt: toDateString(record.updatedAt),
115+
};
116+
}
117+
101118
function toCycle(record: SdkCycleLike): CycleRecord {
102119
return {
103120
id: record.id,
@@ -317,6 +334,38 @@ export class LinearGateway {
317334
};
318335
}
319336

337+
public async listDocuments(options: ListOptions): Promise<PageResult<DocumentRecord>> {
338+
const connection = await this.client.documents(toListVariables(options));
339+
return {
340+
items: connection.nodes.map(toDocument),
341+
nextCursor: connection.pageInfo.endCursor ?? null,
342+
};
343+
}
344+
345+
public async getDocument(id: string): Promise<DocumentRecord> {
346+
return toDocument(await this.client.document(id));
347+
}
348+
349+
public async createDocument(input: SdkDocumentInput): Promise<DocumentRecord> {
350+
const payload = await this.client.createDocument(input);
351+
return toDocument(await requireEntity(payload.document, "document"));
352+
}
353+
354+
public async updateDocument(id: string, input: SdkDocumentUpdateInput): Promise<DocumentRecord> {
355+
const payload = await this.client.updateDocument(id, input);
356+
return toDocument(await requireEntity(payload.document, "document"));
357+
}
358+
359+
public async deleteDocument(
360+
id: string,
361+
): Promise<{ readonly id?: string; readonly success: boolean }> {
362+
const payload = await this.client.deleteDocument(id);
363+
return {
364+
id: payload.entityId,
365+
success: payload.success,
366+
};
367+
}
368+
320369
public async listCycles(options: ListOptions): Promise<PageResult<CycleRecord>> {
321370
const connection = await this.client.cycles(toListVariables(options));
322371
return {

packages/linear-core/src/entities/models.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ export interface ProjectRecord {
2121
readonly updatedAt: string;
2222
}
2323

24+
export interface DocumentRecord {
25+
readonly id: string;
26+
readonly title: string;
27+
readonly content?: string;
28+
readonly url: string;
29+
readonly projectId?: string;
30+
readonly initiativeId?: string;
31+
readonly createdAt: string;
32+
readonly updatedAt: string;
33+
}
34+
2435
export interface InitiativeRecord {
2536
readonly id: string;
2637
readonly name: string;

packages/linear-core/src/entities/sdk-types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export type SdkLinearClient = Pick<
1717
| "createProject"
1818
| "updateProject"
1919
| "deleteProject"
20+
| "documents"
21+
| "document"
22+
| "createDocument"
23+
| "updateDocument"
24+
| "deleteDocument"
2025
| "cycles"
2126
| "cycle"
2227
| "createCycle"
@@ -58,6 +63,7 @@ export type SdkLinearClient = Pick<
5863
export type SdkIssueLike = Awaited<ReturnType<LinearClient["issue"]>>;
5964
export type SdkInitiativeLike = Awaited<ReturnType<LinearClient["initiative"]>>;
6065
export type SdkProjectLike = Awaited<ReturnType<LinearClient["project"]>>;
66+
export type SdkDocumentLike = Awaited<ReturnType<LinearClient["document"]>>;
6167
export type SdkCycleLike = Awaited<ReturnType<LinearClient["cycle"]>>;
6268
export type SdkTeamLike = Awaited<ReturnType<LinearClient["team"]>>;
6369
export type SdkUserLike = Awaited<ReturnType<LinearClient["user"]>>;
@@ -70,6 +76,7 @@ export type SdkTemplateLike = Awaited<ReturnType<LinearClient["template"]>>;
7076
export type SdkIssueConnectionLike = Awaited<ReturnType<LinearClient["issues"]>>;
7177
export type SdkInitiativeConnectionLike = Awaited<ReturnType<LinearClient["initiatives"]>>;
7278
export type SdkProjectConnectionLike = Awaited<ReturnType<LinearClient["projects"]>>;
79+
export type SdkDocumentConnectionLike = Awaited<ReturnType<LinearClient["documents"]>>;
7380
export type SdkCycleConnectionLike = Awaited<ReturnType<LinearClient["cycles"]>>;
7481
export type SdkTeamConnectionLike = Awaited<ReturnType<LinearClient["teams"]>>;
7582
export type SdkUserConnectionLike = Awaited<ReturnType<LinearClient["users"]>>;
@@ -82,6 +89,7 @@ export type SdkTemplateListLike = Awaited<LinearClient["templates"]>;
8289
export type SdkIssuePayloadLike = Awaited<ReturnType<LinearClient["createIssue"]>>;
8390
export type SdkInitiativePayloadLike = Awaited<ReturnType<LinearClient["createInitiative"]>>;
8491
export type SdkProjectPayloadLike = Awaited<ReturnType<LinearClient["createProject"]>>;
92+
export type SdkDocumentPayloadLike = Awaited<ReturnType<LinearClient["createDocument"]>>;
8593
export type SdkCyclePayloadLike = Awaited<ReturnType<LinearClient["createCycle"]>>;
8694
export type SdkTeamPayloadLike = Awaited<ReturnType<LinearClient["createTeam"]>>;
8795
export type SdkUserPayloadLike = Awaited<ReturnType<LinearClient["updateUser"]>>;
@@ -97,6 +105,8 @@ export type SdkInitiativeInput = Parameters<LinearClient["createInitiative"]>[0]
97105
export type SdkInitiativeUpdateInput = Parameters<LinearClient["updateInitiative"]>[1];
98106
export type SdkProjectInput = Parameters<LinearClient["createProject"]>[0];
99107
export type SdkProjectUpdateInput = Parameters<LinearClient["updateProject"]>[1];
108+
export type SdkDocumentInput = Parameters<LinearClient["createDocument"]>[0];
109+
export type SdkDocumentUpdateInput = Parameters<LinearClient["updateDocument"]>[1];
100110
export type SdkCycleInput = Parameters<LinearClient["createCycle"]>[0];
101111
export type SdkCycleUpdateInput = Parameters<LinearClient["updateCycle"]>[1];
102112
export type SdkTeamInput = Parameters<LinearClient["createTeam"]>[0];

packages/linear-core/src/types/public.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export type LinearEntity =
33
| "issues"
44
| "initiatives"
55
| "projects"
6+
| "documents"
67
| "cycles"
78
| "teams"
89
| "users"

packages/linear-core/tests/linear-gateway.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,37 @@ function createTestClient(): SdkLinearClient {
5858
async deleteProject() {
5959
throw createNotImplementedError("deleteProject");
6060
},
61+
async documents() {
62+
return {
63+
nodes: [
64+
{
65+
id: "doc_1",
66+
title: "Agent rollout plan",
67+
content: "## Scope",
68+
url: "https://linear.app/docs/agent-rollout-plan",
69+
projectId: "proj_1",
70+
initiativeId: "init_1",
71+
createdAt: new Date("2026-03-15T00:00:00.000Z"),
72+
updatedAt: new Date("2026-03-16T00:00:00.000Z"),
73+
},
74+
],
75+
pageInfo: {
76+
endCursor: "document-cursor-2",
77+
},
78+
};
79+
},
80+
async document() {
81+
throw createNotImplementedError("document");
82+
},
83+
async createDocument() {
84+
throw createNotImplementedError("createDocument");
85+
},
86+
async updateDocument() {
87+
throw createNotImplementedError("updateDocument");
88+
},
89+
async deleteDocument() {
90+
throw createNotImplementedError("deleteDocument");
91+
},
6192
async cycles() {
6293
throw createNotImplementedError("cycles");
6394
},
@@ -241,6 +272,17 @@ describe("LinearGateway", () => {
241272
expect(result.nextCursor).toBe("initiative-cursor-2");
242273
});
243274

275+
test("lists documents and maps fields", async () => {
276+
const gateway = new LinearGateway(createTestClient());
277+
const result = await gateway.listDocuments({ limit: 10 });
278+
279+
expect(result.items).toHaveLength(1);
280+
expect(result.items[0]?.title).toBe("Agent rollout plan");
281+
expect(result.items[0]?.projectId).toBe("proj_1");
282+
expect(result.items[0]?.initiativeId).toBe("init_1");
283+
expect(result.nextCursor).toBe("document-cursor-2");
284+
});
285+
244286
test("lists templates and maps fields", async () => {
245287
const gateway = new LinearGateway(createTestClient());
246288
const result = await gateway.listTemplates();

0 commit comments

Comments
 (0)