Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/commands/teams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Command } from "commander";
import { createLinearService } from "../utils/linear-service.js";
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";

/**
* Setup teams commands on the program
*
* Registers `teams` command group for listing Linear teams.
* Provides team information including key, name, and description.
*
* @param program - Commander.js program instance to register commands on
*
* @example
* ```typescript
* // In main.ts
* setupTeamsCommands(program);
* // Enables: linearis teams list
* ```
*/
export function setupTeamsCommands(program: Command): void {
const teams = program
.command("teams")
.description("Team operations");

// Show teams help when no subcommand
teams.action(() => {
teams.help();
});

/**
* List all teams
*
* Command: `linearis teams list`
*
* Lists all teams in the workspace with their key, name, and description.
*/
teams
.command("list")
.description("List all teams")
.action(
handleAsyncCommand(async (options: any, command: Command) => {
// Initialize Linear service for team operations
const service = await createLinearService(command.parent!.parent!.opts());

// Fetch all teams from the workspace
const result = await service.getTeams();
outputSuccess(result);
})
);
}
52 changes: 52 additions & 0 deletions src/commands/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Command } from "commander";
import { createLinearService } from "../utils/linear-service.js";
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";

/**
* Setup users commands on the program
*
* Registers `users` command group for listing Linear users.
* Provides user information including id, name, displayName, email, and active status.
*
* @param program - Commander.js program instance to register commands on
*
* @example
* ```typescript
* // In main.ts
* setupUsersCommands(program);
* // Enables: linearis users list
* ```
*/
export function setupUsersCommands(program: Command): void {
const users = program
.command("users")
.description("User operations");

// Show users help when no subcommand
users.action(() => {
users.help();
});

/**
* List all users
*
* Command: `linearis users list`
*
* Lists all users in the workspace with their id, name, displayName, email, and active status.
* Can filter to show only active users with --active flag.
*/
users
.command("list")
.description("List all users")
.option("--active", "Only show active users")
.action(
handleAsyncCommand(async (options: any, command: Command) => {
// Initialize Linear service for user operations
const service = await createLinearService(command.parent!.parent!.opts());

// Fetch all users from the workspace
const result = await service.getUsers(options.active);
outputSuccess(result);
})
);
}
4 changes: 4 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { setupLabelsCommands } from "./commands/labels.js";
import { setupProjectsCommands } from "./commands/projects.js";
import { setupCyclesCommands } from "./commands/cycles.js";
import { setupProjectMilestonesCommands } from "./commands/project-milestones.js";
import { setupTeamsCommands } from "./commands/teams.js";
import { setupUsersCommands } from "./commands/users.js";
import { outputUsageInfo } from "./utils/usage.js";

// Setup main program
Expand All @@ -44,6 +46,8 @@ setupProjectsCommands(program);
setupCyclesCommands(program);
setupProjectMilestonesCommands(program);
setupEmbedsCommands(program);
setupTeamsCommands(program);
setupUsersCommands(program);

// Add usage command
program.command("usage")
Expand Down
12 changes: 6 additions & 6 deletions src/queries/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ export const BATCH_RESOLVE_FOR_SEARCH_QUERY = `
}
}

# Resolve project if provided
projects(filter: { name: { eq: $projectName } }, first: 1) {
# Resolve project if provided (case-insensitive to be user-friendly)
projects(filter: { name: { eqIgnoreCase: $projectName } }, first: 1) {
nodes {
id
name
Expand Down Expand Up @@ -196,8 +196,8 @@ export const BATCH_RESOLVE_FOR_UPDATE_QUERY = `
}
}

# Resolve project if provided
projects(filter: { name: { eq: $projectName } }, first: 1) {
# Resolve project if provided (case-insensitive to be user-friendly)
projects(filter: { name: { eqIgnoreCase: $projectName } }, first: 1) {
nodes {
id
name
Expand Down Expand Up @@ -323,8 +323,8 @@ export const BATCH_RESOLVE_FOR_CREATE_QUERY = `
}
}

# Resolve project if provided
projects(filter: { name: { eq: $projectName } }, first: 1) {
# Resolve project if provided (case-insensitive to be user-friendly)
projects(filter: { name: { eqIgnoreCase: $projectName } }, first: 1) {
nodes {
id
name
Expand Down
54 changes: 53 additions & 1 deletion src/utils/linear-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,57 @@ export class LinearService {
return issues.nodes[0].id;
}

/**
* Get all teams in the workspace
*
* @returns Array of teams with id, key, name, and description
*/
async getTeams(): Promise<any[]> {
const teamsConnection = await this.client.teams({
first: 100,
});

// Sort by name client-side since Linear API doesn't support orderBy: "name"
const teams = teamsConnection.nodes.map((team) => ({
id: team.id,
key: team.key,
name: team.name,
description: team.description || null,
}));

return teams.sort((a, b) => a.name.localeCompare(b.name));
}

/**
* Get all users in the workspace
*
* @param activeOnly - If true, return only active users
* @returns Array of users with id, name, displayName, email, and active status
*/
async getUsers(activeOnly?: boolean): Promise<any[]> {
const filter: any = {};

if (activeOnly) {
filter.active = { eq: true };
}

const usersConnection = await this.client.users({
filter: Object.keys(filter).length > 0 ? filter : undefined,
first: 100,
});

// Sort by name client-side since Linear API doesn't support orderBy: "name"
const users = usersConnection.nodes.map((user) => ({
id: user.id,
name: user.name,
displayName: user.displayName,
email: user.email,
active: user.active,
}));

return users.sort((a, b) => a.name.localeCompare(b.name));
}

/**
* Get all projects
*/
Expand Down Expand Up @@ -626,7 +677,8 @@ export class LinearService {
return projectNameOrId;
}

const filter = buildEqualityFilter("name", projectNameOrId);
// Use case-insensitive matching for better UX
const filter = { name: { eqIgnoreCase: projectNameOrId } };
const projectsConnection = await this.client.projects({ filter, first: 1 });

if (projectsConnection.nodes.length === 0) {
Expand Down
84 changes: 84 additions & 0 deletions tests/integration/teams-cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { beforeAll, describe, expect, it } from "vitest";
import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

/**
* Integration tests for teams CLI commands
*
* These tests verify the teams command works end-to-end with the compiled CLI.
*
* Note: These tests require LINEAR_API_TOKEN to be set in environment.
* If not set, tests will be skipped.
*/

const CLI_PATH = "./dist/main.js";
const hasApiToken = !!process.env.LINEAR_API_TOKEN;

describe("Teams CLI Commands", () => {
beforeAll(async () => {
if (!hasApiToken) {
console.warn(
"\n⚠️ LINEAR_API_TOKEN not set - skipping integration tests\n" +
" To run these tests, set LINEAR_API_TOKEN in your environment\n",
);
}
});

describe("teams --help", () => {
it("should display help text", async () => {
const { stdout } = await execAsync(`node ${CLI_PATH} teams --help`);

expect(stdout).toContain("Usage: linearis teams");
expect(stdout).toContain("Team operations");
expect(stdout).toContain("list");
});
});

describe("teams list", () => {
it.skipIf(!hasApiToken)("should list teams without error", async () => {
const { stdout, stderr } = await execAsync(
`node ${CLI_PATH} teams list`,
);

// Should not have errors
expect(stderr).not.toContain("error");

// Should return valid JSON
const teams = JSON.parse(stdout);
expect(Array.isArray(teams)).toBe(true);
});

it.skipIf(!hasApiToken)("should return valid team structure", async () => {
const { stdout } = await execAsync(`node ${CLI_PATH} teams list`);
const teams = JSON.parse(stdout);

// Should have at least one team
expect(teams.length).toBeGreaterThan(0);

const team = teams[0];

// Verify team has expected fields
expect(team).toHaveProperty("id");
expect(team).toHaveProperty("key");
expect(team).toHaveProperty("name");
// description is optional
expect(team).toHaveProperty("description");
});

it.skipIf(!hasApiToken)("should return teams sorted by name", async () => {
const { stdout } = await execAsync(`node ${CLI_PATH} teams list`);
const teams = JSON.parse(stdout);

if (teams.length > 1) {
// Verify alphabetical order
for (let i = 1; i < teams.length; i++) {
const prev = teams[i - 1].name.toLowerCase();
const curr = teams[i].name.toLowerCase();
expect(prev.localeCompare(curr)).toBeLessThanOrEqual(0);
}
}
});
});
});
Loading