Skip to content

Commit 69e59ee

Browse files
authored
feat(project): add update, delete, and --json support (#127) (#148)
- Add 'project update' subcommand with --name, --description, --status, --lead, --start-date, --target-date, --team options - Add 'project delete' subcommand with --force to skip confirmation - Add --json to 'project list' for machine-readable output with UUIDs - Add --json to 'project create' to return project id/slugId/name/url Enables automation use cases: create, rename, delete, list project IDs programmatically without raw GraphQL API calls.
1 parent ab3f83b commit 69e59ee

14 files changed

Lines changed: 968 additions & 17 deletions

skills/linear-cli/references/project.md

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ Options:
1919
2020
Commands:
2121
22-
list - List projects
23-
view, v <projectId> - View project details
24-
create - Create a new Linear project
22+
list - List projects
23+
view, v <projectId> - View project details
24+
create - Create a new Linear project
25+
update <projectId> - Update a Linear project
26+
delete <projectId> - Delete (trash) a Linear project
2527
```
2628

2729
## Subcommands
@@ -46,7 +48,8 @@ Options:
4648
--all-teams - Show projects from all teams
4749
--status <status> - Filter by status name
4850
-w, --web - Open in web browser
49-
-a, --app - Open in Linear.app
51+
-a, --app - Open in Linear.app
52+
-j, --json - Output as JSON
5053
```
5154

5255
### view
@@ -93,5 +96,50 @@ Options:
9396
--start-date <startDate> - Start date (YYYY-MM-DD)
9497
--target-date <targetDate> - Target completion date (YYYY-MM-DD)
9598
--initiative <initiative> - Add to initiative immediately (ID, slug, or name)
96-
-i, --interactive - Interactive mode (default if no flags provided)
99+
-i, --interactive - Interactive mode (default if no flags provided)
100+
-j, --json - Output created project as JSON
101+
```
102+
103+
### update
104+
105+
> Update a Linear project
106+
107+
```
108+
Usage: linear project update <projectId>
109+
Version: 1.10.0
110+
111+
Description:
112+
113+
Update a Linear project
114+
115+
Options:
116+
117+
-h, --help - Show this help.
118+
-w, --workspace <slug> - Target workspace (uses credentials)
119+
-n, --name <name> - Project name
120+
-d, --description <description> - Project description
121+
-s, --status <status> - Status (planned, started, paused, completed, canceled, backlog)
122+
-l, --lead <lead> - Project lead (username, email, or @me)
123+
--start-date <startDate> - Start date (YYYY-MM-DD)
124+
--target-date <targetDate> - Target date (YYYY-MM-DD)
125+
-t, --team <team> - Team key (can be repeated for multiple teams)
126+
```
127+
128+
### delete
129+
130+
> Delete (trash) a Linear project
131+
132+
```
133+
Usage: linear project delete <projectId>
134+
Version: 1.10.0
135+
136+
Description:
137+
138+
Delete (trash) a Linear project
139+
140+
Options:
141+
142+
-h, --help - Show this help.
143+
-w, --workspace <slug> - Target workspace (uses credentials)
144+
-f, --force - Skip confirmation prompt
97145
```

src/commands/project/project-create.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export const createCommand = new Command()
140140
"-i, --interactive",
141141
"Interactive mode (default if no flags provided)",
142142
)
143+
.option("-j, --json", "Output created project as JSON")
143144
.action(
144145
async (options) => {
145146
const {
@@ -152,6 +153,7 @@ export const createCommand = new Command()
152153
targetDate: providedTargetDate,
153154
initiative: providedInitiative,
154155
interactive: interactiveFlag,
156+
json: jsonOutput,
155157
} = options
156158

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

377-
console.log(`✓ Created project: ${project.name}`)
378-
console.log(` Slug: ${project.slugId}`)
379-
if (project.url) {
380-
console.log(` URL: ${project.url}`)
381-
}
382-
383-
// Add to initiative if specified
379+
// Add to initiative if specified (before JSON output so warnings go to stderr)
384380
if (initiative) {
385381
const initiativeId = await resolveInitiativeId(client, initiative)
386382
if (!initiativeId) {
@@ -395,9 +391,11 @@ export const createCommand = new Command()
395391
},
396392
})
397393

398-
if (linkResult.initiativeToProjectCreate.success) {
394+
if (linkResult.initiativeToProjectCreate.success && !jsonOutput) {
399395
console.log(`✓ Added to initiative: ${initiative}`)
400-
} else {
396+
} else if (
397+
!linkResult.initiativeToProjectCreate.success
398+
) {
401399
console.error(`\nWarning: Failed to add project to initiative`)
402400
}
403401
} catch (error) {
@@ -408,6 +406,27 @@ export const createCommand = new Command()
408406
}
409407
}
410408
}
409+
410+
if (jsonOutput) {
411+
console.log(
412+
JSON.stringify(
413+
{
414+
id: project.id,
415+
slugId: project.slugId,
416+
name: project.name,
417+
url: project.url,
418+
},
419+
null,
420+
2,
421+
),
422+
)
423+
} else {
424+
console.log(`✓ Created project: ${project.name}`)
425+
console.log(` Slug: ${project.slugId}`)
426+
if (project.url) {
427+
console.log(` URL: ${project.url}`)
428+
}
429+
}
411430
} catch (error) {
412431
spinner?.stop()
413432
handleError(error, "Failed to create project")
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Command } from "@cliffy/command"
2+
import { Confirm } from "@cliffy/prompt"
3+
import { gql } from "../../__codegen__/gql.ts"
4+
import { getGraphQLClient } from "../../utils/graphql.ts"
5+
import { resolveProjectId } from "../../utils/linear.ts"
6+
import { shouldShowSpinner } from "../../utils/hyperlink.ts"
7+
import { CliError, handleError, ValidationError } from "../../utils/errors.ts"
8+
9+
const DeleteProject = gql(`
10+
mutation DeleteProject($id: String!) {
11+
projectDelete(id: $id) {
12+
success
13+
entity {
14+
id
15+
name
16+
}
17+
}
18+
}
19+
`)
20+
21+
export const deleteCommand = new Command()
22+
.name("delete")
23+
.description("Delete (trash) a Linear project")
24+
.arguments("<projectId:string>")
25+
.option("-f, --force", "Skip confirmation prompt")
26+
.action(async ({ force }, projectId) => {
27+
if (!force) {
28+
if (!Deno.stdin.isTerminal()) {
29+
throw new ValidationError("Interactive confirmation required", {
30+
suggestion: "Use --force to skip confirmation.",
31+
})
32+
}
33+
const confirmed = await Confirm.prompt({
34+
message: `Are you sure you want to delete project ${projectId}?`,
35+
default: false,
36+
})
37+
38+
if (!confirmed) {
39+
console.log("Deletion canceled")
40+
return
41+
}
42+
}
43+
44+
const { Spinner } = await import("@std/cli/unstable-spinner")
45+
const showSpinner = shouldShowSpinner()
46+
const spinner = showSpinner ? new Spinner() : null
47+
spinner?.start()
48+
49+
try {
50+
const client = getGraphQLClient()
51+
const resolvedId = await resolveProjectId(projectId)
52+
53+
const result = await client.request(DeleteProject, {
54+
id: resolvedId,
55+
})
56+
spinner?.stop()
57+
58+
if (!result.projectDelete.success) {
59+
throw new CliError("Failed to delete project")
60+
}
61+
62+
const entity = result.projectDelete.entity
63+
const displayName = entity?.name ?? projectId
64+
console.log(`✓ Deleted project: ${displayName}`)
65+
} catch (error) {
66+
spinner?.stop()
67+
handleError(error, "Failed to delete project")
68+
}
69+
})

src/commands/project/project-list.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ export const listCommand = new Command()
6767
.option("--status <status:string>", "Filter by status name")
6868
.option("-w, --web", "Open in web browser")
6969
.option("-a, --app", "Open in Linear.app")
70-
.action(async ({ team, allTeams, status, web, app }) => {
70+
.option("-j, --json", "Output as JSON")
71+
.action(async ({ team, allTeams, status, web, app, json }) => {
7172
if (web || app) {
7273
let workspace = getOption("workspace")
7374
if (!workspace) {
@@ -97,7 +98,7 @@ export const listCommand = new Command()
9798
return
9899
}
99100
const { Spinner } = await import("@std/cli/unstable-spinner")
100-
const showSpinner = shouldShowSpinner()
101+
const showSpinner = shouldShowSpinner() && !json
101102
const spinner = showSpinner ? new Spinner() : null
102103
spinner?.start()
103104

@@ -150,7 +151,11 @@ export const listCommand = new Command()
150151
let projects: Project[] = allProjects
151152

152153
if (projects.length === 0) {
153-
console.log("No projects found.")
154+
if (json) {
155+
console.log("[]")
156+
} else {
157+
console.log("No projects found.")
158+
}
154159
return
155160
}
156161

@@ -179,6 +184,38 @@ export const listCommand = new Command()
179184
return a.name.localeCompare(b.name)
180185
})
181186

187+
// JSON output
188+
if (json) {
189+
const jsonOutput = projects.map((project) => ({
190+
id: project.id,
191+
slugId: project.slugId,
192+
name: project.name,
193+
description: project.description,
194+
status: {
195+
id: project.status.id,
196+
name: project.status.name,
197+
type: project.status.type,
198+
},
199+
lead: project.lead
200+
? {
201+
name: project.lead.name,
202+
displayName: project.lead.displayName,
203+
initials: project.lead.initials,
204+
}
205+
: null,
206+
teams: project.teams.nodes.map((t) => t.key),
207+
priority: project.priority,
208+
health: project.health,
209+
startDate: project.startDate,
210+
targetDate: project.targetDate,
211+
url: project.url,
212+
createdAt: project.createdAt,
213+
updatedAt: project.updatedAt,
214+
}))
215+
console.log(JSON.stringify(jsonOutput, null, 2))
216+
return
217+
}
218+
182219
// Helper function to get the most relevant date to display
183220
const getDisplayDate = (
184221
project: GetProjectsQuery["projects"]["nodes"][0],

0 commit comments

Comments
 (0)