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}