diff --git a/USAGE.md b/USAGE.md
index f80d8d8..98b3476 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -58,8 +58,9 @@ arguments:
string
list options:
- --query filter by text search
- --limit max results (default: 50)
+ --query filter by text search
+ --limit max results (default: 50)
+ --after cursor for next page
create options:
--description issue body
@@ -129,7 +130,9 @@ commands:
list [options] list available labels
list options:
- --team filter by team (key, name, or UUID)
+ --team filter by team (key, name, or UUID)
+ --limit max results (default: 50)
+ --after cursor for next page
see also: issues create --labels, issues update --labels
@@ -144,7 +147,8 @@ commands:
list [options] list projects
list options:
- --limit max results (default: 100)
+ --limit max results (default: 100)
+ --after cursor for next page
see also: milestones list --project, documents list --project
@@ -163,9 +167,11 @@ arguments:
cycle identifier (UUID or name)
list options:
- --team filter by team (key, name, or UUID)
- --active only show active cycles
- --window active cycle +/- n neighbors (requires --team)
+ --team filter by team (key, name, or UUID)
+ --active only show active cycles
+ --window active cycle +/- n neighbors (requires --team)
+ --limit max results (default: 50)
+ --after cursor for next page
read options:
--team scope name lookup to team
@@ -193,6 +199,7 @@ arguments:
list options:
--project target project (required)
--limit max results (default: 50)
+ --after cursor for next page
read options:
--project scope name lookup to project
@@ -233,6 +240,7 @@ list options:
--project filter by project name or ID
--issue filter by issue (shows documents attached to the issue)
--limit max results (default: 50)
+ --after cursor for next page
create options:
--title document title (required)
@@ -279,7 +287,11 @@ a team is a group of users that owns issues, cycles, statuses, and
labels. teams are identified by a short key (e.g. ENG), name, or UUID.
commands:
- list list all teams
+ list [options] list all teams
+
+list options:
+ --limit max results (default: 50)
+ --after cursor for next page
---
@@ -292,4 +304,6 @@ commands:
list [options] list workspace members
list options:
- --active only show active users
+ --active only show active users
+ --limit max results (default: 50)
+ --after cursor for next page
diff --git a/graphql/queries/cycles.graphql b/graphql/queries/cycles.graphql
index e2dd851..e6e621f 100644
--- a/graphql/queries/cycles.graphql
+++ b/graphql/queries/cycles.graphql
@@ -58,11 +58,15 @@ fragment CycleWithIssuesFields on Cycle {
# Variables:
# $first: Maximum number of cycles to return (default: 50)
# $filter: Optional CycleFilter for team/status filtering
-query GetCycles($first: Int = 50, $filter: CycleFilter) {
- cycles(first: $first, filter: $filter) {
+query GetCycles($first: Int = 50, $after: String, $filter: CycleFilter) {
+ cycles(first: $first, after: $after, filter: $filter) {
nodes {
...CycleFields
}
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
}
}
diff --git a/graphql/queries/documents.graphql b/graphql/queries/documents.graphql
index 87a6a50..38d7407 100644
--- a/graphql/queries/documents.graphql
+++ b/graphql/queries/documents.graphql
@@ -35,10 +35,14 @@ query GetDocument($id: String!) {
# List documents with optional filtering
#
# Fetches a list of documents with optional filtering criteria.
-query ListDocuments($first: Int!, $filter: DocumentFilter) {
- documents(first: $first, filter: $filter) {
+query ListDocuments($first: Int!, $after: String, $filter: DocumentFilter) {
+ documents(first: $first, after: $after, filter: $filter) {
nodes {
...DocumentFields
}
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
}
}
diff --git a/graphql/queries/issues.graphql b/graphql/queries/issues.graphql
index 6e1ae7f..b6a4ee8 100644
--- a/graphql/queries/issues.graphql
+++ b/graphql/queries/issues.graphql
@@ -191,15 +191,20 @@ fragment CompleteIssueSearchFields on IssueSearchResult {
# Fetches paginated issues excluding completed ones,
# ordered by most recently updated. Includes all relationships
# for comprehensive issue data.
-query GetIssues($first: Int!, $orderBy: PaginationOrderBy) {
+query GetIssues($first: Int!, $after: String, $orderBy: PaginationOrderBy) {
issues(
first: $first
+ after: $after
orderBy: $orderBy
filter: { state: { type: { neq: "completed" } } }
) {
nodes {
...CompleteIssueFields
}
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
}
}
@@ -243,11 +248,20 @@ query GetIssueTeam($issueId: String!) {
#
# Provides full-text search across Linear issues with complete
# relationship data for each match.
-query SearchIssues($term: String!, $first: Int!) {
- searchIssues(term: $term, first: $first, includeArchived: false) {
+query SearchIssues($term: String!, $first: Int!, $after: String) {
+ searchIssues(
+ term: $term
+ first: $first
+ after: $after
+ includeArchived: false
+ ) {
nodes {
...CompleteIssueSearchFields
}
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
}
}
@@ -257,11 +271,13 @@ query SearchIssues($term: String!, $first: Int!) {
# Used by the advanced search functionality with multiple criteria.
query FilteredSearchIssues(
$first: Int!
+ $after: String
$filter: IssueFilter
$orderBy: PaginationOrderBy
) {
issues(
first: $first
+ after: $after
filter: $filter
orderBy: $orderBy
includeArchived: false
@@ -269,6 +285,10 @@ query FilteredSearchIssues(
nodes {
...CompleteIssueFields
}
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
}
}
diff --git a/graphql/queries/labels.graphql b/graphql/queries/labels.graphql
index 1391a74..384c7f1 100644
--- a/graphql/queries/labels.graphql
+++ b/graphql/queries/labels.graphql
@@ -31,10 +31,14 @@ fragment LabelFields on IssueLabel {
# Variables:
# $first: Maximum number of labels to return (default: 50)
# $filter: Optional filter (e.g., { team: { id: { eq: "team-uuid" } } })
-query GetLabels($first: Int = 50, $filter: IssueLabelFilter) {
- issueLabels(first: $first, filter: $filter) {
+query GetLabels($first: Int = 50, $after: String, $filter: IssueLabelFilter) {
+ issueLabels(first: $first, after: $after, filter: $filter) {
nodes {
...LabelFields
}
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
}
}
diff --git a/graphql/queries/project-milestones.graphql b/graphql/queries/project-milestones.graphql
index 8d340a7..24ed9a5 100644
--- a/graphql/queries/project-milestones.graphql
+++ b/graphql/queries/project-milestones.graphql
@@ -8,11 +8,11 @@
# List project milestones in a project
#
# Fetches a list of project milestones for a given project.
-query ListProjectMilestones($projectId: String!, $first: Int!) {
+query ListProjectMilestones($projectId: String!, $first: Int!, $after: String) {
project(id: $projectId) {
id
name
- projectMilestones(first: $first) {
+ projectMilestones(first: $first, after: $after) {
nodes {
id
name
@@ -22,6 +22,10 @@ query ListProjectMilestones($projectId: String!, $first: Int!) {
createdAt
updatedAt
}
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
}
}
}
diff --git a/graphql/queries/projects.graphql b/graphql/queries/projects.graphql
index 45ecc41..4b2cbc3 100644
--- a/graphql/queries/projects.graphql
+++ b/graphql/queries/projects.graphql
@@ -32,10 +32,14 @@ fragment ProjectFields on Project {
#
# Variables:
# $first: Maximum number of projects to return (default: 50)
-query GetProjects($first: Int = 50) {
- projects(first: $first) {
+query GetProjects($first: Int = 50, $after: String) {
+ projects(first: $first, after: $after) {
nodes {
...ProjectFields
}
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
}
}
diff --git a/graphql/queries/teams.graphql b/graphql/queries/teams.graphql
index 5cc24b1..372a76c 100644
--- a/graphql/queries/teams.graphql
+++ b/graphql/queries/teams.graphql
@@ -28,10 +28,14 @@ fragment TeamFields on Team {
#
# Variables:
# $first: Maximum number of teams to return (default: 50)
-query GetTeams($first: Int = 50) {
- teams(first: $first) {
+query GetTeams($first: Int = 50, $after: String) {
+ teams(first: $first, after: $after) {
nodes {
...TeamFields
}
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
}
}
diff --git a/graphql/queries/users.graphql b/graphql/queries/users.graphql
index c74fb7e..253c6ca 100644
--- a/graphql/queries/users.graphql
+++ b/graphql/queries/users.graphql
@@ -31,10 +31,14 @@ fragment UserFields on User {
# Variables:
# $first: Maximum number of users to return (default: 50)
# $filter: Optional filter (e.g., { active: { eq: true } })
-query GetUsers($first: Int = 50, $filter: UserFilter) {
- users(first: $first, filter: $filter) {
+query GetUsers($first: Int = 50, $after: String, $filter: UserFilter) {
+ users(first: $first, after: $after, filter: $filter) {
nodes {
...UserFields
}
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
}
}
diff --git a/src/commands/cycles.ts b/src/commands/cycles.ts
index 163e443..85cd8a1 100644
--- a/src/commands/cycles.ts
+++ b/src/commands/cycles.ts
@@ -5,7 +5,7 @@ import {
notFoundError,
requiresParameterError,
} from "../common/errors.js";
-import { handleCommand, outputSuccess } from "../common/output.js";
+import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import { resolveCycleId } from "../resolvers/cycle-resolver.js";
import { resolveTeamId } from "../resolvers/team-resolver.js";
@@ -15,6 +15,8 @@ interface CycleListOptions extends CommandOptions {
team?: string;
active?: boolean;
window?: string;
+ limit: string;
+ after?: string;
}
interface CycleReadOptions extends CommandOptions {
@@ -46,12 +48,20 @@ export function setupCyclesCommands(program: Command): void {
.option("--team ", "filter by team (key, name, or UUID)")
.option("--active", "only show active cycles")
.option("--window ", "active cycle +/- n neighbors (requires --team)")
+ .option("-l, --limit ", "max results", "50")
+ .option("--after ", "cursor for next page")
.action(
handleCommand(async (...args: unknown[]) => {
const [options, command] = args as [CycleListOptions, Command];
if (options.window && !options.team) {
throw requiresParameterError("--window", "--team");
}
+ if (options.window && options.after) {
+ throw invalidParameterError(
+ "--after",
+ "cannot be used with --window",
+ );
+ }
const ctx = createContext(command.parent!.parent!.opts());
@@ -61,10 +71,11 @@ export function setupCyclesCommands(program: Command): void {
: undefined;
// Fetch cycles
- const allCycles = await listCycles(
+ const result = await listCycles(
ctx.gql,
teamId,
options.active || false,
+ { limit: parseLimit(options.limit), after: options.after },
);
if (options.window) {
@@ -76,7 +87,7 @@ export function setupCyclesCommands(program: Command): void {
);
}
- const activeCycle = allCycles.find((c: Cycle) => c.isActive);
+ const activeCycle = result.nodes.find((c: Cycle) => c.isActive);
if (!activeCycle) {
throw notFoundError("Active cycle", options.team ?? "", "for team");
}
@@ -85,15 +96,18 @@ export function setupCyclesCommands(program: Command): void {
const min = activeNumber - n;
const max = activeNumber + n;
- const filtered = allCycles
+ const filteredNodes = result.nodes
.filter((c: Cycle) => c.number >= min && c.number <= max)
.sort((a: Cycle, b: Cycle) => a.number - b.number);
- outputSuccess(filtered);
+ outputSuccess({
+ nodes: filteredNodes,
+ pageInfo: { hasNextPage: false, endCursor: null },
+ });
return;
}
- outputSuccess(allCycles);
+ outputSuccess(result);
}),
);
@@ -116,7 +130,7 @@ export function setupCyclesCommands(program: Command): void {
const cycleResult = await getCycle(
ctx.gql,
cycleId,
- parseInt(options.limit || "50", 10),
+ parseLimit(options.limit || "50"),
);
outputSuccess(cycleResult);
diff --git a/src/commands/documents.ts b/src/commands/documents.ts
index a99316b..fcf4526 100644
--- a/src/commands/documents.ts
+++ b/src/commands/documents.ts
@@ -1,6 +1,6 @@
import type { Command } from "commander";
import { createContext } from "../common/context.js";
-import { handleCommand, outputSuccess } from "../common/output.js";
+import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import type { DocumentUpdateInput } from "../gql/graphql.js";
import { resolveIssueId } from "../resolvers/issue-resolver.js";
@@ -41,6 +41,7 @@ interface DocumentListOptions {
project?: string;
issue?: string;
limit?: string;
+ after?: string;
}
/** Extracts slug ID from a Linear document URL (e.g. /workspace/document/title-slug-abc123 -> abc123). */
@@ -98,6 +99,7 @@ export function setupDocumentsCommands(program: Command): void {
"filter by issue (shows documents attached to the issue)",
)
.option("-l, --limit ", "max results", "50")
+ .option("--after ", "cursor for next page")
.action(
handleCommand(async (...args: unknown[]) => {
const [options, command] = args as [DocumentListOptions, Command];
@@ -110,12 +112,7 @@ export function setupDocumentsCommands(program: Command): void {
const rootOpts = command.parent!.parent!.opts();
const ctx = createContext(rootOpts);
- const limit = parseInt(options.limit || "50", 10);
- if (Number.isNaN(limit) || limit < 1) {
- throw new Error(
- `Invalid limit "${options.limit}": must be a positive number`,
- );
- }
+ const limit = parseLimit(options.limit || "50");
if (options.issue) {
const issueId = await resolveIssueId(ctx.sdk, options.issue);
@@ -130,7 +127,10 @@ export function setupDocumentsCommands(program: Command): void {
];
if (documentSlugIds.length === 0) {
- outputSuccess([]);
+ outputSuccess({
+ nodes: [],
+ pageInfo: { hasNextPage: false, endCursor: null },
+ });
return;
}
@@ -138,7 +138,10 @@ export function setupDocumentsCommands(program: Command): void {
ctx.gql,
documentSlugIds,
);
- outputSuccess(documents);
+ outputSuccess({
+ nodes: documents,
+ pageInfo: { hasNextPage: false, endCursor: null },
+ });
return;
}
@@ -149,6 +152,7 @@ export function setupDocumentsCommands(program: Command): void {
const documents = await listDocuments(ctx.gql, {
limit,
+ after: options.after,
filter: projectId
? { project: { id: { eq: projectId } } }
: undefined,
diff --git a/src/commands/issues.ts b/src/commands/issues.ts
index 88acc4c..dadab50 100644
--- a/src/commands/issues.ts
+++ b/src/commands/issues.ts
@@ -2,7 +2,7 @@ import type { Command } from "commander";
import type { CommandContext } from "../common/context.js";
import { createContext } from "../common/context.js";
import { isUuid, parseIssueIdentifier } from "../common/identifier.js";
-import { handleCommand, outputSuccess } from "../common/output.js";
+import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import {
type IssueCreateInput,
@@ -34,6 +34,7 @@ import {
interface ListOptions {
query?: string;
limit: string;
+ after?: string;
}
interface CreateOptions {
@@ -174,20 +175,26 @@ export function setupIssuesCommands(program: Command): void {
.description("list issues with optional filters")
.option("--query ", "filter by text search")
.option("-l, --limit ", "max results", "50")
+ .option("--after ", "cursor for next page")
.action(
handleCommand(async (...args: unknown[]) => {
const [options, command] = args as [ListOptions, Command];
const ctx = createContext(command.parent!.parent!.opts());
+ const paginationOptions = {
+ limit: parseLimit(options.limit),
+ after: options.after,
+ };
+
if (options.query) {
const result = await searchIssues(
ctx.gql,
options.query,
- parseInt(options.limit, 10),
+ paginationOptions,
);
outputSuccess(result);
} else {
- const result = await listIssues(ctx.gql, parseInt(options.limit, 10));
+ const result = await listIssues(ctx.gql, paginationOptions);
outputSuccess(result);
}
}),
diff --git a/src/commands/labels.ts b/src/commands/labels.ts
index dc6c4fe..eaffce6 100644
--- a/src/commands/labels.ts
+++ b/src/commands/labels.ts
@@ -1,12 +1,14 @@
import type { Command } from "commander";
import { type CommandOptions, createContext } from "../common/context.js";
-import { handleCommand, outputSuccess } from "../common/output.js";
+import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import { resolveTeamId } from "../resolvers/team-resolver.js";
import { listLabels } from "../services/label-service.js";
interface ListLabelsOptions extends CommandOptions {
team?: string;
+ limit: string;
+ after?: string;
}
export const LABELS_META: DomainMeta = {
@@ -29,6 +31,8 @@ export function setupLabelsCommands(program: Command): void {
.command("list")
.description("list available labels")
.option("--team ", "filter by team (key, name, or UUID)")
+ .option("-l, --limit ", "max results", "50")
+ .option("--after ", "cursor for next page")
.action(
handleCommand(async (...args: unknown[]) => {
const [options, command] = args as [ListLabelsOptions, Command];
@@ -38,7 +42,10 @@ export function setupLabelsCommands(program: Command): void {
? await resolveTeamId(ctx.sdk, options.team)
: undefined;
- const result = await listLabels(ctx.gql, teamId);
+ const result = await listLabels(ctx.gql, teamId, {
+ limit: parseLimit(options.limit),
+ after: options.after,
+ });
outputSuccess(result);
}),
);
diff --git a/src/commands/milestones.ts b/src/commands/milestones.ts
index b411290..efe8af4 100644
--- a/src/commands/milestones.ts
+++ b/src/commands/milestones.ts
@@ -1,6 +1,6 @@
import type { Command } from "commander";
import { createContext } from "../common/context.js";
-import { handleCommand, outputSuccess } from "../common/output.js";
+import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import type { ProjectMilestoneUpdateInput } from "../gql/graphql.js";
import { resolveMilestoneId } from "../resolvers/milestone-resolver.js";
@@ -16,6 +16,7 @@ import {
interface MilestoneListOptions {
project: string;
limit?: string;
+ after?: string;
}
interface MilestoneReadOptions {
@@ -67,6 +68,7 @@ export function setupMilestonesCommands(program: Command): void {
.description("list milestones in a project")
.requiredOption("--project ", "target project (required)")
.option("-l, --limit ", "max results", "50")
+ .option("--after ", "cursor for next page")
.action(
handleCommand(async (...args: unknown[]) => {
const [options, command] = args as [MilestoneListOptions, Command];
@@ -75,11 +77,10 @@ export function setupMilestonesCommands(program: Command): void {
// Resolve project ID
const projectId = await resolveProjectId(ctx.sdk, options.project);
- const milestones = await listMilestones(
- ctx.gql,
- projectId,
- parseInt(options.limit || "50", 10),
- );
+ const milestones = await listMilestones(ctx.gql, projectId, {
+ limit: parseLimit(options.limit || "50"),
+ after: options.after,
+ });
outputSuccess(milestones);
}),
@@ -110,7 +111,7 @@ export function setupMilestonesCommands(program: Command): void {
const milestoneResult = await getMilestone(
ctx.gql,
milestoneId,
- parseInt(options.limit || "50", 10),
+ parseLimit(options.limit || "50"),
);
outputSuccess(milestoneResult);
diff --git a/src/commands/projects.ts b/src/commands/projects.ts
index d49f522..2ad8bbb 100644
--- a/src/commands/projects.ts
+++ b/src/commands/projects.ts
@@ -1,6 +1,6 @@
import type { Command } from "commander";
import { createContext } from "../common/context.js";
-import { handleCommand, outputSuccess } from "../common/output.js";
+import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import { listProjects } from "../services/project-service.js";
@@ -26,11 +26,18 @@ export function setupProjectsCommands(program: Command): void {
.command("list")
.description("list projects")
.option("-l, --limit ", "max results", "100")
+ .option("--after ", "cursor for next page")
.action(
handleCommand(async (...args: unknown[]) => {
- const [options, command] = args as [{ limit: string }, Command];
+ const [options, command] = args as [
+ { limit: string; after?: string },
+ Command,
+ ];
const ctx = createContext(command.parent!.parent!.opts());
- const result = await listProjects(ctx.gql, parseInt(options.limit, 10));
+ const result = await listProjects(ctx.gql, {
+ limit: parseLimit(options.limit),
+ after: options.after,
+ });
outputSuccess(result);
}),
);
diff --git a/src/commands/teams.ts b/src/commands/teams.ts
index 2b7ee52..70b9d83 100644
--- a/src/commands/teams.ts
+++ b/src/commands/teams.ts
@@ -1,6 +1,6 @@
import type { Command } from "commander";
-import { type CommandOptions, createContext } from "../common/context.js";
-import { handleCommand, outputSuccess } from "../common/output.js";
+import { createContext } from "../common/context.js";
+import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import { listTeams } from "../services/team-service.js";
@@ -23,11 +23,19 @@ export function setupTeamsCommands(program: Command): void {
teams
.command("list")
.description("list all teams")
+ .option("-l, --limit ", "max results", "50")
+ .option("--after ", "cursor for next page")
.action(
handleCommand(async (...args: unknown[]) => {
- const [, command] = args as [CommandOptions, Command];
+ const [options, command] = args as [
+ { limit: string; after?: string },
+ Command,
+ ];
const ctx = createContext(command.parent!.parent!.opts());
- const result = await listTeams(ctx.gql);
+ const result = await listTeams(ctx.gql, {
+ limit: parseLimit(options.limit),
+ after: options.after,
+ });
outputSuccess(result);
}),
);
diff --git a/src/commands/users.ts b/src/commands/users.ts
index 4148f32..59df6c0 100644
--- a/src/commands/users.ts
+++ b/src/commands/users.ts
@@ -1,11 +1,13 @@
import type { Command } from "commander";
import { type CommandOptions, createContext } from "../common/context.js";
-import { handleCommand, outputSuccess } from "../common/output.js";
+import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import { listUsers } from "../services/user-service.js";
interface ListUsersOptions extends CommandOptions {
active?: boolean;
+ limit: string;
+ after?: string;
}
export const USERS_META: DomainMeta = {
@@ -28,11 +30,16 @@ export function setupUsersCommands(program: Command): void {
.command("list")
.description("list workspace members")
.option("--active", "only show active users")
+ .option("-l, --limit ", "max results", "50")
+ .option("--after ", "cursor for next page")
.action(
handleCommand(async (...args: unknown[]) => {
const [options, command] = args as [ListUsersOptions, Command];
const ctx = createContext(command.parent!.parent!.opts());
- const result = await listUsers(ctx.gql, options.active || false);
+ const result = await listUsers(ctx.gql, options.active || false, {
+ limit: parseLimit(options.limit),
+ after: options.after,
+ });
outputSuccess(result);
}),
);
diff --git a/src/common/output.ts b/src/common/output.ts
index ca8e11a..3bb42ea 100644
--- a/src/common/output.ts
+++ b/src/common/output.ts
@@ -1,4 +1,8 @@
-import { AUTH_ERROR_CODE, AuthenticationError } from "./errors.js";
+import {
+ AUTH_ERROR_CODE,
+ AuthenticationError,
+ invalidParameterError,
+} from "./errors.js";
export function outputSuccess(data: unknown): void {
console.log(JSON.stringify(data, null, 2));
@@ -28,6 +32,14 @@ export function outputAuthError(error: AuthenticationError): void {
process.exit(AUTH_ERROR_CODE);
}
+export function parseLimit(value: string): number {
+ const limit = parseInt(value, 10);
+ if (Number.isNaN(limit) || limit < 1) {
+ throw invalidParameterError("--limit", "must be a positive integer");
+ }
+ return limit;
+}
+
export function handleCommand(
asyncFn: (...args: unknown[]) => Promise,
): (...args: unknown[]) => Promise {
diff --git a/src/common/types.ts b/src/common/types.ts
index 480bd3d..49aa4f8 100644
--- a/src/common/types.ts
+++ b/src/common/types.ts
@@ -19,6 +19,19 @@ import type {
UpdateProjectMilestoneMutation,
} from "../gql/graphql.js";
+// Pagination types
+export type PageInfo = GetIssuesQuery["issues"]["pageInfo"];
+
+export interface PaginatedResult {
+ nodes: T[];
+ pageInfo: PageInfo;
+}
+
+export interface PaginationOptions {
+ limit?: number;
+ after?: string;
+}
+
// Issue types
export type Issue = GetIssuesQuery["issues"]["nodes"][0];
export type IssueDetail = NonNullable;
diff --git a/src/services/cycle-service.ts b/src/services/cycle-service.ts
index 38f2257..90cdd4f 100644
--- a/src/services/cycle-service.ts
+++ b/src/services/cycle-service.ts
@@ -1,4 +1,5 @@
import type { GraphQLClient } from "../client/graphql-client.js";
+import type { PaginatedResult, PaginationOptions } from "../common/types.js";
import {
type CycleFilter,
GetCycleByIdDocument,
@@ -31,7 +32,9 @@ export async function listCycles(
client: GraphQLClient,
teamId?: string,
activeOnly: boolean = false,
-): Promise {
+ options: PaginationOptions = {},
+): Promise> {
+ const { limit = 50, after } = options;
const filter: CycleFilter = {};
if (teamId) {
@@ -43,20 +46,24 @@ export async function listCycles(
}
const result = await client.request(GetCyclesDocument, {
- first: 50,
+ first: limit,
+ after,
filter,
});
- return result.cycles.nodes.map((cycle) => ({
- id: cycle.id,
- number: cycle.number,
- name: cycle.name ?? `Cycle ${cycle.number}`,
- startsAt: cycle.startsAt,
- endsAt: cycle.endsAt,
- isActive: cycle.isActive,
- isNext: cycle.isNext,
- isPrevious: cycle.isPrevious,
- }));
+ return {
+ nodes: result.cycles.nodes.map((cycle) => ({
+ id: cycle.id,
+ number: cycle.number,
+ name: cycle.name ?? `Cycle ${cycle.number}`,
+ startsAt: cycle.startsAt,
+ endsAt: cycle.endsAt,
+ isActive: cycle.isActive,
+ isNext: cycle.isNext,
+ isPrevious: cycle.isPrevious,
+ })),
+ pageInfo: result.cycles.pageInfo,
+ };
}
export async function getCycle(
diff --git a/src/services/document-service.ts b/src/services/document-service.ts
index 8521c3d..c65c488 100644
--- a/src/services/document-service.ts
+++ b/src/services/document-service.ts
@@ -3,6 +3,7 @@ import type {
CreatedDocument,
Document,
DocumentListItem,
+ PaginatedResult,
UpdatedDocument,
} from "../common/types.js";
import {
@@ -73,18 +74,26 @@ export async function listDocuments(
client: GraphQLClient,
options?: {
limit?: number;
+ after?: string;
filter?: DocumentFilter;
},
-): Promise {
+): Promise> {
const result = await client.request(
ListDocumentsDocument,
{
first: options?.limit ?? 25,
+ after: options?.after,
filter: options?.filter,
},
);
- return result.documents?.nodes ?? [];
+ return {
+ nodes: result.documents?.nodes ?? [],
+ pageInfo: result.documents?.pageInfo ?? {
+ hasNextPage: false,
+ endCursor: null,
+ },
+ };
}
export async function listDocumentsBySlugIds(
diff --git a/src/services/issue-service.ts b/src/services/issue-service.ts
index a1bea30..a333552 100644
--- a/src/services/issue-service.ts
+++ b/src/services/issue-service.ts
@@ -5,6 +5,8 @@ import type {
IssueByIdentifier,
IssueDetail,
IssueSearchResult,
+ PaginatedResult,
+ PaginationOptions,
UpdatedIssue,
} from "../common/types.js";
import {
@@ -26,13 +28,18 @@ import {
export async function listIssues(
client: GraphQLClient,
- limit: number = 25,
-): Promise {
+ options: PaginationOptions = {},
+): Promise> {
+ const { limit = 25, after } = options;
const result = await client.request(GetIssuesDocument, {
first: limit,
+ after,
orderBy: "updatedAt",
});
- return result.issues?.nodes ?? [];
+ return {
+ nodes: result.issues?.nodes ?? [],
+ pageInfo: result.issues.pageInfo,
+ };
}
export async function getIssue(
@@ -68,13 +75,18 @@ export async function getIssueByIdentifier(
export async function searchIssues(
client: GraphQLClient,
term: string,
- limit: number = 25,
-): Promise {
+ options: PaginationOptions = {},
+): Promise> {
+ const { limit = 25, after } = options;
const result = await client.request(SearchIssuesDocument, {
term,
first: limit,
+ after,
});
- return result.searchIssues?.nodes ?? [];
+ return {
+ nodes: result.searchIssues?.nodes ?? [],
+ pageInfo: result.searchIssues.pageInfo,
+ };
}
export async function createIssue(
diff --git a/src/services/label-service.ts b/src/services/label-service.ts
index 733db21..c2e3f4d 100644
--- a/src/services/label-service.ts
+++ b/src/services/label-service.ts
@@ -1,4 +1,5 @@
import type { GraphQLClient } from "../client/graphql-client.js";
+import type { PaginatedResult, PaginationOptions } from "../common/types.js";
import { GetLabelsDocument, type GetLabelsQuery } from "../gql/graphql.js";
export interface Label {
@@ -11,18 +12,24 @@ export interface Label {
export async function listLabels(
client: GraphQLClient,
teamId?: string,
-): Promise