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
70 changes: 70 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Command } from "@cliffy/command"
import { CompletionsCommand } from "@cliffy/command/completions"
import denoConfig from "../deno.json" with { type: "json" }
import { authCommand } from "./commands/auth/auth.ts"
import { issueCommand } from "./commands/issue/issue.ts"
import { teamCommand } from "./commands/team/team.ts"
import { projectCommand } from "./commands/project/project.ts"
import { projectUpdateCommand } from "./commands/project-update/project-update.ts"
import { cycleCommand } from "./commands/cycle/cycle.ts"
import { milestoneCommand } from "./commands/milestone/milestone.ts"
import { initiativeCommand } from "./commands/initiative/initiative.ts"
import { initiativeUpdateCommand } from "./commands/initiative-update/initiative-update.ts"
import { labelCommand } from "./commands/label/label.ts"
import { documentCommand } from "./commands/document/document.ts"
import { configCommand } from "./commands/config.ts"
import { schemaCommand } from "./commands/schema.ts"
import { apiCommand } from "./commands/api.ts"
import { setCliWorkspace } from "./config.ts"

// Import config and credentials setup
import "./config.ts"
import "./credentials.ts"

// The root command. Kept in this internal module (rather than the package entry
// point src/main.ts) so its complex inferred cliffy type stays out of the
// published public API and doesn't trip the no-slow-types check.
export const cli = new Command()
.name("linear")
.version(denoConfig.version)
.description(
`Handy linear commands from the command line.

Environment Variables:
LINEAR_DEBUG=1 Show full error details including stack traces`,
)
.globalOption(
"--workspace <slug:string>",
"Target workspace (uses credentials)",
)
.globalAction((options) => {
setCliWorkspace(options.workspace)
})
.action(() => {
console.log("Use --help to see available commands")
})
.command("auth", authCommand)
.command("issue", issueCommand)
.alias("i")
.command("team", teamCommand)
.alias("t")
.command("project", projectCommand)
.alias("p")
.command("project-update", projectUpdateCommand)
.alias("pu")
.command("cycle", cycleCommand)
.alias("cy")
.command("milestone", milestoneCommand)
.alias("m")
.command("initiative", initiativeCommand)
.alias("init")
.command("initiative-update", initiativeUpdateCommand)
.alias("iu")
.command("label", labelCommand)
.alias("l")
.command("document", documentCommand)
.command("completions", new CompletionsCommand())
.command("config", configCommand)
.alias("configure")
.command("schema", schemaCommand)
.command("api", apiCommand)
2 changes: 1 addition & 1 deletion src/commands/team/team-autolinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const autolinksCommand = new Command()
if (!teamId) {
throw new ValidationError(
"Could not determine team id from directory name",
{ suggestion: "Run `linear configure` to set a team." },
{ suggestion: "Run `linear config` to set a team." },
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/commands/team/team-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const idCommand = new Command()
} else {
throw new ValidationError(
"No team id configured",
{ suggestion: "Run `linear configure` to set a team." },
{ suggestion: "Run `linear config` to set a team." },
)
}
} catch (error) {
Expand Down
70 changes: 4 additions & 66 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,5 @@
import { Command } from "@cliffy/command"
import { CompletionsCommand } from "@cliffy/command/completions"
import denoConfig from "../deno.json" with { type: "json" }
import { authCommand } from "./commands/auth/auth.ts"
import { issueCommand } from "./commands/issue/issue.ts"
import { teamCommand } from "./commands/team/team.ts"
import { projectCommand } from "./commands/project/project.ts"
import { projectUpdateCommand } from "./commands/project-update/project-update.ts"
import { cycleCommand } from "./commands/cycle/cycle.ts"
import { milestoneCommand } from "./commands/milestone/milestone.ts"
import { initiativeCommand } from "./commands/initiative/initiative.ts"
import { initiativeUpdateCommand } from "./commands/initiative-update/initiative-update.ts"
import { labelCommand } from "./commands/label/label.ts"
import { documentCommand } from "./commands/document/document.ts"
import { configCommand } from "./commands/config.ts"
import { schemaCommand } from "./commands/schema.ts"
import { apiCommand } from "./commands/api.ts"
import { setCliWorkspace } from "./config.ts"
import { cli } from "./cli.ts"

// Import config and credentials setup
import "./config.ts"
import "./credentials.ts"

await new Command()
.name("linear")
.version(denoConfig.version)
.description(
`Handy linear commands from the command line.

Environment Variables:
LINEAR_DEBUG=1 Show full error details including stack traces`,
)
.globalOption(
"--workspace <slug:string>",
"Target workspace (uses credentials)",
)
.globalAction((options) => {
setCliWorkspace(options.workspace)
})
.action(() => {
console.log("Use --help to see available commands")
})
.command("auth", authCommand)
.command("issue", issueCommand)
.alias("i")
.command("team", teamCommand)
.alias("t")
.command("project", projectCommand)
.alias("p")
.command("project-update", projectUpdateCommand)
.alias("pu")
.command("cycle", cycleCommand)
.alias("cy")
.command("milestone", milestoneCommand)
.alias("m")
.command("initiative", initiativeCommand)
.alias("init")
.command("initiative-update", initiativeUpdateCommand)
.alias("iu")
.command("label", labelCommand)
.alias("l")
.command("document", documentCommand)
.command("completions", new CompletionsCommand())
.command("config", configCommand)
.command("schema", schemaCommand)
.command("api", apiCommand)
.parse(Deno.args)
if (import.meta.main) {
await cli.parse(Deno.args)
}
5 changes: 3 additions & 2 deletions src/utils/linear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ export async function getIssueIdentifier(
return normalizeIssueIdentifier(`${teamId}-${providedId}`)
}

throw new Error(
"an integer id was provided, but no team is set. run `linear configure`",
throw new ValidationError(
"an integer id was provided, but no team is set",
{ suggestion: "Run `linear config` to set a team." },
)
}

Expand Down
28 changes: 28 additions & 0 deletions test/commands/team/__snapshots__/team-autolinks.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const snapshot = {};

snapshot[`Team Autolinks Command - Help Text 1`] = `
stdout:
"
Usage: autolinks

Description:

Configure GitHub repository autolinks for Linear issues with this team prefix

Options:

-h, --help - Show this help.

"
stderr:
""
`;

snapshot[`Team Autolinks Command - No Team Configured 1`] = `
stdout:
""
stderr:
"✗ Failed to configure autolinks: Could not determine team id from directory name
Run \`linear config\` to set a team.
"
`;
28 changes: 28 additions & 0 deletions test/commands/team/__snapshots__/team-id.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const snapshot = {};

snapshot[`Team Id Command - Help Text 1`] = `
stdout:
"
Usage: id

Description:

Print the configured team id

Options:

-h, --help - Show this help.

"
stderr:
""
`;

snapshot[`Team Id Command - No Team Configured 1`] = `
stdout:
""
stderr:
"✗ Failed to get team id: No team id configured
Run \`linear config\` to set a team.
"
`;
39 changes: 39 additions & 0 deletions test/commands/team/team-autolinks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { snapshotTest as cliffySnapshotTest } from "@cliffy/testing"
import { autolinksCommand } from "../../../src/commands/team/team-autolinks.ts"

// Common Deno args for permissions
const denoArgs = ["--allow-all", "--quiet"]

// Test help output
await cliffySnapshotTest({
name: "Team Autolinks Command - Help Text",
meta: import.meta,
colors: false,
args: ["--help"],
denoArgs,
async fn() {
await autolinksCommand.parse()
},
})

// Regression test for #245: with no team configured, the suggestion must point
// at the real `linear config` command, not the non-existent `linear configure`.
await cliffySnapshotTest({
name: "Team Autolinks Command - No Team Configured",
meta: import.meta,
colors: false,
args: [],
denoArgs,
canFail: true,
async fn() {
// An empty team id is falsy, so getTeamKey() resolves to undefined even
// though the repo's .linear.toml sets one — this exercises the error path
// before any network call is attempted.
Deno.env.set("LINEAR_TEAM_ID", "")
try {
await autolinksCommand.parse()
} finally {
Deno.env.delete("LINEAR_TEAM_ID")
}
},
})
38 changes: 38 additions & 0 deletions test/commands/team/team-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { snapshotTest as cliffySnapshotTest } from "@cliffy/testing"
import { idCommand } from "../../../src/commands/team/team-id.ts"

// Common Deno args for permissions
const denoArgs = ["--allow-all", "--quiet"]

// Test help output
await cliffySnapshotTest({
name: "Team Id Command - Help Text",
meta: import.meta,
colors: false,
args: ["--help"],
denoArgs,
async fn() {
await idCommand.parse()
},
})

// Regression test for #245: with no team configured, the suggestion must point
// at the real `linear config` command, not the non-existent `linear configure`.
await cliffySnapshotTest({
name: "Team Id Command - No Team Configured",
meta: import.meta,
colors: false,
args: [],
denoArgs,
canFail: true,
async fn() {
// An empty team id is falsy, so getTeamKey() resolves to undefined even
// though the repo's .linear.toml sets one — this exercises the error path.
Deno.env.set("LINEAR_TEAM_ID", "")
try {
await idCommand.parse()
} finally {
Deno.env.delete("LINEAR_TEAM_ID")
}
},
})
11 changes: 10 additions & 1 deletion test/main_test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { assertStringIncludes } from "@std/assert"
import { assertEquals, assertStringIncludes } from "@std/assert"
import { getGraphQLClient } from "../src/utils/graphql.ts"
import { cli } from "../src/cli.ts"
import { configCommand } from "../src/commands/config.ts"

// Regression guard for #245: `configure` is a natural name users (and the
// CLI's own help text) reach for, so it resolves to the canonical `config`
// command instead of erroring with "Unknown command".
Deno.test("cli - `configure` is an alias for the config command", () => {
assertEquals(cli.getCommand("configure"), configCommand)
})

// Mock fetch function for testing
const originalFetch = globalThis.fetch
Expand Down
22 changes: 21 additions & 1 deletion test/utils/linear.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assertEquals, assertRejects } from "@std/assert"
import { assertEquals, assertRejects, assertStringIncludes } from "@std/assert"
import {
getIssueIdentifier,
isLinearUuid,
Expand All @@ -23,6 +23,26 @@ Deno.test("getIssueId - handles integer-only IDs with team prefix", async () =>
Deno.env.delete("LINEAR_TEAM_ID")
})

Deno.test("getIssueId - integer-only id without a team points at `linear config`", async () => {
// An empty team id is falsy, so getTeamKey() resolves to undefined even
// though the repo's .linear.toml sets one — this exercises the no-team branch.
Deno.env.set("LINEAR_TEAM_ID", "")

try {
const error = await assertRejects(
() => getIssueIdentifier("123"),
ValidationError,
"no team is set",
)
// Regression guard for #245: the suggestion must name the real command
// (`config`), never the non-existent `configure`.
assertStringIncludes(error.suggestion ?? "", "linear config")
assertEquals(error.suggestion?.includes("configure"), false)
} finally {
Deno.env.delete("LINEAR_TEAM_ID")
}
})

Deno.test("getIssueId - rejects invalid integer patterns", async () => {
Deno.env.set("LINEAR_TEAM_ID", "TEST")

Expand Down
Loading