Linearis CLI

repository·next·Indexed 42 days ago

https://github.com/linearis-oss/linearis

A JSON-only CLI tool for Linear.app version 2026.9.0-next.1, designed for AI agents and developers. It features a two-tier discovery system for token efficiency, smart ID resolution for human-friendly identifiers, and atomic batch operations for issues. Linearis provides structured data output and a modular five-layer architecture to optimize GraphQL queries and agent context usage.

Tokens
43.8K
Snippets
184
Records
233
Agent score
53%

What's inside Linearis

  1. Reference issues by identifier or ID

    next

    When interacting with issues, the CLI is flexible with how you identify them. You can pass any of the following to identify a ticket:

    • Issue Identifier: e.g., ENG-42 or ABC-123 (Recommended)
    • UUID: The unique Linear ID
    • Team Key: e.g., ENG (for team-scoped operations)
    • Name: The title of the issue
  2. Understand the Linearis project structure

    next

    The repository is organized into several functional layers:

    • src/main.ts: The primary entry point where all command groups are registered.
    • src/client/: Contains the GraphQLClient for direct GraphQL execution.
    • src/resolvers/: Handles the resolution of human-readable IDs to UUIDs.
    • src/services/: Contains the core business logic and CRUD operations (e.g., issue-service.ts, project-service.ts).
    • src/commands/: Defines the CLI command structures (e.g., auth.ts, issues.ts).
    • src/common/: Shared utilities including CommandContext, token-storage.ts, and error handling.
    • src/gql/: DO NOT EDIT. This directory contains the output from GraphQL codegen.
    • graphql/: Contains the source queries/ and mutations/ definitions used by codegen.
    • tests/: Organized by layer (resolvers, services, common) using vitest.
    src/
      main.ts                    # Entry point, registers all command groups
      client/
        graphql-client.ts        # GraphQLClient - direct GraphQL execution
      resolvers/                 # Human ID to UUID resolution
      services/                  # Business logic and CRUD
      commands/                  # CLI command definitions
      common/                    # Shared utilities
      gql/                       # GraphQL codegen output (DO NOT EDIT)
    graphql/
      queries/                   # GraphQL query definitions
      mutations/                 # GraphQL mutation definitions
    tests/
      unit/
  3. Manage issue discussions and comments

    next

    Threaded discussions are managed through the issues domain. While a top-level comments domain exists as a deprecated facade, you should use the following pattern for discussions:

    • Use issues discuss or discussions to manage threads.
    • Use replies or reply for specific message interactions.

    It is a best practice to record non-trivial progress in a discussion thread and keep the issue description in sync when status changes occur.

  4. How the Linearis layered architecture works

    next

    Linearis uses a five-layer architecture to separate concerns. Each layer has a specific responsibility and interacts with the GraphQLClient differently:

    1. Client (src/client/): An API client wrapper.
    2. Resolver (src/resolvers/): Converts human-friendly identifiers (like team names or issue keys like ENG-123) into UUIDs using lean lookup queries.
    3. Service (src/services/): Contains business logic and CRUD operations. Services must accept pre-resolved UUIDs, not human-friendly identifiers.
    4. Command (src/commands/): Orchestrates the CLI. It creates the client context, calls resolvers to get UUIDs, calls services, and outputs results.
    5. Common (src/common/): Shared utilities and types.

    Data Flow: CLI Input $\rightarrow$ Command $\rightarrow$ Resolver (ID lookup) $\rightarrow$ Service (Data operations) $\rightarrow$ JSON Output.

  5. Understand the Linearis Five-Layer Architecture

    next

    Linearis is built using a modular, five-layer architecture designed for separation of concerns, type safety, and performance. Understanding these layers helps you know where to place new logic or how to extend the tool:

    1. Client Layer (src/client/): A thin, typed wrapper around the Linear GraphQL API. It handles raw requests and error transformations but contains no business logic.
    2. Resolver Layer (src/resolvers/): Pure functions that convert human-friendly identifiers (like team keys, project names, or issue identifiers like ABC-123) into UUIDs. This layer is essential for user convenience.
    3. Service Layer (src/services/): Pure, typed functions for CRUD operations (e.g., createIssue, listProjects). These functions expect pre-resolved UUIDs as inputs and return typed results.
    4. Command Layer (src/commands/): The orchestration layer. It composes resolvers and services to fulfill CLI commands (e.g., linear issues create).
    5. Common Layer (src/common/): Shared utilities for authentication, context creation, output formatting, and error handling.
  6. Implement the Resolver pattern for ID lookup

    next

    Resolvers are responsible for converting human-friendly identifiers (team keys, names, or issue identifiers like ENG-123) into UUIDs. They reside in src/resolvers/.

    Rules for Resolvers:

    • Always accept a UUID passthrough as the first check (if the input is already a UUID, return it).
    • Return a UUID string, never an object.
    • Use GraphQLClient with lean filter-based lookup queries.
    • Do not import services or perform CRUD/data transformations.
    • Throw descriptive errors using src/common/errors.ts (e.g., notFoundError) if a match isn't found.
    import type { GraphQLClient } from "../client/graphql-client.js";
    import { notFoundError } from "../common/errors.js";
    import { asUuid, isUuid, type UUID } from "../common/identifier.js";
    import { FindTeamsDocument } from "../gql/graphql.js";
    
    export async function resolveTeamId(
      client: GraphQLClient,
      keyOrNameOrId: string,
    ): Promise<UUID> {
      if (isUuid(keyOrNameOrId)) return asUuid(keyOrNameOrId);
    
      // Try by key first
      const byKey = await client.request(FindTeamsDocument, {
        filter: { key: { eq: keyOrNameOrId } },
        first: 1,
      });
      const [byKeyMatch] = byKey.teams.nodes;
      if (byKeyMatch) return asUuid(byKeyMatch.id);
    
      // Fall back to name
      const byName = await client.request(FindTeamsDocument, {
        filter: { name: { eq: keyOrNameOrId } },
        first: 1,
      });
      const [byNameMatch] = byName.teams.nodes;
      if (byNameMatch) return asUuid(byNameMatch.id);
    
      throw notFoundError("Team", keyOrNameOrId);
    }
  7. How mocking works in Linearis tests

    next

    Linearis enforces a layered architecture by mocking exactly one layer deep. The mocking strategy depends on the architectural layer being tested:

    Resolver Tests

    Resolvers depend on GraphQLClient. Mock the request method of the client.

    Service Tests

    Services also depend on GraphQLClient. Mock the request method.

    Client Tests

    Client tests mock the network layer by stubbing the global fetch function.

    Common Tests

    Functions in common/ are pure and do not require mocks.

    // Example: Mocking GraphQLClient for a Resolver or Service
    import type { GraphQLClient } from "../../src/client/graphql-client.js";
    
    const client = {
      request: vi.fn().mockResolvedValue({
        teams: { nodes: [{ id: "uuid-123", key: "ABC" }] },
      }),
    } as unknown as GraphQLClient;
    
    // Example: Mocking fetch for Client tests
    const mockFetch = vi.fn();
    vi.stubGlobal("fetch", mockFetch);
  8. Implement the Command pattern for CLI actions

    next

    Commands in Linearis are thin orchestration layers. They should not contain business logic.

    Key requirements:

    • Use createContext() to get the client context (ctx.gql).
    • Use resolvers to convert human-friendly options into UUIDs.
    • Call services to perform the actual work.
    • Crucial: Every .action() handler must be wrapped with handleCommand() to ensure errors are caught and output as JSON.
    • Register new command groups in src/main.ts using a setup*Commands(program) function.
    import { Command } from "commander";
    import { createContext } from "../common/context.js";
    import { handleCommand, outputSuccess } from "../common/output.js";
    import { resolveTeamId } from "../resolvers/team-resolver.js";
    import { createIssue } from "../services/issue-service.js";
    
    export function setupIssuesCommands(program: Command): void {
      const issues = program.command("issues");
    
      issues
        .command("create <title>")
        .option("--team <id>", "Team key, name, or UUID")
        .action(handleCommand(async (title, options, command) => {
          const ctx = await createContext(command.parent!.parent!.opts());
          const teamId = options.team
            ? await resolveTeamId(ctx.gql, options.team)
            : undefined;
          const result = await createIssue(ctx.gql, { title, teamId });
          outputSuccess(result);
        }));
    }
  9. How usage documentation generation works

    next

    The project automatically generates USAGE.md, a token-optimized documentation file designed for LLM agents.

    The Process:

    1. Each command file exports a DomainMeta object containing the domain name, summary, context, arguments, and cross-references.
    2. Running npm run generate:usage executes linearis usage --all and captures the output into USAGE.md.
    3. The output is structured into an overview (~200 tokens) and per-domain details (~300-500 tokens each).

    Important: Do not edit USAGE.md directly. To change the documentation, edit the DomainMeta objects within the respective command files. The file is regenerated automatically during npm run build and before publishing.

    To manually regenerate the documentation:

    npm run generate:usage
  10. Discover Linearis commands using the two-tier usage system

    next

    Linearis uses a two-tier usage system to keep AI agent context small. Instead of loading the entire API surface, agents can discover capabilities on demand.

    • Global Overview: Run linearis usage to get a compact overview of every available domain (~200 tokens).
    • Domain Reference: Run linearis <domain> usage to get the full command and flag reference for a specific domain (e.g., issues, projects) (~300–500 tokens).
    # Compact overview of all domains
    linearis usage
    
    # Full reference for the issues domain
    linearis issues usage
  11. Use GraphQL fragments for consistent data fetching

    next

    To ensure consistent and complete data fetching across different operations (like list, read, and search), define shared GraphQL fragments in graphql/queries/*.graphql. This reduces duplication and ensures that all services receive the same set of fields for a given entity.

    Example of a shared fragment for issues:

    # graphql/queries/issues.graphql
    
    fragment CompleteIssueFields on Issue {
      id
      identifier
      title
      description
      priority
      estimate
      createdAt
      updatedAt
      state { id name }
      assignee { id name }
      team { id key name }
      project { id name }
      labels { nodes { id name } }
      cycle { id name number }
      parent { id identifier title }
      children { nodes { id identifier title } }
    }
    
    query GetIssues($first: Int!, $orderBy: PaginationOrderBy) {
      issues(first: $first, orderBy: $orderBy, ...) {
        nodes { ...CompleteIssueFields }
      }
    }
  12. How ID Resolution works in Linearis

    next

    Linearis uses a Resolver Layer to bridge the gap between human-readable input and the UUIDs required by the Linear API. When you provide a name or a shorthand identifier in a command, the following process occurs:

    • Issue Resolution: An identifier like ABC-123 is parsed into a team key (ABC) and an issue number (123), then queried to find the corresponding UUID.
    • Team Resolution: The resolver tries to match the input against team keys first, falling back to team names if no key match is found.
    • Project Resolution: Matches input against project names (case-insensitive).
    • Cycle Resolution: Matches cycle names and uses logic to disambiguate between active, next, or previous cycles.

    Pattern for implementing new resolvers:

    1. Accept the GraphQLClient.
    2. Check if the input is already a UUID (early return).
    3. Query the Linear API for a name or key match.
    4. Throw a descriptive error if no match is found.
    5. Return the UUID string.