npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@h-rig/linear-plugin

v0.0.6-alpha.82

Published

Rig package

Downloads

2,449

Readme

@rig/linear-plugin

Linear as a Rig task source — and the canonical example of a plugin that adapts an external HTTP API into Rig's task-source contract.

The two first-party adapters in @rig/standard-plugin cover a CLI wrapper (github-issues shells out to gh) and the local filesystem (files). This package covers the third shape you'll actually build most often: a remote API spoken over HTTP, with credentials, pagination, write-back mutations, and tests that never touch the network. If you are writing a Jira/Asana/Shortcut/ internal-queue adapter, copy this package.

packages/linear-plugin/
├── package.json                       ← workspace package, mirrors standard-plugin
├── tsconfig.json
└── src/
    ├── index.ts                       ← definePlugin wiring (metadata + runtime channels)
    ├── linear-issues-source.ts        ← the adapter: GraphQL client, mapping, write-back
    ├── fixture-transport.ts           ← fixture-driven fake transport for tests
    ├── fixtures.ts                    ← canned Linear GraphQL payloads
    ├── contract.test.ts               ← @rig/plugin-testkit contract suite + factory wiring
    └── linear-issues-source.test.ts   ← mapping, lifecycle, pagination, error surfaces

Using it

// rig.config.ts
import { defineConfig } from "@rig/core";
import linear from "@rig/linear-plugin";

export default defineConfig({
  plugins: [linear()],
  taskSource: {
    kind: "linear",
    options: {
      teamKey: "ENG",                 // required — the "ENG" in ENG-123
      states: ["Todo", "In Progress"], // optional: workflow state names
      assignee: "[email protected]",    // optional: email or display name
      labels: ["rig"],                 // optional: issue must carry ALL listed labels
      // pageSize: 50,                 // optional: issues per GraphQL page (max 250)
      // listLimit: 1000,              // optional: fail-loud ceiling, no silent truncation
    },
  },
  // ... workspace, runtime, etc.
});

Authentication comes from the environment, never from the committed config:

export LINEAR_API_KEY=lin_api_...   # personal API key (Settings → API)

Personal API keys (lin_api_…) are sent raw in the Authorization header; OAuth access tokens (lin_oauth_…) are automatically sent as Bearer <token>. You can also pass the key programmatically: linear({ apiKey }).

Anatomy of an external-API task source

1. Two channels, one definePlugin call

Rig plugins declare what they contribute as Effect-Schema-validated metadata and ship executable code on a sibling runtime channel — schemas can't carry functions. src/index.ts:

export default function linearPlugin(opts: LinearPluginOptions = {}) {
  return definePlugin(
    // Channel 1: metadata. Validated against the RigPlugin schema at load time.
    {
      name: "rig-linear",
      version: "0.1.0",
      contributes: {
        taskSources: [{ id: "linear:issues", kind: "linear", description: "…" }],
      },
    },
    // Channel 2: runtime. The factory turns a TaskSourceConfig into a live source.
    {
      taskSources: [{ id: "linear:issues", kind: "linear", factory(config) { … } }],
    },
  );
}

definePlugin enforces referential integrity: every factory must match a metadata entry by id with the same kind, and (once a plugin opts into the runtime channel) every metadata entry must be backed by a factory. Drift throws at config-load time, not at dispatch time.

At boot, buildTaskSourceRegistry(config, pluginHost) in @rig/runtime reads config.taskSource.kind ("linear"), resolves the factory through the plugin host (which also rejects duplicate kinds across plugins), and instantiates the source.

2. Plugin-specific config rides in options

TaskSourceConfig's top-level fields (path, owner, repo, …) are reserved for the built-in adapters. Everything Linear-specific lives in the documented options extension bag — the factory pulls its own fields out and validates them with actionable errors:

const teamKey = optionString(config, "teamKey");
if (!teamKey) {
  throw new Error('task source linear: options.teamKey is required — …');
}

Process-level concerns (API key, endpoint override, transport injection) arrive through the plugin function's own options instead, because they belong to the environment, not to the committed project config.

3. An injectable transport is the whole testing story

The adapter never calls fetch directly. It calls a fetch-shaped function it was constructed with:

export type LinearTransport = (
  url: string,
  init: { method: "POST"; headers: Record<string, string>; body: string },
) => Promise<{ ok: boolean; status: number; text(): Promise<string> }>;

const transport: LinearTransport = opts.transport ?? fetch; // global fetch satisfies it

That one seam is why this package's test suite makes zero live API calls. fixture-transport.ts exports createFixtureTransport(routes), which routes requests by GraphQL operation name (every query/mutation in the adapter is deliberately named: RigListIssues, RigTeamStates, RigIssueUpdate, …), records every call for payload assertions, and fails loudly on any operation a test didn't explicitly allow:

const fixture = createFixtureTransport({
  RigListIssues: () => listIssuesPage(issueFixtures),
  RigIssueUpdate: () => ({ issueUpdate: { success: true } }),
});
const source = createLinearIssuesTaskSource({
  teamKey: "ENG",
  apiKey: "lin_api_test",
  transport: fixture.transport,
});
// …act…
expect(fixture.callsFor("RigIssueUpdate")[0].variables.input).toEqual({ stateId: … });

graphqlErrors([...]) and httpFailure(status, body) simulate the two failure layers a GraphQL API has (200-with-errors vs. non-200 HTTP).

4. Mapping external records to TaskRecord

TaskRecord requires id, deps, status and is open-ended beyond that. The mapping decisions here:

| TaskRecord field | Source | |---|---| | id | Linear identifier (ENG-123) — human-facing, like GitHub issue numbers | | status | Linear workflow-state type (see table below) | | title, body/description | issue title/description (body is Rig convention; description mirrors Linear) | | priority, priorityLabel | Linear priority (0–4) and its label | | labels | label names; scope:/role:/validator: prefixes get the same treatment as the GitHub adapter | | externalRef | linear:<identifier> | | sourceIssueId | the identifier | | linearIssueId | Linear's internal UUID — kept so mutations can skip a lookup | | url, raw | issue URL, full raw node |

Status mapping is keyed off Linear's fixed state-type taxonomy, not the user-renamable state names:

| Linear state type | Rig status | |---|---| | triage, backlog, unstarted | ready | | started | in_progress | | completed | completed | | canceled | cancelled | | anything unknown | open (fail-soft) |

5. Lifecycle write-back

Rig drives the source through updateStatus(id, status) and updateTask(id, update) (status/title/body/comment/metadata). The Linear implementation mirrors the GitHub adapter's responsibilities with GraphQL mutations:

  • Status → resolve the team's workflow states (RigTeamStates, cached per source instance), pick the lowest-position state whose type matches the Rig status (in_progressstarted, closed/completedcompleted, cancelledcanceled, readyunstarted), then issueUpdate with stateId. Statuses Linear has no workflow concept for (blocked, failed, needs_attention) deliberately leave the state untouched — they surface through comments instead of mangling the board.
  • Title / body / metadata → folded into the same single issueUpdate call. update.metadata is rendered into the Rig-owned <!-- rig:metadata:start/end --> block (same markers as the GitHub adapter, so operator surfaces parse both identically), replacing a previous block in the existing description when present.
  • CommentcommentCreate after the update.
  • Task ids are accepted as either the identifier (ENG-123) or Linear's UUID; identifiers are resolved with one extra issue(id:) query.

Every write fires the optional onTaskChanged callback so the host can refresh snapshots — same pattern as the GitHub adapter.

6. Error surfaces are part of the contract

  • No key configured → "Linear task source: no API key configured. Pass apiKey in plugin options or set the LINEAR_API_KEY environment variable." — thrown before any request.
  • Bad token (HTTP 401 or a GraphQL AUTHENTICATION_ERROR) → "Linear API authentication failed (…). Check that the configured API key (options.apiKey / LINEAR_API_KEY) is a valid Linear key…".
  • Other GraphQL errors → surfaced with the server's message.
  • Non-200 HTTP → status + body excerpt.
  • More matching issues than listLimit → a loud refusal to truncate, telling you exactly which knob to turn — never a silently short task list.

Running the tests

cd packages/linear-plugin
bun test            # 39 tests, no network
bunx tsc --noEmit   # typecheck

contract.test.ts runs pluginContractTests from @rig/plugin-testkit (schema validity, namespaced ids, duplicate-id detection) plus factory-wiring assertions; linear-issues-source.test.ts covers mapping, filters, pagination, lifecycle mutation payloads, and every error surface above.