Skip to content

Commit 3fe2b15

Browse files
committed
Update to grab template when creating issue, auto assign to default user, option to open worktree in vscode
1 parent ad593d2 commit 3fe2b15

7 files changed

Lines changed: 214 additions & 135 deletions

File tree

src/commands/document/document-update.ts

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Command } from "@cliffy/command"
22
import { gql } from "../../__codegen__/gql.ts"
33
import { getGraphQLClient } from "../../utils/graphql.ts"
4-
import { getEditor } from "../../utils/editor.ts"
4+
import { openEditor } from "../../utils/editor.ts"
55
import { readIdsFromStdin } from "../../utils/bulk.ts"
66
import {
77
CliError,
@@ -10,66 +10,6 @@ import {
1010
ValidationError,
1111
} from "../../utils/errors.ts"
1212

13-
/**
14-
* Open editor with initial content and return the edited content
15-
*/
16-
async function openEditorWithContent(
17-
initialContent: string,
18-
): Promise<string | undefined> {
19-
const editor = await getEditor()
20-
if (!editor) {
21-
throw new ValidationError("No editor found", {
22-
suggestion:
23-
"Set EDITOR environment variable or configure git editor with: git config --global core.editor <editor>",
24-
})
25-
}
26-
27-
// Create a temporary file with initial content
28-
const tempFile = await Deno.makeTempFile({ suffix: ".md" })
29-
30-
try {
31-
// Write initial content to temp file
32-
await Deno.writeTextFile(tempFile, initialContent)
33-
34-
// Open the editor
35-
const process = new Deno.Command(editor, {
36-
args: [tempFile],
37-
stdin: "inherit",
38-
stdout: "inherit",
39-
stderr: "inherit",
40-
})
41-
42-
const { success } = await process.output()
43-
44-
if (!success) {
45-
throw new CliError("Editor exited with an error")
46-
}
47-
48-
// Read the content back
49-
const content = await Deno.readTextFile(tempFile)
50-
const cleaned = content.trim()
51-
52-
return cleaned.length > 0 ? cleaned : undefined
53-
} catch (error) {
54-
if (error instanceof CliError || error instanceof ValidationError) {
55-
throw error
56-
}
57-
throw new CliError(
58-
`Failed to open editor: ${
59-
error instanceof Error ? error.message : String(error)
60-
}`,
61-
{ cause: error },
62-
)
63-
} finally {
64-
// Clean up the temporary file
65-
try {
66-
await Deno.remove(tempFile)
67-
} catch {
68-
// Ignore cleanup errors
69-
}
70-
}
71-
}
72-
7313
/**
7414
* Read content from stdin if available (with timeout to avoid hanging)
7515
*/
@@ -173,7 +113,7 @@ export const updateCommand = new Command()
173113
const currentContent = documentData.document.content || ""
174114
console.log(`Opening ${documentData.document.title} in editor...`)
175115

176-
finalContent = await openEditorWithContent(currentContent)
116+
finalContent = await openEditor(currentContent)
177117

178118
if (finalContent === undefined) {
179119
console.log("No changes made, update cancelled.")

src/commands/issue/issue-create.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { Command } from "@cliffy/command"
22
import { Checkbox, Input, Select } from "@cliffy/prompt"
33
import { gql } from "../../__codegen__/gql.ts"
44
import { getGraphQLClient } from "../../utils/graphql.ts"
5+
import { getOption } from "../../config.ts"
56
import { getEditor, openEditor } from "../../utils/editor.ts"
67
import { getPriorityDisplay } from "../../utils/display.ts"
78
import {
89
fetchParentIssueData,
910
getAllTeams,
1011
getCycleIdByNameOrNumber,
12+
getDefaultIssueTemplateDescription,
1113
getIssueId,
1214
getIssueIdentifier,
1315
getIssueLabelIdByNameForTeam,
@@ -254,6 +256,9 @@ async function promptInteractiveIssueCreation(
254256
}> {
255257
// Start user settings and team resolution in background while asking for title
256258
const userSettingsPromise = (async () => {
259+
const configValue = getOption("auto_assign_to_self")
260+
if (configValue !== undefined) return configValue
261+
257262
const client = getGraphQLClient()
258263
const userSettingsQuery = gql(`
259264
query GetUserSettings {
@@ -334,6 +339,7 @@ async function promptInteractiveIssueCreation(
334339
// Preload team-scoped data (do not await yet)
335340
const workflowStatesPromise = getWorkflowStates(teamKey)
336341
const labelsPromise = getLabelsForTeam(teamKey)
342+
const templatePromise = getDefaultIssueTemplateDescription(teamId)
337343

338344
// Description prompt
339345
const editorName = await getEditor()
@@ -350,7 +356,8 @@ async function promptInteractiveIssueCreation(
350356
let finalDescription: string | undefined
351357
if (description === "e" && editorDisplayName) {
352358
console.log(`Opening ${editorDisplayName}...`)
353-
finalDescription = await openEditor()
359+
const templateDescription = await templatePromise
360+
finalDescription = await openEditor(templateDescription)
354361
if (finalDescription && finalDescription.length > 0) {
355362
console.log(
356363
`Description entered (${finalDescription.length} characters)`,

src/commands/issue/issue-start.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,16 @@ export const startCommand = new Command()
3939
"-W, --worktree",
4040
"Create a git worktree instead of switching branches",
4141
)
42+
.option(
43+
"-t, --team <team:string>",
44+
"Team key to use if only an issue number is provided, or to list unstarted issues from",
45+
)
4246
.action(
4347
async (
44-
{ allAssignees, unassigned, fromRef, branch, worktree },
48+
{ allAssignees, unassigned, fromRef, branch, worktree, team },
4549
issueId,
4650
) => {
4751
try {
48-
const teamId = getTeamKey()
49-
if (!teamId) {
50-
throw new ValidationError("Could not determine team ID")
51-
}
52-
5352
// Validate that conflicting flags are not used together
5453
if (allAssignees && unassigned) {
5554
throw new ValidationError(
@@ -59,10 +58,19 @@ export const startCommand = new Command()
5958

6059
// Only resolve the provided issueId, don't infer from VCS
6160
// (start should pick from a list, not continue on current issue)
62-
let resolvedId = issueId ? await getIssueIdentifier(issueId) : undefined
61+
let resolvedId = issueId
62+
? await getIssueIdentifier(issueId, team)
63+
: undefined
6364
if (!resolvedId) {
65+
const listTeamKey = team || getTeamKey()
66+
if (!listTeamKey) {
67+
throw new ValidationError(
68+
"Could not determine team ID. Pass --team or configure team_id.",
69+
)
70+
}
71+
6472
const result = await fetchIssuesForState(
65-
teamId,
73+
listTeamKey,
6674
["unstarted"],
6775
undefined,
6876
unassigned,
@@ -71,7 +79,7 @@ export const startCommand = new Command()
7179
const issues = result.issues?.nodes || []
7280

7381
if (issues.length === 0) {
74-
throw new NotFoundError("Unstarted issues", teamId)
82+
throw new NotFoundError("Unstarted issues", listTeamKey)
7583
}
7684

7785
const answer = await Select.prompt({
@@ -94,8 +102,9 @@ export const startCommand = new Command()
94102
throw new ValidationError("No issue ID resolved")
95103
}
96104

105+
const targetTeamKey = resolvedId.split("-")[0]
97106
const startMode = worktree ? "worktree" : getDefaultStartMode()
98-
await startIssue(resolvedId, teamId, fromRef, branch, startMode)
107+
await startIssue(resolvedId, targetTeamKey, fromRef, branch, startMode)
99108
await checkStaleWorktrees()
100109
} catch (error) {
101110
handleError(error, "Failed to start issue")

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ const OptionsSchema = v.object({
151151
hyperlink_format: v.optional(v.string()),
152152
attachment_dir: v.optional(v.string()),
153153
auto_download_attachments: v.optional(BooleanLike),
154+
auto_assign_to_self: v.optional(BooleanLike),
154155
})
155156

156157
export type Options = v.InferOutput<typeof OptionsSchema>

src/utils/editor.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ export async function getEditor(): Promise<string | null> {
2020
return null
2121
}
2222

23-
export async function openEditor(): Promise<string | undefined> {
23+
export async function openEditor(
24+
initialContent?: string,
25+
): Promise<string | undefined> {
2426
const editor = await getEditor()
2527
if (!editor) {
2628
console.error(
@@ -33,6 +35,11 @@ export async function openEditor(): Promise<string | undefined> {
3335
const tempFile = await Deno.makeTempFile({ suffix: ".md" })
3436

3537
try {
38+
// Pre-fill with initial content if provided
39+
if (initialContent != null) {
40+
await Deno.writeTextFile(tempFile, initialContent)
41+
}
42+
3643
// Open the editor
3744
const process = new Deno.Command(editor, {
3845
args: [tempFile],

src/utils/linear.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export function getTeamKey(): string | undefined {
6666
*/
6767
export async function getIssueIdentifier(
6868
providedId?: string,
69+
fallbackTeamKey?: string,
6970
): Promise<string | undefined> {
7071
if (providedId) {
7172
const normalizedIdentifier = normalizeIssueIdentifier(providedId)
@@ -75,13 +76,13 @@ export async function getIssueIdentifier(
7576
}
7677

7778
if (providedId && /^[1-9][0-9]*$/.test(providedId)) {
78-
const teamId = getTeamKey()
79+
const teamId = fallbackTeamKey || getTeamKey()
7980
if (teamId) {
8081
return normalizeIssueIdentifier(`${teamId}-${providedId}`)
8182
}
8283

8384
throw new Error(
84-
"an integer id was provided, but no team is set. run `linear configure`",
85+
"an integer id was provided, but no team is set. pass --team or run `linear configure`",
8586
)
8687
}
8788

@@ -1231,6 +1232,87 @@ export async function getTeamIdByKey(
12311232
return data.teams?.nodes[0]?.id
12321233
}
12331234

1235+
// deno-lint-ignore no-explicit-any
1236+
function prosemirrorToMarkdown(node: any): string {
1237+
if (!node) return ""
1238+
if (typeof node === "string") return node
1239+
1240+
if (node.type === "text") {
1241+
let text = node.text || ""
1242+
if (node.marks) {
1243+
for (const mark of node.marks) {
1244+
if (mark.type === "strong") text = `**${text}**`
1245+
else if (mark.type === "em") text = `*${text}*`
1246+
else if (mark.type === "code") text = `\`${text}\``
1247+
}
1248+
}
1249+
return text
1250+
}
1251+
1252+
if (Array.isArray(node.content)) {
1253+
const children = node.content.map(prosemirrorToMarkdown).join("")
1254+
1255+
switch (node.type) {
1256+
case "doc":
1257+
return children
1258+
case "paragraph":
1259+
return children + "\n\n"
1260+
case "heading": {
1261+
const level = node.attrs?.level || 1
1262+
return `${"#".repeat(level)} ${children}\n\n`
1263+
}
1264+
case "bulletList":
1265+
// deno-lint-ignore no-explicit-any
1266+
return node.content.map((item: any) =>
1267+
`- ${prosemirrorToMarkdown(item).trim()}`
1268+
).join("\n") + "\n\n"
1269+
case "orderedList":
1270+
// deno-lint-ignore no-explicit-any
1271+
return node.content.map((item: any, i: number) =>
1272+
`${i + 1}. ${prosemirrorToMarkdown(item).trim()}`
1273+
).join("\n") + "\n\n"
1274+
case "listItem":
1275+
return children
1276+
case "codeBlock":
1277+
return `\`\`\`\n${children}\n\`\`\`\n\n`
1278+
case "blockquote":
1279+
return `> ${children}\n\n`
1280+
default:
1281+
return children
1282+
}
1283+
}
1284+
1285+
return ""
1286+
}
1287+
1288+
export async function getDefaultIssueTemplateDescription(
1289+
teamId: string,
1290+
): Promise<string | undefined> {
1291+
const client = getGraphQLClient()
1292+
const query = gql(/* GraphQL */ `
1293+
query GetDefaultIssueTemplate($teamId: String!) {
1294+
team(id: $teamId) {
1295+
defaultTemplateForMembers {
1296+
templateData
1297+
}
1298+
}
1299+
}
1300+
`)
1301+
const data = await client.request(query, { teamId })
1302+
const templateData = data.team?.defaultTemplateForMembers?.templateData
1303+
if (templateData == null) return undefined
1304+
1305+
// templateData is a JSON object; the descriptionData field contains the ProseMirror document
1306+
const parsed = typeof templateData === "string"
1307+
? JSON.parse(templateData)
1308+
: templateData
1309+
1310+
if (!parsed?.descriptionData) return undefined
1311+
1312+
const description = prosemirrorToMarkdown(parsed.descriptionData)
1313+
return description.trim().length > 0 ? description.trim() : undefined
1314+
}
1315+
12341316
export async function searchTeamsByKeySubstring(
12351317
keySubstring: string,
12361318
): Promise<Record<string, string>> {

0 commit comments

Comments
 (0)