@greeana/jira-dev-workflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/bin/jira-dev-workflow.mjs +223 -0
- package/examples/.jira-dev-workflow/.env.example +3 -0
- package/examples/.jira-dev-workflow/config.json +9 -0
- package/examples/.jira-dev-workflow/import.csv +3 -0
- package/package.json +53 -0
- package/skill/jira-dev-workflow/SKILL.md +113 -0
- package/src/add-comment.ts +177 -0
- package/src/attach-file.ts +86 -0
- package/src/comment-adf.ts +151 -0
- package/src/commit-message.ts +10 -0
- package/src/commit-with-issue.ts +90 -0
- package/src/config.ts +184 -0
- package/src/create-issue.ts +191 -0
- package/src/csv.ts +273 -0
- package/src/find-issue.ts +172 -0
- package/src/git.ts +59 -0
- package/src/image-dimensions.ts +137 -0
- package/src/import-issues.ts +196 -0
- package/src/install.ts +323 -0
- package/src/issue-search.ts +129 -0
- package/src/jira-client.ts +325 -0
- package/src/prepare-commit-draft.ts +164 -0
- package/src/prepare-commit.ts +197 -0
- package/src/show-current-issue.ts +66 -0
- package/src/state.ts +36 -0
- package/src/test-connection.ts +107 -0
- package/src/use-issue.ts +84 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { textToAdf } from "./csv";
|
|
4
|
+
import { loadToolEnv, loadWorkflowConfig, readEnv } from "./config";
|
|
5
|
+
import { createJiraClient } from "./jira-client";
|
|
6
|
+
import { saveCurrentIssueState } from "./state";
|
|
7
|
+
|
|
8
|
+
type CliOptions = {
|
|
9
|
+
configPath?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
descriptionFilePath?: string;
|
|
12
|
+
envFilePath?: string;
|
|
13
|
+
issueTypeOverride?: string;
|
|
14
|
+
json: boolean;
|
|
15
|
+
labels: string[];
|
|
16
|
+
projectKeyOverride?: string;
|
|
17
|
+
relatedIssues: string[];
|
|
18
|
+
save: boolean;
|
|
19
|
+
summary?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const options = parseArgs(process.argv.slice(2));
|
|
24
|
+
await loadToolEnv(options.envFilePath);
|
|
25
|
+
const config = await loadWorkflowConfig(options.configPath);
|
|
26
|
+
const jira = createJiraClient();
|
|
27
|
+
|
|
28
|
+
const projectKey = options.projectKeyOverride ?? config.projectKey ?? readEnv("JIRA_PROJECT_KEY");
|
|
29
|
+
const issueType = options.issueTypeOverride ?? config.defaultIssueType;
|
|
30
|
+
const summary = options.summary?.trim();
|
|
31
|
+
const descriptionText = await resolveDescription(options);
|
|
32
|
+
|
|
33
|
+
if (!projectKey) {
|
|
34
|
+
throw new Error("Set projectKey in .jira-dev-workflow/config.json or pass --project-key.");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!summary) {
|
|
38
|
+
throw new Error("Provide --summary.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const createdIssue = await jira.createIssue({
|
|
42
|
+
description: textToAdf(descriptionText),
|
|
43
|
+
issueType,
|
|
44
|
+
labels: options.labels,
|
|
45
|
+
projectKey,
|
|
46
|
+
summary,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
for (const relatedIssue of options.relatedIssues) {
|
|
50
|
+
await jira.addIssueLink(createdIssue.key, relatedIssue, config.issueLinkType);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const browseUrl = jira.getIssueBrowseUrl(createdIssue.key);
|
|
54
|
+
|
|
55
|
+
if (options.save) {
|
|
56
|
+
await saveCurrentIssueState(config, {
|
|
57
|
+
issueKey: createdIssue.key,
|
|
58
|
+
summary,
|
|
59
|
+
url: browseUrl,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const response = {
|
|
64
|
+
issueKey: createdIssue.key,
|
|
65
|
+
projectKey,
|
|
66
|
+
relatedIssues: options.relatedIssues,
|
|
67
|
+
summary,
|
|
68
|
+
url: browseUrl,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (options.json) {
|
|
72
|
+
console.log(JSON.stringify(response, null, 2));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`Created ${createdIssue.key}: ${summary}`);
|
|
77
|
+
console.log(browseUrl);
|
|
78
|
+
|
|
79
|
+
if (options.relatedIssues.length > 0) {
|
|
80
|
+
console.log(`Linked related issues: ${options.relatedIssues.join(", ")}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function resolveDescription(options: CliOptions): Promise<string> {
|
|
85
|
+
if (options.descriptionFilePath) {
|
|
86
|
+
return fs.readFile(options.descriptionFilePath, "utf8");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return options.description ?? "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseArgs(args: string[]): CliOptions {
|
|
93
|
+
const options: CliOptions = {
|
|
94
|
+
json: false,
|
|
95
|
+
labels: [],
|
|
96
|
+
relatedIssues: [],
|
|
97
|
+
save: true,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
101
|
+
const arg = args[index];
|
|
102
|
+
|
|
103
|
+
if (!arg) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (arg === "--config") {
|
|
108
|
+
options.configPath = requireNextValue(args, ++index, "--config");
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (arg === "--description") {
|
|
113
|
+
options.description = requireNextValue(args, ++index, "--description");
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (arg === "--description-file") {
|
|
118
|
+
options.descriptionFilePath = requireNextValue(args, ++index, "--description-file");
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (arg === "--env-file") {
|
|
123
|
+
options.envFilePath = requireNextValue(args, ++index, "--env-file");
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (arg === "--issue-type") {
|
|
128
|
+
options.issueTypeOverride = requireNextValue(args, ++index, "--issue-type");
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (arg === "--json") {
|
|
133
|
+
options.json = true;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (arg === "--label") {
|
|
138
|
+
options.labels.push(requireNextValue(args, ++index, "--label"));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (arg === "--no-save") {
|
|
143
|
+
options.save = false;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (arg === "--project-key") {
|
|
148
|
+
options.projectKeyOverride = requireNextValue(args, ++index, "--project-key");
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (arg === "--related-issue") {
|
|
153
|
+
options.relatedIssues.push(requireNextValue(args, ++index, "--related-issue"));
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (arg === "--summary") {
|
|
158
|
+
options.summary = requireNextValue(args, ++index, "--summary");
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (arg.startsWith("--")) {
|
|
163
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!options.summary) {
|
|
167
|
+
options.summary = arg;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw new Error(`Unexpected positional argument: ${arg}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return options;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function requireNextValue(args: string[], index: number, flagName: string): string {
|
|
178
|
+
const value = args[index];
|
|
179
|
+
|
|
180
|
+
if (!value) {
|
|
181
|
+
throw new Error(`Missing value for ${flagName}.`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
void main().catch((error) => {
|
|
188
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
189
|
+
console.error(message);
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
});
|
package/src/csv.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
export type CsvRow = Record<string, string>;
|
|
2
|
+
|
|
3
|
+
type AdfMark = {
|
|
4
|
+
type: "link";
|
|
5
|
+
attrs: {
|
|
6
|
+
href: string;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type AdfTextNode = {
|
|
11
|
+
type: "text";
|
|
12
|
+
text: string;
|
|
13
|
+
marks?: AdfMark[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type AdfHardBreakNode = {
|
|
17
|
+
type: "hardBreak";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type AdfParagraphNode = {
|
|
21
|
+
type: "paragraph";
|
|
22
|
+
content: Array<AdfTextNode | AdfHardBreakNode>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type AdfMediaNode = {
|
|
26
|
+
type: "media";
|
|
27
|
+
attrs: {
|
|
28
|
+
alt?: string;
|
|
29
|
+
collection: string;
|
|
30
|
+
height?: number;
|
|
31
|
+
id: string;
|
|
32
|
+
type: "file";
|
|
33
|
+
width?: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type AdfMediaSingleNode = {
|
|
38
|
+
type: "mediaSingle";
|
|
39
|
+
attrs: {
|
|
40
|
+
layout: "align-start" | "align-end" | "center" | "full-width" | "wide" | "wrap-left" | "wrap-right";
|
|
41
|
+
width?: number;
|
|
42
|
+
widthType?: "percentage" | "pixel";
|
|
43
|
+
};
|
|
44
|
+
content: [AdfMediaNode];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type AdfDocument = {
|
|
48
|
+
version: 1;
|
|
49
|
+
type: "doc";
|
|
50
|
+
content: Array<AdfParagraphNode | AdfMediaSingleNode>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const urlPattern = /https?:\/\/[^\s]+/g;
|
|
54
|
+
|
|
55
|
+
export function parseCsvRows(text: string): CsvRow[] {
|
|
56
|
+
const rows = parseCsv(text);
|
|
57
|
+
|
|
58
|
+
if (rows.length === 0) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const [headerRow, ...dataRows] = rows;
|
|
63
|
+
const headers = headerRow.map((header) => header.trim());
|
|
64
|
+
|
|
65
|
+
return dataRows.map((dataRow, rowIndex) => {
|
|
66
|
+
if (dataRow.length > headers.length) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`CSV parsing failed: row ${rowIndex + 2} has ${dataRow.length} fields but only ${headers.length} headers.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const record: CsvRow = {};
|
|
73
|
+
|
|
74
|
+
headers.forEach((header, columnIndex) => {
|
|
75
|
+
record[header] = dataRow[columnIndex] ?? "";
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return record;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function textToAdf(text: string): AdfDocument | null {
|
|
83
|
+
const normalized = text.replace(/\r\n/g, "\n").trim();
|
|
84
|
+
|
|
85
|
+
if (normalized.length === 0) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const paragraphs = normalized
|
|
90
|
+
.split(/\n{2,}/)
|
|
91
|
+
.map((paragraph) => paragraph.trim())
|
|
92
|
+
.filter((paragraph) => paragraph.length > 0)
|
|
93
|
+
.map((paragraph) => ({
|
|
94
|
+
type: "paragraph" as const,
|
|
95
|
+
content: linesToParagraphContent(paragraph.split("\n")),
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
if (paragraphs.length === 0) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
version: 1,
|
|
104
|
+
type: "doc",
|
|
105
|
+
content: paragraphs,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function adfToPlainText(value: unknown): string {
|
|
110
|
+
if (!value || typeof value !== "object") {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const document = value as { content?: unknown[] };
|
|
115
|
+
return extractAdfText(document.content ?? []).trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseCsv(text: string): string[][] {
|
|
119
|
+
const rows: string[][] = [];
|
|
120
|
+
const normalized = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
121
|
+
|
|
122
|
+
let row: string[] = [];
|
|
123
|
+
let field = "";
|
|
124
|
+
let inQuotes = false;
|
|
125
|
+
|
|
126
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
127
|
+
const char = normalized[index];
|
|
128
|
+
|
|
129
|
+
if (inQuotes) {
|
|
130
|
+
if (char === "\"") {
|
|
131
|
+
if (normalized[index + 1] === "\"") {
|
|
132
|
+
field += "\"";
|
|
133
|
+
index += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
inQuotes = false;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
field += char;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (char === "\"") {
|
|
146
|
+
inQuotes = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (char === ",") {
|
|
151
|
+
row.push(field);
|
|
152
|
+
field = "";
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (char === "\r" || char === "\n") {
|
|
157
|
+
if (char === "\r" && normalized[index + 1] === "\n") {
|
|
158
|
+
index += 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
row.push(field);
|
|
162
|
+
rows.push(row);
|
|
163
|
+
row = [];
|
|
164
|
+
field = "";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
field += char;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (inQuotes) {
|
|
172
|
+
throw new Error("CSV parsing failed: unterminated quoted field.");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (field.length > 0 || row.length > 0) {
|
|
176
|
+
row.push(field);
|
|
177
|
+
rows.push(row);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
while (rows.length > 0 && rows[rows.length - 1]!.every((cell) => cell.trim() === "")) {
|
|
181
|
+
rows.pop();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return rows;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function linesToParagraphContent(lines: string[]): Array<AdfTextNode | AdfHardBreakNode> {
|
|
188
|
+
const content: Array<AdfTextNode | AdfHardBreakNode> = [];
|
|
189
|
+
|
|
190
|
+
lines.forEach((line, lineIndex) => {
|
|
191
|
+
if (lineIndex > 0) {
|
|
192
|
+
content.push({ type: "hardBreak" });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const nodes = lineToTextNodes(line);
|
|
196
|
+
|
|
197
|
+
if (nodes.length === 0) {
|
|
198
|
+
content.push({ type: "text", text: "" });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
content.push(...nodes);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return content;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function lineToTextNodes(line: string): AdfTextNode[] {
|
|
209
|
+
const nodes: AdfTextNode[] = [];
|
|
210
|
+
let lastIndex = 0;
|
|
211
|
+
|
|
212
|
+
for (const match of line.matchAll(urlPattern)) {
|
|
213
|
+
const startIndex = match.index ?? 0;
|
|
214
|
+
const url = match[0];
|
|
215
|
+
|
|
216
|
+
if (startIndex > lastIndex) {
|
|
217
|
+
nodes.push({
|
|
218
|
+
type: "text",
|
|
219
|
+
text: line.slice(lastIndex, startIndex),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
nodes.push({
|
|
224
|
+
type: "text",
|
|
225
|
+
text: url,
|
|
226
|
+
marks: [
|
|
227
|
+
{
|
|
228
|
+
type: "link",
|
|
229
|
+
attrs: {
|
|
230
|
+
href: url,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
lastIndex = startIndex + url.length;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (lastIndex < line.length) {
|
|
240
|
+
nodes.push({
|
|
241
|
+
type: "text",
|
|
242
|
+
text: line.slice(lastIndex),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return nodes;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function extractAdfText(nodes: unknown[]): string {
|
|
250
|
+
return nodes
|
|
251
|
+
.map((node) => {
|
|
252
|
+
if (!node || typeof node !== "object") {
|
|
253
|
+
return "";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const typedNode = node as { type?: string; text?: string; content?: unknown[] };
|
|
257
|
+
|
|
258
|
+
if (typedNode.type === "text") {
|
|
259
|
+
return typedNode.text ?? "";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (typedNode.type === "hardBreak") {
|
|
263
|
+
return "\n";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (Array.isArray(typedNode.content)) {
|
|
267
|
+
return extractAdfText(typedNode.content);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return "";
|
|
271
|
+
})
|
|
272
|
+
.join("");
|
|
273
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { getDiffSearchText } from "./git";
|
|
2
|
+
import { loadToolEnv, loadWorkflowConfig, readEnv } from "./config";
|
|
3
|
+
import { createJiraClient } from "./jira-client";
|
|
4
|
+
import { searchIssuesForText } from "./issue-search";
|
|
5
|
+
|
|
6
|
+
type CliOptions = {
|
|
7
|
+
configPath?: string;
|
|
8
|
+
envFilePath?: string;
|
|
9
|
+
fromDiff: boolean;
|
|
10
|
+
json: boolean;
|
|
11
|
+
limit?: number;
|
|
12
|
+
projectKeyOverride?: string;
|
|
13
|
+
staged: boolean;
|
|
14
|
+
text?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
const options = parseArgs(process.argv.slice(2));
|
|
19
|
+
await loadToolEnv(options.envFilePath);
|
|
20
|
+
const config = await loadWorkflowConfig(options.configPath);
|
|
21
|
+
const projectKey = options.projectKeyOverride ?? config.projectKey ?? readEnv("JIRA_PROJECT_KEY");
|
|
22
|
+
|
|
23
|
+
if (!projectKey) {
|
|
24
|
+
throw new Error("Set projectKey in .jira-dev-workflow/config.json or pass --project-key.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const searchText = options.text?.trim() || (options.fromDiff ? await getDiffSearchText({ fromDiff: true, staged: options.staged }) : "");
|
|
28
|
+
|
|
29
|
+
if (!searchText) {
|
|
30
|
+
throw new Error("Provide --text or use --from-diff.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const searchLimit = options.limit ?? config.searchLimit;
|
|
34
|
+
const jira = createJiraClient();
|
|
35
|
+
const searchResult = await searchIssuesForText({
|
|
36
|
+
jira,
|
|
37
|
+
limit: searchLimit,
|
|
38
|
+
projectKey,
|
|
39
|
+
searchText,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const response = {
|
|
43
|
+
...searchResult,
|
|
44
|
+
projectKey,
|
|
45
|
+
query: searchText,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (options.json) {
|
|
49
|
+
console.log(JSON.stringify(response, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`Project: ${projectKey}`);
|
|
54
|
+
console.log(`Query: ${searchText}`);
|
|
55
|
+
console.log("");
|
|
56
|
+
|
|
57
|
+
if (response.openMatches.length > 0) {
|
|
58
|
+
console.log("Open matches:");
|
|
59
|
+
for (const match of response.openMatches) {
|
|
60
|
+
console.log(`- ${match.issueKey} [score ${match.score}] ${match.summary}`);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
console.log("Open matches: none");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log("");
|
|
67
|
+
|
|
68
|
+
if (response.closedMatches.length > 0) {
|
|
69
|
+
console.log("Closed matches:");
|
|
70
|
+
for (const match of response.closedMatches) {
|
|
71
|
+
console.log(`- ${match.issueKey} [score ${match.score}] ${match.summary}`);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
console.log("Closed matches: none");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log("");
|
|
78
|
+
console.log(`Recommended action: ${response.recommendedAction}`);
|
|
79
|
+
|
|
80
|
+
if (response.recommendedOpenIssueKey) {
|
|
81
|
+
console.log(`Recommended open issue: ${response.recommendedOpenIssueKey}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (response.recommendedClosedIssueKey) {
|
|
85
|
+
console.log(`Recommended related closed issue: ${response.recommendedClosedIssueKey}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseArgs(args: string[]): CliOptions {
|
|
90
|
+
const options: CliOptions = {
|
|
91
|
+
fromDiff: false,
|
|
92
|
+
json: false,
|
|
93
|
+
staged: true,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
97
|
+
const arg = args[index];
|
|
98
|
+
|
|
99
|
+
if (!arg) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (arg === "--config") {
|
|
104
|
+
options.configPath = requireNextValue(args, ++index, "--config");
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (arg === "--env-file") {
|
|
109
|
+
options.envFilePath = requireNextValue(args, ++index, "--env-file");
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (arg === "--from-diff") {
|
|
114
|
+
options.fromDiff = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (arg === "--json") {
|
|
119
|
+
options.json = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (arg === "--limit") {
|
|
124
|
+
options.limit = Number.parseInt(requireNextValue(args, ++index, "--limit"), 10);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (arg === "--project-key") {
|
|
129
|
+
options.projectKeyOverride = requireNextValue(args, ++index, "--project-key");
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (arg === "--text") {
|
|
134
|
+
options.text = requireNextValue(args, ++index, "--text");
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (arg === "--working-tree") {
|
|
139
|
+
options.staged = false;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (arg === "--staged") {
|
|
144
|
+
options.staged = true;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (arg.startsWith("--")) {
|
|
149
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
options.text = arg;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return options;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function requireNextValue(args: string[], index: number, flagName: string): string {
|
|
159
|
+
const value = args[index];
|
|
160
|
+
|
|
161
|
+
if (!value) {
|
|
162
|
+
throw new Error(`Missing value for ${flagName}.`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
void main().catch((error) => {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
+
console.error(message);
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
});
|
package/src/git.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
export type DiffContext = {
|
|
7
|
+
changedFiles: string[];
|
|
8
|
+
combinedText: string;
|
|
9
|
+
patchHighlights: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function getDiffContext({ staged }: { staged: boolean }): Promise<DiffContext> {
|
|
13
|
+
const nameArgs = staged ? ["diff", "--staged", "--name-only"] : ["diff", "--name-only"];
|
|
14
|
+
const patchArgs = staged
|
|
15
|
+
? ["diff", "--staged", "--unified=0", "--no-color"]
|
|
16
|
+
: ["diff", "--unified=0", "--no-color"];
|
|
17
|
+
|
|
18
|
+
const [{ stdout: fileNames }, { stdout: patch }] = await Promise.all([
|
|
19
|
+
execFileAsync("git", nameArgs, { cwd: process.cwd() }),
|
|
20
|
+
execFileAsync("git", patchArgs, { cwd: process.cwd(), maxBuffer: 1024 * 1024 * 4 }),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const changedFiles = fileNames
|
|
24
|
+
.split(/\r?\n/)
|
|
25
|
+
.map((line) => line.trim())
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
|
|
28
|
+
const patchHighlights = patch
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.filter((line) => line.startsWith("+") || line.startsWith("@@"))
|
|
31
|
+
.map((line) => line.replace(/^[+@ -]+/, "").trim())
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.slice(0, 80);
|
|
34
|
+
|
|
35
|
+
const combinedText = [...changedFiles, ...patchHighlights].join("\n").trim();
|
|
36
|
+
|
|
37
|
+
if (!combinedText) {
|
|
38
|
+
throw new Error(staged ? "No staged changes found." : "No working tree changes found.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
changedFiles,
|
|
43
|
+
combinedText,
|
|
44
|
+
patchHighlights,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getDiffSearchText({ fromDiff, staged }: { fromDiff: boolean; staged: boolean }): Promise<string> {
|
|
49
|
+
if (!fromDiff) {
|
|
50
|
+
throw new Error("Diff extraction requires --from-diff.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const context = await getDiffContext({ staged });
|
|
54
|
+
return context.combinedText;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function commitWithMessage(message: string) {
|
|
58
|
+
await execFileAsync("git", ["commit", "-m", message], { cwd: process.cwd(), maxBuffer: 1024 * 1024 * 4 });
|
|
59
|
+
}
|