Skip to content
Open
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
37 changes: 28 additions & 9 deletions src/commands/project/project-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export const createCommand = new Command()
"-i, --interactive",
"Interactive mode (default if no flags provided)",
)
.option("-j, --json", "Output created project as JSON")
.action(
async (options) => {
const {
Expand All @@ -152,6 +153,7 @@ export const createCommand = new Command()
targetDate: providedTargetDate,
initiative: providedInitiative,
interactive: interactiveFlag,
json: jsonOutput,
} = options

const client = getGraphQLClient()
Expand Down Expand Up @@ -374,13 +376,7 @@ export const createCommand = new Command()
throw new CliError("Failed to create project: no project returned")
}

console.log(`✓ Created project: ${project.name}`)
console.log(` Slug: ${project.slugId}`)
if (project.url) {
console.log(` URL: ${project.url}`)
}

// Add to initiative if specified
// Add to initiative if specified (before JSON output so warnings go to stderr)
if (initiative) {
const initiativeId = await resolveInitiativeId(client, initiative)
if (!initiativeId) {
Expand All @@ -395,9 +391,11 @@ export const createCommand = new Command()
},
})

if (linkResult.initiativeToProjectCreate.success) {
if (linkResult.initiativeToProjectCreate.success && !jsonOutput) {
console.log(`✓ Added to initiative: ${initiative}`)
} else {
} else if (
!linkResult.initiativeToProjectCreate.success
) {
console.error(`\nWarning: Failed to add project to initiative`)
}
} catch (error) {
Expand All @@ -408,6 +406,27 @@ export const createCommand = new Command()
}
}
}

if (jsonOutput) {
console.log(
JSON.stringify(
{
id: project.id,
slugId: project.slugId,
name: project.name,
url: project.url,
},
null,
2,
),
)
} else {
console.log(`✓ Created project: ${project.name}`)
console.log(` Slug: ${project.slugId}`)
if (project.url) {
console.log(` URL: ${project.url}`)
}
}
} catch (error) {
spinner?.stop()
handleError(error, "Failed to create project")
Expand Down
69 changes: 69 additions & 0 deletions src/commands/project/project-delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Command } from "@cliffy/command"
import { Confirm } from "@cliffy/prompt"
import { gql } from "../../__codegen__/gql.ts"
import { getGraphQLClient } from "../../utils/graphql.ts"
import { resolveProjectId } from "../../utils/linear.ts"
import { shouldShowSpinner } from "../../utils/hyperlink.ts"
import { CliError, handleError, ValidationError } from "../../utils/errors.ts"

const DeleteProject = gql(`
mutation DeleteProject($id: String!) {
projectDelete(id: $id) {
success
entity {
id
name
}
}
}
`)

export const deleteCommand = new Command()
.name("delete")
.description("Delete (trash) a Linear project")
.arguments("<projectId:string>")
.option("-f, --force", "Skip confirmation prompt")
.action(async ({ force }, projectId) => {
if (!force) {
if (!Deno.stdin.isTerminal()) {
throw new ValidationError("Interactive confirmation required", {
suggestion: "Use --force to skip confirmation.",
})
}
const confirmed = await Confirm.prompt({
message: `Are you sure you want to delete project ${projectId}?`,
default: false,
})

if (!confirmed) {
console.log("Deletion canceled")
return
}
}

const { Spinner } = await import("@std/cli/unstable-spinner")
const showSpinner = shouldShowSpinner()
const spinner = showSpinner ? new Spinner() : null
spinner?.start()

try {
const client = getGraphQLClient()
const resolvedId = await resolveProjectId(projectId)

const result = await client.request(DeleteProject, {
id: resolvedId,
})
spinner?.stop()

if (!result.projectDelete.success) {
throw new CliError("Failed to delete project")
}

const entity = result.projectDelete.entity
const displayName = entity?.name ?? projectId
console.log(`✓ Deleted project: ${displayName}`)
} catch (error) {
spinner?.stop()
handleError(error, "Failed to delete project")
}
})
43 changes: 40 additions & 3 deletions src/commands/project/project-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ export const listCommand = new Command()
.option("--status <status:string>", "Filter by status name")
.option("-w, --web", "Open in web browser")
.option("-a, --app", "Open in Linear.app")
.action(async ({ team, allTeams, status, web, app }) => {
.option("-j, --json", "Output as JSON")
.action(async ({ team, allTeams, status, web, app, json }) => {
if (web || app) {
let workspace = getOption("workspace")
if (!workspace) {
Expand Down Expand Up @@ -96,7 +97,7 @@ export const listCommand = new Command()
return
}
const { Spinner } = await import("@std/cli/unstable-spinner")
const showSpinner = shouldShowSpinner()
const showSpinner = shouldShowSpinner() && !json
const spinner = showSpinner ? new Spinner() : null
spinner?.start()

Expand Down Expand Up @@ -149,7 +150,11 @@ export const listCommand = new Command()
let projects: Project[] = allProjects

if (projects.length === 0) {
console.log("No projects found.")
if (json) {
console.log("[]")
} else {
console.log("No projects found.")
}
return
}

Expand Down Expand Up @@ -178,6 +183,38 @@ export const listCommand = new Command()
return a.name.localeCompare(b.name)
})

// JSON output
if (json) {
const jsonOutput = projects.map((project) => ({
id: project.id,
slugId: project.slugId,
name: project.name,
description: project.description,
status: {
id: project.status.id,
name: project.status.name,
type: project.status.type,
},
lead: project.lead
? {
name: project.lead.name,
displayName: project.lead.displayName,
initials: project.lead.initials,
}
: null,
teams: project.teams.nodes.map((t) => t.key),
priority: project.priority,
health: project.health,
startDate: project.startDate,
targetDate: project.targetDate,
url: project.url,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
}))
console.log(JSON.stringify(jsonOutput, null, 2))
return
}

// Helper function to get the most relevant date to display
const getDisplayDate = (
project: GetProjectsQuery["projects"]["nodes"][0],
Expand Down
Loading
Loading