linear skill
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
---
|
||||
name: linear
|
||||
description: Access Linear issue tracker - search, view, create, update issues, list teams/projects, and manage comments. Use when the user asks about Linear issues, tasks, tickets, or project management in Linear.
|
||||
---
|
||||
|
||||
# Linear
|
||||
|
||||
Manage Linear issues, projects, and teams via the Linear SDK.
|
||||
|
||||
## Setup
|
||||
|
||||
Run once before first use:
|
||||
|
||||
```bash
|
||||
cd {baseDir} && npm install
|
||||
```
|
||||
|
||||
Requires a `LINEAR_API_KEY` environment variable. Generate one at: https://linear.app/settings/api (Personal API keys).
|
||||
|
||||
Set it in your shell profile or pi settings:
|
||||
|
||||
```bash
|
||||
export LINEAR_API_KEY=lin_api_...
|
||||
```
|
||||
|
||||
## Current User
|
||||
|
||||
```bash
|
||||
node {baseDir}/linear-me.js # Show authenticated user
|
||||
node {baseDir}/linear-me.js --issues # Show user + their active issues
|
||||
```
|
||||
|
||||
## Search Issues
|
||||
|
||||
```bash
|
||||
node {baseDir}/linear-search.js "query" # Text search
|
||||
node {baseDir}/linear-search.js "query" -n 20 # More results
|
||||
node {baseDir}/linear-search.js "query" --team ENG # Filter by team
|
||||
node {baseDir}/linear-search.js "query" --state "In Progress" # Filter by state
|
||||
```
|
||||
|
||||
## List Issues (with filters)
|
||||
|
||||
```bash
|
||||
node {baseDir}/linear-issues.js # All recent issues
|
||||
node {baseDir}/linear-issues.js --team ENG # By team
|
||||
node {baseDir}/linear-issues.js --state "In Progress" # By state
|
||||
node {baseDir}/linear-issues.js --assignee me # My issues
|
||||
node {baseDir}/linear-issues.js --assignee "John" # By assignee name
|
||||
node {baseDir}/linear-issues.js --label "Bug" # By label
|
||||
node {baseDir}/linear-issues.js --project "Q1 Goals" # By project
|
||||
node {baseDir}/linear-issues.js --team ENG --state Todo -n 50 # Combined filters
|
||||
```
|
||||
|
||||
## View Issue Details
|
||||
|
||||
```bash
|
||||
node {baseDir}/linear-issue.js ATT-1234 # Full issue details
|
||||
node {baseDir}/linear-issue.js ATT-1234 --comments # Include comments
|
||||
```
|
||||
|
||||
## Create Issue
|
||||
|
||||
```bash
|
||||
node {baseDir}/linear-create.js --team ENG --title "Fix login bug"
|
||||
node {baseDir}/linear-create.js --team ENG --title "New feature" --description "Details here" --state Todo --priority 2 --assignee me --label "Feature"
|
||||
node {baseDir}/linear-create.js --team ENG --title "Sub-task" --parent ATT-100
|
||||
```
|
||||
|
||||
Priority values: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low
|
||||
|
||||
## Update Issue
|
||||
|
||||
```bash
|
||||
node {baseDir}/linear-update.js ATT-1234 --state "In Progress"
|
||||
node {baseDir}/linear-update.js ATT-1234 --assignee me --priority 2
|
||||
node {baseDir}/linear-update.js ATT-1234 --title "New title" --description "Updated desc"
|
||||
```
|
||||
|
||||
## Add Comment
|
||||
|
||||
```bash
|
||||
node {baseDir}/linear-comment.js ATT-1234 "This is done in PR #567"
|
||||
```
|
||||
|
||||
## List Teams
|
||||
|
||||
```bash
|
||||
node {baseDir}/linear-teams.js
|
||||
```
|
||||
|
||||
## List Projects
|
||||
|
||||
```bash
|
||||
node {baseDir}/linear-projects.js # All projects
|
||||
node {baseDir}/linear-projects.js --team ENG # By team
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Use `--assignee me` to filter by the authenticated user
|
||||
- Issue identifiers follow the pattern `TEAM-NUMBER` (e.g. `ATT-1234`, `ENG-567`)
|
||||
- Descriptions support markdown formatting
|
||||
- State names are case-insensitive (e.g. "todo", "Todo", "TODO" all work)
|
||||
- When creating issues, the team key is required; use `linear-teams.js` to find available teams
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
import { LinearClient } from "@linear/sdk";
|
||||
|
||||
export function getClient() {
|
||||
const apiKey = process.env.LINEAR_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.error("Error: LINEAR_API_KEY environment variable is required.");
|
||||
console.error(
|
||||
"Generate one at: https://linear.app/settings/api (Personal API keys)"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
return new LinearClient({ apiKey });
|
||||
}
|
||||
|
||||
export function formatDate(date) {
|
||||
if (!date) return "";
|
||||
return new Date(date).toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
export function truncate(str, len = 120) {
|
||||
if (!str) return "";
|
||||
return str.length > len ? str.slice(0, len) + "…" : str;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Add a comment to a Linear issue
|
||||
// Usage: linear-comment.js <identifier> <body>
|
||||
|
||||
import { getClient } from "./lib.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const identifier = args[0];
|
||||
const body = args.slice(1).join(" ");
|
||||
|
||||
if (!identifier || !body) {
|
||||
console.log("Usage: linear-comment.js <identifier> <body>");
|
||||
console.log("\nExamples:");
|
||||
console.log(' linear-comment.js ATT-1234 "This is fixed in the latest PR"');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = getClient();
|
||||
|
||||
const results = await client.searchIssues(identifier, { first: 1 });
|
||||
const issue = results.nodes[0];
|
||||
if (!issue) {
|
||||
console.error(`Issue '${identifier}' not found.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await client.createComment({ issueId: issue.id, body });
|
||||
console.log(`Comment added to ${issue.identifier}.`);
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Create a new Linear issue
|
||||
// Usage: linear-create.js --team <key> --title <title> [--description <desc>] [--state <name>] [--priority <0-4>] [--assignee <name|me>] [--label <name>] [--parent <identifier>]
|
||||
|
||||
import { getClient } from "./lib.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function extractArg(flag) {
|
||||
const idx = args.indexOf(flag);
|
||||
if (idx !== -1 && args[idx + 1]) {
|
||||
const val = args[idx + 1];
|
||||
args.splice(idx, 2);
|
||||
return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const teamKey = extractArg("--team");
|
||||
const title = extractArg("--title");
|
||||
const description = extractArg("--description");
|
||||
const stateName = extractArg("--state");
|
||||
const priority = extractArg("--priority");
|
||||
const assigneeName = extractArg("--assignee");
|
||||
const labelName = extractArg("--label");
|
||||
const parentId = extractArg("--parent");
|
||||
|
||||
if (!teamKey || !title) {
|
||||
console.log("Usage: linear-create.js --team <key> --title <title> [options]");
|
||||
console.log("\nRequired:");
|
||||
console.log(" --team <key> Team key (e.g. ENG)");
|
||||
console.log(' --title <title> Issue title');
|
||||
console.log("\nOptional:");
|
||||
console.log(" --description <text> Issue description (markdown)");
|
||||
console.log(" --state <name> Initial state (e.g. 'Todo')");
|
||||
console.log(" --priority <0-4> Priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low");
|
||||
console.log(" --assignee <name|me> Assignee name or 'me'");
|
||||
console.log(" --label <name> Label name");
|
||||
console.log(" --parent <id> Parent issue identifier (e.g. ATT-100)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = getClient();
|
||||
|
||||
// Resolve team
|
||||
const teams = await client.teams({ filter: { key: { eq: teamKey.toUpperCase() } } });
|
||||
const team = teams.nodes[0];
|
||||
if (!team) {
|
||||
console.error(`Team '${teamKey}' not found.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const input = {
|
||||
teamId: team.id,
|
||||
title,
|
||||
};
|
||||
|
||||
if (description) input.description = description;
|
||||
if (priority) input.priority = parseInt(priority, 10);
|
||||
|
||||
// Resolve state
|
||||
if (stateName) {
|
||||
const states = await team.states();
|
||||
const state = states.nodes.find(
|
||||
(s) => s.name.toLowerCase() === stateName.toLowerCase()
|
||||
);
|
||||
if (state) input.stateId = state.id;
|
||||
else console.warn(`Warning: State '${stateName}' not found, using default.`);
|
||||
}
|
||||
|
||||
// Resolve assignee
|
||||
if (assigneeName) {
|
||||
if (assigneeName.toLowerCase() === "me") {
|
||||
const me = await client.viewer;
|
||||
input.assigneeId = me.id;
|
||||
} else {
|
||||
const users = await client.users({ filter: { name: { containsIgnoreCase: assigneeName } } });
|
||||
if (users.nodes[0]) input.assigneeId = users.nodes[0].id;
|
||||
else console.warn(`Warning: User '${assigneeName}' not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve label
|
||||
if (labelName) {
|
||||
const labels = await client.issueLabels({ filter: { name: { eqIgnoreCase: labelName } } });
|
||||
if (labels.nodes[0]) input.labelIds = [labels.nodes[0].id];
|
||||
else console.warn(`Warning: Label '${labelName}' not found.`);
|
||||
}
|
||||
|
||||
// Resolve parent
|
||||
if (parentId) {
|
||||
const parentSearch = await client.searchIssues(parentId, { first: 1 });
|
||||
if (parentSearch.nodes[0]) input.parentId = parentSearch.nodes[0].id;
|
||||
else console.warn(`Warning: Parent '${parentId}' not found.`);
|
||||
}
|
||||
|
||||
const result = await client.createIssue(input);
|
||||
const issue = await result.issue;
|
||||
|
||||
console.log(`Created: ${issue.identifier} - ${issue.title}`);
|
||||
console.log(`URL: ${issue.url}`);
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Get details for a specific Linear issue
|
||||
// Usage: linear-issue.js <identifier> [--comments]
|
||||
|
||||
import { getClient, formatDate } from "./lib.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
const showComments = args.includes("--comments");
|
||||
const filtered = args.filter((a) => a !== "--comments");
|
||||
|
||||
const identifier = filtered[0];
|
||||
|
||||
if (!identifier) {
|
||||
console.log("Usage: linear-issue.js <identifier> [--comments]");
|
||||
console.log("\nExamples:");
|
||||
console.log(" linear-issue.js ATT-1234");
|
||||
console.log(" linear-issue.js ATT-1234 --comments");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = getClient();
|
||||
|
||||
// Parse team key and issue number from identifier (e.g. "SIP-1205")
|
||||
const parts = identifier.match(/^([A-Za-z]+)-(\d+)$/);
|
||||
if (!parts) {
|
||||
console.error(`Invalid identifier format: ${identifier}. Expected format: TEAM-123`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const teamKey = parts[1].toUpperCase();
|
||||
const issueNumber = parseInt(parts[2], 10);
|
||||
|
||||
// Find the issue by team key + number
|
||||
const issues = await client.issues({
|
||||
filter: {
|
||||
team: { key: { eq: teamKey } },
|
||||
number: { eq: issueNumber },
|
||||
},
|
||||
first: 1,
|
||||
});
|
||||
|
||||
const issue = issues.nodes[0];
|
||||
if (!issue) {
|
||||
console.error(`Issue ${identifier} not found.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const state = await issue.state;
|
||||
const team = await issue.team;
|
||||
const assignee = await issue.assignee;
|
||||
const labels = await issue.labels();
|
||||
const parent = await issue.parent;
|
||||
const project = await issue.project;
|
||||
const cycle = await issue.cycle;
|
||||
|
||||
console.log(`=== ${issue.identifier}: ${issue.title} ===`);
|
||||
console.log(`URL: ${issue.url}`);
|
||||
console.log(`State: ${state?.name || "Unknown"}`);
|
||||
console.log(`Priority: ${issue.priorityLabel}`);
|
||||
console.log(`Team: ${team?.key || "?"}`);
|
||||
console.log(`Assignee: ${assignee?.name || "Unassigned"}`);
|
||||
if (project) console.log(`Project: ${project.name}`);
|
||||
if (cycle) console.log(`Cycle: ${cycle.name || cycle.number}`);
|
||||
if (parent) console.log(`Parent: ${parent.identifier} - ${parent.title}`);
|
||||
if (labels.nodes.length > 0) {
|
||||
console.log(`Labels: ${labels.nodes.map((l) => l.name).join(", ")}`);
|
||||
}
|
||||
console.log(`Created: ${formatDate(issue.createdAt)}`);
|
||||
console.log(`Updated: ${formatDate(issue.updatedAt)}`);
|
||||
if (issue.dueDate) console.log(`Due: ${issue.dueDate}`);
|
||||
console.log(`\nDescription:\n${issue.description || "(empty)"}`);
|
||||
|
||||
if (showComments) {
|
||||
const comments = await issue.comments();
|
||||
if (comments.nodes.length > 0) {
|
||||
console.log(`\n--- Comments (${comments.nodes.length}) ---`);
|
||||
for (const comment of comments.nodes) {
|
||||
const author = await comment.user;
|
||||
console.log(`\n[${formatDate(comment.createdAt)}] ${author?.name || "Unknown"}:`);
|
||||
console.log(comment.body);
|
||||
}
|
||||
} else {
|
||||
console.log("\nNo comments.");
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// List Linear issues with filters
|
||||
// Usage: linear-issues.js [--team <key>] [--state <name>] [--assignee <name|me>] [--label <name>] [--project <name>] [-n <num>]
|
||||
|
||||
import { getClient, formatDate, truncate } from "./lib.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function extractArg(flag) {
|
||||
const idx = args.indexOf(flag);
|
||||
if (idx !== -1 && args[idx + 1]) {
|
||||
const val = args[idx + 1];
|
||||
args.splice(idx, 2);
|
||||
return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const numResults = parseInt(extractArg("-n") || "25", 10);
|
||||
const teamKey = extractArg("--team");
|
||||
const stateName = extractArg("--state");
|
||||
const assigneeName = extractArg("--assignee");
|
||||
const labelName = extractArg("--label");
|
||||
const projectName = extractArg("--project");
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
console.log("Usage: linear-issues.js [options]");
|
||||
console.log("\nOptions:");
|
||||
console.log(" --team <key> Filter by team key (e.g. ENG)");
|
||||
console.log(" --state <name> Filter by state (e.g. 'In Progress', 'Todo')");
|
||||
console.log(" --assignee <name> Filter by assignee name or 'me'");
|
||||
console.log(" --label <name> Filter by label name");
|
||||
console.log(" --project <name> Filter by project name");
|
||||
console.log(" -n <num> Number of results (default: 25)");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const client = getClient();
|
||||
|
||||
// Build filter
|
||||
const filter = {};
|
||||
|
||||
if (teamKey) {
|
||||
filter.team = { key: { eq: teamKey.toUpperCase() } };
|
||||
}
|
||||
|
||||
if (stateName) {
|
||||
filter.state = { name: { eqIgnoreCase: stateName } };
|
||||
}
|
||||
|
||||
if (assigneeName) {
|
||||
if (assigneeName.toLowerCase() === "me") {
|
||||
const me = await client.viewer;
|
||||
filter.assignee = { id: { eq: me.id } };
|
||||
} else {
|
||||
filter.assignee = { name: { containsIgnoreCase: assigneeName } };
|
||||
}
|
||||
}
|
||||
|
||||
if (labelName) {
|
||||
filter.labels = { name: { eqIgnoreCase: labelName } };
|
||||
}
|
||||
|
||||
if (projectName) {
|
||||
filter.project = { name: { containsIgnoreCase: projectName } };
|
||||
}
|
||||
|
||||
const issues = await client.issues({
|
||||
filter,
|
||||
first: numResults,
|
||||
orderBy: "updatedAt",
|
||||
});
|
||||
|
||||
if (issues.nodes.length === 0) {
|
||||
console.log("No issues found matching filters.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
for (const issue of issues.nodes) {
|
||||
const state = await issue.state;
|
||||
const team = await issue.team;
|
||||
const assignee = await issue.assignee;
|
||||
|
||||
console.log(
|
||||
`${issue.identifier.padEnd(12)} ${(state?.name || "?").padEnd(14)} ${(issue.priorityLabel || "").padEnd(8)} ${(assignee?.name || "Unassigned").padEnd(20)} ${truncate(issue.title, 80)}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`\n${issues.nodes.length} issue(s) shown.`);
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Show current authenticated user and their assigned issues
|
||||
// Usage: linear-me.js [--issues]
|
||||
|
||||
import { getClient, truncate } from "./lib.js";
|
||||
|
||||
const showIssues = process.argv.includes("--issues");
|
||||
|
||||
const client = getClient();
|
||||
|
||||
const me = await client.viewer;
|
||||
console.log(`User: ${me.name}`);
|
||||
console.log(`Email: ${me.email}`);
|
||||
console.log(`ID: ${me.id}`);
|
||||
|
||||
if (showIssues) {
|
||||
const issues = await me.assignedIssues({
|
||||
first: 25,
|
||||
filter: {
|
||||
state: { type: { nin: ["completed", "canceled"] } },
|
||||
},
|
||||
orderBy: "updatedAt",
|
||||
});
|
||||
|
||||
console.log(`\n--- Active Assigned Issues (${issues.nodes.length}) ---`);
|
||||
for (const issue of issues.nodes) {
|
||||
const state = await issue.state;
|
||||
console.log(
|
||||
`${issue.identifier.padEnd(12)} ${(state?.name || "?").padEnd(14)} ${(issue.priorityLabel || "").padEnd(8)} ${truncate(issue.title, 80)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// List Linear projects
|
||||
// Usage: linear-projects.js [--team <key>] [-n <num>]
|
||||
|
||||
import { getClient, formatDate } from "./lib.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function extractArg(flag) {
|
||||
const idx = args.indexOf(flag);
|
||||
if (idx !== -1 && args[idx + 1]) {
|
||||
const val = args[idx + 1];
|
||||
args.splice(idx, 2);
|
||||
return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const numResults = parseInt(extractArg("-n") || "25", 10);
|
||||
const teamKey = extractArg("--team");
|
||||
|
||||
const client = getClient();
|
||||
|
||||
const filter = {};
|
||||
if (teamKey) {
|
||||
filter.accessibleTeams = { key: { eq: teamKey.toUpperCase() } };
|
||||
}
|
||||
|
||||
const projects = await client.projects({ filter, first: numResults });
|
||||
|
||||
if (projects.nodes.length === 0) {
|
||||
console.log("No projects found.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
for (const project of projects.nodes) {
|
||||
const lead = await project.lead;
|
||||
console.log(`--- ${project.name} ---`);
|
||||
console.log(`State: ${project.state} | Progress: ${Math.round(project.progress * 100)}%`);
|
||||
if (lead) console.log(`Lead: ${lead.name}`);
|
||||
if (project.targetDate) console.log(`Target: ${project.targetDate}`);
|
||||
console.log(`URL: ${project.url}`);
|
||||
console.log("");
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Search Linear issues by text query
|
||||
// Usage: linear-search.js <query> [-n <num>] [--team <key>] [--state <name>]
|
||||
|
||||
import { getClient, formatDate, truncate } from "./lib.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
let numResults = 10;
|
||||
const nIdx = args.indexOf("-n");
|
||||
if (nIdx !== -1 && args[nIdx + 1]) {
|
||||
numResults = parseInt(args[nIdx + 1], 10);
|
||||
args.splice(nIdx, 2);
|
||||
}
|
||||
|
||||
let teamFilter = null;
|
||||
const teamIdx = args.indexOf("--team");
|
||||
if (teamIdx !== -1 && args[teamIdx + 1]) {
|
||||
teamFilter = args[teamIdx + 1];
|
||||
args.splice(teamIdx, 2);
|
||||
}
|
||||
|
||||
let stateFilter = null;
|
||||
const stateIdx = args.indexOf("--state");
|
||||
if (stateIdx !== -1 && args[stateIdx + 1]) {
|
||||
stateFilter = args[stateIdx + 1];
|
||||
args.splice(stateIdx, 2);
|
||||
}
|
||||
|
||||
const query = args.join(" ");
|
||||
|
||||
if (!query) {
|
||||
console.log("Usage: linear-search.js <query> [-n <num>] [--team <key>] [--state <name>]");
|
||||
console.log("\nOptions:");
|
||||
console.log(" -n <num> Number of results (default: 10)");
|
||||
console.log(" --team <key> Filter by team key (e.g. ENG)");
|
||||
console.log(" --state <name> Filter by state name (e.g. 'In Progress')");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = getClient();
|
||||
|
||||
const results = await client.searchIssues(query, { first: numResults });
|
||||
|
||||
for (const issue of results.nodes) {
|
||||
const state = await issue.state;
|
||||
const team = await issue.team;
|
||||
const assignee = await issue.assignee;
|
||||
|
||||
if (teamFilter && team?.key?.toLowerCase() !== teamFilter.toLowerCase()) continue;
|
||||
if (stateFilter && state?.name?.toLowerCase() !== stateFilter.toLowerCase()) continue;
|
||||
|
||||
console.log(`--- ${issue.identifier} ---`);
|
||||
console.log(`Title: ${issue.title}`);
|
||||
console.log(`State: ${state?.name || "Unknown"}`);
|
||||
console.log(`Priority: ${issue.priorityLabel}`);
|
||||
console.log(`Team: ${team?.key || "?"} | Assignee: ${assignee?.name || "Unassigned"}`);
|
||||
console.log(`Created: ${formatDate(issue.createdAt)} | Updated: ${formatDate(issue.updatedAt)}`);
|
||||
if (issue.description) console.log(`Description: ${truncate(issue.description, 200)}`);
|
||||
console.log(`URL: ${issue.url}`);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
if (results.nodes.length === 0) {
|
||||
console.log("No results found.");
|
||||
}
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// List all Linear teams
|
||||
// Usage: linear-teams.js
|
||||
|
||||
import { getClient } from "./lib.js";
|
||||
|
||||
const client = getClient();
|
||||
|
||||
const teams = await client.teams();
|
||||
|
||||
console.log("Teams:");
|
||||
for (const team of teams.nodes) {
|
||||
console.log(` ${team.key.padEnd(8)} ${team.name}`);
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Update an existing Linear issue
|
||||
// Usage: linear-update.js <identifier> [--title <title>] [--state <name>] [--priority <0-4>] [--assignee <name|me>] [--description <text>]
|
||||
|
||||
import { getClient } from "./lib.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
const identifier = args[0];
|
||||
if (!identifier || identifier.startsWith("--")) {
|
||||
console.log("Usage: linear-update.js <identifier> [options]");
|
||||
console.log("\nOptions:");
|
||||
console.log(" --title <title> New title");
|
||||
console.log(" --state <name> New state (e.g. 'In Progress')");
|
||||
console.log(" --priority <0-4> New priority");
|
||||
console.log(" --assignee <name|me> New assignee");
|
||||
console.log(" --description <text> New description");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
args.shift();
|
||||
|
||||
function extractArg(flag) {
|
||||
const idx = args.indexOf(flag);
|
||||
if (idx !== -1 && args[idx + 1]) {
|
||||
const val = args[idx + 1];
|
||||
args.splice(idx, 2);
|
||||
return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = extractArg("--title");
|
||||
const stateName = extractArg("--state");
|
||||
const priority = extractArg("--priority");
|
||||
const assigneeName = extractArg("--assignee");
|
||||
const description = extractArg("--description");
|
||||
|
||||
const client = getClient();
|
||||
|
||||
// Find the issue
|
||||
const results = await client.searchIssues(identifier, { first: 1 });
|
||||
const issue = results.nodes[0];
|
||||
if (!issue) {
|
||||
console.error(`Issue '${identifier}' not found.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const input = {};
|
||||
|
||||
if (title) input.title = title;
|
||||
if (description) input.description = description;
|
||||
if (priority) input.priority = parseInt(priority, 10);
|
||||
|
||||
// Resolve state
|
||||
if (stateName) {
|
||||
const team = await issue.team;
|
||||
const states = await team.states();
|
||||
const state = states.nodes.find(
|
||||
(s) => s.name.toLowerCase() === stateName.toLowerCase()
|
||||
);
|
||||
if (state) input.stateId = state.id;
|
||||
else {
|
||||
console.error(`State '${stateName}' not found. Available states:`);
|
||||
for (const s of states.nodes) console.error(` - ${s.name}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve assignee
|
||||
if (assigneeName) {
|
||||
if (assigneeName.toLowerCase() === "me") {
|
||||
const me = await client.viewer;
|
||||
input.assigneeId = me.id;
|
||||
} else {
|
||||
const users = await client.users({ filter: { name: { containsIgnoreCase: assigneeName } } });
|
||||
if (users.nodes[0]) input.assigneeId = users.nodes[0].id;
|
||||
else {
|
||||
console.error(`User '${assigneeName}' not found.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(input).length === 0) {
|
||||
console.log("No updates specified. Use --title, --state, --priority, --assignee, or --description.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await client.updateIssue(issue.id, input);
|
||||
console.log(`Updated ${issue.identifier}: ${issue.title}`);
|
||||
console.log(`URL: ${issue.url}`);
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"name": "linear-skill",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "linear-skill",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@linear/sdk": "^37.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-typed-document-node/core": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
|
||||
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@linear/sdk": {
|
||||
"version": "37.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@linear/sdk/-/sdk-37.0.0.tgz",
|
||||
"integrity": "sha512-EAZCXtV414Nwtvrwn7Ucu3E8BbYYKsc3HqZCGf1mHUE7FhZGtfISu295DOVv89WhhXlp2N344EMg3K0nnhLxtA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@graphql-typed-document-node/core": "^3.1.0",
|
||||
"graphql": "^15.4.0",
|
||||
"isomorphic-unfetch": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.x",
|
||||
"yarn": "1.x"
|
||||
}
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "15.10.1",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz",
|
||||
"integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-unfetch": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz",
|
||||
"integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.1",
|
||||
"unfetch": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unfetch": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
|
||||
"integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "linear-skill",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Linear API skill for pi - manage issues, projects, and teams",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@linear/sdk": "^37.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user