clairo 1.0.6 → 1.0.8
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/dist/cli.js +1318 -877
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,15 +4,31 @@
|
|
|
4
4
|
import meow from "meow";
|
|
5
5
|
|
|
6
6
|
// src/app.tsx
|
|
7
|
-
import { useCallback as
|
|
7
|
+
import { useCallback as useCallback10, useMemo as useMemo2, useState as useState17 } from "react";
|
|
8
8
|
import { Box as Box16, useApp, useInput as useInput13 } from "ink";
|
|
9
9
|
|
|
10
10
|
// src/components/github/GitHubView.tsx
|
|
11
|
-
import {
|
|
12
|
-
import { useCallback, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
|
|
11
|
+
import { useCallback as useCallback9, useEffect as useEffect9, useRef as useRef5, useState as useState12 } from "react";
|
|
13
12
|
import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
|
|
14
13
|
import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
|
|
15
14
|
|
|
15
|
+
// src/hooks/github/useGitRepo.ts
|
|
16
|
+
import { useCallback, useEffect as useEffect2, useMemo, useState as useState2 } from "react";
|
|
17
|
+
|
|
18
|
+
// src/lib/duckEvents.ts
|
|
19
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
20
|
+
var duckEvents = {
|
|
21
|
+
emit: (event) => {
|
|
22
|
+
listeners.forEach((fn) => fn(event));
|
|
23
|
+
},
|
|
24
|
+
subscribe: (fn) => {
|
|
25
|
+
listeners.add(fn);
|
|
26
|
+
return () => {
|
|
27
|
+
listeners.delete(fn);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
16
32
|
// src/lib/config/index.ts
|
|
17
33
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
18
34
|
import { homedir } from "os";
|
|
@@ -176,9 +192,7 @@ async function isGhAuthenticated() {
|
|
|
176
192
|
function getRepoFromRemote(remoteUrl) {
|
|
177
193
|
const sshMatch = remoteUrl.match(/git@github\.com:(.+?)(?:\.git)?$/);
|
|
178
194
|
if (sshMatch) return sshMatch[1].replace(/\.git$/, "");
|
|
179
|
-
const httpsMatch = remoteUrl.match(
|
|
180
|
-
/https:\/\/github\.com\/(.+?)(?:\.git)?$/
|
|
181
|
-
);
|
|
195
|
+
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/(.+?)(?:\.git)?$/);
|
|
182
196
|
if (httpsMatch) return httpsMatch[1].replace(/\.git$/, "");
|
|
183
197
|
return null;
|
|
184
198
|
}
|
|
@@ -199,9 +213,7 @@ async function listPRsForBranch(branch, repo) {
|
|
|
199
213
|
}
|
|
200
214
|
const fields = "number,title,state,author,createdAt,isDraft";
|
|
201
215
|
try {
|
|
202
|
-
const { stdout } = await execAsync(
|
|
203
|
-
`gh pr view --json ${fields} 2>/dev/null`
|
|
204
|
-
);
|
|
216
|
+
const { stdout } = await execAsync(`gh pr view --json ${fields} 2>/dev/null`);
|
|
205
217
|
const pr = JSON.parse(stdout);
|
|
206
218
|
return { success: true, data: [pr] };
|
|
207
219
|
} catch {
|
|
@@ -211,10 +223,8 @@ async function listPRsForBranch(branch, repo) {
|
|
|
211
223
|
`gh pr list --state open --json ${fields},headRefName --repo "${repo}" 2>/dev/null`
|
|
212
224
|
);
|
|
213
225
|
const allPrs = JSON.parse(stdout);
|
|
214
|
-
const prs = allPrs.filter(
|
|
215
|
-
|
|
216
|
-
);
|
|
217
|
-
const result = prs.map(({ headRefName: _, ...rest }) => rest);
|
|
226
|
+
const prs = allPrs.filter((pr) => pr.headRefName === branch || pr.headRefName.endsWith(`:${branch}`));
|
|
227
|
+
const result = prs.map(({ headRefName: _headRefName, ...rest }) => rest);
|
|
218
228
|
return { success: true, data: result };
|
|
219
229
|
} catch {
|
|
220
230
|
return { success: false, error: "Failed to fetch PRs", errorType: "api_error" };
|
|
@@ -254,9 +264,7 @@ async function getPRDetails(prNumber, repo) {
|
|
|
254
264
|
"reviews",
|
|
255
265
|
"statusCheckRollup"
|
|
256
266
|
].join(",");
|
|
257
|
-
const { stdout } = await execAsync(
|
|
258
|
-
`gh pr view ${prNumber} --json ${fields} --repo "${repo}"`
|
|
259
|
-
);
|
|
267
|
+
const { stdout } = await execAsync(`gh pr view ${prNumber} --json ${fields} --repo "${repo}"`);
|
|
260
268
|
const pr = JSON.parse(stdout);
|
|
261
269
|
return { success: true, data: pr };
|
|
262
270
|
} catch {
|
|
@@ -267,6 +275,274 @@ async function getPRDetails(prNumber, repo) {
|
|
|
267
275
|
};
|
|
268
276
|
}
|
|
269
277
|
}
|
|
278
|
+
function openPRCreationPage(owner, branch, onComplete) {
|
|
279
|
+
const headFlag = `${owner}:${branch}`;
|
|
280
|
+
exec(`gh pr create --web --head "${headFlag}"`, (error) => {
|
|
281
|
+
process.stdout.emit("resize");
|
|
282
|
+
onComplete == null ? void 0 : onComplete(error);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/hooks/useTerminalFocus.ts
|
|
287
|
+
import { useEffect, useState } from "react";
|
|
288
|
+
function useTerminalFocus() {
|
|
289
|
+
const [isFocused, setIsFocused] = useState(null);
|
|
290
|
+
const [focusCount, setFocusCount] = useState(0);
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
process.stdout.write("\x1B[?1004h");
|
|
293
|
+
const handleData = (data) => {
|
|
294
|
+
const str = data.toString();
|
|
295
|
+
if (str.includes("\x1B[I")) {
|
|
296
|
+
setIsFocused(true);
|
|
297
|
+
setFocusCount((c) => c + 1);
|
|
298
|
+
}
|
|
299
|
+
if (str.includes("\x1B[O")) {
|
|
300
|
+
setIsFocused(false);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
process.stdin.on("data", handleData);
|
|
304
|
+
return () => {
|
|
305
|
+
process.stdout.write("\x1B[?1004l");
|
|
306
|
+
process.stdin.off("data", handleData);
|
|
307
|
+
};
|
|
308
|
+
}, []);
|
|
309
|
+
return { isFocused, focusCount };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/hooks/github/useGitRepo.ts
|
|
313
|
+
function useGitRepo() {
|
|
314
|
+
const [isRepo, setIsRepo] = useState2(null);
|
|
315
|
+
const [repoPath, setRepoPath] = useState2(null);
|
|
316
|
+
const [remotes, setRemotes] = useState2([]);
|
|
317
|
+
const [currentBranch, setCurrentBranch] = useState2(null);
|
|
318
|
+
const [selectedRemote, setSelectedRemote] = useState2(null);
|
|
319
|
+
const [loading, setLoading] = useState2(true);
|
|
320
|
+
const [error, setError] = useState2(void 0);
|
|
321
|
+
const { focusCount } = useTerminalFocus();
|
|
322
|
+
const currentRepoSlug = useMemo(() => {
|
|
323
|
+
if (!selectedRemote) return null;
|
|
324
|
+
const remote = remotes.find((r) => r.name === selectedRemote);
|
|
325
|
+
if (!remote) return null;
|
|
326
|
+
return getRepoFromRemote(remote.url);
|
|
327
|
+
}, [selectedRemote, remotes]);
|
|
328
|
+
useEffect2(() => {
|
|
329
|
+
const gitRepoCheck = isGitRepo();
|
|
330
|
+
setIsRepo(gitRepoCheck);
|
|
331
|
+
if (!gitRepoCheck) {
|
|
332
|
+
setLoading(false);
|
|
333
|
+
setError("Not a git repository");
|
|
334
|
+
duckEvents.emit("error");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const rootResult = getRepoRoot();
|
|
338
|
+
if (rootResult.success) {
|
|
339
|
+
setRepoPath(rootResult.data);
|
|
340
|
+
}
|
|
341
|
+
const branchResult = getCurrentBranch();
|
|
342
|
+
if (branchResult.success) {
|
|
343
|
+
setCurrentBranch(branchResult.data);
|
|
344
|
+
}
|
|
345
|
+
const remotesResult = listRemotes();
|
|
346
|
+
if (!remotesResult.success) {
|
|
347
|
+
setError(remotesResult.error);
|
|
348
|
+
duckEvents.emit("error");
|
|
349
|
+
setLoading(false);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
setRemotes(remotesResult.data);
|
|
353
|
+
const remoteNames = remotesResult.data.map((r) => r.name);
|
|
354
|
+
const defaultRemote = getSelectedRemote(rootResult.success ? rootResult.data : "", remoteNames);
|
|
355
|
+
setSelectedRemote(defaultRemote);
|
|
356
|
+
setLoading(false);
|
|
357
|
+
}, []);
|
|
358
|
+
useEffect2(() => {
|
|
359
|
+
if (!isRepo || focusCount === 0) return;
|
|
360
|
+
const result = getCurrentBranch();
|
|
361
|
+
if (result.success && result.data !== currentBranch) {
|
|
362
|
+
setCurrentBranch(result.data);
|
|
363
|
+
}
|
|
364
|
+
}, [isRepo, focusCount]);
|
|
365
|
+
const selectRemote = useCallback(
|
|
366
|
+
(remoteName) => {
|
|
367
|
+
setSelectedRemote(remoteName);
|
|
368
|
+
if (repoPath) {
|
|
369
|
+
updateRepoConfig(repoPath, { selectedRemote: remoteName });
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
[repoPath]
|
|
373
|
+
);
|
|
374
|
+
const refreshBranch = useCallback(() => {
|
|
375
|
+
const branchResult = getCurrentBranch();
|
|
376
|
+
if (branchResult.success) {
|
|
377
|
+
setCurrentBranch(branchResult.data);
|
|
378
|
+
return branchResult.data;
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}, []);
|
|
382
|
+
return {
|
|
383
|
+
isRepo,
|
|
384
|
+
repoPath,
|
|
385
|
+
remotes,
|
|
386
|
+
currentBranch,
|
|
387
|
+
selectedRemote,
|
|
388
|
+
currentRepoSlug,
|
|
389
|
+
selectRemote,
|
|
390
|
+
refreshBranch,
|
|
391
|
+
loading,
|
|
392
|
+
error
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/hooks/github/usePRPolling.ts
|
|
397
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useRef, useState as useState3 } from "react";
|
|
398
|
+
function usePRPolling() {
|
|
399
|
+
const prNumbersBeforeCreate = useRef(/* @__PURE__ */ new Set());
|
|
400
|
+
const pollingIntervalRef = useRef(null);
|
|
401
|
+
const [isPolling, setIsPolling] = useState3(false);
|
|
402
|
+
const stopPolling = useCallback2(() => {
|
|
403
|
+
if (pollingIntervalRef.current) {
|
|
404
|
+
clearInterval(pollingIntervalRef.current);
|
|
405
|
+
pollingIntervalRef.current = null;
|
|
406
|
+
}
|
|
407
|
+
setIsPolling(false);
|
|
408
|
+
}, []);
|
|
409
|
+
const startPolling = useCallback2(
|
|
410
|
+
(options) => {
|
|
411
|
+
const {
|
|
412
|
+
branch,
|
|
413
|
+
repoSlug,
|
|
414
|
+
existingPRNumbers,
|
|
415
|
+
onNewPR,
|
|
416
|
+
onPRsUpdated,
|
|
417
|
+
maxAttempts = 24,
|
|
418
|
+
pollInterval = 5e3
|
|
419
|
+
} = options;
|
|
420
|
+
stopPolling();
|
|
421
|
+
prNumbersBeforeCreate.current = new Set(existingPRNumbers);
|
|
422
|
+
let attempts = 0;
|
|
423
|
+
setIsPolling(true);
|
|
424
|
+
pollingIntervalRef.current = setInterval(async () => {
|
|
425
|
+
attempts++;
|
|
426
|
+
if (attempts > maxAttempts) {
|
|
427
|
+
stopPolling();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const result = await listPRsForBranch(branch, repoSlug);
|
|
431
|
+
if (result.success) {
|
|
432
|
+
onPRsUpdated(result.data);
|
|
433
|
+
const newPR = result.data.find((pr) => !prNumbersBeforeCreate.current.has(pr.number));
|
|
434
|
+
if (newPR) {
|
|
435
|
+
stopPolling();
|
|
436
|
+
onNewPR(newPR);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}, pollInterval);
|
|
440
|
+
},
|
|
441
|
+
[stopPolling]
|
|
442
|
+
);
|
|
443
|
+
useEffect3(() => {
|
|
444
|
+
return () => {
|
|
445
|
+
if (pollingIntervalRef.current) {
|
|
446
|
+
clearInterval(pollingIntervalRef.current);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
}, []);
|
|
450
|
+
return {
|
|
451
|
+
startPolling,
|
|
452
|
+
stopPolling,
|
|
453
|
+
isPolling
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/hooks/github/usePullRequests.ts
|
|
458
|
+
import { useCallback as useCallback3, useState as useState4 } from "react";
|
|
459
|
+
function usePullRequests() {
|
|
460
|
+
const [prs, setPrs] = useState4([]);
|
|
461
|
+
const [selectedPR, setSelectedPR] = useState4(null);
|
|
462
|
+
const [prDetails, setPrDetails] = useState4(null);
|
|
463
|
+
const [loading, setLoading] = useState4({
|
|
464
|
+
prs: false,
|
|
465
|
+
details: false
|
|
466
|
+
});
|
|
467
|
+
const [errors, setErrors] = useState4({});
|
|
468
|
+
const refreshPRs = useCallback3(async (branch, repoSlug) => {
|
|
469
|
+
setLoading((prev) => ({ ...prev, prs: true }));
|
|
470
|
+
setPrs([]);
|
|
471
|
+
setSelectedPR(null);
|
|
472
|
+
setPrDetails(null);
|
|
473
|
+
try {
|
|
474
|
+
const result = await listPRsForBranch(branch, repoSlug);
|
|
475
|
+
if (result.success) {
|
|
476
|
+
setPrs(result.data);
|
|
477
|
+
setErrors((prev) => ({ ...prev, prs: void 0 }));
|
|
478
|
+
return result.data[0] ?? null;
|
|
479
|
+
} else {
|
|
480
|
+
setErrors((prev) => ({ ...prev, prs: result.error }));
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
} catch (err) {
|
|
484
|
+
setErrors((prev) => ({ ...prev, prs: String(err) }));
|
|
485
|
+
duckEvents.emit("error");
|
|
486
|
+
return null;
|
|
487
|
+
} finally {
|
|
488
|
+
setLoading((prev) => ({ ...prev, prs: false }));
|
|
489
|
+
}
|
|
490
|
+
}, []);
|
|
491
|
+
const refreshDetails = useCallback3(async (pr, repoSlug) => {
|
|
492
|
+
setLoading((prev) => ({ ...prev, details: true }));
|
|
493
|
+
try {
|
|
494
|
+
const result = await getPRDetails(pr.number, repoSlug);
|
|
495
|
+
if (result.success) {
|
|
496
|
+
setPrDetails(result.data);
|
|
497
|
+
setErrors((prev) => ({ ...prev, details: void 0 }));
|
|
498
|
+
} else {
|
|
499
|
+
setErrors((prev) => ({ ...prev, details: result.error }));
|
|
500
|
+
}
|
|
501
|
+
} catch (err) {
|
|
502
|
+
setErrors((prev) => ({ ...prev, details: String(err) }));
|
|
503
|
+
duckEvents.emit("error");
|
|
504
|
+
} finally {
|
|
505
|
+
setLoading((prev) => ({ ...prev, details: false }));
|
|
506
|
+
}
|
|
507
|
+
}, []);
|
|
508
|
+
const fetchPRsAndDetails = useCallback3(
|
|
509
|
+
async (branch, repoSlug) => {
|
|
510
|
+
const firstPR = await refreshPRs(branch, repoSlug);
|
|
511
|
+
if (firstPR) {
|
|
512
|
+
setSelectedPR(firstPR);
|
|
513
|
+
refreshDetails(firstPR, repoSlug);
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
[refreshPRs, refreshDetails]
|
|
517
|
+
);
|
|
518
|
+
const selectPR = useCallback3(
|
|
519
|
+
(pr, repoSlug) => {
|
|
520
|
+
setSelectedPR(pr);
|
|
521
|
+
if (repoSlug) {
|
|
522
|
+
refreshDetails(pr, repoSlug);
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
[refreshDetails]
|
|
526
|
+
);
|
|
527
|
+
const setError = useCallback3((key, message) => {
|
|
528
|
+
setErrors((prev) => ({ ...prev, [key]: message }));
|
|
529
|
+
}, []);
|
|
530
|
+
return {
|
|
531
|
+
prs,
|
|
532
|
+
selectedPR,
|
|
533
|
+
prDetails,
|
|
534
|
+
refreshPRs,
|
|
535
|
+
refreshDetails,
|
|
536
|
+
fetchPRsAndDetails,
|
|
537
|
+
selectPR,
|
|
538
|
+
loading,
|
|
539
|
+
errors,
|
|
540
|
+
setError,
|
|
541
|
+
// Expose setters for cases where external code needs to update state directly
|
|
542
|
+
setPrs,
|
|
543
|
+
setSelectedPR
|
|
544
|
+
};
|
|
545
|
+
}
|
|
270
546
|
|
|
271
547
|
// src/lib/jira/parser.ts
|
|
272
548
|
var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
@@ -285,8 +561,8 @@ function parseTicketKey(input) {
|
|
|
285
561
|
}
|
|
286
562
|
return null;
|
|
287
563
|
}
|
|
288
|
-
function
|
|
289
|
-
const match =
|
|
564
|
+
function extractTicketKey(text) {
|
|
565
|
+
const match = text.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
|
|
290
566
|
if (match) {
|
|
291
567
|
const candidate = match[1].toUpperCase();
|
|
292
568
|
if (isValidTicketKeyFormat(candidate)) {
|
|
@@ -492,10 +768,10 @@ async function applyTransition(auth, ticketKey, transitionId) {
|
|
|
492
768
|
}
|
|
493
769
|
|
|
494
770
|
// src/lib/logs/index.ts
|
|
495
|
-
import {
|
|
771
|
+
import { spawnSync } from "child_process";
|
|
772
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
496
773
|
import { homedir as homedir2 } from "os";
|
|
497
774
|
import { join as join2 } from "path";
|
|
498
|
-
import { spawnSync } from "child_process";
|
|
499
775
|
var LOGS_DIRECTORY = join2(homedir2(), ".clairo", "logs");
|
|
500
776
|
function ensureLogsDirectory() {
|
|
501
777
|
if (!existsSync2(LOGS_DIRECTORY)) {
|
|
@@ -622,15 +898,15 @@ ${oldStatus} \u2192 ${newStatus}
|
|
|
622
898
|
|
|
623
899
|
// src/components/github/PRDetailsBox.tsx
|
|
624
900
|
import open from "open";
|
|
625
|
-
import { useRef } from "react";
|
|
901
|
+
import { useRef as useRef2 } from "react";
|
|
626
902
|
import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
|
|
627
903
|
import { ScrollView } from "ink-scroll-view";
|
|
628
904
|
|
|
629
905
|
// src/components/ui/Markdown.tsx
|
|
906
|
+
import Table from "cli-table3";
|
|
907
|
+
import { marked } from "marked";
|
|
630
908
|
import { Box, Text } from "ink";
|
|
631
909
|
import Link from "ink-link";
|
|
632
|
-
import { marked } from "marked";
|
|
633
|
-
import Table from "cli-table3";
|
|
634
910
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
635
911
|
function Markdown({ children }) {
|
|
636
912
|
const tokens = marked.lexer(children);
|
|
@@ -642,10 +918,12 @@ function TokenRenderer({ token }) {
|
|
|
642
918
|
case "heading":
|
|
643
919
|
return /* @__PURE__ */ jsx(Box, { marginTop: token.depth === 1 ? 0 : 1, children: /* @__PURE__ */ jsx(Text, { bold: true, underline: token.depth === 1, children: renderInline(token.tokens) }) });
|
|
644
920
|
case "paragraph": {
|
|
645
|
-
const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some(
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
921
|
+
const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some(
|
|
922
|
+
(t) => {
|
|
923
|
+
var _a2;
|
|
924
|
+
return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
|
|
925
|
+
}
|
|
926
|
+
);
|
|
649
927
|
if (hasLinks) {
|
|
650
928
|
return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
|
|
651
929
|
}
|
|
@@ -768,7 +1046,7 @@ function getCheckSortOrder(check) {
|
|
|
768
1046
|
}
|
|
769
1047
|
function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
770
1048
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
771
|
-
const scrollRef =
|
|
1049
|
+
const scrollRef = useRef2(null);
|
|
772
1050
|
const title = "[3] PR Details";
|
|
773
1051
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
774
1052
|
const displayTitle = pr ? `${title} - #${pr.number}` : title;
|
|
@@ -892,118 +1170,517 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
|
892
1170
|
}
|
|
893
1171
|
|
|
894
1172
|
// src/components/github/PullRequestsBox.tsx
|
|
895
|
-
import
|
|
1173
|
+
import open2 from "open";
|
|
1174
|
+
import { useEffect as useEffect7, useState as useState10 } from "react";
|
|
896
1175
|
import { TitledBox } from "@mishieck/ink-titled-box";
|
|
897
1176
|
import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
|
|
1177
|
+
import { ScrollView as ScrollView2 } from "ink-scroll-view";
|
|
898
1178
|
|
|
899
|
-
// src/
|
|
900
|
-
import {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
(
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
prs,
|
|
924
|
-
selectedPR,
|
|
925
|
-
onSelect,
|
|
926
|
-
onCreatePR,
|
|
927
|
-
loading,
|
|
928
|
-
error,
|
|
929
|
-
branch,
|
|
930
|
-
repoSlug,
|
|
931
|
-
isFocused
|
|
932
|
-
}) {
|
|
933
|
-
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
934
|
-
const totalItems = prs.length + 1;
|
|
935
|
-
useEffect(() => {
|
|
936
|
-
const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
|
|
937
|
-
if (idx >= 0) setHighlightedIndex(idx);
|
|
938
|
-
}, [selectedPR, prs]);
|
|
939
|
-
useInput2(
|
|
940
|
-
(input, key) => {
|
|
941
|
-
if (!isFocused) return;
|
|
942
|
-
if (key.upArrow || input === "k") {
|
|
943
|
-
setHighlightedIndex((prev) => Math.max(0, prev - 1));
|
|
944
|
-
}
|
|
945
|
-
if (key.downArrow || input === "j") {
|
|
946
|
-
setHighlightedIndex((prev) => Math.min(totalItems - 1, prev + 1));
|
|
947
|
-
}
|
|
948
|
-
if (input === " ") {
|
|
949
|
-
if (highlightedIndex === prs.length) {
|
|
950
|
-
onCreatePR();
|
|
951
|
-
} else if (prs[highlightedIndex]) {
|
|
952
|
-
onSelect(prs[highlightedIndex]);
|
|
953
|
-
}
|
|
1179
|
+
// src/hooks/jira/useJiraTickets.ts
|
|
1180
|
+
import { useCallback as useCallback4, useState as useState5 } from "react";
|
|
1181
|
+
function useJiraTickets() {
|
|
1182
|
+
const [jiraState, setJiraState] = useState5("not_configured");
|
|
1183
|
+
const [tickets, setTickets] = useState5([]);
|
|
1184
|
+
const [loading, setLoading] = useState5({ configure: false, link: false });
|
|
1185
|
+
const [errors, setErrors] = useState5({});
|
|
1186
|
+
const initializeJiraState = useCallback4(async (repoPath, currentBranch, repoSlug) => {
|
|
1187
|
+
if (!isJiraConfigured(repoPath)) {
|
|
1188
|
+
setJiraState("not_configured");
|
|
1189
|
+
setTickets([]);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const linkedTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1193
|
+
if (linkedTickets.length > 0) {
|
|
1194
|
+
setTickets(linkedTickets);
|
|
1195
|
+
setJiraState("has_tickets");
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
let ticketKey = extractTicketKey(currentBranch);
|
|
1199
|
+
if (!ticketKey && repoSlug) {
|
|
1200
|
+
const prResult = await listPRsForBranch(currentBranch, repoSlug);
|
|
1201
|
+
if (prResult.success && prResult.data.length > 0) {
|
|
1202
|
+
ticketKey = extractTicketKey(prResult.data[0].title);
|
|
954
1203
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1204
|
+
}
|
|
1205
|
+
if (!ticketKey) {
|
|
1206
|
+
setTickets([]);
|
|
1207
|
+
setJiraState("no_tickets");
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1211
|
+
const creds = getJiraCredentials(repoPath);
|
|
1212
|
+
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1213
|
+
setTickets([]);
|
|
1214
|
+
setJiraState("no_tickets");
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
1218
|
+
const result = await getIssue(auth, ticketKey);
|
|
1219
|
+
if (result.success) {
|
|
1220
|
+
const linkedTicket = {
|
|
1221
|
+
key: result.data.key,
|
|
1222
|
+
summary: result.data.fields.summary,
|
|
1223
|
+
status: result.data.fields.status.name,
|
|
1224
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1225
|
+
};
|
|
1226
|
+
addLinkedTicket(repoPath, currentBranch, linkedTicket);
|
|
1227
|
+
setTickets([linkedTicket]);
|
|
1228
|
+
setJiraState("has_tickets");
|
|
1229
|
+
} else {
|
|
1230
|
+
setTickets([]);
|
|
1231
|
+
setJiraState("no_tickets");
|
|
1232
|
+
}
|
|
1233
|
+
}, []);
|
|
1234
|
+
const refreshTickets = useCallback4((repoPath, currentBranch) => {
|
|
1235
|
+
const linkedTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1236
|
+
setTickets(linkedTickets);
|
|
1237
|
+
setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
|
|
1238
|
+
}, []);
|
|
1239
|
+
const configureJira = useCallback4(
|
|
1240
|
+
async (repoPath, siteUrl, email, apiToken) => {
|
|
1241
|
+
setLoading((prev) => ({ ...prev, configure: true }));
|
|
1242
|
+
setErrors((prev) => ({ ...prev, configure: void 0 }));
|
|
1243
|
+
const auth = { siteUrl, email, apiToken };
|
|
1244
|
+
const result = await validateCredentials(auth);
|
|
1245
|
+
if (!result.success) {
|
|
1246
|
+
setErrors((prev) => ({ ...prev, configure: result.error }));
|
|
1247
|
+
duckEvents.emit("error");
|
|
1248
|
+
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1249
|
+
return false;
|
|
959
1250
|
}
|
|
1251
|
+
setJiraSiteUrl(repoPath, siteUrl);
|
|
1252
|
+
setJiraCredentials(repoPath, email, apiToken);
|
|
1253
|
+
setJiraState("no_tickets");
|
|
1254
|
+
duckEvents.emit("jira:configured");
|
|
1255
|
+
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1256
|
+
return true;
|
|
960
1257
|
},
|
|
961
|
-
|
|
1258
|
+
[]
|
|
1259
|
+
);
|
|
1260
|
+
const linkTicket = useCallback4(
|
|
1261
|
+
async (repoPath, currentBranch, ticketInput) => {
|
|
1262
|
+
setLoading((prev) => ({ ...prev, link: true }));
|
|
1263
|
+
setErrors((prev) => ({ ...prev, link: void 0 }));
|
|
1264
|
+
const ticketKey = parseTicketKey(ticketInput);
|
|
1265
|
+
if (!ticketKey) {
|
|
1266
|
+
setErrors((prev) => ({ ...prev, link: "Invalid ticket format. Use PROJ-123 or a Jira URL." }));
|
|
1267
|
+
duckEvents.emit("error");
|
|
1268
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1272
|
+
const creds = getJiraCredentials(repoPath);
|
|
1273
|
+
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1274
|
+
setErrors((prev) => ({ ...prev, link: "Jira not configured" }));
|
|
1275
|
+
duckEvents.emit("error");
|
|
1276
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
1280
|
+
const result = await getIssue(auth, ticketKey);
|
|
1281
|
+
if (!result.success) {
|
|
1282
|
+
setErrors((prev) => ({ ...prev, link: result.error }));
|
|
1283
|
+
duckEvents.emit("error");
|
|
1284
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
1287
|
+
const linkedTicket = {
|
|
1288
|
+
key: result.data.key,
|
|
1289
|
+
summary: result.data.fields.summary,
|
|
1290
|
+
status: result.data.fields.status.name,
|
|
1291
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1292
|
+
};
|
|
1293
|
+
addLinkedTicket(repoPath, currentBranch, linkedTicket);
|
|
1294
|
+
const newTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1295
|
+
setTickets(newTickets);
|
|
1296
|
+
setJiraState("has_tickets");
|
|
1297
|
+
duckEvents.emit("jira:linked");
|
|
1298
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1299
|
+
return true;
|
|
1300
|
+
},
|
|
1301
|
+
[]
|
|
1302
|
+
);
|
|
1303
|
+
const unlinkTicket = useCallback4((repoPath, currentBranch, ticketKey) => {
|
|
1304
|
+
removeLinkedTicket(repoPath, currentBranch, ticketKey);
|
|
1305
|
+
}, []);
|
|
1306
|
+
const clearError = useCallback4((key) => {
|
|
1307
|
+
setErrors((prev) => ({ ...prev, [key]: void 0 }));
|
|
1308
|
+
}, []);
|
|
1309
|
+
return {
|
|
1310
|
+
jiraState,
|
|
1311
|
+
tickets,
|
|
1312
|
+
loading,
|
|
1313
|
+
errors,
|
|
1314
|
+
initializeJiraState,
|
|
1315
|
+
refreshTickets,
|
|
1316
|
+
configureJira,
|
|
1317
|
+
linkTicket,
|
|
1318
|
+
unlinkTicket,
|
|
1319
|
+
clearError
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// src/hooks/logs/useLogs.ts
|
|
1324
|
+
import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef3, useState as useState6 } from "react";
|
|
1325
|
+
function useLogs() {
|
|
1326
|
+
const [logFiles, setLogFiles] = useState6([]);
|
|
1327
|
+
const [selectedDate, setSelectedDate] = useState6(null);
|
|
1328
|
+
const [logContent, setLogContent] = useState6(null);
|
|
1329
|
+
const [highlightedIndex, setHighlightedIndex] = useState6(0);
|
|
1330
|
+
const initializedRef = useRef3(false);
|
|
1331
|
+
const loadLogContent = useCallback5((date) => {
|
|
1332
|
+
if (!date) {
|
|
1333
|
+
setLogContent(null);
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
const content = readLog(date);
|
|
1337
|
+
setLogContent(content);
|
|
1338
|
+
return content;
|
|
1339
|
+
}, []);
|
|
1340
|
+
const refreshLogFiles = useCallback5(() => {
|
|
1341
|
+
const files = listLogFiles();
|
|
1342
|
+
setLogFiles(files);
|
|
1343
|
+
return files;
|
|
1344
|
+
}, []);
|
|
1345
|
+
const initialize = useCallback5(() => {
|
|
1346
|
+
const files = listLogFiles();
|
|
1347
|
+
setLogFiles(files);
|
|
1348
|
+
if (files.length === 0) return;
|
|
1349
|
+
const today = getTodayDate();
|
|
1350
|
+
const todayFile = files.find((f) => f.date === today);
|
|
1351
|
+
if (todayFile) {
|
|
1352
|
+
setSelectedDate(todayFile.date);
|
|
1353
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1354
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1355
|
+
loadLogContent(todayFile.date);
|
|
1356
|
+
} else {
|
|
1357
|
+
setSelectedDate(files[0].date);
|
|
1358
|
+
setHighlightedIndex(0);
|
|
1359
|
+
loadLogContent(files[0].date);
|
|
1360
|
+
}
|
|
1361
|
+
}, [loadLogContent]);
|
|
1362
|
+
useEffect4(() => {
|
|
1363
|
+
if (initializedRef.current) return;
|
|
1364
|
+
initializedRef.current = true;
|
|
1365
|
+
initialize();
|
|
1366
|
+
}, [initialize]);
|
|
1367
|
+
const selectDate = useCallback5(
|
|
1368
|
+
(date) => {
|
|
1369
|
+
setSelectedDate(date);
|
|
1370
|
+
loadLogContent(date);
|
|
1371
|
+
},
|
|
1372
|
+
[loadLogContent]
|
|
1373
|
+
);
|
|
1374
|
+
const refresh = useCallback5(() => {
|
|
1375
|
+
refreshLogFiles();
|
|
1376
|
+
if (selectedDate) {
|
|
1377
|
+
loadLogContent(selectedDate);
|
|
1378
|
+
}
|
|
1379
|
+
}, [refreshLogFiles, selectedDate, loadLogContent]);
|
|
1380
|
+
const handleExternalLogUpdate = useCallback5(() => {
|
|
1381
|
+
const files = listLogFiles();
|
|
1382
|
+
setLogFiles(files);
|
|
1383
|
+
const today = getTodayDate();
|
|
1384
|
+
if (selectedDate === today) {
|
|
1385
|
+
loadLogContent(today);
|
|
1386
|
+
} else if (!selectedDate && files.length > 0) {
|
|
1387
|
+
const todayFile = files.find((f) => f.date === today);
|
|
1388
|
+
if (todayFile) {
|
|
1389
|
+
setSelectedDate(today);
|
|
1390
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1391
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1392
|
+
loadLogContent(today);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}, [selectedDate, loadLogContent]);
|
|
1396
|
+
const handleLogCreated = useCallback5(() => {
|
|
1397
|
+
const files = listLogFiles();
|
|
1398
|
+
setLogFiles(files);
|
|
1399
|
+
const today = getTodayDate();
|
|
1400
|
+
setSelectedDate(today);
|
|
1401
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1402
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1403
|
+
loadLogContent(today);
|
|
1404
|
+
}, [loadLogContent]);
|
|
1405
|
+
return {
|
|
1406
|
+
logFiles,
|
|
1407
|
+
selectedDate,
|
|
1408
|
+
logContent,
|
|
1409
|
+
highlightedIndex,
|
|
1410
|
+
setHighlightedIndex,
|
|
1411
|
+
selectDate,
|
|
1412
|
+
refresh,
|
|
1413
|
+
handleExternalLogUpdate,
|
|
1414
|
+
handleLogCreated
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/hooks/useModal.ts
|
|
1419
|
+
import { useCallback as useCallback6, useState as useState7 } from "react";
|
|
1420
|
+
function useModal() {
|
|
1421
|
+
const [modalType, setModalType] = useState7("none");
|
|
1422
|
+
const open4 = useCallback6((type) => setModalType(type), []);
|
|
1423
|
+
const close = useCallback6(() => setModalType("none"), []);
|
|
1424
|
+
const isOpen = modalType !== "none";
|
|
1425
|
+
return {
|
|
1426
|
+
type: modalType,
|
|
1427
|
+
isOpen,
|
|
1428
|
+
open: open4,
|
|
1429
|
+
close
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// src/hooks/useListNavigation.ts
|
|
1434
|
+
import { useCallback as useCallback7, useState as useState8 } from "react";
|
|
1435
|
+
function useListNavigation(length) {
|
|
1436
|
+
const [index, setIndex] = useState8(0);
|
|
1437
|
+
const prev = useCallback7(() => {
|
|
1438
|
+
setIndex((i) => Math.max(0, i - 1));
|
|
1439
|
+
}, []);
|
|
1440
|
+
const next = useCallback7(() => {
|
|
1441
|
+
setIndex((i) => Math.min(length - 1, i + 1));
|
|
1442
|
+
}, [length]);
|
|
1443
|
+
const clampedIndex = Math.min(index, Math.max(0, length - 1));
|
|
1444
|
+
const reset = useCallback7(() => setIndex(0), []);
|
|
1445
|
+
return {
|
|
1446
|
+
index: length === 0 ? 0 : clampedIndex,
|
|
1447
|
+
prev,
|
|
1448
|
+
next,
|
|
1449
|
+
reset,
|
|
1450
|
+
setIndex
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/hooks/useScrollToIndex.ts
|
|
1455
|
+
import { useEffect as useEffect5, useRef as useRef4 } from "react";
|
|
1456
|
+
function useScrollToIndex(index) {
|
|
1457
|
+
const scrollRef = useRef4(null);
|
|
1458
|
+
useEffect5(() => {
|
|
1459
|
+
const ref = scrollRef.current;
|
|
1460
|
+
if (!ref) return;
|
|
1461
|
+
const pos = ref.getItemPosition(index);
|
|
1462
|
+
const viewportHeight = ref.getViewportHeight();
|
|
1463
|
+
const scrollOffset = ref.getScrollOffset();
|
|
1464
|
+
if (!pos) return;
|
|
1465
|
+
if (pos.top < scrollOffset) {
|
|
1466
|
+
ref.scrollTo(pos.top);
|
|
1467
|
+
} else if (pos.top + pos.height > scrollOffset + viewportHeight) {
|
|
1468
|
+
ref.scrollTo(pos.top + pos.height - viewportHeight);
|
|
1469
|
+
}
|
|
1470
|
+
}, [index]);
|
|
1471
|
+
return scrollRef;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// src/hooks/useRubberDuck.ts
|
|
1475
|
+
import { useCallback as useCallback8, useEffect as useEffect6, useState as useState9 } from "react";
|
|
1476
|
+
var DUCK_MESSAGES = [
|
|
1477
|
+
"Quack.",
|
|
1478
|
+
"Quack quack quack.",
|
|
1479
|
+
"Have you tried explaining it out loud?",
|
|
1480
|
+
"It's always DNS.",
|
|
1481
|
+
"Did you check the logs?",
|
|
1482
|
+
"Maybe add a console.log?",
|
|
1483
|
+
"Is it plugged in?",
|
|
1484
|
+
"Works on my machine.",
|
|
1485
|
+
"Have you tried reading the error message?",
|
|
1486
|
+
"I believe in you!",
|
|
1487
|
+
"It's probably a race condition.",
|
|
1488
|
+
"Have you tried turning it off and on again?",
|
|
1489
|
+
"Are you sure it compiled?",
|
|
1490
|
+
"It's not a bug, it's a feature.",
|
|
1491
|
+
"Did you clear the cache?",
|
|
1492
|
+
"Try deleting node_modules.",
|
|
1493
|
+
"That's quackers!",
|
|
1494
|
+
"Rubber duck debugging, activate!",
|
|
1495
|
+
"*supportive quacking*"
|
|
1496
|
+
];
|
|
1497
|
+
var REACTION_MESSAGES = {
|
|
1498
|
+
"pr:merged": ["Quack! It shipped!", "Merged!", "To production we go!"],
|
|
1499
|
+
"pr:opened": ["A new PR! Exciting!", "Time for review!", "Fresh code incoming!"],
|
|
1500
|
+
"pr:reviewed": ["Feedback time!", "Reviews are in!", "*attentive quacking*"],
|
|
1501
|
+
"pr:approved": ["Approved!", "LGTM!", "Ship it!"],
|
|
1502
|
+
"pr:changes-requested": ["Some changes needed...", "Back to the drawing board!", "Iterate iterate!"],
|
|
1503
|
+
error: ["Uh oh...", "There there...", "*concerned quacking*", "Quack... not good."],
|
|
1504
|
+
"jira:transition": ["Ticket moving!", "Progress!", "Workflow in motion!"],
|
|
1505
|
+
"jira:linked": ["Ticket linked!", "Jira connection made!", "Tracking enabled!"],
|
|
1506
|
+
"jira:configured": ["Jira ready!", "Integration complete!", "Connected to Jira!"]
|
|
1507
|
+
};
|
|
1508
|
+
function useRubberDuck() {
|
|
1509
|
+
const [state, setState] = useState9({
|
|
1510
|
+
visible: false,
|
|
1511
|
+
message: DUCK_MESSAGES[0]
|
|
1512
|
+
});
|
|
1513
|
+
const getRandomMessage = useCallback8(() => {
|
|
1514
|
+
const index = Math.floor(Math.random() * DUCK_MESSAGES.length);
|
|
1515
|
+
return DUCK_MESSAGES[index];
|
|
1516
|
+
}, []);
|
|
1517
|
+
const toggleDuck = useCallback8(() => {
|
|
1518
|
+
setState((prev) => ({
|
|
1519
|
+
...prev,
|
|
1520
|
+
visible: !prev.visible,
|
|
1521
|
+
message: !prev.visible ? getRandomMessage() : prev.message
|
|
1522
|
+
}));
|
|
1523
|
+
}, [getRandomMessage]);
|
|
1524
|
+
const quack = useCallback8(() => {
|
|
1525
|
+
if (state.visible) {
|
|
1526
|
+
setState((prev) => ({
|
|
1527
|
+
...prev,
|
|
1528
|
+
message: getRandomMessage()
|
|
1529
|
+
}));
|
|
1530
|
+
}
|
|
1531
|
+
}, [state.visible, getRandomMessage]);
|
|
1532
|
+
const getReactionMessage = useCallback8((event) => {
|
|
1533
|
+
const messages = REACTION_MESSAGES[event];
|
|
1534
|
+
return messages[Math.floor(Math.random() * messages.length)];
|
|
1535
|
+
}, []);
|
|
1536
|
+
useEffect6(() => {
|
|
1537
|
+
const unsubscribe = duckEvents.subscribe((event) => {
|
|
1538
|
+
setState((prev) => ({
|
|
1539
|
+
...prev,
|
|
1540
|
+
visible: true,
|
|
1541
|
+
message: getReactionMessage(event)
|
|
1542
|
+
}));
|
|
1543
|
+
});
|
|
1544
|
+
return unsubscribe;
|
|
1545
|
+
}, [getReactionMessage]);
|
|
1546
|
+
return {
|
|
1547
|
+
visible: state.visible,
|
|
1548
|
+
message: state.message,
|
|
1549
|
+
toggleDuck,
|
|
1550
|
+
quack
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// src/lib/clipboard.ts
|
|
1555
|
+
import { exec as exec2 } from "child_process";
|
|
1556
|
+
async function copyToClipboard(text) {
|
|
1557
|
+
var _a, _b;
|
|
1558
|
+
const command = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
|
|
1559
|
+
try {
|
|
1560
|
+
const child = exec2(command);
|
|
1561
|
+
(_a = child.stdin) == null ? void 0 : _a.write(text);
|
|
1562
|
+
(_b = child.stdin) == null ? void 0 : _b.end();
|
|
1563
|
+
await new Promise((resolve, reject) => {
|
|
1564
|
+
child.on("close", (code) => {
|
|
1565
|
+
if (code === 0) resolve();
|
|
1566
|
+
else reject(new Error(`Clipboard command exited with code ${code}`));
|
|
1567
|
+
});
|
|
1568
|
+
});
|
|
1569
|
+
return true;
|
|
1570
|
+
} catch {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// src/components/github/PullRequestsBox.tsx
|
|
1576
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1577
|
+
function PullRequestsBox({
|
|
1578
|
+
prs,
|
|
1579
|
+
selectedPR,
|
|
1580
|
+
onSelect,
|
|
1581
|
+
onCreatePR,
|
|
1582
|
+
loading,
|
|
1583
|
+
error,
|
|
1584
|
+
branch,
|
|
1585
|
+
repoSlug,
|
|
1586
|
+
isFocused
|
|
1587
|
+
}) {
|
|
1588
|
+
const [highlightedIndex, setHighlightedIndex] = useState10(0);
|
|
1589
|
+
const [copied, setCopied] = useState10(false);
|
|
1590
|
+
const scrollRef = useScrollToIndex(highlightedIndex);
|
|
1591
|
+
const totalItems = prs.length + 1;
|
|
1592
|
+
useEffect7(() => {
|
|
1593
|
+
const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
|
|
1594
|
+
if (idx >= 0) setHighlightedIndex(idx);
|
|
1595
|
+
}, [selectedPR, prs]);
|
|
1596
|
+
useInput2(
|
|
1597
|
+
(input, key) => {
|
|
1598
|
+
if (!isFocused) return;
|
|
1599
|
+
if (key.upArrow || input === "k") {
|
|
1600
|
+
setHighlightedIndex((prev) => Math.max(0, prev - 1));
|
|
1601
|
+
}
|
|
1602
|
+
if (key.downArrow || input === "j") {
|
|
1603
|
+
setHighlightedIndex((prev) => Math.min(totalItems - 1, prev + 1));
|
|
1604
|
+
}
|
|
1605
|
+
if (input === " ") {
|
|
1606
|
+
if (highlightedIndex === prs.length) {
|
|
1607
|
+
onCreatePR();
|
|
1608
|
+
} else if (prs[highlightedIndex]) {
|
|
1609
|
+
onSelect(prs[highlightedIndex]);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if (input === "y" && repoSlug && prs[highlightedIndex]) {
|
|
1613
|
+
const pr = prs[highlightedIndex];
|
|
1614
|
+
const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
|
|
1615
|
+
copyToClipboard(url);
|
|
1616
|
+
setCopied(true);
|
|
1617
|
+
setTimeout(() => setCopied(false), 1500);
|
|
1618
|
+
}
|
|
1619
|
+
if (input === "o" && repoSlug && prs[highlightedIndex]) {
|
|
1620
|
+
const pr = prs[highlightedIndex];
|
|
1621
|
+
const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
|
|
1622
|
+
open2(url).catch(() => {
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
{ isActive: isFocused }
|
|
1627
|
+
);
|
|
1628
|
+
const title = "[2] Pull Requests";
|
|
1629
|
+
const subtitle = branch ? ` (${branch})` : "";
|
|
1630
|
+
const copiedIndicator = copied ? " [Copied!]" : "";
|
|
1631
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
1632
|
+
return /* @__PURE__ */ jsx3(
|
|
1633
|
+
TitledBox,
|
|
1634
|
+
{
|
|
1635
|
+
borderStyle: "round",
|
|
1636
|
+
titles: [`${title}${subtitle}${copiedIndicator}`],
|
|
1637
|
+
borderColor,
|
|
1638
|
+
height: 5,
|
|
1639
|
+
children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
|
|
1640
|
+
loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
|
|
1641
|
+
error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
|
|
1642
|
+
!loading && !error && /* @__PURE__ */ jsxs3(ScrollView2, { ref: scrollRef, children: [
|
|
1643
|
+
prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }, "empty"),
|
|
1644
|
+
prs.map((pr, idx) => {
|
|
1645
|
+
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
1646
|
+
const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
|
|
1647
|
+
const cursor = isHighlighted ? ">" : " ";
|
|
1648
|
+
const indicator = isSelected ? " *" : "";
|
|
1649
|
+
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1650
|
+
/* @__PURE__ */ jsxs3(Text3, { color: isHighlighted ? "yellow" : void 0, children: [
|
|
1651
|
+
cursor,
|
|
1652
|
+
" "
|
|
1653
|
+
] }),
|
|
1654
|
+
/* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
|
|
1655
|
+
"#",
|
|
1656
|
+
pr.number,
|
|
1657
|
+
" ",
|
|
1658
|
+
pr.isDraft ? "[Draft] " : "",
|
|
1659
|
+
pr.title
|
|
1660
|
+
] }),
|
|
1661
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: indicator })
|
|
1662
|
+
] }, pr.number);
|
|
1663
|
+
}),
|
|
1664
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
|
|
1665
|
+
isFocused && highlightedIndex === prs.length ? "> " : " ",
|
|
1666
|
+
"+ Create new PR"
|
|
1667
|
+
] }, "create")
|
|
1668
|
+
] })
|
|
1669
|
+
] })
|
|
1670
|
+
}
|
|
962
1671
|
);
|
|
963
|
-
const title = "[2] Pull Requests";
|
|
964
|
-
const subtitle = branch ? ` (${branch})` : "";
|
|
965
|
-
const borderColor = isFocused ? "yellow" : void 0;
|
|
966
|
-
return /* @__PURE__ */ jsx3(TitledBox, { borderStyle: "round", titles: [`${title}${subtitle}`], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
|
|
967
|
-
loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
|
|
968
|
-
error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
|
|
969
|
-
!loading && !error && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
970
|
-
prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }),
|
|
971
|
-
prs.map((pr, idx) => {
|
|
972
|
-
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
973
|
-
const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
|
|
974
|
-
const cursor = isHighlighted ? ">" : " ";
|
|
975
|
-
const indicator = isSelected ? " *" : "";
|
|
976
|
-
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
977
|
-
/* @__PURE__ */ jsxs3(Text3, { color: isHighlighted ? "yellow" : void 0, children: [
|
|
978
|
-
cursor,
|
|
979
|
-
" "
|
|
980
|
-
] }),
|
|
981
|
-
/* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
|
|
982
|
-
"#",
|
|
983
|
-
pr.number,
|
|
984
|
-
" ",
|
|
985
|
-
pr.isDraft ? "[Draft] " : "",
|
|
986
|
-
pr.title
|
|
987
|
-
] }),
|
|
988
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: indicator })
|
|
989
|
-
] }, pr.number);
|
|
990
|
-
})
|
|
991
|
-
] }),
|
|
992
|
-
/* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
|
|
993
|
-
isFocused && highlightedIndex === prs.length ? "> " : " ",
|
|
994
|
-
"+ Create new PR"
|
|
995
|
-
] })
|
|
996
|
-
] }) });
|
|
997
1672
|
}
|
|
998
1673
|
|
|
999
1674
|
// src/components/github/RemotesBox.tsx
|
|
1000
|
-
import { useEffect as
|
|
1675
|
+
import { useEffect as useEffect8, useState as useState11 } from "react";
|
|
1001
1676
|
import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
|
|
1002
1677
|
import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
|
|
1678
|
+
import { ScrollView as ScrollView3 } from "ink-scroll-view";
|
|
1003
1679
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1004
1680
|
function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
|
|
1005
|
-
const [highlightedIndex, setHighlightedIndex] =
|
|
1006
|
-
|
|
1681
|
+
const [highlightedIndex, setHighlightedIndex] = useState11(0);
|
|
1682
|
+
const scrollRef = useScrollToIndex(highlightedIndex);
|
|
1683
|
+
useEffect8(() => {
|
|
1007
1684
|
const idx = remotes.findIndex((r) => r.name === selectedRemote);
|
|
1008
1685
|
if (idx >= 0) setHighlightedIndex(idx);
|
|
1009
1686
|
}, [selectedRemote, remotes]);
|
|
@@ -1024,11 +1701,11 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
|
|
|
1024
1701
|
);
|
|
1025
1702
|
const title = "[1] Remotes";
|
|
1026
1703
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
1027
|
-
return /* @__PURE__ */ jsx4(TitledBox2, { borderStyle: "round", titles: [title], borderColor,
|
|
1704
|
+
return /* @__PURE__ */ jsx4(TitledBox2, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
|
|
1028
1705
|
loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
|
|
1029
1706
|
error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
|
|
1030
1707
|
!loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
|
|
1031
|
-
!loading && !error && remotes.map((remote, idx) => {
|
|
1708
|
+
!loading && !error && remotes.length > 0 && /* @__PURE__ */ jsx4(ScrollView3, { ref: scrollRef, children: remotes.map((remote, idx) => {
|
|
1032
1709
|
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
1033
1710
|
const isSelected = remote.name === selectedRemote;
|
|
1034
1711
|
const cursor = isHighlighted ? ">" : " ";
|
|
@@ -1046,215 +1723,108 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
|
|
|
1046
1723
|
] }),
|
|
1047
1724
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: indicator })
|
|
1048
1725
|
] }, remote.name);
|
|
1049
|
-
})
|
|
1726
|
+
}) })
|
|
1050
1727
|
] }) });
|
|
1051
1728
|
}
|
|
1052
1729
|
|
|
1053
1730
|
// src/components/github/GitHubView.tsx
|
|
1054
1731
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1055
|
-
function GitHubView({ isFocused,
|
|
1056
|
-
const
|
|
1057
|
-
const
|
|
1058
|
-
const
|
|
1059
|
-
const [
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
useEffect3(() => {
|
|
1073
|
-
if (!isFocused) {
|
|
1074
|
-
onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
const bindings = [];
|
|
1078
|
-
if (focusedBox === "remotes") {
|
|
1079
|
-
bindings.push({ key: "Space", label: "Select Remote" });
|
|
1080
|
-
} else if (focusedBox === "prs") {
|
|
1081
|
-
bindings.push({ key: "Space", label: "Select" });
|
|
1082
|
-
bindings.push({ key: "n", label: "New PR", color: "green" });
|
|
1083
|
-
bindings.push({ key: "r", label: "Refresh" });
|
|
1084
|
-
bindings.push({ key: "o", label: "Open", color: "green" });
|
|
1085
|
-
bindings.push({ key: "y", label: "Copy Link" });
|
|
1086
|
-
} else if (focusedBox === "details") {
|
|
1087
|
-
bindings.push({ key: "r", label: "Refresh" });
|
|
1088
|
-
bindings.push({ key: "o", label: "Open", color: "green" });
|
|
1089
|
-
}
|
|
1090
|
-
onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
|
|
1091
|
-
}, [isFocused, focusedBox, onKeybindingsChange]);
|
|
1092
|
-
useEffect3(() => {
|
|
1093
|
-
const gitRepoCheck = isGitRepo();
|
|
1094
|
-
setIsRepo(gitRepoCheck);
|
|
1095
|
-
if (!gitRepoCheck) {
|
|
1096
|
-
setLoading((prev) => ({ ...prev, remotes: false }));
|
|
1097
|
-
setErrors((prev) => ({ ...prev, remotes: "Not a git repository" }));
|
|
1098
|
-
return;
|
|
1099
|
-
}
|
|
1100
|
-
const rootResult = getRepoRoot();
|
|
1101
|
-
if (rootResult.success) {
|
|
1102
|
-
setRepoPath(rootResult.data);
|
|
1103
|
-
}
|
|
1104
|
-
const branchResult = getCurrentBranch();
|
|
1105
|
-
if (branchResult.success) {
|
|
1106
|
-
setCurrentBranch(branchResult.data);
|
|
1107
|
-
}
|
|
1108
|
-
const remotesResult = listRemotes();
|
|
1109
|
-
if (remotesResult.success) {
|
|
1110
|
-
setRemotes(remotesResult.data);
|
|
1111
|
-
const remoteNames = remotesResult.data.map((r) => r.name);
|
|
1112
|
-
const defaultRemote = getSelectedRemote(rootResult.success ? rootResult.data : "", remoteNames);
|
|
1113
|
-
setSelectedRemote(defaultRemote);
|
|
1114
|
-
} else {
|
|
1115
|
-
setErrors((prev) => ({ ...prev, remotes: remotesResult.error }));
|
|
1116
|
-
}
|
|
1117
|
-
setLoading((prev) => ({ ...prev, remotes: false }));
|
|
1118
|
-
}, []);
|
|
1119
|
-
const refreshPRs = useCallback(async () => {
|
|
1120
|
-
if (!currentBranch || !currentRepoSlug) return;
|
|
1121
|
-
setLoading((prev) => ({ ...prev, prs: true }));
|
|
1122
|
-
try {
|
|
1123
|
-
const result = await listPRsForBranch(currentBranch, currentRepoSlug);
|
|
1124
|
-
if (result.success) {
|
|
1125
|
-
setPrs(result.data);
|
|
1126
|
-
if (result.data.length > 0) {
|
|
1127
|
-
setSelectedPR((prev) => prev ?? result.data[0]);
|
|
1128
|
-
}
|
|
1129
|
-
setErrors((prev) => ({ ...prev, prs: void 0 }));
|
|
1130
|
-
} else {
|
|
1131
|
-
setErrors((prev) => ({ ...prev, prs: result.error }));
|
|
1132
|
-
}
|
|
1133
|
-
} catch (err) {
|
|
1134
|
-
setErrors((prev) => ({ ...prev, prs: String(err) }));
|
|
1135
|
-
} finally {
|
|
1136
|
-
setLoading((prev) => ({ ...prev, prs: false }));
|
|
1137
|
-
}
|
|
1138
|
-
}, [currentBranch, currentRepoSlug]);
|
|
1139
|
-
const refreshDetails = useCallback(async () => {
|
|
1140
|
-
if (!selectedPR || !currentRepoSlug) return;
|
|
1141
|
-
setLoading((prev) => ({ ...prev, details: true }));
|
|
1142
|
-
try {
|
|
1143
|
-
const result = await getPRDetails(selectedPR.number, currentRepoSlug);
|
|
1144
|
-
if (result.success) {
|
|
1145
|
-
setPrDetails(result.data);
|
|
1146
|
-
setErrors((prev) => ({ ...prev, details: void 0 }));
|
|
1147
|
-
} else {
|
|
1148
|
-
setErrors((prev) => ({ ...prev, details: result.error }));
|
|
1149
|
-
}
|
|
1150
|
-
} catch (err) {
|
|
1151
|
-
setErrors((prev) => ({ ...prev, details: String(err) }));
|
|
1152
|
-
} finally {
|
|
1153
|
-
setLoading((prev) => ({ ...prev, details: false }));
|
|
1154
|
-
}
|
|
1155
|
-
}, [selectedPR, currentRepoSlug]);
|
|
1156
|
-
useEffect3(() => {
|
|
1157
|
-
if (!selectedRemote || !currentBranch) return;
|
|
1158
|
-
const remote = remotes.find((r) => r.name === selectedRemote);
|
|
1159
|
-
if (!remote) return;
|
|
1160
|
-
const repo = getRepoFromRemote(remote.url);
|
|
1161
|
-
if (!repo) return;
|
|
1162
|
-
setCurrentRepoSlug(repo);
|
|
1163
|
-
setPrs([]);
|
|
1164
|
-
setSelectedPR(null);
|
|
1165
|
-
}, [selectedRemote, currentBranch, remotes]);
|
|
1166
|
-
useEffect3(() => {
|
|
1167
|
-
if (currentRepoSlug && currentBranch) {
|
|
1168
|
-
refreshPRs();
|
|
1732
|
+
function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
1733
|
+
const repo = useGitRepo();
|
|
1734
|
+
const pullRequests = usePullRequests();
|
|
1735
|
+
const polling = usePRPolling();
|
|
1736
|
+
const [focusedBox, setFocusedBox] = useState12("remotes");
|
|
1737
|
+
const lastFetchedRef = useRef5(null);
|
|
1738
|
+
useEffect9(() => {
|
|
1739
|
+
if (repo.loading || !repo.currentBranch || !repo.currentRepoSlug) return;
|
|
1740
|
+
const current = { branch: repo.currentBranch, repoSlug: repo.currentRepoSlug };
|
|
1741
|
+
const last = lastFetchedRef.current;
|
|
1742
|
+
if (last && last.branch === current.branch && last.repoSlug === current.repoSlug) return;
|
|
1743
|
+
lastFetchedRef.current = current;
|
|
1744
|
+
pullRequests.fetchPRsAndDetails(repo.currentBranch, repo.currentRepoSlug);
|
|
1745
|
+
}, [repo.loading, repo.currentBranch, repo.currentRepoSlug, pullRequests.fetchPRsAndDetails]);
|
|
1746
|
+
useEffect9(() => {
|
|
1747
|
+
if (isFocused) {
|
|
1748
|
+
repo.refreshBranch();
|
|
1169
1749
|
}
|
|
1170
|
-
}, [
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1176
|
-
refreshDetails();
|
|
1177
|
-
}, [selectedPR, currentRepoSlug, refreshDetails]);
|
|
1178
|
-
const handleRemoteSelect = useCallback(
|
|
1750
|
+
}, [isFocused, repo.refreshBranch]);
|
|
1751
|
+
useEffect9(() => {
|
|
1752
|
+
onFocusedBoxChange == null ? void 0 : onFocusedBoxChange(focusedBox);
|
|
1753
|
+
}, [focusedBox, onFocusedBoxChange]);
|
|
1754
|
+
const handleRemoteSelect = useCallback9(
|
|
1179
1755
|
(remoteName) => {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1756
|
+
repo.selectRemote(remoteName);
|
|
1757
|
+
const remote = repo.remotes.find((r) => r.name === remoteName);
|
|
1758
|
+
if (!remote || !repo.currentBranch) return;
|
|
1759
|
+
const repoSlug = getRepoFromRemote(remote.url);
|
|
1760
|
+
if (!repoSlug) return;
|
|
1761
|
+
lastFetchedRef.current = { branch: repo.currentBranch, repoSlug };
|
|
1762
|
+
pullRequests.fetchPRsAndDetails(repo.currentBranch, repoSlug);
|
|
1184
1763
|
},
|
|
1185
|
-
[
|
|
1764
|
+
[repo.selectRemote, repo.remotes, repo.currentBranch, pullRequests.fetchPRsAndDetails]
|
|
1186
1765
|
);
|
|
1187
|
-
const handlePRSelect =
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1766
|
+
const handlePRSelect = useCallback9(
|
|
1767
|
+
(pr) => {
|
|
1768
|
+
pullRequests.selectPR(pr, repo.currentRepoSlug);
|
|
1769
|
+
},
|
|
1770
|
+
[pullRequests.selectPR, repo.currentRepoSlug]
|
|
1771
|
+
);
|
|
1772
|
+
const createPRContext = useRef5({ repo, pullRequests, onLogUpdated });
|
|
1773
|
+
createPRContext.current = { repo, pullRequests, onLogUpdated };
|
|
1774
|
+
const handleCreatePR = useCallback9(() => {
|
|
1775
|
+
const { repo: repo2, pullRequests: pullRequests2 } = createPRContext.current;
|
|
1776
|
+
if (!repo2.currentBranch) {
|
|
1777
|
+
pullRequests2.setError("prs", "No branch detected");
|
|
1778
|
+
duckEvents.emit("error");
|
|
1195
1779
|
return;
|
|
1196
1780
|
}
|
|
1197
|
-
const remoteResult = findRemoteWithBranch(currentBranch);
|
|
1781
|
+
const remoteResult = findRemoteWithBranch(repo2.currentBranch);
|
|
1198
1782
|
if (!remoteResult.success) {
|
|
1199
|
-
|
|
1783
|
+
pullRequests2.setError("prs", "Push your branch to a remote first");
|
|
1784
|
+
duckEvents.emit("error");
|
|
1200
1785
|
return;
|
|
1201
1786
|
}
|
|
1202
|
-
|
|
1203
|
-
const headFlag = `${remoteResult.data.owner}:${currentBranch}`;
|
|
1204
|
-
exec3(`gh pr create --web --head "${headFlag}"`, (error) => {
|
|
1205
|
-
process.stdout.emit("resize");
|
|
1787
|
+
openPRCreationPage(remoteResult.data.owner, repo2.currentBranch, (error) => {
|
|
1206
1788
|
if (error) {
|
|
1207
|
-
|
|
1789
|
+
pullRequests2.setError("prs", `Failed to create PR: ${error.message}`);
|
|
1790
|
+
duckEvents.emit("error");
|
|
1208
1791
|
}
|
|
1209
1792
|
});
|
|
1210
|
-
if (!currentRepoSlug) return;
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
setPrs(result.data);
|
|
1229
|
-
const newPR = result.data.find((pr) => !prNumbersBeforeCreate.current.has(pr.number));
|
|
1230
|
-
if (newPR) {
|
|
1231
|
-
if (pollingIntervalRef.current) {
|
|
1232
|
-
clearInterval(pollingIntervalRef.current);
|
|
1233
|
-
pollingIntervalRef.current = null;
|
|
1234
|
-
}
|
|
1235
|
-
const tickets = repoPath && currentBranch ? getLinkedTickets(repoPath, currentBranch).map((t) => t.key) : [];
|
|
1236
|
-
logPRCreated(newPR.number, newPR.title, tickets);
|
|
1237
|
-
onLogUpdated == null ? void 0 : onLogUpdated();
|
|
1238
|
-
setSelectedPR(newPR);
|
|
1793
|
+
if (!repo2.currentRepoSlug) return;
|
|
1794
|
+
polling.startPolling({
|
|
1795
|
+
branch: repo2.currentBranch,
|
|
1796
|
+
repoSlug: repo2.currentRepoSlug,
|
|
1797
|
+
existingPRNumbers: pullRequests2.prs.map((pr) => pr.number),
|
|
1798
|
+
onPRsUpdated: (prs) => {
|
|
1799
|
+
pullRequests2.setPrs(prs);
|
|
1800
|
+
},
|
|
1801
|
+
onNewPR: (newPR) => {
|
|
1802
|
+
var _a;
|
|
1803
|
+
const ctx = createPRContext.current;
|
|
1804
|
+
const tickets = ctx.repo.repoPath && ctx.repo.currentBranch ? getLinkedTickets(ctx.repo.repoPath, ctx.repo.currentBranch).map((t) => t.key) : [];
|
|
1805
|
+
logPRCreated(newPR.number, newPR.title, tickets);
|
|
1806
|
+
duckEvents.emit("pr:opened");
|
|
1807
|
+
(_a = ctx.onLogUpdated) == null ? void 0 : _a.call(ctx);
|
|
1808
|
+
ctx.pullRequests.setSelectedPR(newPR);
|
|
1809
|
+
if (ctx.repo.currentRepoSlug) {
|
|
1810
|
+
ctx.pullRequests.refreshDetails(newPR, ctx.repo.currentRepoSlug);
|
|
1239
1811
|
}
|
|
1240
1812
|
}
|
|
1241
|
-
}
|
|
1242
|
-
}, [
|
|
1243
|
-
useEffect3(() => {
|
|
1244
|
-
return () => {
|
|
1245
|
-
if (pollingIntervalRef.current) {
|
|
1246
|
-
clearInterval(pollingIntervalRef.current);
|
|
1247
|
-
}
|
|
1248
|
-
};
|
|
1249
|
-
}, []);
|
|
1813
|
+
});
|
|
1814
|
+
}, [polling.startPolling]);
|
|
1250
1815
|
useInput4(
|
|
1251
1816
|
(input) => {
|
|
1252
1817
|
if (input === "1") setFocusedBox("remotes");
|
|
1253
1818
|
if (input === "2") setFocusedBox("prs");
|
|
1254
1819
|
if (input === "3") setFocusedBox("details");
|
|
1255
1820
|
if (input === "r") {
|
|
1256
|
-
|
|
1257
|
-
if (focusedBox === "
|
|
1821
|
+
const freshBranch = repo.refreshBranch() ?? repo.currentBranch;
|
|
1822
|
+
if (focusedBox === "prs" && freshBranch && repo.currentRepoSlug) {
|
|
1823
|
+
pullRequests.fetchPRsAndDetails(freshBranch, repo.currentRepoSlug);
|
|
1824
|
+
}
|
|
1825
|
+
if (focusedBox === "details" && pullRequests.selectedPR && repo.currentRepoSlug) {
|
|
1826
|
+
pullRequests.refreshDetails(pullRequests.selectedPR, repo.currentRepoSlug);
|
|
1827
|
+
}
|
|
1258
1828
|
}
|
|
1259
1829
|
if (input === "n" && focusedBox === "prs") {
|
|
1260
1830
|
handleCreatePR();
|
|
@@ -1262,41 +1832,41 @@ function GitHubView({ isFocused, onKeybindingsChange, onLogUpdated }) {
|
|
|
1262
1832
|
},
|
|
1263
1833
|
{ isActive: isFocused }
|
|
1264
1834
|
);
|
|
1265
|
-
if (isRepo === false) {
|
|
1835
|
+
if (repo.isRepo === false) {
|
|
1266
1836
|
return /* @__PURE__ */ jsx5(TitledBox3, { borderStyle: "round", titles: ["Error"], flexGrow: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: "Current directory is not a git repository" }) });
|
|
1267
1837
|
}
|
|
1268
1838
|
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, children: [
|
|
1269
1839
|
/* @__PURE__ */ jsx5(
|
|
1270
1840
|
RemotesBox,
|
|
1271
1841
|
{
|
|
1272
|
-
remotes,
|
|
1273
|
-
selectedRemote,
|
|
1842
|
+
remotes: repo.remotes,
|
|
1843
|
+
selectedRemote: repo.selectedRemote,
|
|
1274
1844
|
onSelect: handleRemoteSelect,
|
|
1275
|
-
loading: loading
|
|
1276
|
-
error:
|
|
1845
|
+
loading: repo.loading,
|
|
1846
|
+
error: repo.error,
|
|
1277
1847
|
isFocused: isFocused && focusedBox === "remotes"
|
|
1278
1848
|
}
|
|
1279
1849
|
),
|
|
1280
1850
|
/* @__PURE__ */ jsx5(
|
|
1281
1851
|
PullRequestsBox,
|
|
1282
1852
|
{
|
|
1283
|
-
prs,
|
|
1284
|
-
selectedPR,
|
|
1853
|
+
prs: pullRequests.prs,
|
|
1854
|
+
selectedPR: pullRequests.selectedPR,
|
|
1285
1855
|
onSelect: handlePRSelect,
|
|
1286
1856
|
onCreatePR: handleCreatePR,
|
|
1287
|
-
loading: loading.prs,
|
|
1288
|
-
error: errors.prs,
|
|
1289
|
-
branch: currentBranch,
|
|
1290
|
-
repoSlug: currentRepoSlug,
|
|
1857
|
+
loading: pullRequests.loading.prs,
|
|
1858
|
+
error: pullRequests.errors.prs,
|
|
1859
|
+
branch: repo.currentBranch,
|
|
1860
|
+
repoSlug: repo.currentRepoSlug,
|
|
1291
1861
|
isFocused: isFocused && focusedBox === "prs"
|
|
1292
1862
|
}
|
|
1293
1863
|
),
|
|
1294
1864
|
/* @__PURE__ */ jsx5(
|
|
1295
1865
|
PRDetailsBox,
|
|
1296
1866
|
{
|
|
1297
|
-
pr: prDetails,
|
|
1298
|
-
loading: loading.details,
|
|
1299
|
-
error: errors.details,
|
|
1867
|
+
pr: pullRequests.prDetails,
|
|
1868
|
+
loading: pullRequests.loading.details,
|
|
1869
|
+
error: pullRequests.errors.details,
|
|
1300
1870
|
isFocused: isFocused && focusedBox === "details"
|
|
1301
1871
|
}
|
|
1302
1872
|
)
|
|
@@ -1304,27 +1874,95 @@ function GitHubView({ isFocused, onKeybindingsChange, onLogUpdated }) {
|
|
|
1304
1874
|
}
|
|
1305
1875
|
|
|
1306
1876
|
// src/components/jira/JiraView.tsx
|
|
1307
|
-
import
|
|
1308
|
-
import
|
|
1877
|
+
import open3 from "open";
|
|
1878
|
+
import { useEffect as useEffect11, useRef as useRef6 } from "react";
|
|
1879
|
+
|
|
1880
|
+
// src/components/jira/LinkTicketModal.tsx
|
|
1881
|
+
import { useState as useState13 } from "react";
|
|
1882
|
+
import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
|
|
1883
|
+
|
|
1884
|
+
// src/components/ui/TextInput.tsx
|
|
1885
|
+
import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
|
|
1886
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1887
|
+
function TextInput({ value, onChange, placeholder, isActive, mask }) {
|
|
1888
|
+
useInput5(
|
|
1889
|
+
(input, key) => {
|
|
1890
|
+
if (key.backspace || key.delete) {
|
|
1891
|
+
if (value.length > 0) {
|
|
1892
|
+
onChange(value.slice(0, -1));
|
|
1893
|
+
}
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
if (key.return || key.escape || key.upArrow || key.downArrow || key.tab) {
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
if (input && input.length === 1 && input.charCodeAt(0) >= 32) {
|
|
1900
|
+
onChange(value + input);
|
|
1901
|
+
}
|
|
1902
|
+
},
|
|
1903
|
+
{ isActive }
|
|
1904
|
+
);
|
|
1905
|
+
const displayValue = mask ? "*".repeat(value.length) : value;
|
|
1906
|
+
const showPlaceholder = value.length === 0 && placeholder;
|
|
1907
|
+
return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { children: [
|
|
1908
|
+
showPlaceholder ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx6(Text6, { children: displayValue }),
|
|
1909
|
+
isActive && /* @__PURE__ */ jsx6(Text6, { backgroundColor: "yellow", children: " " })
|
|
1910
|
+
] }) });
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// src/components/jira/LinkTicketModal.tsx
|
|
1914
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1915
|
+
function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
|
|
1916
|
+
const [ticketInput, setTicketInput] = useState13("");
|
|
1917
|
+
const canSubmit = ticketInput.trim().length > 0;
|
|
1918
|
+
useInput6(
|
|
1919
|
+
(_input, key) => {
|
|
1920
|
+
if (loading) return;
|
|
1921
|
+
if (key.escape) {
|
|
1922
|
+
onCancel();
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
if (key.return && canSubmit) {
|
|
1926
|
+
onSubmit(ticketInput.trim());
|
|
1927
|
+
}
|
|
1928
|
+
},
|
|
1929
|
+
{ isActive: !loading }
|
|
1930
|
+
);
|
|
1931
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
|
|
1932
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "yellow", children: "Link Jira Ticket" }),
|
|
1933
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Type ticket ID, Enter to submit, Esc to cancel" }),
|
|
1934
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
|
|
1935
|
+
error && /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: error }) }),
|
|
1936
|
+
/* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1937
|
+
/* @__PURE__ */ jsx7(Text7, { color: "blue", children: "Ticket: " }),
|
|
1938
|
+
/* @__PURE__ */ jsx7(TextInput, { value: ticketInput, onChange: setTicketInput, placeholder: "PROJ-123", isActive: !loading })
|
|
1939
|
+
] }),
|
|
1940
|
+
loading && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "Fetching ticket..." }) }),
|
|
1941
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Examples: PROJ-123 or https://company.atlassian.net/browse/PROJ-123" }) })
|
|
1942
|
+
] });
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// src/components/jira/JiraView.tsx
|
|
1309
1946
|
import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
|
|
1310
1947
|
import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
|
|
1311
1948
|
|
|
1312
1949
|
// src/components/jira/ChangeStatusModal.tsx
|
|
1313
|
-
import { useEffect as
|
|
1314
|
-
import { Box as
|
|
1950
|
+
import { useEffect as useEffect10, useState as useState14 } from "react";
|
|
1951
|
+
import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
|
|
1315
1952
|
import SelectInput from "ink-select-input";
|
|
1316
|
-
import { jsx as
|
|
1953
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1317
1954
|
function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onCancel }) {
|
|
1318
|
-
const [transitions, setTransitions] =
|
|
1319
|
-
const [loading, setLoading] =
|
|
1320
|
-
const [applying, setApplying] =
|
|
1321
|
-
const [error, setError] =
|
|
1322
|
-
|
|
1955
|
+
const [transitions, setTransitions] = useState14([]);
|
|
1956
|
+
const [loading, setLoading] = useState14(true);
|
|
1957
|
+
const [applying, setApplying] = useState14(false);
|
|
1958
|
+
const [error, setError] = useState14(null);
|
|
1959
|
+
useEffect10(() => {
|
|
1323
1960
|
const fetchTransitions = async () => {
|
|
1324
1961
|
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1325
1962
|
const creds = getJiraCredentials(repoPath);
|
|
1326
1963
|
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1327
1964
|
setError("Jira not configured");
|
|
1965
|
+
duckEvents.emit("error");
|
|
1328
1966
|
setLoading(false);
|
|
1329
1967
|
return;
|
|
1330
1968
|
}
|
|
@@ -1334,6 +1972,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1334
1972
|
setTransitions(result.data);
|
|
1335
1973
|
} else {
|
|
1336
1974
|
setError(result.error);
|
|
1975
|
+
duckEvents.emit("error");
|
|
1337
1976
|
}
|
|
1338
1977
|
setLoading(false);
|
|
1339
1978
|
};
|
|
@@ -1346,6 +1985,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1346
1985
|
const creds = getJiraCredentials(repoPath);
|
|
1347
1986
|
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1348
1987
|
setError("Jira not configured");
|
|
1988
|
+
duckEvents.emit("error");
|
|
1349
1989
|
setApplying(false);
|
|
1350
1990
|
return;
|
|
1351
1991
|
}
|
|
@@ -1354,13 +1994,15 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1354
1994
|
if (result.success) {
|
|
1355
1995
|
const transition = transitions.find((t) => t.id === item.value);
|
|
1356
1996
|
const newStatus = (transition == null ? void 0 : transition.to.name) ?? item.label;
|
|
1997
|
+
duckEvents.emit("jira:transition");
|
|
1357
1998
|
onComplete(newStatus);
|
|
1358
1999
|
} else {
|
|
1359
2000
|
setError(result.error);
|
|
2001
|
+
duckEvents.emit("error");
|
|
1360
2002
|
setApplying(false);
|
|
1361
2003
|
}
|
|
1362
2004
|
};
|
|
1363
|
-
|
|
2005
|
+
useInput7(
|
|
1364
2006
|
(_input, key) => {
|
|
1365
2007
|
if (key.escape && !applying) {
|
|
1366
2008
|
onCancel();
|
|
@@ -1372,24 +2014,27 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1372
2014
|
label: t.name,
|
|
1373
2015
|
value: t.id
|
|
1374
2016
|
}));
|
|
1375
|
-
const initialIndex = Math.max(
|
|
1376
|
-
|
|
1377
|
-
|
|
2017
|
+
const initialIndex = Math.max(
|
|
2018
|
+
0,
|
|
2019
|
+
transitions.findIndex((t) => t.to.name === currentStatus)
|
|
2020
|
+
);
|
|
2021
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
|
|
2022
|
+
/* @__PURE__ */ jsxs8(Text8, { bold: true, color: "yellow", children: [
|
|
1378
2023
|
"Change Status: ",
|
|
1379
2024
|
ticketKey
|
|
1380
2025
|
] }),
|
|
1381
|
-
loading && /* @__PURE__ */
|
|
1382
|
-
error && /* @__PURE__ */
|
|
1383
|
-
!loading && !error && transitions.length === 0 && /* @__PURE__ */
|
|
1384
|
-
!loading && !error && transitions.length > 0 && !applying && /* @__PURE__ */
|
|
1385
|
-
applying && /* @__PURE__ */
|
|
1386
|
-
/* @__PURE__ */
|
|
2026
|
+
loading && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Loading transitions..." }),
|
|
2027
|
+
error && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "red", children: error }) }),
|
|
2028
|
+
!loading && !error && transitions.length === 0 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "No available transitions" }),
|
|
2029
|
+
!loading && !error && transitions.length > 0 && !applying && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx8(SelectInput, { items, initialIndex, onSelect: handleSelect }) }),
|
|
2030
|
+
applying && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Updating status..." }) }),
|
|
2031
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Esc to cancel" }) })
|
|
1387
2032
|
] });
|
|
1388
2033
|
}
|
|
1389
2034
|
|
|
1390
2035
|
// src/components/jira/ConfigureJiraSiteModal.tsx
|
|
1391
|
-
import { useState as
|
|
1392
|
-
import { Box as
|
|
2036
|
+
import { useState as useState15 } from "react";
|
|
2037
|
+
import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
|
|
1393
2038
|
|
|
1394
2039
|
// src/lib/editor.ts
|
|
1395
2040
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -1420,7 +2065,7 @@ function openInEditor(content, filename) {
|
|
|
1420
2065
|
}
|
|
1421
2066
|
|
|
1422
2067
|
// src/components/jira/ConfigureJiraSiteModal.tsx
|
|
1423
|
-
import { jsx as
|
|
2068
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1424
2069
|
function ConfigureJiraSiteModal({
|
|
1425
2070
|
initialSiteUrl,
|
|
1426
2071
|
initialEmail,
|
|
@@ -1429,13 +2074,13 @@ function ConfigureJiraSiteModal({
|
|
|
1429
2074
|
loading,
|
|
1430
2075
|
error
|
|
1431
2076
|
}) {
|
|
1432
|
-
const [siteUrl, setSiteUrl] =
|
|
1433
|
-
const [email, setEmail] =
|
|
1434
|
-
const [apiToken, setApiToken] =
|
|
1435
|
-
const [selectedItem, setSelectedItem] =
|
|
2077
|
+
const [siteUrl, setSiteUrl] = useState15(initialSiteUrl ?? "");
|
|
2078
|
+
const [email, setEmail] = useState15(initialEmail ?? "");
|
|
2079
|
+
const [apiToken, setApiToken] = useState15("");
|
|
2080
|
+
const [selectedItem, setSelectedItem] = useState15("siteUrl");
|
|
1436
2081
|
const items = ["siteUrl", "email", "apiToken", "submit"];
|
|
1437
2082
|
const canSubmit = siteUrl.trim() && email.trim() && apiToken.trim();
|
|
1438
|
-
|
|
2083
|
+
useInput8(
|
|
1439
2084
|
(input, key) => {
|
|
1440
2085
|
if (loading) return;
|
|
1441
2086
|
if (key.escape) {
|
|
@@ -1484,104 +2129,31 @@ function ConfigureJiraSiteModal({
|
|
|
1484
2129
|
const prefix = isSelected ? "> " : " ";
|
|
1485
2130
|
const color = isSelected ? "yellow" : void 0;
|
|
1486
2131
|
const displayValue = isSensitive && value ? "*".repeat(Math.min(value.length, 20)) : value;
|
|
1487
|
-
return /* @__PURE__ */
|
|
1488
|
-
/* @__PURE__ */
|
|
2132
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
2133
|
+
/* @__PURE__ */ jsxs9(Text9, { color, bold: isSelected, children: [
|
|
1489
2134
|
prefix,
|
|
1490
2135
|
label
|
|
1491
2136
|
] }),
|
|
1492
|
-
value !== void 0 && /* @__PURE__ */
|
|
2137
|
+
value !== void 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: displayValue || "(empty - press Enter to edit)" }) })
|
|
1493
2138
|
] });
|
|
1494
2139
|
};
|
|
1495
|
-
return /* @__PURE__ */
|
|
1496
|
-
/* @__PURE__ */
|
|
1497
|
-
/* @__PURE__ */
|
|
1498
|
-
/* @__PURE__ */
|
|
1499
|
-
error && /* @__PURE__ */
|
|
2140
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
|
|
2141
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Configure Jira Site" }),
|
|
2142
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Up/Down to select, Enter to edit, Esc to cancel" }),
|
|
2143
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
2144
|
+
error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
|
|
1500
2145
|
renderItem("siteUrl", "Site URL (e.g., https://company.atlassian.net)", siteUrl),
|
|
1501
|
-
/* @__PURE__ */
|
|
2146
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
1502
2147
|
renderItem("email", "Email", email),
|
|
1503
|
-
/* @__PURE__ */
|
|
2148
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
1504
2149
|
renderItem("apiToken", "API Token", apiToken, true),
|
|
1505
|
-
/* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
|
|
1506
|
-
/* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
|
|
1507
|
-
selectedItem === "submit" ? "> " : " ",
|
|
1508
|
-
canSubmit ? "[Save Configuration]" : "[Fill all fields first]"
|
|
1509
|
-
] }) }),
|
|
1510
|
-
loading && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "Validating credentials..." }) }),
|
|
1511
|
-
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens" }) })
|
|
1512
|
-
] });
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
// src/components/jira/LinkTicketModal.tsx
|
|
1516
|
-
import { useState as useState6 } from "react";
|
|
1517
|
-
import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
|
|
1518
|
-
|
|
1519
|
-
// src/components/ui/TextInput.tsx
|
|
1520
|
-
import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
|
|
1521
|
-
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1522
|
-
function TextInput({ value, onChange, placeholder, isActive, mask }) {
|
|
1523
|
-
useInput7(
|
|
1524
|
-
(input, key) => {
|
|
1525
|
-
if (key.backspace || key.delete) {
|
|
1526
|
-
if (value.length > 0) {
|
|
1527
|
-
onChange(value.slice(0, -1));
|
|
1528
|
-
}
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
if (key.return || key.escape || key.upArrow || key.downArrow || key.tab) {
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
if (input && input.length === 1 && input.charCodeAt(0) >= 32) {
|
|
1535
|
-
onChange(value + input);
|
|
1536
|
-
}
|
|
1537
|
-
},
|
|
1538
|
-
{ isActive }
|
|
1539
|
-
);
|
|
1540
|
-
const displayValue = mask ? "*".repeat(value.length) : value;
|
|
1541
|
-
const showPlaceholder = value.length === 0 && placeholder;
|
|
1542
|
-
return /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
1543
|
-
showPlaceholder ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx8(Text8, { children: displayValue }),
|
|
1544
|
-
isActive && /* @__PURE__ */ jsx8(Text8, { backgroundColor: "yellow", children: " " })
|
|
1545
|
-
] }) });
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
// src/components/jira/LinkTicketModal.tsx
|
|
1549
|
-
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1550
|
-
function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
|
|
1551
|
-
const [ticketInput, setTicketInput] = useState6("");
|
|
1552
|
-
const canSubmit = ticketInput.trim().length > 0;
|
|
1553
|
-
useInput8(
|
|
1554
|
-
(_input, key) => {
|
|
1555
|
-
if (loading) return;
|
|
1556
|
-
if (key.escape) {
|
|
1557
|
-
onCancel();
|
|
1558
|
-
return;
|
|
1559
|
-
}
|
|
1560
|
-
if (key.return && canSubmit) {
|
|
1561
|
-
onSubmit(ticketInput.trim());
|
|
1562
|
-
}
|
|
1563
|
-
},
|
|
1564
|
-
{ isActive: !loading }
|
|
1565
|
-
);
|
|
1566
|
-
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
|
|
1567
|
-
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "yellow", children: "Link Jira Ticket" }),
|
|
1568
|
-
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Type ticket ID, Enter to submit, Esc to cancel" }),
|
|
1569
2150
|
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
value: ticketInput,
|
|
1577
|
-
onChange: setTicketInput,
|
|
1578
|
-
placeholder: "PROJ-123",
|
|
1579
|
-
isActive: !loading
|
|
1580
|
-
}
|
|
1581
|
-
)
|
|
1582
|
-
] }),
|
|
1583
|
-
loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Fetching ticket..." }) }),
|
|
1584
|
-
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Examples: PROJ-123 or https://company.atlassian.net/browse/PROJ-123" }) })
|
|
2151
|
+
/* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(Text9, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
|
|
2152
|
+
selectedItem === "submit" ? "> " : " ",
|
|
2153
|
+
canSubmit ? "[Save Configuration]" : "[Fill all fields first]"
|
|
2154
|
+
] }) }),
|
|
2155
|
+
loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Validating credentials..." }) }),
|
|
2156
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens" }) })
|
|
1585
2157
|
] });
|
|
1586
2158
|
}
|
|
1587
2159
|
|
|
@@ -1606,225 +2178,106 @@ function TicketItem({ ticketKey, summary, status, isHighlighted, isSelected }) {
|
|
|
1606
2178
|
|
|
1607
2179
|
// src/components/jira/JiraView.tsx
|
|
1608
2180
|
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1609
|
-
function JiraView({ isFocused, onModalChange,
|
|
1610
|
-
const
|
|
1611
|
-
const
|
|
1612
|
-
const
|
|
1613
|
-
const
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
}, [isFocused]);
|
|
1629
|
-
useEffect5(() => {
|
|
1630
|
-
onModalChange == null ? void 0 : onModalChange(showConfigureModal || showLinkModal || showStatusModal);
|
|
1631
|
-
}, [showConfigureModal, showLinkModal, showStatusModal, onModalChange]);
|
|
1632
|
-
useEffect5(() => {
|
|
1633
|
-
if (!isFocused || showConfigureModal || showLinkModal || showStatusModal) {
|
|
1634
|
-
onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
const bindings = [];
|
|
1638
|
-
if (jiraState === "not_configured") {
|
|
1639
|
-
bindings.push({ key: "c", label: "Configure Jira" });
|
|
1640
|
-
} else if (jiraState === "no_tickets") {
|
|
1641
|
-
bindings.push({ key: "l", label: "Link Ticket" });
|
|
1642
|
-
} else if (jiraState === "has_tickets") {
|
|
1643
|
-
bindings.push({ key: "l", label: "Link" });
|
|
1644
|
-
bindings.push({ key: "s", label: "Status" });
|
|
1645
|
-
bindings.push({ key: "d", label: "Unlink", color: "red" });
|
|
1646
|
-
bindings.push({ key: "o", label: "Open", color: "green" });
|
|
1647
|
-
bindings.push({ key: "y", label: "Copy Link" });
|
|
1648
|
-
}
|
|
1649
|
-
onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
|
|
1650
|
-
}, [isFocused, jiraState, showConfigureModal, showLinkModal, showStatusModal, onKeybindingsChange]);
|
|
1651
|
-
useEffect5(() => {
|
|
1652
|
-
const gitRepoCheck = isGitRepo();
|
|
1653
|
-
setIsRepo(gitRepoCheck);
|
|
1654
|
-
if (!gitRepoCheck) return;
|
|
1655
|
-
const rootResult = getRepoRoot();
|
|
1656
|
-
if (rootResult.success) {
|
|
1657
|
-
setRepoPath(rootResult.data);
|
|
1658
|
-
}
|
|
1659
|
-
const branchResult = getCurrentBranch();
|
|
1660
|
-
if (branchResult.success) {
|
|
1661
|
-
setCurrentBranch(branchResult.data);
|
|
1662
|
-
}
|
|
1663
|
-
}, []);
|
|
1664
|
-
useEffect5(() => {
|
|
1665
|
-
if (!repoPath || !currentBranch) return;
|
|
1666
|
-
if (!isJiraConfigured(repoPath)) {
|
|
1667
|
-
setJiraState("not_configured");
|
|
1668
|
-
setTickets([]);
|
|
1669
|
-
return;
|
|
2181
|
+
function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated }) {
|
|
2182
|
+
const repo = useGitRepo();
|
|
2183
|
+
const jira = useJiraTickets();
|
|
2184
|
+
const modal = useModal();
|
|
2185
|
+
const nav = useListNavigation(jira.tickets.length);
|
|
2186
|
+
const lastInitRef = useRef6(null);
|
|
2187
|
+
useEffect11(() => {
|
|
2188
|
+
if (repo.loading || !repo.repoPath || !repo.currentBranch) return;
|
|
2189
|
+
const current = { branch: repo.currentBranch };
|
|
2190
|
+
const last = lastInitRef.current;
|
|
2191
|
+
if (last && last.branch === current.branch) return;
|
|
2192
|
+
lastInitRef.current = current;
|
|
2193
|
+
jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
|
|
2194
|
+
}, [repo.loading, repo.repoPath, repo.currentBranch, repo.currentRepoSlug, jira.initializeJiraState]);
|
|
2195
|
+
useEffect11(() => {
|
|
2196
|
+
if (isFocused) {
|
|
2197
|
+
repo.refreshBranch();
|
|
2198
|
+
} else {
|
|
2199
|
+
modal.close();
|
|
1670
2200
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
}, [
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
if (!
|
|
1680
|
-
const
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
if (!
|
|
1685
|
-
const
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
status: result.data.fields.status.name,
|
|
1692
|
-
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1693
|
-
};
|
|
1694
|
-
addLinkedTicket(repoPath, currentBranch, linkedTicket);
|
|
1695
|
-
setTickets([linkedTicket]);
|
|
1696
|
-
setJiraState("has_tickets");
|
|
1697
|
-
}
|
|
1698
|
-
});
|
|
1699
|
-
}, [repoPath, currentBranch, jiraState]);
|
|
1700
|
-
const refreshTickets = useCallback2(() => {
|
|
1701
|
-
if (!repoPath || !currentBranch) return;
|
|
1702
|
-
const linkedTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1703
|
-
setTickets(linkedTickets);
|
|
1704
|
-
setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
|
|
1705
|
-
}, [repoPath, currentBranch]);
|
|
1706
|
-
const handleConfigureSubmit = useCallback2(
|
|
1707
|
-
async (siteUrl, email, apiToken) => {
|
|
1708
|
-
if (!repoPath) return;
|
|
1709
|
-
setLoading((prev) => ({ ...prev, configure: true }));
|
|
1710
|
-
setErrors((prev) => ({ ...prev, configure: void 0 }));
|
|
1711
|
-
const auth = { siteUrl, email, apiToken };
|
|
1712
|
-
const result = await validateCredentials(auth);
|
|
1713
|
-
if (!result.success) {
|
|
1714
|
-
setErrors((prev) => ({ ...prev, configure: result.error }));
|
|
1715
|
-
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1716
|
-
return;
|
|
1717
|
-
}
|
|
1718
|
-
setJiraSiteUrl(repoPath, siteUrl);
|
|
1719
|
-
setJiraCredentials(repoPath, email, apiToken);
|
|
1720
|
-
setShowConfigureModal(false);
|
|
1721
|
-
setJiraState("no_tickets");
|
|
1722
|
-
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1723
|
-
},
|
|
1724
|
-
[repoPath]
|
|
1725
|
-
);
|
|
1726
|
-
const handleLinkSubmit = useCallback2(
|
|
1727
|
-
async (ticketInput) => {
|
|
1728
|
-
if (!repoPath || !currentBranch) return;
|
|
1729
|
-
setLoading((prev) => ({ ...prev, link: true }));
|
|
1730
|
-
setErrors((prev) => ({ ...prev, link: void 0 }));
|
|
1731
|
-
const ticketKey = parseTicketKey(ticketInput);
|
|
1732
|
-
if (!ticketKey) {
|
|
1733
|
-
setErrors((prev) => ({ ...prev, link: "Invalid ticket format. Use PROJ-123 or a Jira URL." }));
|
|
1734
|
-
setLoading((prev) => ({ ...prev, link: false }));
|
|
1735
|
-
return;
|
|
1736
|
-
}
|
|
1737
|
-
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1738
|
-
const creds = getJiraCredentials(repoPath);
|
|
1739
|
-
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1740
|
-
setErrors((prev) => ({ ...prev, link: "Jira not configured" }));
|
|
1741
|
-
setLoading((prev) => ({ ...prev, link: false }));
|
|
1742
|
-
return;
|
|
1743
|
-
}
|
|
1744
|
-
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
1745
|
-
const result = await getIssue(auth, ticketKey);
|
|
1746
|
-
if (!result.success) {
|
|
1747
|
-
setErrors((prev) => ({ ...prev, link: result.error }));
|
|
1748
|
-
setLoading((prev) => ({ ...prev, link: false }));
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
|
-
const linkedTicket = {
|
|
1752
|
-
key: result.data.key,
|
|
1753
|
-
summary: result.data.fields.summary,
|
|
1754
|
-
status: result.data.fields.status.name,
|
|
1755
|
-
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1756
|
-
};
|
|
1757
|
-
addLinkedTicket(repoPath, currentBranch, linkedTicket);
|
|
1758
|
-
refreshTickets();
|
|
1759
|
-
setShowLinkModal(false);
|
|
1760
|
-
setLoading((prev) => ({ ...prev, link: false }));
|
|
1761
|
-
},
|
|
1762
|
-
[repoPath, currentBranch, refreshTickets]
|
|
1763
|
-
);
|
|
1764
|
-
const handleUnlinkTicket = useCallback2(() => {
|
|
1765
|
-
if (!repoPath || !currentBranch || tickets.length === 0) return;
|
|
1766
|
-
const ticket = tickets[highlightedIndex];
|
|
2201
|
+
}, [isFocused, repo.refreshBranch, modal.close]);
|
|
2202
|
+
useEffect11(() => {
|
|
2203
|
+
onModalChange == null ? void 0 : onModalChange(modal.isOpen);
|
|
2204
|
+
}, [modal.isOpen, onModalChange]);
|
|
2205
|
+
useEffect11(() => {
|
|
2206
|
+
onJiraStateChange == null ? void 0 : onJiraStateChange(jira.jiraState);
|
|
2207
|
+
}, [jira.jiraState, onJiraStateChange]);
|
|
2208
|
+
const handleConfigureSubmit = async (siteUrl, email, apiToken) => {
|
|
2209
|
+
if (!repo.repoPath) return;
|
|
2210
|
+
const success = await jira.configureJira(repo.repoPath, siteUrl, email, apiToken);
|
|
2211
|
+
if (success) modal.close();
|
|
2212
|
+
};
|
|
2213
|
+
const handleLinkSubmit = async (ticketInput) => {
|
|
2214
|
+
if (!repo.repoPath || !repo.currentBranch) return;
|
|
2215
|
+
const success = await jira.linkTicket(repo.repoPath, repo.currentBranch, ticketInput);
|
|
2216
|
+
if (success) modal.close();
|
|
2217
|
+
};
|
|
2218
|
+
const handleUnlinkTicket = () => {
|
|
2219
|
+
if (!repo.repoPath || !repo.currentBranch || jira.tickets.length === 0) return;
|
|
2220
|
+
const ticket = jira.tickets[nav.index];
|
|
1767
2221
|
if (ticket) {
|
|
1768
|
-
|
|
1769
|
-
refreshTickets();
|
|
1770
|
-
|
|
2222
|
+
jira.unlinkTicket(repo.repoPath, repo.currentBranch, ticket.key);
|
|
2223
|
+
jira.refreshTickets(repo.repoPath, repo.currentBranch);
|
|
2224
|
+
nav.prev();
|
|
1771
2225
|
}
|
|
1772
|
-
}
|
|
1773
|
-
const handleOpenInBrowser =
|
|
1774
|
-
if (!repoPath || tickets.length === 0) return;
|
|
1775
|
-
const ticket = tickets[
|
|
1776
|
-
const siteUrl = getJiraSiteUrl(repoPath);
|
|
2226
|
+
};
|
|
2227
|
+
const handleOpenInBrowser = () => {
|
|
2228
|
+
if (!repo.repoPath || jira.tickets.length === 0) return;
|
|
2229
|
+
const ticket = jira.tickets[nav.index];
|
|
2230
|
+
const siteUrl = getJiraSiteUrl(repo.repoPath);
|
|
1777
2231
|
if (ticket && siteUrl) {
|
|
1778
|
-
|
|
1779
|
-
open2(url).catch(() => {
|
|
2232
|
+
open3(`${siteUrl}/browse/${ticket.key}`).catch(() => {
|
|
1780
2233
|
});
|
|
1781
2234
|
}
|
|
1782
|
-
}
|
|
2235
|
+
};
|
|
2236
|
+
const handleCopyLink = () => {
|
|
2237
|
+
if (!repo.repoPath || jira.tickets.length === 0) return;
|
|
2238
|
+
const ticket = jira.tickets[nav.index];
|
|
2239
|
+
const siteUrl = getJiraSiteUrl(repo.repoPath);
|
|
2240
|
+
if (ticket && siteUrl) {
|
|
2241
|
+
copyToClipboard(`${siteUrl}/browse/${ticket.key}`);
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
2244
|
+
const handleStatusComplete = (newStatus) => {
|
|
2245
|
+
if (!repo.repoPath || !repo.currentBranch) return;
|
|
2246
|
+
const ticket = jira.tickets[nav.index];
|
|
2247
|
+
if (!ticket) return;
|
|
2248
|
+
updateTicketStatus(repo.repoPath, repo.currentBranch, ticket.key, newStatus);
|
|
2249
|
+
logJiraStatusChanged(ticket.key, ticket.summary, ticket.status, newStatus);
|
|
2250
|
+
onLogUpdated == null ? void 0 : onLogUpdated();
|
|
2251
|
+
modal.close();
|
|
2252
|
+
jira.refreshTickets(repo.repoPath, repo.currentBranch);
|
|
2253
|
+
};
|
|
1783
2254
|
useInput9(
|
|
1784
2255
|
(input, key) => {
|
|
1785
|
-
if (
|
|
1786
|
-
|
|
1787
|
-
setShowConfigureModal(true);
|
|
2256
|
+
if (input === "c" && jira.jiraState === "not_configured") {
|
|
2257
|
+
modal.open("configure");
|
|
1788
2258
|
return;
|
|
1789
2259
|
}
|
|
1790
|
-
if (input === "l" && jiraState !== "not_configured") {
|
|
1791
|
-
|
|
2260
|
+
if (input === "l" && jira.jiraState !== "not_configured") {
|
|
2261
|
+
modal.open("link");
|
|
1792
2262
|
return;
|
|
1793
2263
|
}
|
|
1794
|
-
if (jiraState === "has_tickets") {
|
|
1795
|
-
if (key.upArrow || input === "k")
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
if (
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
if (input === "s") {
|
|
1802
|
-
setShowStatusModal(true);
|
|
1803
|
-
}
|
|
1804
|
-
if (input === "d") {
|
|
1805
|
-
handleUnlinkTicket();
|
|
1806
|
-
}
|
|
1807
|
-
if (input === "o") {
|
|
1808
|
-
handleOpenInBrowser();
|
|
1809
|
-
}
|
|
1810
|
-
if (input === "y" && repoPath) {
|
|
1811
|
-
const ticket = tickets[highlightedIndex];
|
|
1812
|
-
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1813
|
-
if (ticket && siteUrl) {
|
|
1814
|
-
const url = `${siteUrl}/browse/${ticket.key}`;
|
|
1815
|
-
copyToClipboard(url);
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
2264
|
+
if (jira.jiraState === "has_tickets") {
|
|
2265
|
+
if (key.upArrow || input === "k") nav.prev();
|
|
2266
|
+
if (key.downArrow || input === "j") nav.next();
|
|
2267
|
+
if (input === "s") modal.open("status");
|
|
2268
|
+
if (input === "d") handleUnlinkTicket();
|
|
2269
|
+
if (input === "o") handleOpenInBrowser();
|
|
2270
|
+
if (input === "y") handleCopyLink();
|
|
1818
2271
|
}
|
|
1819
2272
|
},
|
|
1820
|
-
{ isActive: isFocused && !
|
|
2273
|
+
{ isActive: isFocused && !modal.isOpen }
|
|
1821
2274
|
);
|
|
1822
|
-
if (isRepo === false) {
|
|
2275
|
+
if (repo.isRepo === false) {
|
|
1823
2276
|
return /* @__PURE__ */ jsx11(TitledBox4, { borderStyle: "round", titles: ["Jira"], flexShrink: 0, children: /* @__PURE__ */ jsx11(Text11, { color: "red", children: "Not a git repository" }) });
|
|
1824
2277
|
}
|
|
1825
|
-
if (
|
|
1826
|
-
const siteUrl = repoPath ? getJiraSiteUrl(repoPath) : void 0;
|
|
1827
|
-
const creds = repoPath ? getJiraCredentials(repoPath) : { email: null, apiToken: null };
|
|
2278
|
+
if (modal.type === "configure") {
|
|
2279
|
+
const siteUrl = repo.repoPath ? getJiraSiteUrl(repo.repoPath) : void 0;
|
|
2280
|
+
const creds = repo.repoPath ? getJiraCredentials(repo.repoPath) : { email: null, apiToken: null };
|
|
1828
2281
|
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
|
|
1829
2282
|
ConfigureJiraSiteModal,
|
|
1830
2283
|
{
|
|
@@ -1832,60 +2285,53 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange, onLogUpdated
|
|
|
1832
2285
|
initialEmail: creds.email ?? void 0,
|
|
1833
2286
|
onSubmit: handleConfigureSubmit,
|
|
1834
2287
|
onCancel: () => {
|
|
1835
|
-
|
|
1836
|
-
|
|
2288
|
+
modal.close();
|
|
2289
|
+
jira.clearError("configure");
|
|
1837
2290
|
},
|
|
1838
|
-
loading: loading.configure,
|
|
1839
|
-
error: errors.configure
|
|
2291
|
+
loading: jira.loading.configure,
|
|
2292
|
+
error: jira.errors.configure
|
|
1840
2293
|
}
|
|
1841
2294
|
) });
|
|
1842
2295
|
}
|
|
1843
|
-
if (
|
|
2296
|
+
if (modal.type === "link") {
|
|
1844
2297
|
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
|
|
1845
2298
|
LinkTicketModal,
|
|
1846
2299
|
{
|
|
1847
2300
|
onSubmit: handleLinkSubmit,
|
|
1848
2301
|
onCancel: () => {
|
|
1849
|
-
|
|
1850
|
-
|
|
2302
|
+
modal.close();
|
|
2303
|
+
jira.clearError("link");
|
|
1851
2304
|
},
|
|
1852
|
-
loading: loading.link,
|
|
1853
|
-
error: errors.link
|
|
2305
|
+
loading: jira.loading.link,
|
|
2306
|
+
error: jira.errors.link
|
|
1854
2307
|
}
|
|
1855
2308
|
) });
|
|
1856
2309
|
}
|
|
1857
|
-
if (
|
|
1858
|
-
const ticket = tickets[
|
|
2310
|
+
if (modal.type === "status" && repo.repoPath && repo.currentBranch && jira.tickets[nav.index]) {
|
|
2311
|
+
const ticket = jira.tickets[nav.index];
|
|
1859
2312
|
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
|
|
1860
2313
|
ChangeStatusModal,
|
|
1861
2314
|
{
|
|
1862
|
-
repoPath,
|
|
2315
|
+
repoPath: repo.repoPath,
|
|
1863
2316
|
ticketKey: ticket.key,
|
|
1864
2317
|
currentStatus: ticket.status,
|
|
1865
|
-
onComplete:
|
|
1866
|
-
|
|
1867
|
-
updateTicketStatus(repoPath, currentBranch, ticket.key, newStatus);
|
|
1868
|
-
logJiraStatusChanged(ticket.key, ticket.summary, oldStatus, newStatus);
|
|
1869
|
-
onLogUpdated == null ? void 0 : onLogUpdated();
|
|
1870
|
-
setShowStatusModal(false);
|
|
1871
|
-
refreshTickets();
|
|
1872
|
-
},
|
|
1873
|
-
onCancel: () => setShowStatusModal(false)
|
|
2318
|
+
onComplete: handleStatusComplete,
|
|
2319
|
+
onCancel: modal.close
|
|
1874
2320
|
}
|
|
1875
2321
|
) });
|
|
1876
2322
|
}
|
|
1877
2323
|
const title = "[4] Jira";
|
|
1878
2324
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
1879
2325
|
return /* @__PURE__ */ jsx11(TitledBox4, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, children: [
|
|
1880
|
-
jiraState === "not_configured" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No Jira site configured" }),
|
|
1881
|
-
jiraState === "no_tickets" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No tickets linked to this branch" }),
|
|
1882
|
-
jiraState === "has_tickets" && tickets.map((ticket, idx) => /* @__PURE__ */ jsx11(
|
|
2326
|
+
jira.jiraState === "not_configured" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No Jira site configured" }),
|
|
2327
|
+
jira.jiraState === "no_tickets" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No tickets linked to this branch" }),
|
|
2328
|
+
jira.jiraState === "has_tickets" && jira.tickets.map((ticket, idx) => /* @__PURE__ */ jsx11(
|
|
1883
2329
|
TicketItem,
|
|
1884
2330
|
{
|
|
1885
2331
|
ticketKey: ticket.key,
|
|
1886
2332
|
summary: ticket.summary,
|
|
1887
2333
|
status: ticket.status,
|
|
1888
|
-
isHighlighted: idx ===
|
|
2334
|
+
isHighlighted: idx === nav.index
|
|
1889
2335
|
},
|
|
1890
2336
|
ticket.key
|
|
1891
2337
|
))
|
|
@@ -1893,84 +2339,25 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange, onLogUpdated
|
|
|
1893
2339
|
}
|
|
1894
2340
|
|
|
1895
2341
|
// src/components/logs/LogsView.tsx
|
|
1896
|
-
import {
|
|
2342
|
+
import { useEffect as useEffect12 } from "react";
|
|
1897
2343
|
import { Box as Box14, useInput as useInput12 } from "ink";
|
|
1898
2344
|
|
|
1899
|
-
// src/components/logs/
|
|
2345
|
+
// src/components/logs/LogViewerBox.tsx
|
|
2346
|
+
import { useRef as useRef7, useState as useState16 } from "react";
|
|
1900
2347
|
import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
|
|
1901
2348
|
import { Box as Box12, Text as Text12, useInput as useInput10 } from "ink";
|
|
1902
|
-
import {
|
|
1903
|
-
function LogsHistoryBox({
|
|
1904
|
-
logFiles,
|
|
1905
|
-
selectedDate,
|
|
1906
|
-
highlightedIndex,
|
|
1907
|
-
onHighlight,
|
|
1908
|
-
onSelect,
|
|
1909
|
-
isFocused
|
|
1910
|
-
}) {
|
|
1911
|
-
const title = "[5] Logs";
|
|
1912
|
-
const borderColor = isFocused ? "yellow" : void 0;
|
|
1913
|
-
useInput10(
|
|
1914
|
-
(input, key) => {
|
|
1915
|
-
if (logFiles.length === 0) return;
|
|
1916
|
-
if (key.upArrow || input === "k") {
|
|
1917
|
-
onHighlight(Math.max(0, highlightedIndex - 1));
|
|
1918
|
-
}
|
|
1919
|
-
if (key.downArrow || input === "j") {
|
|
1920
|
-
onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
|
|
1921
|
-
}
|
|
1922
|
-
if (key.return) {
|
|
1923
|
-
const file = logFiles[highlightedIndex];
|
|
1924
|
-
if (file) {
|
|
1925
|
-
onSelect(file.date);
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
},
|
|
1929
|
-
{ isActive: isFocused }
|
|
1930
|
-
);
|
|
1931
|
-
return /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
|
|
1932
|
-
logFiles.length === 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "No logs yet" }),
|
|
1933
|
-
logFiles.map((file, idx) => {
|
|
1934
|
-
const isHighlighted = idx === highlightedIndex;
|
|
1935
|
-
const isSelected = file.date === selectedDate;
|
|
1936
|
-
const cursor = isHighlighted ? ">" : " ";
|
|
1937
|
-
const indicator = isSelected ? " *" : "";
|
|
1938
|
-
return /* @__PURE__ */ jsxs12(Box12, { children: [
|
|
1939
|
-
/* @__PURE__ */ jsxs12(Text12, { color: isHighlighted ? "yellow" : void 0, children: [
|
|
1940
|
-
cursor,
|
|
1941
|
-
" "
|
|
1942
|
-
] }),
|
|
1943
|
-
/* @__PURE__ */ jsx12(
|
|
1944
|
-
Text12,
|
|
1945
|
-
{
|
|
1946
|
-
color: file.isToday ? "green" : void 0,
|
|
1947
|
-
bold: file.isToday,
|
|
1948
|
-
children: file.date
|
|
1949
|
-
}
|
|
1950
|
-
),
|
|
1951
|
-
file.isToday && /* @__PURE__ */ jsx12(Text12, { color: "green", children: " (today)" }),
|
|
1952
|
-
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: indicator })
|
|
1953
|
-
] }, file.date);
|
|
1954
|
-
})
|
|
1955
|
-
] }) });
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
// src/components/logs/LogViewerBox.tsx
|
|
1959
|
-
import { useRef as useRef3, useState as useState8 } from "react";
|
|
1960
|
-
import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
|
|
1961
|
-
import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
|
|
1962
|
-
import { ScrollView as ScrollView2 } from "ink-scroll-view";
|
|
2349
|
+
import { ScrollView as ScrollView4 } from "ink-scroll-view";
|
|
1963
2350
|
import TextInput2 from "ink-text-input";
|
|
1964
2351
|
|
|
1965
2352
|
// src/lib/claude/api.ts
|
|
1966
|
-
import { exec as
|
|
2353
|
+
import { exec as exec3 } from "child_process";
|
|
1967
2354
|
function runClaudePrompt(prompt) {
|
|
1968
2355
|
let childProcess = null;
|
|
1969
2356
|
let cancelled = false;
|
|
1970
2357
|
const promise = new Promise((resolve) => {
|
|
1971
2358
|
const escapedPrompt = prompt.replace(/'/g, "'\\''").replace(/\n/g, "\\n");
|
|
1972
2359
|
const command = `claude -p $'${escapedPrompt}' --output-format json < /dev/null`;
|
|
1973
|
-
childProcess =
|
|
2360
|
+
childProcess = exec3(command, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
1974
2361
|
if (cancelled) {
|
|
1975
2362
|
resolve({
|
|
1976
2363
|
success: false,
|
|
@@ -2046,18 +2433,18 @@ Generate the standup notes:`;
|
|
|
2046
2433
|
}
|
|
2047
2434
|
|
|
2048
2435
|
// src/components/logs/LogViewerBox.tsx
|
|
2049
|
-
import { jsx as
|
|
2436
|
+
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2050
2437
|
function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
|
|
2051
|
-
const scrollRef =
|
|
2052
|
-
const [isInputMode, setIsInputMode] =
|
|
2053
|
-
const [inputValue, setInputValue] =
|
|
2054
|
-
const [isGeneratingStandup, setIsGeneratingStandup] =
|
|
2055
|
-
const [standupResult, setStandupResult] =
|
|
2056
|
-
const claudeProcessRef =
|
|
2438
|
+
const scrollRef = useRef7(null);
|
|
2439
|
+
const [isInputMode, setIsInputMode] = useState16(false);
|
|
2440
|
+
const [inputValue, setInputValue] = useState16("");
|
|
2441
|
+
const [isGeneratingStandup, setIsGeneratingStandup] = useState16(false);
|
|
2442
|
+
const [standupResult, setStandupResult] = useState16(null);
|
|
2443
|
+
const claudeProcessRef = useRef7(null);
|
|
2057
2444
|
const title = "[6] Log Content";
|
|
2058
2445
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
2059
2446
|
const displayTitle = date ? `${title} - ${date}.md` : title;
|
|
2060
|
-
|
|
2447
|
+
useInput10(
|
|
2061
2448
|
(input, key) => {
|
|
2062
2449
|
var _a, _b, _c;
|
|
2063
2450
|
if (key.escape && isInputMode) {
|
|
@@ -2136,14 +2523,14 @@ ${value.trim()}
|
|
|
2136
2523
|
setIsInputMode(false);
|
|
2137
2524
|
onRefresh();
|
|
2138
2525
|
};
|
|
2139
|
-
return /* @__PURE__ */
|
|
2140
|
-
/* @__PURE__ */
|
|
2141
|
-
!date && /* @__PURE__ */
|
|
2142
|
-
date && content === null && /* @__PURE__ */
|
|
2143
|
-
date && content !== null && content.trim() === "" && /* @__PURE__ */
|
|
2144
|
-
date && content && content.trim() !== "" && /* @__PURE__ */
|
|
2526
|
+
return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", flexGrow: 1, children: [
|
|
2527
|
+
/* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx12(ScrollView4, { ref: scrollRef, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
|
|
2528
|
+
!date && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Select a log file to view" }),
|
|
2529
|
+
date && content === null && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Log file not found" }),
|
|
2530
|
+
date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Empty log file" }),
|
|
2531
|
+
date && content && content.trim() !== "" && /* @__PURE__ */ jsx12(Markdown, { children: content })
|
|
2145
2532
|
] }) }) }) }),
|
|
2146
|
-
isInputMode && /* @__PURE__ */
|
|
2533
|
+
isInputMode && /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: ["Add Entry"], borderColor: "yellow", children: /* @__PURE__ */ jsx12(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx12(
|
|
2147
2534
|
TextInput2,
|
|
2148
2535
|
{
|
|
2149
2536
|
value: inputValue,
|
|
@@ -2151,114 +2538,88 @@ ${value.trim()}
|
|
|
2151
2538
|
onSubmit: handleInputSubmit
|
|
2152
2539
|
}
|
|
2153
2540
|
) }) }),
|
|
2154
|
-
isGeneratingStandup && /* @__PURE__ */
|
|
2155
|
-
/* @__PURE__ */
|
|
2156
|
-
/* @__PURE__ */
|
|
2541
|
+
isGeneratingStandup && /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: ["Standup Notes"], borderColor: "yellow", children: /* @__PURE__ */ jsxs12(Box12, { paddingX: 1, flexDirection: "column", children: [
|
|
2542
|
+
/* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "Generating standup notes..." }),
|
|
2543
|
+
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Press Esc to cancel" })
|
|
2157
2544
|
] }) }),
|
|
2158
|
-
standupResult && /* @__PURE__ */
|
|
2159
|
-
|
|
2545
|
+
standupResult && /* @__PURE__ */ jsx12(
|
|
2546
|
+
TitledBox5,
|
|
2160
2547
|
{
|
|
2161
2548
|
borderStyle: "round",
|
|
2162
2549
|
titles: ["Standup Notes"],
|
|
2163
2550
|
borderColor: standupResult.type === "error" ? "red" : "green",
|
|
2164
|
-
children: /* @__PURE__ */
|
|
2165
|
-
standupResult.type === "error" ? /* @__PURE__ */
|
|
2166
|
-
/* @__PURE__ */
|
|
2551
|
+
children: /* @__PURE__ */ jsxs12(Box12, { paddingX: 1, flexDirection: "column", children: [
|
|
2552
|
+
standupResult.type === "error" ? /* @__PURE__ */ jsx12(Text12, { color: "red", children: standupResult.message }) : /* @__PURE__ */ jsx12(Markdown, { children: standupResult.message }),
|
|
2553
|
+
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Press Esc to dismiss" })
|
|
2167
2554
|
] })
|
|
2168
2555
|
}
|
|
2169
2556
|
)
|
|
2170
2557
|
] });
|
|
2171
2558
|
}
|
|
2172
2559
|
|
|
2173
|
-
// src/components/logs/
|
|
2174
|
-
import {
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
}
|
|
2195
|
-
onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
|
|
2196
|
-
}, [isFocused, focusedBox, onKeybindingsChange]);
|
|
2197
|
-
const refreshLogFiles = useCallback3(() => {
|
|
2198
|
-
const files = listLogFiles();
|
|
2199
|
-
setLogFiles(files);
|
|
2200
|
-
if (files.length > 0 && !selectedDate) {
|
|
2201
|
-
const today = getTodayDate();
|
|
2202
|
-
const todayFile = files.find((f) => f.date === today);
|
|
2203
|
-
if (todayFile) {
|
|
2204
|
-
setSelectedDate(todayFile.date);
|
|
2205
|
-
const idx = files.findIndex((f) => f.date === today);
|
|
2206
|
-
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
2207
|
-
} else {
|
|
2208
|
-
setSelectedDate(files[0].date);
|
|
2209
|
-
setHighlightedIndex(0);
|
|
2560
|
+
// src/components/logs/LogsHistoryBox.tsx
|
|
2561
|
+
import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
|
|
2562
|
+
import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
|
|
2563
|
+
import { ScrollView as ScrollView5 } from "ink-scroll-view";
|
|
2564
|
+
import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2565
|
+
function LogsHistoryBox({
|
|
2566
|
+
logFiles,
|
|
2567
|
+
selectedDate,
|
|
2568
|
+
highlightedIndex,
|
|
2569
|
+
onHighlight,
|
|
2570
|
+
onSelect,
|
|
2571
|
+
isFocused
|
|
2572
|
+
}) {
|
|
2573
|
+
const scrollRef = useScrollToIndex(highlightedIndex);
|
|
2574
|
+
const title = "[5] Logs";
|
|
2575
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
2576
|
+
useInput11(
|
|
2577
|
+
(input, key) => {
|
|
2578
|
+
if (logFiles.length === 0) return;
|
|
2579
|
+
if (key.upArrow || input === "k") {
|
|
2580
|
+
onHighlight(Math.max(0, highlightedIndex - 1));
|
|
2210
2581
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
const content = readLog(selectedDate);
|
|
2219
|
-
setLogContent(content);
|
|
2220
|
-
} else {
|
|
2221
|
-
setLogContent(null);
|
|
2222
|
-
}
|
|
2223
|
-
}, [selectedDate]);
|
|
2224
|
-
useEffect6(() => {
|
|
2225
|
-
if (refreshKey !== void 0 && refreshKey > 0) {
|
|
2226
|
-
const files = listLogFiles();
|
|
2227
|
-
setLogFiles(files);
|
|
2228
|
-
const today = getTodayDate();
|
|
2229
|
-
if (selectedDate === today) {
|
|
2230
|
-
const content = readLog(today);
|
|
2231
|
-
setLogContent(content);
|
|
2232
|
-
} else if (!selectedDate && files.length > 0) {
|
|
2233
|
-
const todayFile = files.find((f) => f.date === today);
|
|
2234
|
-
if (todayFile) {
|
|
2235
|
-
setSelectedDate(today);
|
|
2236
|
-
const idx = files.findIndex((f) => f.date === today);
|
|
2237
|
-
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
2582
|
+
if (key.downArrow || input === "j") {
|
|
2583
|
+
onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
|
|
2584
|
+
}
|
|
2585
|
+
if (key.return) {
|
|
2586
|
+
const file = logFiles[highlightedIndex];
|
|
2587
|
+
if (file) {
|
|
2588
|
+
onSelect(file.date);
|
|
2238
2589
|
}
|
|
2239
2590
|
}
|
|
2591
|
+
},
|
|
2592
|
+
{ isActive: isFocused }
|
|
2593
|
+
);
|
|
2594
|
+
return /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
|
|
2595
|
+
logFiles.length === 0 && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "No logs yet" }),
|
|
2596
|
+
logFiles.length > 0 && /* @__PURE__ */ jsx13(ScrollView5, { ref: scrollRef, children: logFiles.map((file, idx) => {
|
|
2597
|
+
const isHighlighted = idx === highlightedIndex;
|
|
2598
|
+
const isSelected = file.date === selectedDate;
|
|
2599
|
+
const cursor = isHighlighted ? ">" : " ";
|
|
2600
|
+
const indicator = isSelected ? " *" : "";
|
|
2601
|
+
return /* @__PURE__ */ jsxs13(Box13, { children: [
|
|
2602
|
+
/* @__PURE__ */ jsxs13(Text13, { color: isHighlighted ? "yellow" : void 0, children: [
|
|
2603
|
+
cursor,
|
|
2604
|
+
" "
|
|
2605
|
+
] }),
|
|
2606
|
+
/* @__PURE__ */ jsx13(Text13, { color: file.isToday ? "green" : void 0, bold: file.isToday, children: file.date }),
|
|
2607
|
+
file.isToday && /* @__PURE__ */ jsx13(Text13, { color: "green", children: " (today)" }),
|
|
2608
|
+
/* @__PURE__ */ jsx13(Text13, { dimColor: true, children: indicator })
|
|
2609
|
+
] }, file.date);
|
|
2610
|
+
}) })
|
|
2611
|
+
] }) });
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// src/components/logs/LogsView.tsx
|
|
2615
|
+
import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2616
|
+
function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
|
|
2617
|
+
const logs = useLogs();
|
|
2618
|
+
useEffect12(() => {
|
|
2619
|
+
if (refreshKey !== void 0 && refreshKey > 0) {
|
|
2620
|
+
logs.handleExternalLogUpdate();
|
|
2240
2621
|
}
|
|
2241
|
-
}, [refreshKey,
|
|
2242
|
-
const handleSelectDate = useCallback3((date) => {
|
|
2243
|
-
setSelectedDate(date);
|
|
2244
|
-
}, []);
|
|
2245
|
-
const handleRefresh = useCallback3(() => {
|
|
2246
|
-
refreshLogFiles();
|
|
2247
|
-
if (selectedDate) {
|
|
2248
|
-
const content = readLog(selectedDate);
|
|
2249
|
-
setLogContent(content);
|
|
2250
|
-
}
|
|
2251
|
-
}, [refreshLogFiles, selectedDate]);
|
|
2252
|
-
const handleLogCreated = useCallback3(() => {
|
|
2253
|
-
const files = listLogFiles();
|
|
2254
|
-
setLogFiles(files);
|
|
2255
|
-
const today = getTodayDate();
|
|
2256
|
-
setSelectedDate(today);
|
|
2257
|
-
const idx = files.findIndex((f) => f.date === today);
|
|
2258
|
-
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
2259
|
-
const content = readLog(today);
|
|
2260
|
-
setLogContent(content);
|
|
2261
|
-
}, []);
|
|
2622
|
+
}, [refreshKey, logs.handleExternalLogUpdate]);
|
|
2262
2623
|
useInput12(
|
|
2263
2624
|
(input) => {
|
|
2264
2625
|
if (input === "5") onFocusedBoxChange("history");
|
|
@@ -2270,22 +2631,22 @@ function LogsView({ isFocused, onKeybindingsChange, refreshKey, focusedBox, onFo
|
|
|
2270
2631
|
/* @__PURE__ */ jsx14(
|
|
2271
2632
|
LogsHistoryBox,
|
|
2272
2633
|
{
|
|
2273
|
-
logFiles,
|
|
2274
|
-
selectedDate,
|
|
2275
|
-
highlightedIndex,
|
|
2276
|
-
onHighlight: setHighlightedIndex,
|
|
2277
|
-
onSelect:
|
|
2634
|
+
logFiles: logs.logFiles,
|
|
2635
|
+
selectedDate: logs.selectedDate,
|
|
2636
|
+
highlightedIndex: logs.highlightedIndex,
|
|
2637
|
+
onHighlight: logs.setHighlightedIndex,
|
|
2638
|
+
onSelect: logs.selectDate,
|
|
2278
2639
|
isFocused: isFocused && focusedBox === "history"
|
|
2279
2640
|
}
|
|
2280
2641
|
),
|
|
2281
2642
|
/* @__PURE__ */ jsx14(
|
|
2282
2643
|
LogViewerBox,
|
|
2283
2644
|
{
|
|
2284
|
-
date: selectedDate,
|
|
2285
|
-
content: logContent,
|
|
2645
|
+
date: logs.selectedDate,
|
|
2646
|
+
content: logs.logContent,
|
|
2286
2647
|
isFocused: isFocused && focusedBox === "viewer",
|
|
2287
|
-
onRefresh:
|
|
2288
|
-
onLogCreated: handleLogCreated
|
|
2648
|
+
onRefresh: logs.refresh,
|
|
2649
|
+
onLogCreated: logs.handleLogCreated
|
|
2289
2650
|
}
|
|
2290
2651
|
)
|
|
2291
2652
|
] });
|
|
@@ -2299,27 +2660,98 @@ var globalBindings = [
|
|
|
2299
2660
|
{ key: "j/k", label: "Navigate" },
|
|
2300
2661
|
{ key: "Ctrl+C", label: "Quit" }
|
|
2301
2662
|
];
|
|
2302
|
-
var modalBindings = [
|
|
2303
|
-
|
|
2304
|
-
]
|
|
2305
|
-
function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
|
|
2663
|
+
var modalBindings = [{ key: "Esc", label: "Cancel" }];
|
|
2664
|
+
var DUCK_ASCII = "<(')___";
|
|
2665
|
+
function KeybindingsBar({ contextBindings = [], modalOpen = false, duck }) {
|
|
2306
2666
|
const allBindings = modalOpen ? [...contextBindings, ...modalBindings] : [...contextBindings, ...globalBindings];
|
|
2307
|
-
return /* @__PURE__ */
|
|
2308
|
-
/* @__PURE__ */
|
|
2309
|
-
|
|
2310
|
-
|
|
2667
|
+
return /* @__PURE__ */ jsxs15(Box15, { flexShrink: 0, paddingX: 1, gap: 2, children: [
|
|
2668
|
+
allBindings.map((binding) => /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
|
|
2669
|
+
/* @__PURE__ */ jsx15(Text14, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
|
|
2670
|
+
/* @__PURE__ */ jsx15(Text14, { dimColor: true, children: binding.label })
|
|
2671
|
+
] }, binding.key)),
|
|
2672
|
+
(duck == null ? void 0 : duck.visible) && /* @__PURE__ */ jsxs15(Box15, { flexGrow: 1, justifyContent: "flex-end", gap: 1, children: [
|
|
2673
|
+
/* @__PURE__ */ jsx15(Text14, { children: DUCK_ASCII }),
|
|
2674
|
+
/* @__PURE__ */ jsx15(Text14, { dimColor: true, children: duck.message })
|
|
2675
|
+
] })
|
|
2676
|
+
] });
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// src/constants/github.ts
|
|
2680
|
+
var GITHUB_KEYBINDINGS = {
|
|
2681
|
+
remotes: [{ key: "Space", label: "Select Remote" }],
|
|
2682
|
+
prs: [
|
|
2683
|
+
{ key: "Space", label: "Select" },
|
|
2684
|
+
{ key: "n", label: "New PR", color: "green" },
|
|
2685
|
+
{ key: "r", label: "Refresh" },
|
|
2686
|
+
{ key: "o", label: "Open", color: "green" },
|
|
2687
|
+
{ key: "y", label: "Copy Link" }
|
|
2688
|
+
],
|
|
2689
|
+
details: [
|
|
2690
|
+
{ key: "r", label: "Refresh" },
|
|
2691
|
+
{ key: "o", label: "Open", color: "green" }
|
|
2692
|
+
]
|
|
2693
|
+
};
|
|
2694
|
+
|
|
2695
|
+
// src/constants/jira.ts
|
|
2696
|
+
var JIRA_KEYBINDINGS = {
|
|
2697
|
+
not_configured: [{ key: "c", label: "Configure Jira" }],
|
|
2698
|
+
no_tickets: [{ key: "l", label: "Link Ticket" }],
|
|
2699
|
+
has_tickets: [
|
|
2700
|
+
{ key: "l", label: "Link" },
|
|
2701
|
+
{ key: "s", label: "Status" },
|
|
2702
|
+
{ key: "d", label: "Unlink", color: "red" },
|
|
2703
|
+
{ key: "o", label: "Open", color: "green" },
|
|
2704
|
+
{ key: "y", label: "Copy Link" }
|
|
2705
|
+
]
|
|
2706
|
+
};
|
|
2707
|
+
|
|
2708
|
+
// src/constants/logs.ts
|
|
2709
|
+
var LOGS_KEYBINDINGS = {
|
|
2710
|
+
history: [{ key: "Enter", label: "Select" }],
|
|
2711
|
+
viewer: [
|
|
2712
|
+
{ key: "i", label: "Add Entry" },
|
|
2713
|
+
{ key: "e", label: "Edit" },
|
|
2714
|
+
{ key: "n", label: "New Log", color: "green" },
|
|
2715
|
+
{ key: "c", label: "Standup" },
|
|
2716
|
+
{ key: "r", label: "Refresh" }
|
|
2717
|
+
]
|
|
2718
|
+
};
|
|
2719
|
+
|
|
2720
|
+
// src/lib/keybindings.ts
|
|
2721
|
+
function computeKeybindings(focusedView, state) {
|
|
2722
|
+
switch (focusedView) {
|
|
2723
|
+
case "github":
|
|
2724
|
+
return GITHUB_KEYBINDINGS[state.github.focusedBox];
|
|
2725
|
+
case "jira":
|
|
2726
|
+
if (state.jira.modalOpen) return [];
|
|
2727
|
+
return JIRA_KEYBINDINGS[state.jira.jiraState];
|
|
2728
|
+
case "logs":
|
|
2729
|
+
return LOGS_KEYBINDINGS[state.logs.focusedBox];
|
|
2730
|
+
default:
|
|
2731
|
+
return [];
|
|
2732
|
+
}
|
|
2311
2733
|
}
|
|
2312
2734
|
|
|
2313
2735
|
// src/app.tsx
|
|
2314
2736
|
import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2315
2737
|
function App() {
|
|
2316
2738
|
const { exit } = useApp();
|
|
2317
|
-
const [focusedView, setFocusedView] =
|
|
2318
|
-
const [modalOpen, setModalOpen] =
|
|
2319
|
-
const [
|
|
2320
|
-
const
|
|
2321
|
-
const [
|
|
2322
|
-
const
|
|
2739
|
+
const [focusedView, setFocusedView] = useState17("github");
|
|
2740
|
+
const [modalOpen, setModalOpen] = useState17(false);
|
|
2741
|
+
const [logRefreshKey, setLogRefreshKey] = useState17(0);
|
|
2742
|
+
const duck = useRubberDuck();
|
|
2743
|
+
const [githubFocusedBox, setGithubFocusedBox] = useState17("remotes");
|
|
2744
|
+
const [jiraState, setJiraState] = useState17("not_configured");
|
|
2745
|
+
const [logsFocusedBox, setLogsFocusedBox] = useState17("history");
|
|
2746
|
+
const keybindings = useMemo2(
|
|
2747
|
+
() => computeKeybindings(focusedView, {
|
|
2748
|
+
github: { focusedBox: githubFocusedBox },
|
|
2749
|
+
jira: { jiraState, modalOpen },
|
|
2750
|
+
logs: { focusedBox: logsFocusedBox }
|
|
2751
|
+
}),
|
|
2752
|
+
[focusedView, githubFocusedBox, jiraState, modalOpen, logsFocusedBox]
|
|
2753
|
+
);
|
|
2754
|
+
const handleLogUpdated = useCallback10(() => {
|
|
2323
2755
|
setLogRefreshKey((prev) => prev + 1);
|
|
2324
2756
|
}, []);
|
|
2325
2757
|
useInput13(
|
|
@@ -2341,6 +2773,12 @@ function App() {
|
|
|
2341
2773
|
setFocusedView("logs");
|
|
2342
2774
|
setLogsFocusedBox("viewer");
|
|
2343
2775
|
}
|
|
2776
|
+
if (input === "d") {
|
|
2777
|
+
duck.toggleDuck();
|
|
2778
|
+
}
|
|
2779
|
+
if (input === "q" && duck.visible) {
|
|
2780
|
+
duck.quack();
|
|
2781
|
+
}
|
|
2344
2782
|
},
|
|
2345
2783
|
{ isActive: !modalOpen }
|
|
2346
2784
|
);
|
|
@@ -2351,7 +2789,7 @@ function App() {
|
|
|
2351
2789
|
GitHubView,
|
|
2352
2790
|
{
|
|
2353
2791
|
isFocused: focusedView === "github",
|
|
2354
|
-
|
|
2792
|
+
onFocusedBoxChange: setGithubFocusedBox,
|
|
2355
2793
|
onLogUpdated: handleLogUpdated
|
|
2356
2794
|
}
|
|
2357
2795
|
),
|
|
@@ -2360,7 +2798,7 @@ function App() {
|
|
|
2360
2798
|
{
|
|
2361
2799
|
isFocused: focusedView === "jira",
|
|
2362
2800
|
onModalChange: setModalOpen,
|
|
2363
|
-
|
|
2801
|
+
onJiraStateChange: setJiraState,
|
|
2364
2802
|
onLogUpdated: handleLogUpdated
|
|
2365
2803
|
}
|
|
2366
2804
|
)
|
|
@@ -2369,14 +2807,20 @@ function App() {
|
|
|
2369
2807
|
LogsView,
|
|
2370
2808
|
{
|
|
2371
2809
|
isFocused: focusedView === "logs",
|
|
2372
|
-
onKeybindingsChange: focusedView === "logs" ? setContextBindings : void 0,
|
|
2373
2810
|
refreshKey: logRefreshKey,
|
|
2374
2811
|
focusedBox: logsFocusedBox,
|
|
2375
2812
|
onFocusedBoxChange: setLogsFocusedBox
|
|
2376
2813
|
}
|
|
2377
2814
|
) })
|
|
2378
2815
|
] }),
|
|
2379
|
-
/* @__PURE__ */ jsx16(
|
|
2816
|
+
/* @__PURE__ */ jsx16(
|
|
2817
|
+
KeybindingsBar,
|
|
2818
|
+
{
|
|
2819
|
+
contextBindings: keybindings,
|
|
2820
|
+
modalOpen,
|
|
2821
|
+
duck: { visible: duck.visible, message: duck.message }
|
|
2822
|
+
}
|
|
2823
|
+
)
|
|
2380
2824
|
] });
|
|
2381
2825
|
}
|
|
2382
2826
|
|
|
@@ -2384,17 +2828,14 @@ function App() {
|
|
|
2384
2828
|
import { render as inkRender } from "ink";
|
|
2385
2829
|
|
|
2386
2830
|
// src/lib/Screen.tsx
|
|
2831
|
+
import { useCallback as useCallback11, useEffect as useEffect13, useState as useState18 } from "react";
|
|
2387
2832
|
import { Box as Box17, useStdout as useStdout2 } from "ink";
|
|
2388
|
-
import { useCallback as useCallback5, useEffect as useEffect7, useState as useState11 } from "react";
|
|
2389
2833
|
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
2390
2834
|
function Screen({ children }) {
|
|
2391
2835
|
const { stdout } = useStdout2();
|
|
2392
|
-
const getSize =
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
);
|
|
2396
|
-
const [size, setSize] = useState11(getSize);
|
|
2397
|
-
useEffect7(() => {
|
|
2836
|
+
const getSize = useCallback11(() => ({ height: stdout.rows, width: stdout.columns }), [stdout]);
|
|
2837
|
+
const [size, setSize] = useState18(getSize);
|
|
2838
|
+
useEffect13(() => {
|
|
2398
2839
|
const onResize = () => setSize(getSize());
|
|
2399
2840
|
stdout.on("resize", onResize);
|
|
2400
2841
|
return () => {
|