clairo 0.1.0 → 0.3.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 +22 -241
- package/dist/cli.js +1114 -372
- package/package.json +8 -1
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
import meow from "meow";
|
|
5
5
|
|
|
6
6
|
// src/app.tsx
|
|
7
|
-
import {
|
|
7
|
+
import { useState as useState8 } from "react";
|
|
8
|
+
import { Box as Box12, useApp, useInput as useInput10 } from "ink";
|
|
8
9
|
|
|
9
10
|
// src/components/github/GitHubView.tsx
|
|
10
|
-
import {
|
|
11
|
+
import { exec as exec3 } from "child_process";
|
|
12
|
+
import { useCallback, useEffect as useEffect3, useState as useState3 } from "react";
|
|
11
13
|
import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
|
|
12
|
-
import { Box as
|
|
14
|
+
import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
|
|
13
15
|
|
|
14
16
|
// src/lib/config/index.ts
|
|
15
17
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -126,8 +128,6 @@ function getCurrentBranch() {
|
|
|
126
128
|
|
|
127
129
|
// src/lib/github/index.ts
|
|
128
130
|
import { exec } from "child_process";
|
|
129
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
130
|
-
import { join as join2 } from "path";
|
|
131
131
|
import { promisify } from "util";
|
|
132
132
|
var execAsync = promisify(exec);
|
|
133
133
|
async function isGhInstalled() {
|
|
@@ -170,13 +170,25 @@ async function listPRsForBranch(branch, repo) {
|
|
|
170
170
|
errorType: "not_authenticated"
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
|
+
const fields = "number,title,state,author,createdAt,isDraft";
|
|
174
|
+
try {
|
|
175
|
+
const { stdout } = await execAsync(
|
|
176
|
+
`gh pr view --json ${fields} 2>/dev/null`
|
|
177
|
+
);
|
|
178
|
+
const pr = JSON.parse(stdout);
|
|
179
|
+
return { success: true, data: [pr] };
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
173
182
|
try {
|
|
174
|
-
const fields = "number,title,state,author,createdAt,isDraft";
|
|
175
183
|
const { stdout } = await execAsync(
|
|
176
|
-
`gh pr list --
|
|
184
|
+
`gh pr list --state open --json ${fields},headRefName --repo "${repo}" 2>/dev/null`
|
|
177
185
|
);
|
|
178
|
-
const
|
|
179
|
-
|
|
186
|
+
const allPrs = JSON.parse(stdout);
|
|
187
|
+
const prs = allPrs.filter(
|
|
188
|
+
(pr) => pr.headRefName === branch || pr.headRefName.endsWith(`:${branch}`)
|
|
189
|
+
);
|
|
190
|
+
const result = prs.map(({ headRefName: _, ...rest }) => rest);
|
|
191
|
+
return { success: true, data: result };
|
|
180
192
|
} catch {
|
|
181
193
|
return { success: false, error: "Failed to fetch PRs", errorType: "api_error" };
|
|
182
194
|
}
|
|
@@ -201,6 +213,7 @@ async function getPRDetails(prNumber, repo) {
|
|
|
201
213
|
"number",
|
|
202
214
|
"title",
|
|
203
215
|
"body",
|
|
216
|
+
"url",
|
|
204
217
|
"state",
|
|
205
218
|
"author",
|
|
206
219
|
"createdAt",
|
|
@@ -227,170 +240,14 @@ async function getPRDetails(prNumber, repo) {
|
|
|
227
240
|
};
|
|
228
241
|
}
|
|
229
242
|
}
|
|
230
|
-
function getPRTemplate(repoPath) {
|
|
231
|
-
const templatePaths = [
|
|
232
|
-
".github/PULL_REQUEST_TEMPLATE.md",
|
|
233
|
-
".github/pull_request_template.md",
|
|
234
|
-
"PULL_REQUEST_TEMPLATE.md",
|
|
235
|
-
"pull_request_template.md",
|
|
236
|
-
"docs/PULL_REQUEST_TEMPLATE.md"
|
|
237
|
-
];
|
|
238
|
-
for (const templatePath of templatePaths) {
|
|
239
|
-
const fullPath = join2(repoPath, templatePath);
|
|
240
|
-
if (existsSync2(fullPath)) {
|
|
241
|
-
try {
|
|
242
|
-
return readFileSync2(fullPath, "utf-8");
|
|
243
|
-
} catch {
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
async function createPR(repo, title, body, baseBranch) {
|
|
251
|
-
if (!await isGhInstalled()) {
|
|
252
|
-
return {
|
|
253
|
-
success: false,
|
|
254
|
-
error: "GitHub CLI (gh) is not installed",
|
|
255
|
-
errorType: "not_installed"
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
if (!await isGhAuthenticated()) {
|
|
259
|
-
return {
|
|
260
|
-
success: false,
|
|
261
|
-
error: "Not authenticated. Run 'gh auth login'",
|
|
262
|
-
errorType: "not_authenticated"
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
try {
|
|
266
|
-
const baseArg = baseBranch ? `--base "${baseBranch}"` : "";
|
|
267
|
-
const fields = "number,title,state,author,createdAt,isDraft";
|
|
268
|
-
const escapedTitle = title.replace(/"/g, '\\"');
|
|
269
|
-
const escapedBody = body.replace(/"/g, '\\"');
|
|
270
|
-
const { stdout } = await execAsync(
|
|
271
|
-
`gh pr create --title "${escapedTitle}" --body "${escapedBody}" ${baseArg} --repo "${repo}" --json ${fields}`
|
|
272
|
-
);
|
|
273
|
-
const pr = JSON.parse(stdout);
|
|
274
|
-
return { success: true, data: pr };
|
|
275
|
-
} catch (err) {
|
|
276
|
-
const message = err instanceof Error ? err.message : "Failed to create PR";
|
|
277
|
-
return {
|
|
278
|
-
success: false,
|
|
279
|
-
error: message,
|
|
280
|
-
errorType: "api_error"
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// src/components/github/CreatePRModal.tsx
|
|
286
|
-
import { spawnSync } from "child_process";
|
|
287
|
-
import { mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
288
|
-
import { tmpdir } from "os";
|
|
289
|
-
import { join as join3 } from "path";
|
|
290
|
-
import { useState } from "react";
|
|
291
|
-
import { Box, Text, useInput } from "ink";
|
|
292
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
293
|
-
function openInEditor(content, filename) {
|
|
294
|
-
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
295
|
-
const tempDir = mkdtempSync(join3(tmpdir(), "clairo-"));
|
|
296
|
-
const tempFile = join3(tempDir, filename);
|
|
297
|
-
try {
|
|
298
|
-
writeFileSync2(tempFile, content);
|
|
299
|
-
const result = spawnSync(editor, [tempFile], {
|
|
300
|
-
stdio: "inherit"
|
|
301
|
-
});
|
|
302
|
-
process.stdout.write("\x1B[2J\x1B[H");
|
|
303
|
-
if (result.status !== 0) {
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
return readFileSync3(tempFile, "utf-8");
|
|
307
|
-
} finally {
|
|
308
|
-
try {
|
|
309
|
-
rmSync(tempDir, { recursive: true });
|
|
310
|
-
} catch {
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
function CreatePRModal({ template, onSubmit, onCancel, loading, error }) {
|
|
315
|
-
const [title, setTitle] = useState("");
|
|
316
|
-
const [body, setBody] = useState(template ?? "");
|
|
317
|
-
const [selectedItem, setSelectedItem] = useState("title");
|
|
318
|
-
const items = ["title", "body", "submit"];
|
|
319
|
-
useInput(
|
|
320
|
-
(input, key) => {
|
|
321
|
-
if (loading) return;
|
|
322
|
-
if (key.escape) {
|
|
323
|
-
onCancel();
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (key.upArrow || input === "k") {
|
|
327
|
-
setSelectedItem((prev) => {
|
|
328
|
-
const idx = items.indexOf(prev);
|
|
329
|
-
return items[Math.max(0, idx - 1)];
|
|
330
|
-
});
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (key.downArrow || input === "j") {
|
|
334
|
-
setSelectedItem((prev) => {
|
|
335
|
-
const idx = items.indexOf(prev);
|
|
336
|
-
return items[Math.min(items.length - 1, idx + 1)];
|
|
337
|
-
});
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
if (key.return) {
|
|
341
|
-
if (selectedItem === "title") {
|
|
342
|
-
const newTitle = openInEditor(title, "PR_TITLE.txt");
|
|
343
|
-
if (newTitle !== null) {
|
|
344
|
-
setTitle(newTitle.split("\n")[0].trim());
|
|
345
|
-
}
|
|
346
|
-
} else if (selectedItem === "body") {
|
|
347
|
-
const newBody = openInEditor(body, "PR_DESCRIPTION.md");
|
|
348
|
-
if (newBody !== null) {
|
|
349
|
-
setBody(newBody);
|
|
350
|
-
}
|
|
351
|
-
} else if (selectedItem === "submit") {
|
|
352
|
-
if (title.trim()) {
|
|
353
|
-
onSubmit(title.trim(), body);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
},
|
|
358
|
-
{ isActive: !loading }
|
|
359
|
-
);
|
|
360
|
-
const renderItem = (item, label, value) => {
|
|
361
|
-
const isSelected = selectedItem === item;
|
|
362
|
-
const prefix = isSelected ? "> " : " ";
|
|
363
|
-
const color = isSelected ? "cyan" : void 0;
|
|
364
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
365
|
-
/* @__PURE__ */ jsxs(Text, { color, bold: isSelected, children: [
|
|
366
|
-
prefix,
|
|
367
|
-
label
|
|
368
|
-
] }),
|
|
369
|
-
value !== void 0 && /* @__PURE__ */ jsx(Box, { marginLeft: 4, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: value || "(empty - press Enter to edit)" }) })
|
|
370
|
-
] });
|
|
371
|
-
};
|
|
372
|
-
const truncatedBody = body ? body.split("\n").slice(0, 2).join(" ").slice(0, 60) + (body.length > 60 ? "..." : "") : "";
|
|
373
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
|
|
374
|
-
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Create Pull Request" }),
|
|
375
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Up/Down to select, Enter to edit, Esc to cancel" }),
|
|
376
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1 }),
|
|
377
|
-
error && /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) }),
|
|
378
|
-
renderItem("title", "Title", title),
|
|
379
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1 }),
|
|
380
|
-
renderItem("body", "Description", truncatedBody),
|
|
381
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1 }),
|
|
382
|
-
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
|
|
383
|
-
selectedItem === "submit" ? "> " : " ",
|
|
384
|
-
title.trim() ? "[Submit PR]" : "[Enter title first]"
|
|
385
|
-
] }) }),
|
|
386
|
-
loading && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Creating PR..." }) })
|
|
387
|
-
] });
|
|
388
|
-
}
|
|
389
243
|
|
|
390
244
|
// src/components/github/PRDetailsBox.tsx
|
|
245
|
+
import { useRef } from "react";
|
|
246
|
+
import open from "open";
|
|
391
247
|
import { TitledBox } from "@mishieck/ink-titled-box";
|
|
392
|
-
import { Box
|
|
393
|
-
import {
|
|
248
|
+
import { Box, Text, useInput } from "ink";
|
|
249
|
+
import { ScrollView } from "ink-scroll-view";
|
|
250
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
394
251
|
function getCheckColor(check) {
|
|
395
252
|
const conclusion = check.conclusion ?? check.state;
|
|
396
253
|
if (conclusion === "SUCCESS") return "green";
|
|
@@ -412,63 +269,124 @@ function getCheckIcon(check) {
|
|
|
412
269
|
return "?";
|
|
413
270
|
}
|
|
414
271
|
function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
415
|
-
var _a, _b, _c, _d, _e, _f;
|
|
416
|
-
const
|
|
417
|
-
const
|
|
272
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
273
|
+
const scrollRef = useRef(null);
|
|
274
|
+
const title = "[3] PR Details";
|
|
275
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
418
276
|
const displayTitle = pr ? `${title} - #${pr.number}` : title;
|
|
419
277
|
const reviewStatus = (pr == null ? void 0 : pr.reviewDecision) ?? "PENDING";
|
|
420
278
|
const reviewColor = reviewStatus === "APPROVED" ? "green" : reviewStatus === "CHANGES_REQUESTED" ? "red" : "yellow";
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
279
|
+
const getMergeDisplay = () => {
|
|
280
|
+
if (!pr) return { text: "UNKNOWN", color: "yellow" };
|
|
281
|
+
if (pr.state === "MERGED") return { text: "MERGED", color: "magenta" };
|
|
282
|
+
if (pr.state === "CLOSED") return { text: "CLOSED", color: "red" };
|
|
283
|
+
if (pr.mergeable === "MERGEABLE") return { text: "MERGEABLE", color: "green" };
|
|
284
|
+
if (pr.mergeable === "CONFLICTING") return { text: "CONFLICTING", color: "red" };
|
|
285
|
+
return { text: pr.mergeable ?? "UNKNOWN", color: "yellow" };
|
|
286
|
+
};
|
|
287
|
+
const mergeDisplay = getMergeDisplay();
|
|
288
|
+
useInput(
|
|
289
|
+
(input, key) => {
|
|
290
|
+
var _a2, _b2;
|
|
291
|
+
if (key.upArrow || input === "k") {
|
|
292
|
+
(_a2 = scrollRef.current) == null ? void 0 : _a2.scrollBy(-1);
|
|
293
|
+
}
|
|
294
|
+
if (key.downArrow || input === "j") {
|
|
295
|
+
(_b2 = scrollRef.current) == null ? void 0 : _b2.scrollBy(1);
|
|
296
|
+
}
|
|
297
|
+
if (input === "o" && (pr == null ? void 0 : pr.url)) {
|
|
298
|
+
open(pr.url).catch(() => {
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{ isActive: isFocused }
|
|
303
|
+
);
|
|
304
|
+
return /* @__PURE__ */ jsx(TitledBox, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 2, children: /* @__PURE__ */ jsx(Box, { flexGrow: 1, overflow: "hidden", children: /* @__PURE__ */ jsx(ScrollView, { ref: scrollRef, flexGrow: 1, children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
|
|
305
|
+
loading && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading details..." }),
|
|
306
|
+
error && /* @__PURE__ */ jsx(Text, { color: "red", children: error }),
|
|
307
|
+
!loading && !error && !pr && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Select a PR to view details" }),
|
|
308
|
+
!loading && !error && pr && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
309
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: pr.title }),
|
|
310
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
429
311
|
"by ",
|
|
430
312
|
((_a = pr.author) == null ? void 0 : _a.login) ?? "unknown",
|
|
431
313
|
" | ",
|
|
432
314
|
((_b = pr.commits) == null ? void 0 : _b.length) ?? 0,
|
|
433
315
|
" commits"
|
|
434
316
|
] }),
|
|
435
|
-
/* @__PURE__ */
|
|
436
|
-
/* @__PURE__ */
|
|
437
|
-
/* @__PURE__ */
|
|
438
|
-
/* @__PURE__ */
|
|
439
|
-
/* @__PURE__ */
|
|
440
|
-
/* @__PURE__ */
|
|
317
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
318
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Review: " }),
|
|
319
|
+
/* @__PURE__ */ jsx(Text, { color: reviewColor, children: reviewStatus }),
|
|
320
|
+
/* @__PURE__ */ jsx(Text, { children: " | " }),
|
|
321
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Status: " }),
|
|
322
|
+
/* @__PURE__ */ jsx(Text, { color: mergeDisplay.color, children: mergeDisplay.text })
|
|
323
|
+
] }),
|
|
324
|
+
(((_c = pr.assignees) == null ? void 0 : _c.length) ?? 0) > 0 && /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
325
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Assignees: " }),
|
|
326
|
+
/* @__PURE__ */ jsx(Text, { children: pr.assignees.map((a) => a.login).join(", ") })
|
|
441
327
|
] }),
|
|
442
|
-
(((
|
|
443
|
-
/* @__PURE__ */
|
|
444
|
-
|
|
328
|
+
(((_d = pr.reviews) == null ? void 0 : _d.length) ?? 0) > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
329
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Reviews:" }),
|
|
330
|
+
pr.reviews.map((review, idx) => {
|
|
331
|
+
const color = review.state === "APPROVED" ? "green" : review.state === "CHANGES_REQUESTED" ? "red" : review.state === "COMMENTED" ? "blue" : "yellow";
|
|
332
|
+
const icon = review.state === "APPROVED" ? "\u2713" : review.state === "CHANGES_REQUESTED" ? "\u2717" : review.state === "COMMENTED" ? "\u{1F4AC}" : "\u25CB";
|
|
333
|
+
return /* @__PURE__ */ jsxs(Text, { color, children: [
|
|
334
|
+
" ",
|
|
335
|
+
icon,
|
|
336
|
+
" ",
|
|
337
|
+
review.author.login
|
|
338
|
+
] }, idx);
|
|
339
|
+
})
|
|
445
340
|
] }),
|
|
446
|
-
(((
|
|
447
|
-
/* @__PURE__ */
|
|
448
|
-
/* @__PURE__ */
|
|
341
|
+
(((_e = pr.reviewRequests) == null ? void 0 : _e.length) ?? 0) > 0 && /* @__PURE__ */ jsxs(Box, { children: [
|
|
342
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Pending: " }),
|
|
343
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: pr.reviewRequests.map((r) => r.login ?? r.name ?? r.slug ?? "Team").join(", ") })
|
|
449
344
|
] }),
|
|
450
|
-
(((
|
|
451
|
-
/* @__PURE__ */
|
|
452
|
-
(
|
|
345
|
+
(((_f = pr.statusCheckRollup) == null ? void 0 : _f.length) ?? 0) > 0 && /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
346
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Checks:" }),
|
|
347
|
+
(_g = pr.statusCheckRollup) == null ? void 0 : _g.map((check, idx) => /* @__PURE__ */ jsxs(Text, { color: getCheckColor(check), children: [
|
|
453
348
|
" ",
|
|
454
349
|
getCheckIcon(check),
|
|
455
350
|
" ",
|
|
456
351
|
check.name ?? check.context
|
|
457
352
|
] }, idx))
|
|
458
353
|
] }),
|
|
459
|
-
pr.body && /* @__PURE__ */
|
|
460
|
-
/* @__PURE__ */
|
|
461
|
-
/* @__PURE__ */
|
|
354
|
+
pr.body && /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
355
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Description:" }),
|
|
356
|
+
/* @__PURE__ */ jsx(Text, { children: pr.body })
|
|
462
357
|
] })
|
|
463
358
|
] })
|
|
464
|
-
] }) });
|
|
359
|
+
] }) }) }) });
|
|
465
360
|
}
|
|
466
361
|
|
|
467
362
|
// src/components/github/PullRequestsBox.tsx
|
|
468
|
-
import { useEffect, useState
|
|
363
|
+
import { useEffect, useState } from "react";
|
|
469
364
|
import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
|
|
470
|
-
import { Box as
|
|
471
|
-
|
|
365
|
+
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
366
|
+
|
|
367
|
+
// src/lib/clipboard.ts
|
|
368
|
+
import { exec as exec2 } from "child_process";
|
|
369
|
+
async function copyToClipboard(text) {
|
|
370
|
+
var _a, _b;
|
|
371
|
+
const command = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
|
|
372
|
+
try {
|
|
373
|
+
const child = exec2(command);
|
|
374
|
+
(_a = child.stdin) == null ? void 0 : _a.write(text);
|
|
375
|
+
(_b = child.stdin) == null ? void 0 : _b.end();
|
|
376
|
+
await new Promise((resolve, reject) => {
|
|
377
|
+
child.on("close", (code) => {
|
|
378
|
+
if (code === 0) resolve();
|
|
379
|
+
else reject(new Error(`Clipboard command exited with code ${code}`));
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
return true;
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/components/github/PullRequestsBox.tsx
|
|
389
|
+
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
472
390
|
function PullRequestsBox({
|
|
473
391
|
prs,
|
|
474
392
|
selectedPR,
|
|
@@ -477,9 +395,10 @@ function PullRequestsBox({
|
|
|
477
395
|
loading,
|
|
478
396
|
error,
|
|
479
397
|
branch,
|
|
398
|
+
repoSlug,
|
|
480
399
|
isFocused
|
|
481
400
|
}) {
|
|
482
|
-
const [highlightedIndex, setHighlightedIndex] =
|
|
401
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
483
402
|
const totalItems = prs.length + 1;
|
|
484
403
|
useEffect(() => {
|
|
485
404
|
const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
|
|
@@ -501,22 +420,27 @@ function PullRequestsBox({
|
|
|
501
420
|
onSelect(prs[highlightedIndex]);
|
|
502
421
|
}
|
|
503
422
|
}
|
|
423
|
+
if (input === "y" && repoSlug && prs[highlightedIndex]) {
|
|
424
|
+
const pr = prs[highlightedIndex];
|
|
425
|
+
const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
|
|
426
|
+
copyToClipboard(url);
|
|
427
|
+
}
|
|
504
428
|
},
|
|
505
429
|
{ isActive: isFocused }
|
|
506
430
|
);
|
|
507
|
-
const title = "2 Pull Requests";
|
|
431
|
+
const title = "[2] Pull Requests";
|
|
508
432
|
const subtitle = branch ? ` (${branch})` : "";
|
|
509
|
-
const borderColor = isFocused ? "
|
|
510
|
-
return /* @__PURE__ */
|
|
511
|
-
loading && /* @__PURE__ */
|
|
512
|
-
error && /* @__PURE__ */
|
|
513
|
-
!loading && !error && /* @__PURE__ */
|
|
514
|
-
prs.length === 0 && /* @__PURE__ */
|
|
433
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
434
|
+
return /* @__PURE__ */ jsx2(TitledBox2, { borderStyle: "round", titles: [`${title}${subtitle}`], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
|
|
435
|
+
loading && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Loading PRs..." }),
|
|
436
|
+
error && /* @__PURE__ */ jsx2(Text2, { color: "red", children: error }),
|
|
437
|
+
!loading && !error && /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
438
|
+
prs.length === 0 && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No PRs for this branch" }),
|
|
515
439
|
prs.map((pr, idx) => {
|
|
516
440
|
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
517
441
|
const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
|
|
518
442
|
const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
|
|
519
|
-
return /* @__PURE__ */
|
|
443
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: isSelected ? "green" : void 0, children: [
|
|
520
444
|
prefix,
|
|
521
445
|
"#",
|
|
522
446
|
pr.number,
|
|
@@ -525,7 +449,7 @@ function PullRequestsBox({
|
|
|
525
449
|
pr.title
|
|
526
450
|
] }, pr.number);
|
|
527
451
|
}),
|
|
528
|
-
/* @__PURE__ */
|
|
452
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "blue", children: [
|
|
529
453
|
isFocused && highlightedIndex === prs.length ? "> " : " ",
|
|
530
454
|
"+ Create new PR"
|
|
531
455
|
] })
|
|
@@ -534,12 +458,12 @@ function PullRequestsBox({
|
|
|
534
458
|
}
|
|
535
459
|
|
|
536
460
|
// src/components/github/RemotesBox.tsx
|
|
537
|
-
import { useEffect as useEffect2, useState as
|
|
461
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
538
462
|
import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
|
|
539
|
-
import { Box as
|
|
540
|
-
import { jsx as
|
|
463
|
+
import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
|
|
464
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
541
465
|
function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
|
|
542
|
-
const [highlightedIndex, setHighlightedIndex] =
|
|
466
|
+
const [highlightedIndex, setHighlightedIndex] = useState2(0);
|
|
543
467
|
useEffect2(() => {
|
|
544
468
|
const idx = remotes.findIndex((r) => r.name === selectedRemote);
|
|
545
469
|
if (idx >= 0) setHighlightedIndex(idx);
|
|
@@ -559,17 +483,17 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
|
|
|
559
483
|
},
|
|
560
484
|
{ isActive: isFocused }
|
|
561
485
|
);
|
|
562
|
-
const title = "1 Remotes";
|
|
563
|
-
const borderColor = isFocused ? "
|
|
564
|
-
return /* @__PURE__ */
|
|
565
|
-
loading && /* @__PURE__ */
|
|
566
|
-
error && /* @__PURE__ */
|
|
567
|
-
!loading && !error && remotes.length === 0 && /* @__PURE__ */
|
|
486
|
+
const title = "[1] Remotes";
|
|
487
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
488
|
+
return /* @__PURE__ */ jsx3(TitledBox3, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
|
|
489
|
+
loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading..." }),
|
|
490
|
+
error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
|
|
491
|
+
!loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No remotes configured" }),
|
|
568
492
|
!loading && !error && remotes.map((remote, idx) => {
|
|
569
493
|
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
570
494
|
const isSelected = remote.name === selectedRemote;
|
|
571
495
|
const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
|
|
572
|
-
return /* @__PURE__ */
|
|
496
|
+
return /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
|
|
573
497
|
prefix,
|
|
574
498
|
remote.name,
|
|
575
499
|
" (",
|
|
@@ -581,27 +505,43 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
|
|
|
581
505
|
}
|
|
582
506
|
|
|
583
507
|
// src/components/github/GitHubView.tsx
|
|
584
|
-
import { jsx as
|
|
585
|
-
function GitHubView() {
|
|
586
|
-
const [isRepo, setIsRepo] =
|
|
587
|
-
const [repoPath, setRepoPath] =
|
|
588
|
-
const [remotes, setRemotes] =
|
|
589
|
-
const [currentBranch, setCurrentBranch] =
|
|
590
|
-
const [currentRepoSlug, setCurrentRepoSlug] =
|
|
591
|
-
const [selectedRemote, setSelectedRemote] =
|
|
592
|
-
const [selectedPR, setSelectedPR] =
|
|
593
|
-
const [prs, setPrs] =
|
|
594
|
-
const [prDetails, setPrDetails] =
|
|
595
|
-
const [loading, setLoading] =
|
|
508
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
509
|
+
function GitHubView({ isFocused, onKeybindingsChange }) {
|
|
510
|
+
const [isRepo, setIsRepo] = useState3(null);
|
|
511
|
+
const [repoPath, setRepoPath] = useState3(null);
|
|
512
|
+
const [remotes, setRemotes] = useState3([]);
|
|
513
|
+
const [currentBranch, setCurrentBranch] = useState3(null);
|
|
514
|
+
const [currentRepoSlug, setCurrentRepoSlug] = useState3(null);
|
|
515
|
+
const [selectedRemote, setSelectedRemote] = useState3(null);
|
|
516
|
+
const [selectedPR, setSelectedPR] = useState3(null);
|
|
517
|
+
const [prs, setPrs] = useState3([]);
|
|
518
|
+
const [prDetails, setPrDetails] = useState3(null);
|
|
519
|
+
const [loading, setLoading] = useState3({
|
|
596
520
|
remotes: true,
|
|
597
521
|
prs: false,
|
|
598
|
-
details: false
|
|
599
|
-
createPR: false
|
|
522
|
+
details: false
|
|
600
523
|
});
|
|
601
|
-
const [errors, setErrors] =
|
|
602
|
-
const [
|
|
603
|
-
|
|
604
|
-
|
|
524
|
+
const [errors, setErrors] = useState3({});
|
|
525
|
+
const [focusedBox, setFocusedBox] = useState3("remotes");
|
|
526
|
+
useEffect3(() => {
|
|
527
|
+
if (!isFocused) {
|
|
528
|
+
onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const bindings = [];
|
|
532
|
+
if (focusedBox === "remotes") {
|
|
533
|
+
bindings.push({ key: "Enter", label: "Select Remote" });
|
|
534
|
+
} else if (focusedBox === "prs") {
|
|
535
|
+
bindings.push({ key: "n", label: "New PR", color: "green" });
|
|
536
|
+
bindings.push({ key: "r", label: "Refresh" });
|
|
537
|
+
bindings.push({ key: "o", label: "Open", color: "green" });
|
|
538
|
+
bindings.push({ key: "y", label: "Copy Link" });
|
|
539
|
+
} else if (focusedBox === "details") {
|
|
540
|
+
bindings.push({ key: "r", label: "Refresh" });
|
|
541
|
+
bindings.push({ key: "o", label: "Open", color: "green" });
|
|
542
|
+
}
|
|
543
|
+
onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
|
|
544
|
+
}, [isFocused, focusedBox, onKeybindingsChange]);
|
|
605
545
|
useEffect3(() => {
|
|
606
546
|
const gitRepoCheck = isGitRepo();
|
|
607
547
|
setIsRepo(gitRepoCheck);
|
|
@@ -613,8 +553,6 @@ function GitHubView() {
|
|
|
613
553
|
const rootResult = getRepoRoot();
|
|
614
554
|
if (rootResult.success) {
|
|
615
555
|
setRepoPath(rootResult.data);
|
|
616
|
-
const template = getPRTemplate(rootResult.data);
|
|
617
|
-
setPrTemplate(template);
|
|
618
556
|
}
|
|
619
557
|
const branchResult = getCurrentBranch();
|
|
620
558
|
if (branchResult.success) {
|
|
@@ -631,6 +569,43 @@ function GitHubView() {
|
|
|
631
569
|
}
|
|
632
570
|
setLoading((prev) => ({ ...prev, remotes: false }));
|
|
633
571
|
}, []);
|
|
572
|
+
const refreshPRs = useCallback(async () => {
|
|
573
|
+
if (!currentBranch || !currentRepoSlug) return;
|
|
574
|
+
setLoading((prev) => ({ ...prev, prs: true }));
|
|
575
|
+
try {
|
|
576
|
+
const result = await listPRsForBranch(currentBranch, currentRepoSlug);
|
|
577
|
+
if (result.success) {
|
|
578
|
+
setPrs(result.data);
|
|
579
|
+
if (result.data.length > 0) {
|
|
580
|
+
setSelectedPR((prev) => prev ?? result.data[0]);
|
|
581
|
+
}
|
|
582
|
+
setErrors((prev) => ({ ...prev, prs: void 0 }));
|
|
583
|
+
} else {
|
|
584
|
+
setErrors((prev) => ({ ...prev, prs: result.error }));
|
|
585
|
+
}
|
|
586
|
+
} catch (err) {
|
|
587
|
+
setErrors((prev) => ({ ...prev, prs: String(err) }));
|
|
588
|
+
} finally {
|
|
589
|
+
setLoading((prev) => ({ ...prev, prs: false }));
|
|
590
|
+
}
|
|
591
|
+
}, [currentBranch, currentRepoSlug]);
|
|
592
|
+
const refreshDetails = useCallback(async () => {
|
|
593
|
+
if (!selectedPR || !currentRepoSlug) return;
|
|
594
|
+
setLoading((prev) => ({ ...prev, details: true }));
|
|
595
|
+
try {
|
|
596
|
+
const result = await getPRDetails(selectedPR.number, currentRepoSlug);
|
|
597
|
+
if (result.success) {
|
|
598
|
+
setPrDetails(result.data);
|
|
599
|
+
setErrors((prev) => ({ ...prev, details: void 0 }));
|
|
600
|
+
} else {
|
|
601
|
+
setErrors((prev) => ({ ...prev, details: result.error }));
|
|
602
|
+
}
|
|
603
|
+
} catch (err) {
|
|
604
|
+
setErrors((prev) => ({ ...prev, details: String(err) }));
|
|
605
|
+
} finally {
|
|
606
|
+
setLoading((prev) => ({ ...prev, details: false }));
|
|
607
|
+
}
|
|
608
|
+
}, [selectedPR, currentRepoSlug]);
|
|
634
609
|
useEffect3(() => {
|
|
635
610
|
if (!selectedRemote || !currentBranch) return;
|
|
636
611
|
const remote = remotes.find((r) => r.name === selectedRemote);
|
|
@@ -638,52 +613,21 @@ function GitHubView() {
|
|
|
638
613
|
const repo = getRepoFromRemote(remote.url);
|
|
639
614
|
if (!repo) return;
|
|
640
615
|
setCurrentRepoSlug(repo);
|
|
641
|
-
setLoading((prev) => ({ ...prev, prs: true }));
|
|
642
616
|
setPrs([]);
|
|
643
617
|
setSelectedPR(null);
|
|
644
|
-
const fetchPRs = async () => {
|
|
645
|
-
try {
|
|
646
|
-
const result = await listPRsForBranch(currentBranch, repo);
|
|
647
|
-
if (result.success) {
|
|
648
|
-
setPrs(result.data);
|
|
649
|
-
if (result.data.length > 0) {
|
|
650
|
-
setSelectedPR(result.data[0]);
|
|
651
|
-
}
|
|
652
|
-
setErrors((prev) => ({ ...prev, prs: void 0 }));
|
|
653
|
-
} else {
|
|
654
|
-
setErrors((prev) => ({ ...prev, prs: result.error }));
|
|
655
|
-
}
|
|
656
|
-
} catch (err) {
|
|
657
|
-
setErrors((prev) => ({ ...prev, prs: String(err) }));
|
|
658
|
-
} finally {
|
|
659
|
-
setLoading((prev) => ({ ...prev, prs: false }));
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
|
-
fetchPRs();
|
|
663
618
|
}, [selectedRemote, currentBranch, remotes]);
|
|
619
|
+
useEffect3(() => {
|
|
620
|
+
if (currentRepoSlug && currentBranch) {
|
|
621
|
+
refreshPRs();
|
|
622
|
+
}
|
|
623
|
+
}, [currentRepoSlug, currentBranch, refreshPRs]);
|
|
664
624
|
useEffect3(() => {
|
|
665
625
|
if (!selectedPR || !currentRepoSlug) {
|
|
666
626
|
setPrDetails(null);
|
|
667
627
|
return;
|
|
668
628
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
try {
|
|
672
|
-
const result = await getPRDetails(selectedPR.number, currentRepoSlug);
|
|
673
|
-
if (result.success) {
|
|
674
|
-
setPrDetails(result.data);
|
|
675
|
-
setErrors((prev) => ({ ...prev, details: void 0 }));
|
|
676
|
-
} else {
|
|
677
|
-
setErrors((prev) => ({ ...prev, details: result.error }));
|
|
678
|
-
}
|
|
679
|
-
} catch (err) {
|
|
680
|
-
setErrors((prev) => ({ ...prev, details: String(err) }));
|
|
681
|
-
} finally {
|
|
682
|
-
setLoading((prev) => ({ ...prev, details: false }));
|
|
683
|
-
}
|
|
684
|
-
};
|
|
685
|
-
fetchDetails();
|
|
686
|
-
}, [selectedPR, currentRepoSlug]);
|
|
629
|
+
refreshDetails();
|
|
630
|
+
}, [selectedPR, currentRepoSlug, refreshDetails]);
|
|
687
631
|
const handleRemoteSelect = useCallback(
|
|
688
632
|
(remoteName) => {
|
|
689
633
|
setSelectedRemote(remoteName);
|
|
@@ -697,69 +641,27 @@ function GitHubView() {
|
|
|
697
641
|
setSelectedPR(pr);
|
|
698
642
|
}, []);
|
|
699
643
|
const handleCreatePR = useCallback(() => {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const handleCreatePRSubmit = useCallback(
|
|
704
|
-
async (title, body) => {
|
|
705
|
-
if (!currentRepoSlug) return;
|
|
706
|
-
setLoading((prev) => ({ ...prev, createPR: true }));
|
|
707
|
-
setErrors((prev) => ({ ...prev, createPR: void 0 }));
|
|
708
|
-
try {
|
|
709
|
-
const result = await createPR(currentRepoSlug, title, body);
|
|
710
|
-
if (result.success) {
|
|
711
|
-
setShowCreatePR(false);
|
|
712
|
-
if (currentBranch) {
|
|
713
|
-
const prsResult = await listPRsForBranch(currentBranch, currentRepoSlug);
|
|
714
|
-
if (prsResult.success) {
|
|
715
|
-
setPrs(prsResult.data);
|
|
716
|
-
const newPR = prsResult.data.find((p) => p.number === result.data.number);
|
|
717
|
-
if (newPR) {
|
|
718
|
-
setSelectedPR(newPR);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
} else {
|
|
723
|
-
setErrors((prev) => ({ ...prev, createPR: result.error }));
|
|
724
|
-
}
|
|
725
|
-
} catch (err) {
|
|
726
|
-
setErrors((prev) => ({ ...prev, createPR: String(err) }));
|
|
727
|
-
} finally {
|
|
728
|
-
setLoading((prev) => ({ ...prev, createPR: false }));
|
|
729
|
-
}
|
|
730
|
-
},
|
|
731
|
-
[currentRepoSlug, currentBranch]
|
|
732
|
-
);
|
|
733
|
-
const handleCreatePRCancel = useCallback(() => {
|
|
734
|
-
setShowCreatePR(false);
|
|
735
|
-
setErrors((prev) => ({ ...prev, createPR: void 0 }));
|
|
644
|
+
exec3("gh pr create --web", () => {
|
|
645
|
+
process.stdout.emit("resize");
|
|
646
|
+
});
|
|
736
647
|
}, []);
|
|
737
648
|
useInput4(
|
|
738
649
|
(input) => {
|
|
739
|
-
if (showCreatePR) return;
|
|
740
650
|
if (input === "1") setFocusedBox("remotes");
|
|
741
651
|
if (input === "2") setFocusedBox("prs");
|
|
742
652
|
if (input === "3") setFocusedBox("details");
|
|
653
|
+
if (input === "r") {
|
|
654
|
+
if (focusedBox === "prs") refreshPRs();
|
|
655
|
+
if (focusedBox === "details") refreshDetails();
|
|
656
|
+
}
|
|
743
657
|
},
|
|
744
|
-
{ isActive:
|
|
658
|
+
{ isActive: isFocused }
|
|
745
659
|
);
|
|
746
660
|
if (isRepo === false) {
|
|
747
|
-
return /* @__PURE__ */
|
|
748
|
-
}
|
|
749
|
-
if (showCreatePR) {
|
|
750
|
-
return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx5(
|
|
751
|
-
CreatePRModal,
|
|
752
|
-
{
|
|
753
|
-
template: prTemplate,
|
|
754
|
-
onSubmit: handleCreatePRSubmit,
|
|
755
|
-
onCancel: handleCreatePRCancel,
|
|
756
|
-
loading: loading.createPR,
|
|
757
|
-
error: errors.createPR
|
|
758
|
-
}
|
|
759
|
-
) });
|
|
661
|
+
return /* @__PURE__ */ jsx4(TitledBox4, { borderStyle: "round", titles: ["Error"], flexGrow: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "red", children: "Current directory is not a git repository" }) });
|
|
760
662
|
}
|
|
761
|
-
return /* @__PURE__ */
|
|
762
|
-
/* @__PURE__ */
|
|
663
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", flexGrow: 1, children: [
|
|
664
|
+
/* @__PURE__ */ jsx4(
|
|
763
665
|
RemotesBox,
|
|
764
666
|
{
|
|
765
667
|
remotes,
|
|
@@ -767,10 +669,10 @@ function GitHubView() {
|
|
|
767
669
|
onSelect: handleRemoteSelect,
|
|
768
670
|
loading: loading.remotes,
|
|
769
671
|
error: errors.remotes,
|
|
770
|
-
isFocused: focusedBox === "remotes"
|
|
672
|
+
isFocused: isFocused && focusedBox === "remotes"
|
|
771
673
|
}
|
|
772
674
|
),
|
|
773
|
-
/* @__PURE__ */
|
|
675
|
+
/* @__PURE__ */ jsx4(
|
|
774
676
|
PullRequestsBox,
|
|
775
677
|
{
|
|
776
678
|
prs,
|
|
@@ -780,83 +682,923 @@ function GitHubView() {
|
|
|
780
682
|
loading: loading.prs,
|
|
781
683
|
error: errors.prs,
|
|
782
684
|
branch: currentBranch,
|
|
783
|
-
|
|
685
|
+
repoSlug: currentRepoSlug,
|
|
686
|
+
isFocused: isFocused && focusedBox === "prs"
|
|
784
687
|
}
|
|
785
688
|
),
|
|
786
|
-
/* @__PURE__ */
|
|
689
|
+
/* @__PURE__ */ jsx4(
|
|
787
690
|
PRDetailsBox,
|
|
788
691
|
{
|
|
789
692
|
pr: prDetails,
|
|
790
693
|
loading: loading.details,
|
|
791
694
|
error: errors.details,
|
|
792
|
-
isFocused: focusedBox === "details"
|
|
695
|
+
isFocused: isFocused && focusedBox === "details"
|
|
793
696
|
}
|
|
794
697
|
)
|
|
795
698
|
] });
|
|
796
699
|
}
|
|
797
700
|
|
|
798
|
-
// src/components/
|
|
799
|
-
import
|
|
800
|
-
import
|
|
801
|
-
import {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
701
|
+
// src/components/jira/JiraView.tsx
|
|
702
|
+
import { useCallback as useCallback2, useEffect as useEffect5, useState as useState7 } from "react";
|
|
703
|
+
import open2 from "open";
|
|
704
|
+
import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
|
|
705
|
+
import { Box as Box10, Text as Text10, useInput as useInput9 } from "ink";
|
|
706
|
+
|
|
707
|
+
// src/lib/jira/parser.ts
|
|
708
|
+
var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
709
|
+
function isValidTicketKeyFormat(key) {
|
|
710
|
+
return TICKET_KEY_PATTERN.test(key.toUpperCase());
|
|
711
|
+
}
|
|
712
|
+
function parseTicketKey(input) {
|
|
713
|
+
const trimmed = input.trim();
|
|
714
|
+
const urlMatch = trimmed.match(/\/browse\/([A-Za-z][A-Za-z0-9]+-\d+)/i);
|
|
715
|
+
if (urlMatch) {
|
|
716
|
+
return urlMatch[1].toUpperCase();
|
|
717
|
+
}
|
|
718
|
+
const upperInput = trimmed.toUpperCase();
|
|
719
|
+
if (isValidTicketKeyFormat(upperInput)) {
|
|
720
|
+
return upperInput;
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
function extractTicketKeyFromBranch(branchName) {
|
|
725
|
+
const match = branchName.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
|
|
726
|
+
if (match) {
|
|
727
|
+
const candidate = match[1].toUpperCase();
|
|
728
|
+
if (isValidTicketKeyFormat(candidate)) {
|
|
729
|
+
return candidate;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/lib/jira/config.ts
|
|
736
|
+
function isJiraConfigured(repoPath) {
|
|
737
|
+
const config = getRepoConfig(repoPath);
|
|
738
|
+
return !!(config.jiraSiteUrl && config.jiraEmail && config.jiraApiToken);
|
|
739
|
+
}
|
|
740
|
+
function getJiraSiteUrl(repoPath) {
|
|
741
|
+
const config = getRepoConfig(repoPath);
|
|
742
|
+
return config.jiraSiteUrl ?? null;
|
|
743
|
+
}
|
|
744
|
+
function setJiraSiteUrl(repoPath, siteUrl) {
|
|
745
|
+
updateRepoConfig(repoPath, { jiraSiteUrl: siteUrl });
|
|
746
|
+
}
|
|
747
|
+
function getJiraCredentials(repoPath) {
|
|
748
|
+
const config = getRepoConfig(repoPath);
|
|
749
|
+
return {
|
|
750
|
+
email: config.jiraEmail ?? null,
|
|
751
|
+
apiToken: config.jiraApiToken ?? null
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function setJiraCredentials(repoPath, email, apiToken) {
|
|
755
|
+
updateRepoConfig(repoPath, { jiraEmail: email, jiraApiToken: apiToken });
|
|
756
|
+
}
|
|
757
|
+
function getLinkedTickets(repoPath, branch) {
|
|
758
|
+
var _a;
|
|
759
|
+
const config = getRepoConfig(repoPath);
|
|
760
|
+
return ((_a = config.branchTickets) == null ? void 0 : _a[branch]) ?? [];
|
|
761
|
+
}
|
|
762
|
+
function addLinkedTicket(repoPath, branch, ticket) {
|
|
763
|
+
const config = getRepoConfig(repoPath);
|
|
764
|
+
const branchTickets = config.branchTickets ?? {};
|
|
765
|
+
const tickets = branchTickets[branch] ?? [];
|
|
766
|
+
if (tickets.some((t) => t.key === ticket.key)) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
updateRepoConfig(repoPath, {
|
|
770
|
+
branchTickets: {
|
|
771
|
+
...branchTickets,
|
|
772
|
+
[branch]: [...tickets, ticket]
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
function removeLinkedTicket(repoPath, branch, ticketKey) {
|
|
777
|
+
const config = getRepoConfig(repoPath);
|
|
778
|
+
const branchTickets = config.branchTickets ?? {};
|
|
779
|
+
const tickets = branchTickets[branch] ?? [];
|
|
780
|
+
updateRepoConfig(repoPath, {
|
|
781
|
+
branchTickets: {
|
|
782
|
+
...branchTickets,
|
|
783
|
+
[branch]: tickets.filter((t) => t.key !== ticketKey)
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
function updateTicketStatus(repoPath, branch, ticketKey, newStatus) {
|
|
788
|
+
const config = getRepoConfig(repoPath);
|
|
789
|
+
const branchTickets = config.branchTickets ?? {};
|
|
790
|
+
const tickets = branchTickets[branch] ?? [];
|
|
791
|
+
updateRepoConfig(repoPath, {
|
|
792
|
+
branchTickets: {
|
|
793
|
+
...branchTickets,
|
|
794
|
+
[branch]: tickets.map((t) => t.key === ticketKey ? { ...t, status: newStatus } : t)
|
|
814
795
|
}
|
|
815
796
|
});
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/lib/jira/api.ts
|
|
800
|
+
function createAuthHeader(email, apiToken) {
|
|
801
|
+
const credentials = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
802
|
+
return `Basic ${credentials}`;
|
|
803
|
+
}
|
|
804
|
+
async function jiraFetch(auth, endpoint, options) {
|
|
805
|
+
const url = `${auth.siteUrl}/rest/api/3${endpoint}`;
|
|
806
|
+
const method = (options == null ? void 0 : options.method) ?? "GET";
|
|
807
|
+
try {
|
|
808
|
+
const headers = {
|
|
809
|
+
Authorization: createAuthHeader(auth.email, auth.apiToken),
|
|
810
|
+
Accept: "application/json"
|
|
811
|
+
};
|
|
812
|
+
const fetchOptions = { method, headers };
|
|
813
|
+
if (options == null ? void 0 : options.body) {
|
|
814
|
+
headers["Content-Type"] = "application/json";
|
|
815
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
816
|
+
}
|
|
817
|
+
const response = await fetch(url, fetchOptions);
|
|
818
|
+
if (!response.ok) {
|
|
819
|
+
const text = await response.text();
|
|
820
|
+
return { ok: false, status: response.status, error: text };
|
|
821
|
+
}
|
|
822
|
+
if (response.status === 204) {
|
|
823
|
+
return { ok: true, status: response.status, data: null };
|
|
824
|
+
}
|
|
825
|
+
const data = await response.json();
|
|
826
|
+
return { ok: true, status: response.status, data };
|
|
827
|
+
} catch (err) {
|
|
828
|
+
const message = err instanceof Error ? err.message : "Network error";
|
|
829
|
+
return { ok: false, status: 0, error: message };
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async function validateCredentials(auth) {
|
|
833
|
+
const result = await jiraFetch(auth, "/myself");
|
|
834
|
+
if (!result.ok) {
|
|
835
|
+
if (result.status === 401 || result.status === 403) {
|
|
836
|
+
return {
|
|
837
|
+
success: false,
|
|
838
|
+
error: "Invalid credentials. Check your email and API token.",
|
|
839
|
+
errorType: "auth_error"
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
success: false,
|
|
844
|
+
error: result.error ?? "Failed to connect to Jira",
|
|
845
|
+
errorType: "api_error"
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
return { success: true, data: result.data };
|
|
849
|
+
}
|
|
850
|
+
async function getIssue(auth, ticketKey) {
|
|
851
|
+
const result = await jiraFetch(auth, `/issue/${ticketKey}?fields=summary,status`);
|
|
852
|
+
if (!result.ok) {
|
|
853
|
+
if (result.status === 401 || result.status === 403) {
|
|
854
|
+
return {
|
|
855
|
+
success: false,
|
|
856
|
+
error: "Authentication failed",
|
|
857
|
+
errorType: "auth_error"
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
if (result.status === 404) {
|
|
861
|
+
return {
|
|
862
|
+
success: false,
|
|
863
|
+
error: `Ticket ${ticketKey} not found`,
|
|
864
|
+
errorType: "invalid_ticket"
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
return {
|
|
868
|
+
success: false,
|
|
869
|
+
error: result.error ?? "Failed to fetch issue",
|
|
870
|
+
errorType: "api_error"
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
return { success: true, data: result.data };
|
|
874
|
+
}
|
|
875
|
+
async function getTransitions(auth, ticketKey) {
|
|
876
|
+
const result = await jiraFetch(auth, `/issue/${ticketKey}/transitions`);
|
|
877
|
+
if (!result.ok) {
|
|
878
|
+
if (result.status === 401 || result.status === 403) {
|
|
879
|
+
return {
|
|
880
|
+
success: false,
|
|
881
|
+
error: "Authentication failed",
|
|
882
|
+
errorType: "auth_error"
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
if (result.status === 404) {
|
|
886
|
+
return {
|
|
887
|
+
success: false,
|
|
888
|
+
error: `Ticket ${ticketKey} not found`,
|
|
889
|
+
errorType: "invalid_ticket"
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
success: false,
|
|
894
|
+
error: result.error ?? "Failed to fetch transitions",
|
|
895
|
+
errorType: "api_error"
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const data = result.data;
|
|
899
|
+
return { success: true, data: data.transitions };
|
|
900
|
+
}
|
|
901
|
+
async function applyTransition(auth, ticketKey, transitionId) {
|
|
902
|
+
const result = await jiraFetch(auth, `/issue/${ticketKey}/transitions`, {
|
|
903
|
+
method: "POST",
|
|
904
|
+
body: { transition: { id: transitionId } }
|
|
905
|
+
});
|
|
906
|
+
if (!result.ok) {
|
|
907
|
+
if (result.status === 401 || result.status === 403) {
|
|
908
|
+
return {
|
|
909
|
+
success: false,
|
|
910
|
+
error: "Authentication failed",
|
|
911
|
+
errorType: "auth_error"
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
if (result.status === 404) {
|
|
915
|
+
return {
|
|
916
|
+
success: false,
|
|
917
|
+
error: `Ticket ${ticketKey} not found`,
|
|
918
|
+
errorType: "invalid_ticket"
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
success: false,
|
|
923
|
+
error: result.error ?? "Failed to apply transition",
|
|
924
|
+
errorType: "api_error"
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
return { success: true, data: null };
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/components/jira/ChangeStatusModal.tsx
|
|
931
|
+
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
932
|
+
import { Box as Box5, Text as Text5, useInput as useInput5 } from "ink";
|
|
933
|
+
import SelectInput from "ink-select-input";
|
|
934
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
935
|
+
function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onCancel }) {
|
|
936
|
+
const [transitions, setTransitions] = useState4([]);
|
|
937
|
+
const [loading, setLoading] = useState4(true);
|
|
938
|
+
const [applying, setApplying] = useState4(false);
|
|
939
|
+
const [error, setError] = useState4(null);
|
|
940
|
+
useEffect4(() => {
|
|
941
|
+
const fetchTransitions = async () => {
|
|
942
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
943
|
+
const creds = getJiraCredentials(repoPath);
|
|
944
|
+
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
945
|
+
setError("Jira not configured");
|
|
946
|
+
setLoading(false);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
950
|
+
const result = await getTransitions(auth, ticketKey);
|
|
951
|
+
if (result.success) {
|
|
952
|
+
setTransitions(result.data);
|
|
953
|
+
} else {
|
|
954
|
+
setError(result.error);
|
|
955
|
+
}
|
|
956
|
+
setLoading(false);
|
|
957
|
+
};
|
|
958
|
+
fetchTransitions();
|
|
959
|
+
}, [repoPath, ticketKey]);
|
|
960
|
+
const handleSelect = async (item) => {
|
|
961
|
+
setApplying(true);
|
|
962
|
+
setError(null);
|
|
963
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
964
|
+
const creds = getJiraCredentials(repoPath);
|
|
965
|
+
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
966
|
+
setError("Jira not configured");
|
|
967
|
+
setApplying(false);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
971
|
+
const result = await applyTransition(auth, ticketKey, item.value);
|
|
972
|
+
if (result.success) {
|
|
973
|
+
const transition = transitions.find((t) => t.id === item.value);
|
|
974
|
+
const newStatus = (transition == null ? void 0 : transition.to.name) ?? item.label;
|
|
975
|
+
onComplete(newStatus);
|
|
976
|
+
} else {
|
|
977
|
+
setError(result.error);
|
|
978
|
+
setApplying(false);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
useInput5(
|
|
982
|
+
(_input, key) => {
|
|
983
|
+
if (key.escape && !applying) {
|
|
984
|
+
onCancel();
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
{ isActive: !applying }
|
|
988
|
+
);
|
|
989
|
+
const items = transitions.map((t) => ({
|
|
990
|
+
label: t.name,
|
|
991
|
+
value: t.id
|
|
992
|
+
}));
|
|
993
|
+
const initialIndex = Math.max(0, transitions.findIndex((t) => t.to.name === currentStatus));
|
|
994
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
|
|
995
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, color: "yellow", children: [
|
|
996
|
+
"Change Status: ",
|
|
997
|
+
ticketKey
|
|
998
|
+
] }),
|
|
999
|
+
loading && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Loading transitions..." }),
|
|
1000
|
+
error && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: error }) }),
|
|
1001
|
+
!loading && !error && transitions.length === 0 && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No available transitions" }),
|
|
1002
|
+
!loading && !error && transitions.length > 0 && !applying && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx5(SelectInput, { items, initialIndex, onSelect: handleSelect }) }),
|
|
1003
|
+
applying && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Updating status..." }) }),
|
|
1004
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Esc to cancel" }) })
|
|
1005
|
+
] });
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/components/jira/ConfigureJiraSiteModal.tsx
|
|
1009
|
+
import { useState as useState5 } from "react";
|
|
1010
|
+
import { Box as Box6, Text as Text6, useInput as useInput6 } from "ink";
|
|
1011
|
+
|
|
1012
|
+
// src/lib/editor.ts
|
|
1013
|
+
import { spawnSync } from "child_process";
|
|
1014
|
+
import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1015
|
+
import { tmpdir } from "os";
|
|
1016
|
+
import { join as join2 } from "path";
|
|
1017
|
+
function openInEditor(content, filename) {
|
|
1018
|
+
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
1019
|
+
const tempDir = mkdtempSync(join2(tmpdir(), "clairo-"));
|
|
1020
|
+
const tempFile = join2(tempDir, filename);
|
|
1021
|
+
try {
|
|
1022
|
+
writeFileSync2(tempFile, content);
|
|
1023
|
+
const result = spawnSync(editor, [tempFile], {
|
|
1024
|
+
stdio: "inherit"
|
|
1025
|
+
});
|
|
1026
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
1027
|
+
process.stdout.emit("resize");
|
|
1028
|
+
if (result.status !== 0) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
return readFileSync2(tempFile, "utf-8");
|
|
1032
|
+
} finally {
|
|
1033
|
+
try {
|
|
1034
|
+
rmSync(tempDir, { recursive: true });
|
|
1035
|
+
} catch {
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/components/jira/ConfigureJiraSiteModal.tsx
|
|
1041
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1042
|
+
function ConfigureJiraSiteModal({
|
|
1043
|
+
initialSiteUrl,
|
|
1044
|
+
initialEmail,
|
|
1045
|
+
onSubmit,
|
|
1046
|
+
onCancel,
|
|
1047
|
+
loading,
|
|
1048
|
+
error
|
|
1049
|
+
}) {
|
|
1050
|
+
const [siteUrl, setSiteUrl] = useState5(initialSiteUrl ?? "");
|
|
1051
|
+
const [email, setEmail] = useState5(initialEmail ?? "");
|
|
1052
|
+
const [apiToken, setApiToken] = useState5("");
|
|
1053
|
+
const [selectedItem, setSelectedItem] = useState5("siteUrl");
|
|
1054
|
+
const items = ["siteUrl", "email", "apiToken", "submit"];
|
|
1055
|
+
const canSubmit = siteUrl.trim() && email.trim() && apiToken.trim();
|
|
1056
|
+
useInput6(
|
|
1057
|
+
(input, key) => {
|
|
1058
|
+
if (loading) return;
|
|
1059
|
+
if (key.escape) {
|
|
1060
|
+
onCancel();
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (key.upArrow || input === "k") {
|
|
1064
|
+
setSelectedItem((prev) => {
|
|
1065
|
+
const idx = items.indexOf(prev);
|
|
1066
|
+
return items[Math.max(0, idx - 1)];
|
|
1067
|
+
});
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
if (key.downArrow || input === "j") {
|
|
1071
|
+
setSelectedItem((prev) => {
|
|
1072
|
+
const idx = items.indexOf(prev);
|
|
1073
|
+
return items[Math.min(items.length - 1, idx + 1)];
|
|
1074
|
+
});
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (key.return) {
|
|
1078
|
+
if (selectedItem === "siteUrl") {
|
|
1079
|
+
const newValue = openInEditor(siteUrl, "JIRA_SITE_URL.txt");
|
|
1080
|
+
if (newValue !== null) {
|
|
1081
|
+
setSiteUrl(newValue.split("\n")[0].trim());
|
|
1082
|
+
}
|
|
1083
|
+
} else if (selectedItem === "email") {
|
|
1084
|
+
const newValue = openInEditor(email, "JIRA_EMAIL.txt");
|
|
1085
|
+
if (newValue !== null) {
|
|
1086
|
+
setEmail(newValue.split("\n")[0].trim());
|
|
1087
|
+
}
|
|
1088
|
+
} else if (selectedItem === "apiToken") {
|
|
1089
|
+
const newValue = openInEditor(apiToken, "JIRA_API_TOKEN.txt");
|
|
1090
|
+
if (newValue !== null) {
|
|
1091
|
+
setApiToken(newValue.split("\n")[0].trim());
|
|
1092
|
+
}
|
|
1093
|
+
} else if (selectedItem === "submit" && canSubmit) {
|
|
1094
|
+
onSubmit(siteUrl.trim(), email.trim(), apiToken.trim());
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
{ isActive: !loading }
|
|
1099
|
+
);
|
|
1100
|
+
const renderItem = (item, label, value, isSensitive) => {
|
|
1101
|
+
const isSelected = selectedItem === item;
|
|
1102
|
+
const prefix = isSelected ? "> " : " ";
|
|
1103
|
+
const color = isSelected ? "yellow" : void 0;
|
|
1104
|
+
const displayValue = isSensitive && value ? "*".repeat(Math.min(value.length, 20)) : value;
|
|
1105
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
1106
|
+
/* @__PURE__ */ jsxs6(Text6, { color, bold: isSelected, children: [
|
|
1107
|
+
prefix,
|
|
1108
|
+
label
|
|
1109
|
+
] }),
|
|
1110
|
+
value !== void 0 && /* @__PURE__ */ jsx6(Box6, { marginLeft: 4, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: displayValue || "(empty - press Enter to edit)" }) })
|
|
1111
|
+
] });
|
|
1112
|
+
};
|
|
1113
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
|
|
1114
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: "Configure Jira Site" }),
|
|
1115
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Up/Down to select, Enter to edit, Esc to cancel" }),
|
|
1116
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1 }),
|
|
1117
|
+
error && /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: error }) }),
|
|
1118
|
+
renderItem("siteUrl", "Site URL (e.g., https://company.atlassian.net)", siteUrl),
|
|
1119
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1 }),
|
|
1120
|
+
renderItem("email", "Email", email),
|
|
1121
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1 }),
|
|
1122
|
+
renderItem("apiToken", "API Token", apiToken, true),
|
|
1123
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1 }),
|
|
1124
|
+
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
|
|
1125
|
+
selectedItem === "submit" ? "> " : " ",
|
|
1126
|
+
canSubmit ? "[Save Configuration]" : "[Fill all fields first]"
|
|
1127
|
+
] }) }),
|
|
1128
|
+
loading && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "Validating credentials..." }) }),
|
|
1129
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens" }) })
|
|
1130
|
+
] });
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/components/jira/LinkTicketModal.tsx
|
|
1134
|
+
import { useState as useState6 } from "react";
|
|
1135
|
+
import { Box as Box8, Text as Text8, useInput as useInput8 } from "ink";
|
|
1136
|
+
|
|
1137
|
+
// src/components/ui/TextInput.tsx
|
|
1138
|
+
import { Box as Box7, Text as Text7, useInput as useInput7 } from "ink";
|
|
1139
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1140
|
+
function TextInput({ value, onChange, placeholder, isActive, mask }) {
|
|
1141
|
+
useInput7(
|
|
1142
|
+
(input, key) => {
|
|
1143
|
+
if (key.backspace || key.delete) {
|
|
1144
|
+
if (value.length > 0) {
|
|
1145
|
+
onChange(value.slice(0, -1));
|
|
1146
|
+
}
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (key.return || key.escape || key.upArrow || key.downArrow || key.tab) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (input && input.length === 1 && input.charCodeAt(0) >= 32) {
|
|
1153
|
+
onChange(value + input);
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
{ isActive }
|
|
1157
|
+
);
|
|
1158
|
+
const displayValue = mask ? "*".repeat(value.length) : value;
|
|
1159
|
+
const showPlaceholder = value.length === 0 && placeholder;
|
|
1160
|
+
return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { children: [
|
|
1161
|
+
showPlaceholder ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx7(Text7, { children: displayValue }),
|
|
1162
|
+
isActive && /* @__PURE__ */ jsx7(Text7, { backgroundColor: "yellow", children: " " })
|
|
1163
|
+
] }) });
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/components/jira/LinkTicketModal.tsx
|
|
1167
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1168
|
+
function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
|
|
1169
|
+
const [ticketInput, setTicketInput] = useState6("");
|
|
1170
|
+
const canSubmit = ticketInput.trim().length > 0;
|
|
1171
|
+
useInput8(
|
|
1172
|
+
(_input, key) => {
|
|
1173
|
+
if (loading) return;
|
|
1174
|
+
if (key.escape) {
|
|
1175
|
+
onCancel();
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (key.return && canSubmit) {
|
|
1179
|
+
onSubmit(ticketInput.trim());
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
{ isActive: !loading }
|
|
1183
|
+
);
|
|
1184
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
|
|
1185
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "yellow", children: "Link Jira Ticket" }),
|
|
1186
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Type ticket ID, Enter to submit, Esc to cancel" }),
|
|
1187
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1 }),
|
|
1188
|
+
error && /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "red", children: error }) }),
|
|
1189
|
+
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
1190
|
+
/* @__PURE__ */ jsx8(Text8, { color: "blue", children: "Ticket: " }),
|
|
1191
|
+
/* @__PURE__ */ jsx8(
|
|
1192
|
+
TextInput,
|
|
1193
|
+
{
|
|
1194
|
+
value: ticketInput,
|
|
1195
|
+
onChange: setTicketInput,
|
|
1196
|
+
placeholder: "PROJ-123",
|
|
1197
|
+
isActive: !loading
|
|
1198
|
+
}
|
|
1199
|
+
)
|
|
1200
|
+
] }),
|
|
1201
|
+
loading && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Fetching ticket..." }) }),
|
|
1202
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Examples: PROJ-123 or https://company.atlassian.net/browse/PROJ-123" }) })
|
|
819
1203
|
] });
|
|
820
1204
|
}
|
|
821
1205
|
|
|
1206
|
+
// src/components/jira/TicketItem.tsx
|
|
1207
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
1208
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1209
|
+
function TicketItem({ ticketKey, summary, status, isHighlighted, isSelected }) {
|
|
1210
|
+
const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
|
|
1211
|
+
const textColor = isSelected ? "green" : void 0;
|
|
1212
|
+
return /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(Text9, { color: textColor, children: [
|
|
1213
|
+
prefix,
|
|
1214
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "blue", children: ticketKey }),
|
|
1215
|
+
" ",
|
|
1216
|
+
summary,
|
|
1217
|
+
status && /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
1218
|
+
" [",
|
|
1219
|
+
status,
|
|
1220
|
+
"]"
|
|
1221
|
+
] })
|
|
1222
|
+
] }) });
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/components/jira/JiraView.tsx
|
|
1226
|
+
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1227
|
+
function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
|
|
1228
|
+
const [repoPath, setRepoPath] = useState7(null);
|
|
1229
|
+
const [currentBranch, setCurrentBranch] = useState7(null);
|
|
1230
|
+
const [isRepo, setIsRepo] = useState7(null);
|
|
1231
|
+
const [jiraState, setJiraState] = useState7("not_configured");
|
|
1232
|
+
const [tickets, setTickets] = useState7([]);
|
|
1233
|
+
const [highlightedIndex, setHighlightedIndex] = useState7(0);
|
|
1234
|
+
const [showConfigureModal, setShowConfigureModal] = useState7(false);
|
|
1235
|
+
const [showLinkModal, setShowLinkModal] = useState7(false);
|
|
1236
|
+
const [showStatusModal, setShowStatusModal] = useState7(false);
|
|
1237
|
+
const [loading, setLoading] = useState7({ configure: false, link: false });
|
|
1238
|
+
const [errors, setErrors] = useState7({});
|
|
1239
|
+
useEffect5(() => {
|
|
1240
|
+
if (!isFocused) {
|
|
1241
|
+
setShowConfigureModal(false);
|
|
1242
|
+
setShowLinkModal(false);
|
|
1243
|
+
setShowStatusModal(false);
|
|
1244
|
+
setErrors({});
|
|
1245
|
+
}
|
|
1246
|
+
}, [isFocused]);
|
|
1247
|
+
useEffect5(() => {
|
|
1248
|
+
onModalChange == null ? void 0 : onModalChange(showConfigureModal || showLinkModal || showStatusModal);
|
|
1249
|
+
}, [showConfigureModal, showLinkModal, showStatusModal, onModalChange]);
|
|
1250
|
+
useEffect5(() => {
|
|
1251
|
+
if (!isFocused || showConfigureModal || showLinkModal || showStatusModal) {
|
|
1252
|
+
onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const bindings = [];
|
|
1256
|
+
if (jiraState === "not_configured") {
|
|
1257
|
+
bindings.push({ key: "c", label: "Configure Jira" });
|
|
1258
|
+
} else if (jiraState === "no_tickets") {
|
|
1259
|
+
bindings.push({ key: "l", label: "Link Ticket" });
|
|
1260
|
+
} else if (jiraState === "has_tickets") {
|
|
1261
|
+
bindings.push({ key: "l", label: "Link" });
|
|
1262
|
+
bindings.push({ key: "s", label: "Status" });
|
|
1263
|
+
bindings.push({ key: "d", label: "Unlink", color: "red" });
|
|
1264
|
+
bindings.push({ key: "o", label: "Open", color: "green" });
|
|
1265
|
+
bindings.push({ key: "y", label: "Copy Link" });
|
|
1266
|
+
}
|
|
1267
|
+
onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
|
|
1268
|
+
}, [isFocused, jiraState, showConfigureModal, showLinkModal, showStatusModal, onKeybindingsChange]);
|
|
1269
|
+
useEffect5(() => {
|
|
1270
|
+
const gitRepoCheck = isGitRepo();
|
|
1271
|
+
setIsRepo(gitRepoCheck);
|
|
1272
|
+
if (!gitRepoCheck) return;
|
|
1273
|
+
const rootResult = getRepoRoot();
|
|
1274
|
+
if (rootResult.success) {
|
|
1275
|
+
setRepoPath(rootResult.data);
|
|
1276
|
+
}
|
|
1277
|
+
const branchResult = getCurrentBranch();
|
|
1278
|
+
if (branchResult.success) {
|
|
1279
|
+
setCurrentBranch(branchResult.data);
|
|
1280
|
+
}
|
|
1281
|
+
}, []);
|
|
1282
|
+
useEffect5(() => {
|
|
1283
|
+
if (!repoPath || !currentBranch) return;
|
|
1284
|
+
if (!isJiraConfigured(repoPath)) {
|
|
1285
|
+
setJiraState("not_configured");
|
|
1286
|
+
setTickets([]);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
const linkedTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1290
|
+
setTickets(linkedTickets);
|
|
1291
|
+
setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
|
|
1292
|
+
}, [repoPath, currentBranch]);
|
|
1293
|
+
useEffect5(() => {
|
|
1294
|
+
if (!repoPath || !currentBranch) return;
|
|
1295
|
+
if (jiraState !== "no_tickets") return;
|
|
1296
|
+
const ticketKey = extractTicketKeyFromBranch(currentBranch);
|
|
1297
|
+
if (!ticketKey) return;
|
|
1298
|
+
const existingTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1299
|
+
if (existingTickets.some((t) => t.key === ticketKey)) return;
|
|
1300
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1301
|
+
const creds = getJiraCredentials(repoPath);
|
|
1302
|
+
if (!siteUrl || !creds.email || !creds.apiToken) return;
|
|
1303
|
+
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
1304
|
+
getIssue(auth, ticketKey).then((result) => {
|
|
1305
|
+
if (result.success) {
|
|
1306
|
+
const linkedTicket = {
|
|
1307
|
+
key: result.data.key,
|
|
1308
|
+
summary: result.data.fields.summary,
|
|
1309
|
+
status: result.data.fields.status.name,
|
|
1310
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1311
|
+
};
|
|
1312
|
+
addLinkedTicket(repoPath, currentBranch, linkedTicket);
|
|
1313
|
+
setTickets([linkedTicket]);
|
|
1314
|
+
setJiraState("has_tickets");
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
}, [repoPath, currentBranch, jiraState]);
|
|
1318
|
+
const refreshTickets = useCallback2(() => {
|
|
1319
|
+
if (!repoPath || !currentBranch) return;
|
|
1320
|
+
const linkedTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1321
|
+
setTickets(linkedTickets);
|
|
1322
|
+
setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
|
|
1323
|
+
}, [repoPath, currentBranch]);
|
|
1324
|
+
const handleConfigureSubmit = useCallback2(
|
|
1325
|
+
async (siteUrl, email, apiToken) => {
|
|
1326
|
+
if (!repoPath) return;
|
|
1327
|
+
setLoading((prev) => ({ ...prev, configure: true }));
|
|
1328
|
+
setErrors((prev) => ({ ...prev, configure: void 0 }));
|
|
1329
|
+
const auth = { siteUrl, email, apiToken };
|
|
1330
|
+
const result = await validateCredentials(auth);
|
|
1331
|
+
if (!result.success) {
|
|
1332
|
+
setErrors((prev) => ({ ...prev, configure: result.error }));
|
|
1333
|
+
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
setJiraSiteUrl(repoPath, siteUrl);
|
|
1337
|
+
setJiraCredentials(repoPath, email, apiToken);
|
|
1338
|
+
setShowConfigureModal(false);
|
|
1339
|
+
setJiraState("no_tickets");
|
|
1340
|
+
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1341
|
+
},
|
|
1342
|
+
[repoPath]
|
|
1343
|
+
);
|
|
1344
|
+
const handleLinkSubmit = useCallback2(
|
|
1345
|
+
async (ticketInput) => {
|
|
1346
|
+
if (!repoPath || !currentBranch) return;
|
|
1347
|
+
setLoading((prev) => ({ ...prev, link: true }));
|
|
1348
|
+
setErrors((prev) => ({ ...prev, link: void 0 }));
|
|
1349
|
+
const ticketKey = parseTicketKey(ticketInput);
|
|
1350
|
+
if (!ticketKey) {
|
|
1351
|
+
setErrors((prev) => ({ ...prev, link: "Invalid ticket format. Use PROJ-123 or a Jira URL." }));
|
|
1352
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1356
|
+
const creds = getJiraCredentials(repoPath);
|
|
1357
|
+
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1358
|
+
setErrors((prev) => ({ ...prev, link: "Jira not configured" }));
|
|
1359
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
1363
|
+
const result = await getIssue(auth, ticketKey);
|
|
1364
|
+
if (!result.success) {
|
|
1365
|
+
setErrors((prev) => ({ ...prev, link: result.error }));
|
|
1366
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const linkedTicket = {
|
|
1370
|
+
key: result.data.key,
|
|
1371
|
+
summary: result.data.fields.summary,
|
|
1372
|
+
status: result.data.fields.status.name,
|
|
1373
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1374
|
+
};
|
|
1375
|
+
addLinkedTicket(repoPath, currentBranch, linkedTicket);
|
|
1376
|
+
refreshTickets();
|
|
1377
|
+
setShowLinkModal(false);
|
|
1378
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1379
|
+
},
|
|
1380
|
+
[repoPath, currentBranch, refreshTickets]
|
|
1381
|
+
);
|
|
1382
|
+
const handleUnlinkTicket = useCallback2(() => {
|
|
1383
|
+
if (!repoPath || !currentBranch || tickets.length === 0) return;
|
|
1384
|
+
const ticket = tickets[highlightedIndex];
|
|
1385
|
+
if (ticket) {
|
|
1386
|
+
removeLinkedTicket(repoPath, currentBranch, ticket.key);
|
|
1387
|
+
refreshTickets();
|
|
1388
|
+
setHighlightedIndex((prev) => Math.max(0, prev - 1));
|
|
1389
|
+
}
|
|
1390
|
+
}, [repoPath, currentBranch, tickets, highlightedIndex, refreshTickets]);
|
|
1391
|
+
const handleOpenInBrowser = useCallback2(() => {
|
|
1392
|
+
if (!repoPath || tickets.length === 0) return;
|
|
1393
|
+
const ticket = tickets[highlightedIndex];
|
|
1394
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1395
|
+
if (ticket && siteUrl) {
|
|
1396
|
+
const url = `${siteUrl}/browse/${ticket.key}`;
|
|
1397
|
+
open2(url).catch(() => {
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
}, [repoPath, tickets, highlightedIndex]);
|
|
1401
|
+
useInput9(
|
|
1402
|
+
(input, key) => {
|
|
1403
|
+
if (showConfigureModal || showLinkModal || showStatusModal) return;
|
|
1404
|
+
if (input === "c" && jiraState === "not_configured") {
|
|
1405
|
+
setShowConfigureModal(true);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (input === "l" && jiraState !== "not_configured") {
|
|
1409
|
+
setShowLinkModal(true);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (jiraState === "has_tickets") {
|
|
1413
|
+
if (key.upArrow || input === "k") {
|
|
1414
|
+
setHighlightedIndex((prev) => Math.max(0, prev - 1));
|
|
1415
|
+
}
|
|
1416
|
+
if (key.downArrow || input === "j") {
|
|
1417
|
+
setHighlightedIndex((prev) => Math.min(tickets.length - 1, prev + 1));
|
|
1418
|
+
}
|
|
1419
|
+
if (input === "s") {
|
|
1420
|
+
setShowStatusModal(true);
|
|
1421
|
+
}
|
|
1422
|
+
if (input === "d") {
|
|
1423
|
+
handleUnlinkTicket();
|
|
1424
|
+
}
|
|
1425
|
+
if (input === "o") {
|
|
1426
|
+
handleOpenInBrowser();
|
|
1427
|
+
}
|
|
1428
|
+
if (input === "y" && repoPath) {
|
|
1429
|
+
const ticket = tickets[highlightedIndex];
|
|
1430
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1431
|
+
if (ticket && siteUrl) {
|
|
1432
|
+
const url = `${siteUrl}/browse/${ticket.key}`;
|
|
1433
|
+
copyToClipboard(url);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
{ isActive: isFocused && !showConfigureModal && !showLinkModal && !showStatusModal }
|
|
1439
|
+
);
|
|
1440
|
+
if (isRepo === false) {
|
|
1441
|
+
return /* @__PURE__ */ jsx10(TitledBox5, { borderStyle: "round", titles: ["Jira"], flexShrink: 0, children: /* @__PURE__ */ jsx10(Text10, { color: "red", children: "Not a git repository" }) });
|
|
1442
|
+
}
|
|
1443
|
+
if (showConfigureModal) {
|
|
1444
|
+
const siteUrl = repoPath ? getJiraSiteUrl(repoPath) : void 0;
|
|
1445
|
+
const creds = repoPath ? getJiraCredentials(repoPath) : { email: null, apiToken: null };
|
|
1446
|
+
return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx10(
|
|
1447
|
+
ConfigureJiraSiteModal,
|
|
1448
|
+
{
|
|
1449
|
+
initialSiteUrl: siteUrl ?? void 0,
|
|
1450
|
+
initialEmail: creds.email ?? void 0,
|
|
1451
|
+
onSubmit: handleConfigureSubmit,
|
|
1452
|
+
onCancel: () => {
|
|
1453
|
+
setShowConfigureModal(false);
|
|
1454
|
+
setErrors((prev) => ({ ...prev, configure: void 0 }));
|
|
1455
|
+
},
|
|
1456
|
+
loading: loading.configure,
|
|
1457
|
+
error: errors.configure
|
|
1458
|
+
}
|
|
1459
|
+
) });
|
|
1460
|
+
}
|
|
1461
|
+
if (showLinkModal) {
|
|
1462
|
+
return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx10(
|
|
1463
|
+
LinkTicketModal,
|
|
1464
|
+
{
|
|
1465
|
+
onSubmit: handleLinkSubmit,
|
|
1466
|
+
onCancel: () => {
|
|
1467
|
+
setShowLinkModal(false);
|
|
1468
|
+
setErrors((prev) => ({ ...prev, link: void 0 }));
|
|
1469
|
+
},
|
|
1470
|
+
loading: loading.link,
|
|
1471
|
+
error: errors.link
|
|
1472
|
+
}
|
|
1473
|
+
) });
|
|
1474
|
+
}
|
|
1475
|
+
if (showStatusModal && repoPath && currentBranch && tickets[highlightedIndex]) {
|
|
1476
|
+
const ticket = tickets[highlightedIndex];
|
|
1477
|
+
return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx10(
|
|
1478
|
+
ChangeStatusModal,
|
|
1479
|
+
{
|
|
1480
|
+
repoPath,
|
|
1481
|
+
ticketKey: ticket.key,
|
|
1482
|
+
currentStatus: ticket.status,
|
|
1483
|
+
onComplete: (newStatus) => {
|
|
1484
|
+
updateTicketStatus(repoPath, currentBranch, ticket.key, newStatus);
|
|
1485
|
+
setShowStatusModal(false);
|
|
1486
|
+
refreshTickets();
|
|
1487
|
+
},
|
|
1488
|
+
onCancel: () => setShowStatusModal(false)
|
|
1489
|
+
}
|
|
1490
|
+
) });
|
|
1491
|
+
}
|
|
1492
|
+
const title = "[4] Jira";
|
|
1493
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
1494
|
+
return /* @__PURE__ */ jsx10(TitledBox5, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
|
|
1495
|
+
jiraState === "not_configured" && /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "No Jira site configured" }),
|
|
1496
|
+
jiraState === "no_tickets" && /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "No tickets linked to this branch" }),
|
|
1497
|
+
jiraState === "has_tickets" && tickets.map((ticket, idx) => /* @__PURE__ */ jsx10(
|
|
1498
|
+
TicketItem,
|
|
1499
|
+
{
|
|
1500
|
+
ticketKey: ticket.key,
|
|
1501
|
+
summary: ticket.summary,
|
|
1502
|
+
status: ticket.status,
|
|
1503
|
+
isHighlighted: idx === highlightedIndex
|
|
1504
|
+
},
|
|
1505
|
+
ticket.key
|
|
1506
|
+
))
|
|
1507
|
+
] }) });
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/components/ui/KeybindingsBar.tsx
|
|
1511
|
+
import { Box as Box11, Text as Text11 } from "ink";
|
|
1512
|
+
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1513
|
+
var globalBindings = [
|
|
1514
|
+
{ key: "1-4", label: "Focus" },
|
|
1515
|
+
{ key: "j/k", label: "Navigate" },
|
|
1516
|
+
{ key: "Ctrl+C", label: "Quit" }
|
|
1517
|
+
];
|
|
1518
|
+
var modalBindings = [
|
|
1519
|
+
{ key: "Esc", label: "Cancel" }
|
|
1520
|
+
];
|
|
1521
|
+
function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
|
|
1522
|
+
const allBindings = modalOpen ? [...contextBindings, ...modalBindings] : [...contextBindings, ...globalBindings];
|
|
1523
|
+
return /* @__PURE__ */ jsx11(Box11, { flexShrink: 0, paddingX: 1, gap: 2, children: allBindings.map((binding) => /* @__PURE__ */ jsxs11(Box11, { gap: 1, children: [
|
|
1524
|
+
/* @__PURE__ */ jsx11(Text11, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
|
|
1525
|
+
/* @__PURE__ */ jsx11(Text11, { dimColor: true, children: binding.label })
|
|
1526
|
+
] }, binding.key)) });
|
|
1527
|
+
}
|
|
1528
|
+
|
|
822
1529
|
// src/app.tsx
|
|
823
|
-
import { jsx as
|
|
1530
|
+
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
824
1531
|
function App() {
|
|
825
|
-
|
|
1532
|
+
const { exit } = useApp();
|
|
1533
|
+
const [focusedView, setFocusedView] = useState8("github");
|
|
1534
|
+
const [modalOpen, setModalOpen] = useState8(false);
|
|
1535
|
+
const [contextBindings, setContextBindings] = useState8([]);
|
|
1536
|
+
useInput10(
|
|
1537
|
+
(input, key) => {
|
|
1538
|
+
if (key.ctrl && input === "c") {
|
|
1539
|
+
exit();
|
|
1540
|
+
}
|
|
1541
|
+
if (input === "1" || input === "2" || input === "3") {
|
|
1542
|
+
setFocusedView("github");
|
|
1543
|
+
}
|
|
1544
|
+
if (input === "4") {
|
|
1545
|
+
setFocusedView("jira");
|
|
1546
|
+
}
|
|
1547
|
+
},
|
|
1548
|
+
{ isActive: !modalOpen }
|
|
1549
|
+
);
|
|
1550
|
+
return /* @__PURE__ */ jsxs12(Box12, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children: [
|
|
1551
|
+
/* @__PURE__ */ jsx12(
|
|
1552
|
+
GitHubView,
|
|
1553
|
+
{
|
|
1554
|
+
isFocused: focusedView === "github",
|
|
1555
|
+
onKeybindingsChange: focusedView === "github" ? setContextBindings : void 0
|
|
1556
|
+
}
|
|
1557
|
+
),
|
|
1558
|
+
/* @__PURE__ */ jsx12(
|
|
1559
|
+
JiraView,
|
|
1560
|
+
{
|
|
1561
|
+
isFocused: focusedView === "jira",
|
|
1562
|
+
onModalChange: setModalOpen,
|
|
1563
|
+
onKeybindingsChange: focusedView === "jira" ? setContextBindings : void 0
|
|
1564
|
+
}
|
|
1565
|
+
),
|
|
1566
|
+
/* @__PURE__ */ jsx12(KeybindingsBar, { contextBindings, modalOpen })
|
|
1567
|
+
] });
|
|
826
1568
|
}
|
|
827
1569
|
|
|
828
1570
|
// src/lib/render.tsx
|
|
829
1571
|
import { render as inkRender } from "ink";
|
|
830
1572
|
|
|
831
1573
|
// src/lib/Screen.tsx
|
|
832
|
-
import { Box as
|
|
833
|
-
import { useCallback as
|
|
834
|
-
import { jsx as
|
|
1574
|
+
import { Box as Box13, useStdout } from "ink";
|
|
1575
|
+
import { useCallback as useCallback3, useEffect as useEffect6, useState as useState9 } from "react";
|
|
1576
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
835
1577
|
function Screen({ children }) {
|
|
836
1578
|
const { stdout } = useStdout();
|
|
837
|
-
const getSize =
|
|
1579
|
+
const getSize = useCallback3(
|
|
838
1580
|
() => ({ height: stdout.rows, width: stdout.columns }),
|
|
839
1581
|
[stdout]
|
|
840
1582
|
);
|
|
841
|
-
const [size, setSize] =
|
|
842
|
-
|
|
1583
|
+
const [size, setSize] = useState9(getSize);
|
|
1584
|
+
useEffect6(() => {
|
|
843
1585
|
const onResize = () => setSize(getSize());
|
|
844
1586
|
stdout.on("resize", onResize);
|
|
845
1587
|
return () => {
|
|
846
1588
|
stdout.off("resize", onResize);
|
|
847
1589
|
};
|
|
848
1590
|
}, [stdout, getSize]);
|
|
849
|
-
return /* @__PURE__ */
|
|
1591
|
+
return /* @__PURE__ */ jsx13(Box13, { height: size.height, width: size.width, children });
|
|
850
1592
|
}
|
|
851
1593
|
|
|
852
1594
|
// src/lib/render.tsx
|
|
853
|
-
import { jsx as
|
|
1595
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
854
1596
|
var ENTER_ALT_BUFFER = "\x1B[?1049h";
|
|
855
1597
|
var EXIT_ALT_BUFFER = "\x1B[?1049l";
|
|
856
1598
|
var CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
857
1599
|
function render(node, options) {
|
|
858
1600
|
process.stdout.write(ENTER_ALT_BUFFER + CLEAR_SCREEN);
|
|
859
|
-
const element = /* @__PURE__ */
|
|
1601
|
+
const element = /* @__PURE__ */ jsx14(Screen, { children: node });
|
|
860
1602
|
const instance = inkRender(element, options);
|
|
861
1603
|
setImmediate(() => instance.rerender(element));
|
|
862
1604
|
const cleanup = () => process.stdout.write(EXIT_ALT_BUFFER);
|
|
@@ -877,7 +1619,7 @@ function render(node, options) {
|
|
|
877
1619
|
}
|
|
878
1620
|
|
|
879
1621
|
// src/cli.tsx
|
|
880
|
-
import { jsx as
|
|
1622
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
881
1623
|
meow(
|
|
882
1624
|
`
|
|
883
1625
|
Usage
|
|
@@ -899,4 +1641,4 @@ meow(
|
|
|
899
1641
|
}
|
|
900
1642
|
}
|
|
901
1643
|
);
|
|
902
|
-
render(/* @__PURE__ */
|
|
1644
|
+
render(/* @__PURE__ */ jsx15(App, {}));
|