Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
resolveApiToken,
type TokenSource,
} from "../common/auth.js";
import { createGraphQLClient } from "../common/context.js";
import { createGraphQLClient, getRootOpts } from "../common/context.js";
import { handleCommand, outputSuccess } from "../common/output.js";
import { clearToken, saveToken } from "../common/token-storage.js";
import type { Viewer } from "../common/types.js";
Expand Down Expand Up @@ -124,7 +124,7 @@ export function setupAuthCommands(program: Command): void {
try {
if (!options.force) {
try {
const rootOpts = command.parent!.parent!.opts() as CommandOptions;
const rootOpts = getRootOpts(command);
const { token, source } = resolveApiToken(rootOpts);
try {
const viewer = await validateApiToken(token);
Expand Down Expand Up @@ -202,7 +202,7 @@ export function setupAuthCommands(program: Command): void {
.action(
handleCommand(async (...args: unknown[]) => {
const [, command] = args as [CommandOptions, Command];
const rootOpts = command.parent!.parent!.opts() as CommandOptions;
const rootOpts = getRootOpts(command);

let token: string;
let source: TokenSource;
Expand Down Expand Up @@ -243,7 +243,7 @@ export function setupAuthCommands(program: Command): void {
.action(
handleCommand(async (...args: unknown[]) => {
const [, command] = args as [CommandOptions, Command];
const rootOpts = command.parent!.parent!.opts() as CommandOptions;
const rootOpts = getRootOpts(command);

clearToken();

Expand Down
8 changes: 6 additions & 2 deletions src/commands/comments.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Command } from "commander";
import { type CommandOptions, createContext } from "../common/context.js";
import {
type CommandOptions,
createContext,
getRootOpts,
} from "../common/context.js";
import { handleCommand, outputSuccess } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import { resolveIssueId } from "../resolvers/issue-resolver.js";
Expand Down Expand Up @@ -41,7 +45,7 @@ export function setupCommentsCommands(program: Command): void {
CreateCommentOptions,
Command,
];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

if (!options.body) {
throw new Error("--body is required");
Expand Down
10 changes: 7 additions & 3 deletions src/commands/cycles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Command } from "commander";
import { type CommandOptions, createContext } from "../common/context.js";
import {
type CommandOptions,
createContext,
getRootOpts,
} from "../common/context.js";
import {
invalidParameterError,
notFoundError,
Expand Down Expand Up @@ -63,7 +67,7 @@ export function setupCyclesCommands(program: Command): void {
);
}

const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

// Resolve team filter if provided
const teamId = options.team
Expand Down Expand Up @@ -123,7 +127,7 @@ export function setupCyclesCommands(program: Command): void {
CycleReadOptions,
Command,
];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

const cycleId = await resolveCycleId(ctx.sdk, cycle, options.team);

Expand Down
12 changes: 6 additions & 6 deletions src/commands/documents.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Command } from "commander";
import { createContext } from "../common/context.js";
import { createContext, getRootOpts } from "../common/context.js";
import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import type { DocumentUpdateInput } from "../gql/graphql.js";
Expand Down Expand Up @@ -109,7 +109,7 @@ export function setupDocumentsCommands(program: Command): void {
);
}

const rootOpts = command.parent!.parent!.opts();
const rootOpts = getRootOpts(command);
const ctx = createContext(rootOpts);

const limit = parseLimit(options.limit || "50");
Expand Down Expand Up @@ -168,7 +168,7 @@ export function setupDocumentsCommands(program: Command): void {
.action(
handleCommand(async (...args: unknown[]) => {
const [document, , command] = args as [string, unknown, Command];
const rootOpts = command.parent!.parent!.opts();
const rootOpts = getRootOpts(command);
const ctx = createContext(rootOpts);

const documentResult = await getDocument(ctx.gql, document);
Expand All @@ -189,7 +189,7 @@ export function setupDocumentsCommands(program: Command): void {
.action(
handleCommand(async (...args: unknown[]) => {
const [options, command] = args as [DocumentCreateOptions, Command];
const rootOpts = command.parent!.parent!.opts();
const rootOpts = getRootOpts(command);
const ctx = createContext(rootOpts);

const projectId = options.project
Expand Down Expand Up @@ -247,7 +247,7 @@ export function setupDocumentsCommands(program: Command): void {
DocumentUpdateOptions,
Command,
];
const rootOpts = command.parent!.parent!.opts();
const rootOpts = getRootOpts(command);
const ctx = createContext(rootOpts);

const input: DocumentUpdateInput = {};
Expand All @@ -270,7 +270,7 @@ export function setupDocumentsCommands(program: Command): void {
.action(
handleCommand(async (...args: unknown[]) => {
const [document, , command] = args as [string, unknown, Command];
const rootOpts = command.parent!.parent!.opts();
const rootOpts = getRootOpts(command);
const ctx = createContext(rootOpts);

await deleteDocument(ctx.gql, document);
Expand Down
5 changes: 3 additions & 2 deletions src/commands/files.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Command } from "commander";
import { type CommandOptions, getApiToken } from "../common/auth.js";
import { getRootOpts } from "../common/context.js";
import { handleCommand, outputSuccess } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import { FileService } from "../services/file-service.js";
Expand Down Expand Up @@ -37,7 +38,7 @@ export function setupFilesCommands(program: Command): void {
CommandOptions & { output?: string; overwrite?: boolean },
Command,
];
const apiToken = getApiToken(command.parent!.parent!.opts());
const apiToken = getApiToken(getRootOpts(command));
const fileService = new FileService(apiToken);
const result = await fileService.downloadFile(url, {
output: options.output,
Expand All @@ -61,7 +62,7 @@ export function setupFilesCommands(program: Command): void {
.action(
handleCommand(async (...args: unknown[]) => {
const [filePath, , command] = args as [string, CommandOptions, Command];
const apiToken = getApiToken(command.parent!.parent!.opts());
const apiToken = getApiToken(getRootOpts(command));
const fileService = new FileService(apiToken);
const result = await fileService.uploadFile(filePath);

Expand Down
10 changes: 5 additions & 5 deletions src/commands/issues.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Command } from "commander";
import type { CommandContext } from "../common/context.js";
import { createContext } from "../common/context.js";
import { createContext, getRootOpts } from "../common/context.js";
import { isUuid, parseIssueIdentifier } from "../common/identifier.js";
import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
Expand Down Expand Up @@ -179,7 +179,7 @@ export function setupIssuesCommands(program: Command): void {
.action(
handleCommand(async (...args: unknown[]) => {
const [options, command] = args as [ListOptions, Command];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

const paginationOptions = {
limit: parseLimit(options.limit),
Expand Down Expand Up @@ -210,7 +210,7 @@ export function setupIssuesCommands(program: Command): void {
.action(
handleCommand(async (...args: unknown[]) => {
const [issue, , command] = args as [string, unknown, Command];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

if (isUuid(issue)) {
const result = await getIssue(ctx.gql, issue);
Expand Down Expand Up @@ -251,7 +251,7 @@ export function setupIssuesCommands(program: Command): void {
CreateOptions,
Command,
];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

validateRelationFlags(options);

Expand Down Expand Up @@ -403,7 +403,7 @@ export function setupIssuesCommands(program: Command): void {

validateRelationFlags(options);

const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

const resolvedIssueId = await resolveIssueId(ctx.sdk, issue);

Expand Down
8 changes: 6 additions & 2 deletions src/commands/labels.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Command } from "commander";
import { type CommandOptions, createContext } from "../common/context.js";
import {
type CommandOptions,
createContext,
getRootOpts,
} from "../common/context.js";
import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import { resolveTeamId } from "../resolvers/team-resolver.js";
Expand Down Expand Up @@ -36,7 +40,7 @@ export function setupLabelsCommands(program: Command): void {
.action(
handleCommand(async (...args: unknown[]) => {
const [options, command] = args as [ListLabelsOptions, Command];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

const teamId = options.team
? await resolveTeamId(ctx.sdk, options.team)
Expand Down
10 changes: 5 additions & 5 deletions src/commands/milestones.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Command } from "commander";
import { createContext } from "../common/context.js";
import { createContext, getRootOpts } from "../common/context.js";
import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import type { ProjectMilestoneUpdateInput } from "../gql/graphql.js";
Expand Down Expand Up @@ -72,7 +72,7 @@ export function setupMilestonesCommands(program: Command): void {
.action(
handleCommand(async (...args: unknown[]) => {
const [options, command] = args as [MilestoneListOptions, Command];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

// Resolve project ID
const projectId = await resolveProjectId(ctx.sdk, options.project);
Expand All @@ -99,7 +99,7 @@ export function setupMilestonesCommands(program: Command): void {
MilestoneReadOptions,
Command,
];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

const milestoneId = await resolveMilestoneId(
ctx.gql,
Expand Down Expand Up @@ -132,7 +132,7 @@ export function setupMilestonesCommands(program: Command): void {
MilestoneCreateOptions,
Command,
];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

// Resolve project ID
const projectId = await resolveProjectId(ctx.sdk, options.project);
Expand Down Expand Up @@ -167,7 +167,7 @@ export function setupMilestonesCommands(program: Command): void {
MilestoneUpdateOptions,
Command,
];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));

const milestoneId = await resolveMilestoneId(
ctx.gql,
Expand Down
4 changes: 2 additions & 2 deletions src/commands/projects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Command } from "commander";
import { createContext } from "../common/context.js";
import { createContext, getRootOpts } from "../common/context.js";
import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import { listProjects } from "../services/project-service.js";
Expand Down Expand Up @@ -33,7 +33,7 @@ export function setupProjectsCommands(program: Command): void {
{ limit: string; after?: string },
Command,
];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));
const result = await listProjects(ctx.gql, {
limit: parseLimit(options.limit),
after: options.after,
Expand Down
4 changes: 2 additions & 2 deletions src/commands/teams.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Command } from "commander";
import { createContext } from "../common/context.js";
import { createContext, getRootOpts } 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";
Expand Down Expand Up @@ -31,7 +31,7 @@ export function setupTeamsCommands(program: Command): void {
{ limit: string; after?: string },
Command,
];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));
const result = await listTeams(ctx.gql, {
limit: parseLimit(options.limit),
after: options.after,
Expand Down
8 changes: 6 additions & 2 deletions src/commands/users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Command } from "commander";
import { type CommandOptions, createContext } from "../common/context.js";
import {
type CommandOptions,
createContext,
getRootOpts,
} from "../common/context.js";
import { handleCommand, outputSuccess, parseLimit } from "../common/output.js";
import { type DomainMeta, formatDomainUsage } from "../common/usage.js";
import { listUsers } from "../services/user-service.js";
Expand Down Expand Up @@ -35,7 +39,7 @@ export function setupUsersCommands(program: Command): void {
.action(
handleCommand(async (...args: unknown[]) => {
const [options, command] = args as [ListUsersOptions, Command];
const ctx = createContext(command.parent!.parent!.opts());
const ctx = createContext(getRootOpts(command));
const result = await listUsers(ctx.gql, options.active || false, {
limit: parseLimit(options.limit),
after: options.after,
Expand Down
9 changes: 9 additions & 0 deletions src/common/context.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import type { Command } from "commander";
import { GraphQLClient } from "../client/graphql-client.js";
import { LinearSdkClient } from "../client/linear-client.js";
import { type CommandOptions, getApiToken } from "./auth.js";

export type { CommandOptions };

export function getRootOpts(command: Command): CommandOptions {
let current: Command | null = command;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: current doesn't need to be Command | null — it starts as command (non-null) and the loop only assigns current.parent when it's truthy. You can simplify to:

let current = command;
while (current.parent) {
  current = current.parent;
}

This drops the optional chaining and keeps the type as Command throughout — no null narrowing needed, and current.opts() is unambiguously safe.

while (current?.parent) {
Comment on lines +9 to +10
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRootOpts declares current as Command | null and then returns current.opts(). With tsconfig.json strict: true, current remains possibly null after the loop (because current = current.parent can assign a nullable type), so this should fail type-checking/build. Consider making current non-nullable (e.g., initialize as let current = command; and loop while current.parent), or assert non-null at the end after proving it via the loop condition.

Suggested change
let current: Command | null = command;
while (current?.parent) {
let current = command;
while (current.parent) {

Copilot uses AI. Check for mistakes.
current = current.parent;
}
return current.opts() as CommandOptions;
}

export interface CommandContext {
gql: GraphQLClient;
sdk: LinearSdkClient;
Expand Down
8 changes: 5 additions & 3 deletions tests/unit/commands/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ vi.mock("../../../src/services/auth-service.js", () => ({
validateToken: vi.fn(),
}));

vi.mock("../../../src/common/context.js", () => ({
createGraphQLClient: vi.fn(() => ({})),
}));
vi.mock("../../../src/common/context.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../../src/common/context.js")>();
return { ...actual, createGraphQLClient: vi.fn(() => ({})) };
});

vi.mock("../../../src/common/auth.js", async (importOriginal) => {
const actual =
Expand Down
17 changes: 11 additions & 6 deletions tests/unit/commands/issues.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import { Command } from "commander";
import { beforeEach, describe, expect, it, vi } from "vitest";

// Mock all external dependencies before importing the module under test
vi.mock("../../../src/common/context.js", () => ({
createContext: vi.fn(() => ({
gql: { request: vi.fn() },
sdk: { sdk: {} },
})),
}));
vi.mock("../../../src/common/context.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../../src/common/context.js")>();
return {
...actual,
createContext: vi.fn(() => ({
gql: { request: vi.fn() },
sdk: { sdk: {} },
})),
};
});

vi.mock("../../../src/common/output.js", async (importOriginal) => {
const actual =
Expand Down
Loading