main
1/**
2 * Attachment-related action handlers for Jira extension
3 */
4
5import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
6import type { JiraDetails } from "./types";
7import { getErrorMessage, serializedConfirm } from "./utils";
8
9/**
10 * Attach file to issue
11 */
12export async function handleAttach(
13 pi: ExtensionAPI,
14 params: any,
15 signal: AbortSignal | undefined,
16 onUpdate: any,
17 ctx: ExtensionContext,
18): Promise<any> {
19 if (!params.key) {
20 return {
21 content: [{ type: "text", text: "Error: 'key' parameter is required for attach action" }],
22 details: { action: "attach", error: "missing_key" } as JiraDetails,
23 isError: true,
24 };
25 }
26
27 if (!params.file) {
28 return {
29 content: [{ type: "text", text: "Error: 'file' parameter is required for attach action" }],
30 details: { action: "attach", error: "missing_file" } as JiraDetails,
31 isError: true,
32 };
33 }
34
35 // Check if file exists
36 const checkResult = await pi.exec("test", ["-f", params.file], { signal });
37 if (checkResult.code !== 0) {
38 return {
39 content: [{ type: "text", text: `Error: File not found: ${params.file}` }],
40 details: { action: "attach", error: "file_not_found" } as JiraDetails,
41 isError: true,
42 };
43 }
44
45 // APPROVAL GATE
46 if (ctx.hasUI) {
47 const confirmMessage = `Issue: ${params.key}\nFile: ${params.file}\n\nThis will upload the file as an attachment.`;
48
49 const confirmed = await serializedConfirm(ctx, "Attach file?", confirmMessage);
50
51 if (!confirmed) {
52 ctx.ui.notify("Attach cancelled", "info");
53 return {
54 content: [{ type: "text", text: "Attach operation cancelled by user" }],
55 details: { action: "attach", cancelled: true, issueKey: params.key } as JiraDetails,
56 };
57 }
58 }
59
60 onUpdate?.({ content: [{ type: "text", text: `Attaching ${params.file} to ${params.key}...` }] });
61
62 // Attach file using jira CLI
63 const result = await pi.exec("jira", ["issue", "attach", params.key, params.file], { signal, timeout: 60000 });
64
65 if (result.code !== 0) {
66 return {
67 content: [{ type: "text", text: getErrorMessage(result.stderr, "Attach file") }],
68 details: { action: "attach", error: result.stderr, issueKey: params.key } as JiraDetails,
69 isError: true,
70 };
71 }
72
73 return {
74 content: [{ type: "text", text: `Attached ${params.file} to ${params.key}` }],
75 details: { action: "attach", output: result.stdout, issueKey: params.key } as JiraDetails,
76 };
77}
78
79/**
80 * List attachments for an issue
81 */
82export async function handleListAttachments(
83 pi: ExtensionAPI,
84 params: any,
85 signal: AbortSignal | undefined,
86 onUpdate: any,
87 ctx: ExtensionContext,
88): Promise<any> {
89 if (!params.key) {
90 return {
91 content: [{ type: "text", text: "Error: 'key' parameter is required for list-attachments action" }],
92 details: { action: "list-attachments", error: "missing_key" } as JiraDetails,
93 isError: true,
94 };
95 }
96
97 onUpdate?.({ content: [{ type: "text", text: `Fetching attachments for ${params.key}...` }] });
98
99 // View issue to get attachments
100 const result = await pi.exec("jira", ["issue", "view", params.key, "--plain"], { signal, timeout: 20000 });
101
102 if (result.code !== 0) {
103 return {
104 content: [{ type: "text", text: getErrorMessage(result.stderr, "List attachments") }],
105 details: { action: "list-attachments", error: result.stderr, issueKey: params.key } as JiraDetails,
106 isError: true,
107 };
108 }
109
110 // Parse output to find attachments section
111 // The jira CLI view output typically includes an "Attachments:" section
112 const lines = result.stdout.split("\n");
113 let inAttachments = false;
114 const attachments: string[] = [];
115
116 for (const line of lines) {
117 if (line.match(/^Attachments?:/i)) {
118 inAttachments = true;
119 continue;
120 }
121
122 if (inAttachments) {
123 // Stop at next section or empty line
124 if (line.match(/^[A-Z][a-z]+:/) || line.trim() === "") {
125 break;
126 }
127 if (line.trim()) {
128 attachments.push(line.trim());
129 }
130 }
131 }
132
133 const output =
134 attachments.length > 0
135 ? `Attachments for ${params.key}:\n\n` + attachments.join("\n")
136 : `No attachments found for ${params.key}`;
137
138 return {
139 content: [{ type: "text", text: output }],
140 details: { action: "list-attachments", output, issueKey: params.key } as JiraDetails,
141 };
142}