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.
Files changed (3) hide show
  1. package/README.md +22 -241
  2. package/dist/cli.js +1114 -372
  3. 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 { Box as Box7 } from "ink";
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 { useCallback, useEffect as useEffect3, useState as useState4 } from "react";
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 Box5, Text as Text5, useInput as useInput4 } from "ink";
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 --head "${branch}" --json ${fields} --repo "${repo}"`
184
+ `gh pr list --state open --json ${fields},headRefName --repo "${repo}" 2>/dev/null`
177
185
  );
178
- const prs = JSON.parse(stdout);
179
- return { success: true, data: prs };
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 as Box2, Text as Text2 } from "ink";
393
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
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 title = "3 PR Details";
417
- const borderColor = isFocused ? "cyan" : void 0;
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 mergeableColor = (pr == null ? void 0 : pr.mergeable) === "MERGEABLE" ? "green" : (pr == null ? void 0 : pr.mergeable) === "CONFLICTING" ? "red" : "yellow";
422
- return /* @__PURE__ */ jsx2(TitledBox, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 2, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
423
- loading && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Loading details..." }),
424
- error && /* @__PURE__ */ jsx2(Text2, { color: "red", children: error }),
425
- !loading && !error && !pr && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Select a PR to view details" }),
426
- !loading && !error && pr && /* @__PURE__ */ jsxs2(Fragment, { children: [
427
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: pr.title }),
428
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
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__ */ jsxs2(Box2, { marginTop: 1, children: [
436
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Review: " }),
437
- /* @__PURE__ */ jsx2(Text2, { color: reviewColor, children: reviewStatus }),
438
- /* @__PURE__ */ jsx2(Text2, { children: " | " }),
439
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Merge: " }),
440
- /* @__PURE__ */ jsx2(Text2, { color: mergeableColor, children: pr.mergeable ?? "UNKNOWN" })
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
- (((_c = pr.assignees) == null ? void 0 : _c.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
443
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Assignees: " }),
444
- /* @__PURE__ */ jsx2(Text2, { children: pr.assignees.map((a) => a.login).join(", ") })
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
- (((_d = pr.reviewRequests) == null ? void 0 : _d.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { children: [
447
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Reviewers: " }),
448
- /* @__PURE__ */ jsx2(Text2, { children: pr.reviewRequests.map((r) => r.login ?? r.name ?? r.slug ?? "Team").join(", ") })
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
- (((_e = pr.statusCheckRollup) == null ? void 0 : _e.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
451
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Checks:" }),
452
- (_f = pr.statusCheckRollup) == null ? void 0 : _f.map((check, idx) => /* @__PURE__ */ jsxs2(Text2, { color: getCheckColor(check), children: [
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__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
460
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Description:" }),
461
- /* @__PURE__ */ jsx2(Text2, { children: pr.body })
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 as useState2 } from "react";
363
+ import { useEffect, useState } from "react";
469
364
  import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
470
- import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
471
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
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] = useState2(0);
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 ? "cyan" : void 0;
510
- return /* @__PURE__ */ jsx3(TitledBox2, { borderStyle: "round", titles: [`${title}${subtitle}`], borderColor, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, children: [
511
- loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
512
- error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
513
- !loading && !error && /* @__PURE__ */ jsxs3(Fragment2, { children: [
514
- prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }),
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__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
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__ */ jsxs3(Text3, { color: "blue", children: [
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 useState3 } from "react";
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 Box4, Text as Text4, useInput as useInput3 } from "ink";
540
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
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] = useState3(0);
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 ? "cyan" : void 0;
564
- return /* @__PURE__ */ jsx4(TitledBox3, { borderStyle: "round", titles: [title], borderColor, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, children: [
565
- loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
566
- error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
567
- !loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
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__ */ jsxs4(Text4, { color: isSelected ? "green" : void 0, children: [
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 jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
585
- function GitHubView() {
586
- const [isRepo, setIsRepo] = useState4(null);
587
- const [repoPath, setRepoPath] = useState4(null);
588
- const [remotes, setRemotes] = useState4([]);
589
- const [currentBranch, setCurrentBranch] = useState4(null);
590
- const [currentRepoSlug, setCurrentRepoSlug] = useState4(null);
591
- const [selectedRemote, setSelectedRemote] = useState4(null);
592
- const [selectedPR, setSelectedPR] = useState4(null);
593
- const [prs, setPrs] = useState4([]);
594
- const [prDetails, setPrDetails] = useState4(null);
595
- const [loading, setLoading] = useState4({
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] = useState4({});
602
- const [showCreatePR, setShowCreatePR] = useState4(false);
603
- const [prTemplate, setPrTemplate] = useState4(null);
604
- const [focusedBox, setFocusedBox] = useState4("remotes");
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
- setLoading((prev) => ({ ...prev, details: true }));
670
- const fetchDetails = async () => {
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
- setShowCreatePR(true);
701
- setErrors((prev) => ({ ...prev, createPR: void 0 }));
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: !showCreatePR }
658
+ { isActive: isFocused }
745
659
  );
746
660
  if (isRepo === false) {
747
- return /* @__PURE__ */ jsx5(TitledBox4, { borderStyle: "round", titles: ["Error"], flexGrow: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: "Current directory is not a git repository" }) });
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__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, children: [
762
- /* @__PURE__ */ jsx5(
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__ */ jsx5(
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
- isFocused: focusedBox === "prs"
685
+ repoSlug: currentRepoSlug,
686
+ isFocused: isFocused && focusedBox === "prs"
784
687
  }
785
688
  ),
786
- /* @__PURE__ */ jsx5(
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/ui/Tabs.tsx
799
- import React, { useState as useState5 } from "react";
800
- import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
801
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
802
- function TabItem({ children }) {
803
- return /* @__PURE__ */ jsx6(Box6, { flexGrow: 1, children });
804
- }
805
- function Tabs({ children, defaultTab }) {
806
- const childArray = React.Children.toArray(children);
807
- const tabNames = childArray.map((child) => child.props.name);
808
- const [activeTab, setActiveTab] = useState5(defaultTab ?? tabNames[0]);
809
- useInput5((_input, key) => {
810
- if (key.tab && activeTab) {
811
- const currentIndex = tabNames.indexOf(activeTab);
812
- const newIndex = key.shift ? currentIndex === 0 ? tabNames.length - 1 : currentIndex - 1 : (currentIndex + 1) % tabNames.length;
813
- setActiveTab(tabNames[newIndex]);
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
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", flexGrow: 1, minHeight: 0, children: [
817
- /* @__PURE__ */ jsx6(Box6, { paddingX: 1, gap: 1, flexShrink: 0, children: tabNames.map((name) => /* @__PURE__ */ jsx6(Text6, { inverse: activeTab === name, bold: activeTab === name, children: `${name} ` }, name)) }),
818
- /* @__PURE__ */ jsx6(Box6, { flexGrow: 1, marginTop: 1, overflow: "hidden", children: childArray.map((child) => /* @__PURE__ */ jsx6(Box6, { display: child.props.name === activeTab ? "flex" : "none", flexGrow: 1, children: child }, child.props.name)) })
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 jsx7 } from "react/jsx-runtime";
1530
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
824
1531
  function App() {
825
- return /* @__PURE__ */ jsx7(Box7, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children: /* @__PURE__ */ jsx7(Tabs, { children: /* @__PURE__ */ jsx7(TabItem, { name: "GitHub", children: /* @__PURE__ */ jsx7(GitHubView, {}) }) }) });
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 Box8, useStdout } from "ink";
833
- import { useCallback as useCallback2, useEffect as useEffect4, useState as useState6 } from "react";
834
- import { jsx as jsx8 } from "react/jsx-runtime";
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 = useCallback2(
1579
+ const getSize = useCallback3(
838
1580
  () => ({ height: stdout.rows, width: stdout.columns }),
839
1581
  [stdout]
840
1582
  );
841
- const [size, setSize] = useState6(getSize);
842
- useEffect4(() => {
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__ */ jsx8(Box8, { height: size.height, width: size.width, children });
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 jsx9 } from "react/jsx-runtime";
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__ */ jsx9(Screen, { children: node });
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 jsx10 } from "react/jsx-runtime";
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__ */ jsx10(App, {}));
1644
+ render(/* @__PURE__ */ jsx15(App, {}));