Linearis follows a modular, five-layer architecture with clear separation of concerns. The application uses a command-based structure with Commander.js, typed GraphQL operations, standalone resolver functions, and service functions that eliminate code duplication.
The architecture emphasizes performance through GraphQL batch operations, single-query optimizations, and smart ID resolution for user convenience. All components are fully typed with TypeScript - no any types in the new architecture. The system uses both direct GraphQL queries (via typed client) and Linear SDK (for ID resolution).
Thin wrappers around GraphQL and Linear SDK with no business logic.
-
graphql-client.ts - Typed GraphQL client
- Takes
DocumentNodefrom codegen - Returns typed results via generics
- Handles error transformation
- No ID resolution or business logic
- Takes
-
linear-client.ts - Linear SDK wrapper
- Simple wrapper exposing
sdkproperty - Used by resolvers for lookups
- No business logic
- Simple wrapper exposing
Pure functions that convert human-friendly identifiers to UUIDs.
-
team-resolver.ts -
resolveTeamId(client, keyOrNameOrId)- Tries team key first, falls back to name
- Returns UUID
-
project-resolver.ts -
resolveProjectId(client, nameOrId) -
label-resolver.ts -
resolveLabelId(client, nameOrId),resolveLabelIds(client, namesOrIds) -
issue-resolver.ts -
resolveIssueId(client, issueIdOrIdentifier)- Parses ABC-123 format -
status-resolver.ts -
resolveStatusId(client, nameOrId, teamId?) -
cycle-resolver.ts -
resolveCycleId(client, nameOrId, teamFilter?)- Complex disambiguation -
milestone-resolver.ts -
resolveMilestoneId(gqlClient, sdkClient, nameOrId, projectNameOrId?)
Pattern:
- Accept SDK or GraphQL client
- Check if input is UUID (early return)
- Query Linear API for name/key match
- Throw descriptive error if not found
- Return UUID string
Pure, typed functions for CRUD operations. Receive pre-resolved UUIDs.
- issue-service.ts -
listIssues(),getIssue(),searchIssues(),createIssue(),updateIssue() - document-service.ts -
getDocument(),createDocument(),updateDocument(),listDocuments(),deleteDocument() - attachment-service.ts -
createAttachment(),deleteAttachment(),listAttachments() - milestone-service.ts -
listMilestones(),getMilestone(),createMilestone(),updateMilestone() - cycle-service.ts -
listCycles(),getCycle() - team-service.ts -
listTeams() - user-service.ts -
listUsers() - project-service.ts -
listProjects() - label-service.ts -
listLabels() - comment-service.ts -
createComment() - file-service.ts - File upload/download operations
Pattern:
- Accept
GraphQLClientorLinearSdkClient - Take pre-resolved UUIDs in inputs
- Use codegen
DocumentNodetypes - Return typed results
- Throw on failure
Thin orchestration layer that composes resolvers and services.
- auth.ts - Authentication commands (login, status, logout) — interactive, for humans
- issues.ts - Issue commands (list, search, read, create, update)
- documents.ts - Document commands with attachment operations
- project-milestones.ts - Milestone commands
- cycles.ts - Cycle commands
- teams.ts - Team listing
- users.ts - User listing
- projects.ts - Project listing
- labels.ts - Label listing
- comments.ts - Comment creation
- embeds.ts - File download operations
Pattern:
.action(
handleCommand(
async (...args: unknown[]) => {
const [options, command] = args as [OptionsType, Command];
const ctx = await createContext(command.parent!.parent!.opts());
// Resolve IDs
const teamId = await resolveTeamId(ctx.sdk, options.team);
const labelIds = await resolveLabelIds(ctx.sdk, options.labels.split(','));
// Call service
const result = await createIssue(ctx.gql, {
teamId,
labelIds,
title: options.title,
});
outputSuccess(result);
}
)
)Shared utilities used across layers.
- context.ts -
createContext(options)- Creates{ gql, sdk }from auth - auth.ts -
resolveApiToken(options)- Multi-source authentication (flag, env, encrypted storage, legacy file) - output.ts -
outputSuccess(data),outputError(error),handleCommand(fn) - errors.ts -
notFoundError(),multipleMatchesError(),invalidParameterError() - identifier.ts -
isUuid(),parseIssueIdentifier(),tryParseIssueIdentifier() - types.ts - Type aliases from codegen (Issue, Document, Attachment, etc.)
- embed-parser.ts - Linear upload URL parsing utilities
- usage.ts - Token-optimized two-tier usage system (
formatOverview(),formatDomainUsage(),DomainMeta)
- src/main.ts - Main program setup with Commander.js, command routing, and global options
- src/commands/auth.ts - Authentication management (interactive, for humans)
- src/commands/issues.ts - Issue management with resolvers and service composition
- src/commands/documents.ts - Document operations with attachment support
- src/commands/project-milestones.ts - Milestone CRUD operations
- src/commands/cycles.ts - Cycle listing and reading
- src/commands/teams.ts - Team listing
- src/commands/users.ts - User listing
- src/commands/projects.ts - Project listing
- src/commands/labels.ts - Label listing
- src/commands/comments.ts - Comment creation
- src/commands/embeds.ts - File operations
- src/client/graphql-client.ts - Typed GraphQL client with error handling
- src/client/linear-client.ts - Linear SDK wrapper
- src/resolvers/ - Pure resolver functions for converting names/identifiers to UUIDs
- src/services/ - Pure, typed functions for CRUD operations
- src/common/ - Authentication, output formatting, error handling, types
- src/gql/graphql.ts - Generated TypeScript types and DocumentNode exports from GraphQL schema
- src/common/types.ts - Convenient type aliases derived from codegen types
Main Entry Point
- src/main.ts - Sets up Commander.js program with global options and subcommand registration
Client Layer
- src/client/graphql-client.ts - GraphQLClient class with typed request method
- src/client/linear-client.ts - LinearSdkClient wrapper
Resolver Layer
- src/resolvers/team-resolver.ts - Team key/name → UUID
- src/resolvers/issue-resolver.ts - ABC-123 → UUID
- src/resolvers/cycle-resolver.ts - Cycle name → UUID with disambiguation
Service Layer
- src/services/issue-service.ts - Issue CRUD operations
- src/services/document-service.ts - Document CRUD operations
- src/services/milestone-service.ts - Milestone CRUD operations
Common Layer
- src/common/context.ts - createContext factory
- src/common/auth.ts - resolveApiToken with fallback sources (flag, env, encrypted storage, legacy file)
- src/common/token-storage.ts - Encrypted token storage (saveToken, getStoredToken, clearToken)
- src/common/output.ts - outputSuccess, outputError, handleCommand
Query Definitions
- graphql/queries/*.graphql - GraphQL operation definitions
- graphql/mutations/*.graphql - GraphQL mutation definitions
- src/gql/graphql.ts - Generated types and DocumentNode exports
- Command Parsing - src/main.ts parses CLI arguments via Commander.js
- Context Creation - src/common/context.ts creates
{ gql, sdk }from auth options - Authentication - src/common/auth.ts resolves API token from multiple sources
- ID Resolution - src/resolvers/* convert human inputs to UUIDs via SDK
- Service Operations - src/services/* execute typed GraphQL operations
- Response Formatting - src/common/output.ts outputs structured JSON
Linear API uses UUIDs internally, but users prefer human-readable identifiers. Resolution happens in the resolver layer:
Issue Resolution (src/resolvers/issue-resolver.ts)
- Input: "ABC-123" → Parse team key and issue number → Query by team.key + issue.number → Return UUID
Project Resolution (src/resolvers/project-resolver.ts)
- Input: "Mobile App" → Query projects by case-insensitive name → Return project UUID
Team Resolution (src/resolvers/team-resolver.ts)
- Input: "ABC" → Try team key first, fall back to team name → Return team UUID
Cycle Resolution (src/resolvers/cycle-resolver.ts)
- Input: "Sprint 1" → Query cycles by name → Disambiguate by active/next/previous → Return UUID
Single Query Strategy (all services)
// Replaces 1 + (5 × N) API calls with single GraphQL query
const result = await client.request<GetIssuesQuery>(
GetIssuesDocument,
{ first: limit, orderBy: "updatedAt" }
);Typed Operations
All GraphQL operations use codegen types:
- Import
DocumentNodefromsrc/gql/graphql.ts - Pass to
client.request<ResultType>(Document, variables) - Get fully typed results
This eliminates N+1 query problems by using GraphQL's ability to fetch complex relationships in single requests.
- No Code Duplication - ID resolution logic centralized in resolvers
- Type Safety - No
anytypes, everything derived from GraphQL schema - Testability - Pure functions at every layer, easy to unit test
- Maintainability - Clear separation of concerns, easy to locate logic
- Performance - Single-query fetches, batch operations via GraphQL
- Developer Experience - Functions over classes, simple imports, clear data flow