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
81 changes: 43 additions & 38 deletions src/commands/auth/auth-default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,54 @@ import {
hasWorkspace,
setDefaultWorkspace,
} from "../../credentials.ts"
import { AuthError, handleError, NotFoundError } from "../../utils/errors.ts"

export const defaultCommand = new Command()
.name("default")
.description("Set the default workspace")
.arguments("[workspace:string]")
.action(async (_options, workspace?: string) => {
const workspaces = getWorkspaces()

if (workspaces.length === 0) {
console.error("No workspaces configured")
console.error("Run `linear auth login` to add a workspace")
Deno.exit(1)
}

if (workspaces.length === 1) {
console.log(`Only one workspace configured: ${workspaces[0]}`)
return
}

const currentDefault = getDefaultWorkspace()

// If no workspace specified, prompt to select one
if (!workspace) {
workspace = await Select.prompt({
message: "Select default workspace",
options: workspaces.map((ws) => ({
name: ws === currentDefault ? `${ws} (current)` : ws,
value: ws,
})),
})
try {
const workspaces = getWorkspaces()

if (workspaces.length === 0) {
throw new AuthError("No workspaces configured", {
suggestion: "Run `linear auth login` to add a workspace",
})
}

if (workspaces.length === 1) {
console.log(`Only one workspace configured: ${workspaces[0]}`)
return
}

const currentDefault = getDefaultWorkspace()

// If no workspace specified, prompt to select one
if (!workspace) {
workspace = await Select.prompt({
message: "Select default workspace",
options: workspaces.map((ws) => ({
name: ws === currentDefault ? `${ws} (current)` : ws,
value: ws,
})),
})
}

if (!hasWorkspace(workspace)) {
throw new NotFoundError("Workspace", workspace, {
suggestion: `Available workspaces: ${workspaces.join(", ")}`,
})
}

if (workspace === currentDefault) {
console.log(`"${workspace}" is already the default workspace`)
return
}

await setDefaultWorkspace(workspace)
console.log(`Default workspace set to: ${workspace}`)
} catch (error) {
handleError(error, "Failed to set default workspace")
}

if (!hasWorkspace(workspace)) {
console.error(`Workspace "${workspace}" not found`)
console.error(`Available workspaces: ${workspaces.join(", ")}`)
Deno.exit(1)
}

if (workspace === currentDefault) {
console.log(`"${workspace}" is already the default workspace`)
return
}

await setDefaultWorkspace(workspace)
console.log(`Default workspace set to: ${workspace}`)
})
79 changes: 42 additions & 37 deletions src/commands/auth/auth-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getWorkspaces,
} from "../../credentials.ts"
import { padDisplay } from "../../utils/display.ts"
import { handleError } from "../../utils/errors.ts"
import { createGraphQLClient } from "../../utils/graphql.ts"

const viewerQuery = gql(`
Expand Down Expand Up @@ -60,49 +61,53 @@ export const listCommand = new Command()
.name("list")
.description("List configured workspaces")
.action(async () => {
const workspaces = getWorkspaces()
try {
const workspaces = getWorkspaces()

if (workspaces.length === 0) {
console.log("No workspaces configured")
console.log("Run `linear auth login` to add a workspace")
return
}
if (workspaces.length === 0) {
console.log("No workspaces configured")
console.log("Run `linear auth login` to add a workspace")
return
}

const credentials = getAllCredentials()
const credentials = getAllCredentials()

// Fetch info for all workspaces in parallel
const infoPromises = workspaces.map((ws) =>
fetchWorkspaceInfo(ws, credentials[ws]!)
)
const infos = await Promise.all(infoPromises)
// Fetch info for all workspaces in parallel
const infoPromises = workspaces.map((ws) =>
fetchWorkspaceInfo(ws, credentials[ws]!)
)
const infos = await Promise.all(infoPromises)

// Calculate column widths
const workspaceWidth = Math.max(
9, // "WORKSPACE" header
...infos.map((i) => unicodeWidth(i.workspace)),
)
const orgWidth = Math.max(
8, // "ORG NAME" header
...infos.map((i) => unicodeWidth(i.orgName ?? i.error ?? "")),
)
// Calculate column widths
const workspaceWidth = Math.max(
9, // "WORKSPACE" header
...infos.map((i) => unicodeWidth(i.workspace)),
)
const orgWidth = Math.max(
8, // "ORG NAME" header
...infos.map((i) => unicodeWidth(i.orgName ?? i.error ?? "")),
)

// Print header
const header = ` ${padDisplay("WORKSPACE", workspaceWidth)} ${
padDisplay("ORG NAME", orgWidth)
} USER`
console.log(`%c${header}`, "text-decoration: underline")
// Print header
const header = ` ${padDisplay("WORKSPACE", workspaceWidth)} ${
padDisplay("ORG NAME", orgWidth)
} USER`
console.log(`%c${header}`, "text-decoration: underline")

// Print each workspace
for (const info of infos) {
const prefix = info.isDefault ? "* " : " "
const ws = padDisplay(info.workspace, workspaceWidth)
if (info.error) {
const org = padDisplay(info.error, orgWidth)
console.log(`${prefix}${ws} %c${org}%c`, "color: red", "")
} else {
const org = padDisplay(info.orgName ?? "", orgWidth)
const user = `${info.userName} <${info.email}>`
console.log(`${prefix}${ws} ${org} ${user}`)
// Print each workspace
for (const info of infos) {
const prefix = info.isDefault ? "* " : " "
const ws = padDisplay(info.workspace, workspaceWidth)
if (info.error) {
const org = padDisplay(info.error, orgWidth)
console.log(`${prefix}${ws} %c${org}%c`, "color: red", "")
} else {
const org = padDisplay(info.orgName ?? "", orgWidth)
const user = `${info.userName} <${info.email}>`
console.log(`${prefix}${ws} ${org} ${user}`)
}
}
} catch (error) {
handleError(error, "Failed to list workspaces")
}
})
116 changes: 65 additions & 51 deletions src/commands/auth/auth-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import {
getWorkspaces,
hasWorkspace,
} from "../../credentials.ts"
import {
AuthError,
CliError,
handleError,
ValidationError,
} from "../../utils/errors.ts"
import { createGraphQLClient } from "../../utils/graphql.ts"

const viewerQuery = gql(`
Expand All @@ -27,68 +33,76 @@ export const loginCommand = new Command()
.description("Add a workspace credential")
.option("-k, --key <key:string>", "API key (prompted if not provided)")
.action(async (options) => {
let apiKey = options.key
try {
let apiKey = options.key

if (!apiKey) {
apiKey = await Secret.prompt({
message: "Enter your Linear API key",
hint: "Create one at https://linear.app/settings/api",
})
}
if (!apiKey) {
apiKey = await Secret.prompt({
message: "Enter your Linear API key",
hint: "Create one at https://linear.app/settings/api",
})
}

if (!apiKey) {
console.error("No API key provided")
Deno.exit(1)
}
if (!apiKey) {
throw new ValidationError("No API key provided", {
suggestion: "Create one at https://linear.app/settings/api",
})
}

// Validate the API key by querying the API
const client = createGraphQLClient(apiKey)
// Validate the API key by querying the API
const client = createGraphQLClient(apiKey)

try {
const result = await client.request(viewerQuery)
const viewer = result.viewer
const org = viewer.organization
const workspace = org.urlKey
try {
const result = await client.request(viewerQuery)
const viewer = result.viewer
const org = viewer.organization
const workspace = org.urlKey

const alreadyExists = hasWorkspace(workspace)
await addCredential(workspace, apiKey)
const alreadyExists = hasWorkspace(workspace)
await addCredential(workspace, apiKey)

const existingCount = getWorkspaces().length
const existingCount = getWorkspaces().length

if (alreadyExists) {
console.log(
`Updated credentials for workspace: ${org.name} (${workspace})`,
)
} else {
console.log(`Logged in to workspace: ${org.name} (${workspace})`)
}
console.log(` User: ${viewer.name} <${viewer.email}>`)
if (alreadyExists) {
console.log(
`Updated credentials for workspace: ${org.name} (${workspace})`,
)
} else {
console.log(`Logged in to workspace: ${org.name} (${workspace})`)
}
console.log(` User: ${viewer.name} <${viewer.email}>`)

if (existingCount === 1) {
console.log(` Set as default workspace`)
}
if (existingCount === 1) {
console.log(` Set as default workspace`)
}

// Warn if LINEAR_API_KEY is set
if (Deno.env.get("LINEAR_API_KEY")) {
console.log()
console.log(
yellow("Warning: LINEAR_API_KEY environment variable is set."),
)
console.log(yellow("It takes precedence over stored credentials."))
console.log(
yellow(
"Remove it from your shell config to use multi-workspace auth.",
),
// Warn if LINEAR_API_KEY is set
if (Deno.env.get("LINEAR_API_KEY")) {
console.log()
console.log(
yellow("Warning: LINEAR_API_KEY environment variable is set."),
)
console.log(yellow("It takes precedence over stored credentials."))
console.log(
yellow(
"Remove it from your shell config to use multi-workspace auth.",
),
)
}
} catch (error) {
if (error instanceof Error && error.message.includes("401")) {
throw new AuthError("Invalid API key", {
suggestion: "Check that your API key is correct and not expired.",
})
}
throw new CliError(
`Failed to authenticate: ${
error instanceof Error ? error.message : String(error)
}`,
{ cause: error },
)
}
} catch (error) {
if (error instanceof Error && error.message.includes("401")) {
console.error("Invalid API key")
} else if (error instanceof Error) {
console.error(`Failed to authenticate: ${error.message}`)
} else {
console.error("Failed to authenticate")
}
Deno.exit(1)
handleError(error, "Failed to login")
}
})
Loading