diff --git a/src/commands/auth.ts b/src/commands/auth.ts index 1ed3acb..4841038 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -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"; @@ -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); @@ -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; @@ -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(); diff --git a/src/commands/comments.ts b/src/commands/comments.ts index d594844..2c433bf 100644 --- a/src/commands/comments.ts +++ b/src/commands/comments.ts @@ -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"; @@ -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"); diff --git a/src/commands/cycles.ts b/src/commands/cycles.ts index 85cd8a1..c13a9d4 100644 --- a/src/commands/cycles.ts +++ b/src/commands/cycles.ts @@ -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, @@ -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 @@ -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); diff --git a/src/commands/documents.ts b/src/commands/documents.ts index fcf4526..6faf6af 100644 --- a/src/commands/documents.ts +++ b/src/commands/documents.ts @@ -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"; @@ -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"); @@ -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); @@ -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 @@ -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 = {}; @@ -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); diff --git a/src/commands/files.ts b/src/commands/files.ts index 8cb9c08..4230f9d 100644 --- a/src/commands/files.ts +++ b/src/commands/files.ts @@ -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"; @@ -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, @@ -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); diff --git a/src/commands/issues.ts b/src/commands/issues.ts index dadab50..8a0febc 100644 --- a/src/commands/issues.ts +++ b/src/commands/issues.ts @@ -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"; @@ -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), @@ -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); @@ -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); @@ -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); diff --git a/src/commands/labels.ts b/src/commands/labels.ts index eaffce6..02be857 100644 --- a/src/commands/labels.ts +++ b/src/commands/labels.ts @@ -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"; @@ -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) diff --git a/src/commands/milestones.ts b/src/commands/milestones.ts index efe8af4..0371518 100644 --- a/src/commands/milestones.ts +++ b/src/commands/milestones.ts @@ -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"; @@ -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); @@ -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, @@ -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); @@ -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, diff --git a/src/commands/projects.ts b/src/commands/projects.ts index 2ad8bbb..fe8d988 100644 --- a/src/commands/projects.ts +++ b/src/commands/projects.ts @@ -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"; @@ -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, diff --git a/src/commands/teams.ts b/src/commands/teams.ts index 70b9d83..dc3c95c 100644 --- a/src/commands/teams.ts +++ b/src/commands/teams.ts @@ -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"; @@ -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, diff --git a/src/commands/users.ts b/src/commands/users.ts index 59df6c0..bb68a54 100644 --- a/src/commands/users.ts +++ b/src/commands/users.ts @@ -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"; @@ -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, diff --git a/src/common/context.ts b/src/common/context.ts index 82a93ce..97828ee 100644 --- a/src/common/context.ts +++ b/src/common/context.ts @@ -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; + while (current?.parent) { + current = current.parent; + } + return current.opts() as CommandOptions; +} + export interface CommandContext { gql: GraphQLClient; sdk: LinearSdkClient; diff --git a/tests/unit/commands/auth.test.ts b/tests/unit/commands/auth.test.ts index 2186f7f..680bd3a 100644 --- a/tests/unit/commands/auth.test.ts +++ b/tests/unit/commands/auth.test.ts @@ -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(); + return { ...actual, createGraphQLClient: vi.fn(() => ({})) }; +}); vi.mock("../../../src/common/auth.js", async (importOriginal) => { const actual = diff --git a/tests/unit/commands/issues.test.ts b/tests/unit/commands/issues.test.ts index 9085056..7e3f328 100644 --- a/tests/unit/commands/issues.test.ts +++ b/tests/unit/commands/issues.test.ts @@ -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(); + return { + ...actual, + createContext: vi.fn(() => ({ + gql: { request: vi.fn() }, + sdk: { sdk: {} }, + })), + }; +}); vi.mock("../../../src/common/output.js", async (importOriginal) => { const actual =