clairo 1.0.5 → 1.0.7
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 +1038 -626
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,68 +4,14 @@
|
|
|
4
4
|
import meow from "meow";
|
|
5
5
|
|
|
6
6
|
// src/app.tsx
|
|
7
|
-
import { useCallback as
|
|
7
|
+
import { useCallback as useCallback9, useMemo as useMemo2, useState as useState16 } 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 useCallback4, useEffect as useEffect6, useRef as useRef3, useState as useState7 } 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
|
|
|
16
|
-
// src/lib/config/index.ts
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
18
|
-
import { homedir } from "os";
|
|
19
|
-
import { dirname, join } from "path";
|
|
20
|
-
var CONFIG_PATH = join(homedir(), ".clairo", "config.json");
|
|
21
|
-
var DEFAULT_CONFIG = {};
|
|
22
|
-
function loadConfig() {
|
|
23
|
-
try {
|
|
24
|
-
if (!existsSync(CONFIG_PATH)) {
|
|
25
|
-
return DEFAULT_CONFIG;
|
|
26
|
-
}
|
|
27
|
-
const content = readFileSync(CONFIG_PATH, "utf-8");
|
|
28
|
-
return JSON.parse(content);
|
|
29
|
-
} catch {
|
|
30
|
-
return DEFAULT_CONFIG;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function saveConfig(config) {
|
|
34
|
-
const dir = dirname(CONFIG_PATH);
|
|
35
|
-
if (!existsSync(dir)) {
|
|
36
|
-
mkdirSync(dir, { recursive: true });
|
|
37
|
-
}
|
|
38
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// src/lib/github/config.ts
|
|
42
|
-
function getRepoConfig(repoPath) {
|
|
43
|
-
const config = loadConfig();
|
|
44
|
-
const repos = config.repositories ?? {};
|
|
45
|
-
return repos[repoPath] ?? {};
|
|
46
|
-
}
|
|
47
|
-
function updateRepoConfig(repoPath, updates) {
|
|
48
|
-
const config = loadConfig();
|
|
49
|
-
if (!config.repositories) {
|
|
50
|
-
config.repositories = {};
|
|
51
|
-
}
|
|
52
|
-
config.repositories[repoPath] = {
|
|
53
|
-
...config.repositories[repoPath],
|
|
54
|
-
...updates
|
|
55
|
-
};
|
|
56
|
-
saveConfig(config);
|
|
57
|
-
}
|
|
58
|
-
function getSelectedRemote(repoPath, availableRemotes) {
|
|
59
|
-
const repoConfig = getRepoConfig(repoPath);
|
|
60
|
-
if (repoConfig.selectedRemote && availableRemotes.includes(repoConfig.selectedRemote)) {
|
|
61
|
-
return repoConfig.selectedRemote;
|
|
62
|
-
}
|
|
63
|
-
if (availableRemotes.includes("origin")) {
|
|
64
|
-
return "origin";
|
|
65
|
-
}
|
|
66
|
-
return availableRemotes[0] ?? null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
15
|
// src/lib/github/git.ts
|
|
70
16
|
import { execSync } from "child_process";
|
|
71
17
|
function isGitRepo() {
|
|
@@ -267,6 +213,13 @@ async function getPRDetails(prNumber, repo) {
|
|
|
267
213
|
};
|
|
268
214
|
}
|
|
269
215
|
}
|
|
216
|
+
function openPRCreationPage(owner, branch, onComplete) {
|
|
217
|
+
const headFlag = `${owner}:${branch}`;
|
|
218
|
+
exec(`gh pr create --web --head "${headFlag}"`, (error) => {
|
|
219
|
+
process.stdout.emit("resize");
|
|
220
|
+
onComplete == null ? void 0 : onComplete(error);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
270
223
|
|
|
271
224
|
// src/lib/jira/parser.ts
|
|
272
225
|
var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
@@ -285,8 +238,8 @@ function parseTicketKey(input) {
|
|
|
285
238
|
}
|
|
286
239
|
return null;
|
|
287
240
|
}
|
|
288
|
-
function
|
|
289
|
-
const match =
|
|
241
|
+
function extractTicketKey(text) {
|
|
242
|
+
const match = text.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
|
|
290
243
|
if (match) {
|
|
291
244
|
const candidate = match[1].toUpperCase();
|
|
292
245
|
if (isValidTicketKeyFormat(candidate)) {
|
|
@@ -296,6 +249,59 @@ function extractTicketKeyFromBranch(branchName) {
|
|
|
296
249
|
return null;
|
|
297
250
|
}
|
|
298
251
|
|
|
252
|
+
// src/lib/config/index.ts
|
|
253
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
254
|
+
import { homedir } from "os";
|
|
255
|
+
import { dirname, join } from "path";
|
|
256
|
+
var CONFIG_PATH = join(homedir(), ".clairo", "config.json");
|
|
257
|
+
var DEFAULT_CONFIG = {};
|
|
258
|
+
function loadConfig() {
|
|
259
|
+
try {
|
|
260
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
261
|
+
return DEFAULT_CONFIG;
|
|
262
|
+
}
|
|
263
|
+
const content = readFileSync(CONFIG_PATH, "utf-8");
|
|
264
|
+
return JSON.parse(content);
|
|
265
|
+
} catch {
|
|
266
|
+
return DEFAULT_CONFIG;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function saveConfig(config) {
|
|
270
|
+
const dir = dirname(CONFIG_PATH);
|
|
271
|
+
if (!existsSync(dir)) {
|
|
272
|
+
mkdirSync(dir, { recursive: true });
|
|
273
|
+
}
|
|
274
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/lib/github/config.ts
|
|
278
|
+
function getRepoConfig(repoPath) {
|
|
279
|
+
const config = loadConfig();
|
|
280
|
+
const repos = config.repositories ?? {};
|
|
281
|
+
return repos[repoPath] ?? {};
|
|
282
|
+
}
|
|
283
|
+
function updateRepoConfig(repoPath, updates) {
|
|
284
|
+
const config = loadConfig();
|
|
285
|
+
if (!config.repositories) {
|
|
286
|
+
config.repositories = {};
|
|
287
|
+
}
|
|
288
|
+
config.repositories[repoPath] = {
|
|
289
|
+
...config.repositories[repoPath],
|
|
290
|
+
...updates
|
|
291
|
+
};
|
|
292
|
+
saveConfig(config);
|
|
293
|
+
}
|
|
294
|
+
function getSelectedRemote(repoPath, availableRemotes) {
|
|
295
|
+
const repoConfig = getRepoConfig(repoPath);
|
|
296
|
+
if (repoConfig.selectedRemote && availableRemotes.includes(repoConfig.selectedRemote)) {
|
|
297
|
+
return repoConfig.selectedRemote;
|
|
298
|
+
}
|
|
299
|
+
if (availableRemotes.includes("origin")) {
|
|
300
|
+
return "origin";
|
|
301
|
+
}
|
|
302
|
+
return availableRemotes[0] ?? null;
|
|
303
|
+
}
|
|
304
|
+
|
|
299
305
|
// src/lib/jira/config.ts
|
|
300
306
|
function isJiraConfigured(repoPath) {
|
|
301
307
|
const config = getRepoConfig(repoPath);
|
|
@@ -620,9 +626,265 @@ ${oldStatus} \u2192 ${newStatus}
|
|
|
620
626
|
appendToLog(today, entry);
|
|
621
627
|
}
|
|
622
628
|
|
|
629
|
+
// src/hooks/github/useGitRepo.ts
|
|
630
|
+
import { useCallback, useEffect as useEffect2, useMemo, useState as useState2 } from "react";
|
|
631
|
+
|
|
632
|
+
// src/hooks/useTerminalFocus.ts
|
|
633
|
+
import { useEffect, useState } from "react";
|
|
634
|
+
function useTerminalFocus() {
|
|
635
|
+
const [isFocused, setIsFocused] = useState(null);
|
|
636
|
+
const [focusCount, setFocusCount] = useState(0);
|
|
637
|
+
useEffect(() => {
|
|
638
|
+
process.stdout.write("\x1B[?1004h");
|
|
639
|
+
const handleData = (data) => {
|
|
640
|
+
const str = data.toString();
|
|
641
|
+
if (str.includes("\x1B[I")) {
|
|
642
|
+
setIsFocused(true);
|
|
643
|
+
setFocusCount((c) => c + 1);
|
|
644
|
+
}
|
|
645
|
+
if (str.includes("\x1B[O")) {
|
|
646
|
+
setIsFocused(false);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
process.stdin.on("data", handleData);
|
|
650
|
+
return () => {
|
|
651
|
+
process.stdout.write("\x1B[?1004l");
|
|
652
|
+
process.stdin.off("data", handleData);
|
|
653
|
+
};
|
|
654
|
+
}, []);
|
|
655
|
+
return { isFocused, focusCount };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/hooks/github/useGitRepo.ts
|
|
659
|
+
function useGitRepo() {
|
|
660
|
+
const [isRepo, setIsRepo] = useState2(null);
|
|
661
|
+
const [repoPath, setRepoPath] = useState2(null);
|
|
662
|
+
const [remotes, setRemotes] = useState2([]);
|
|
663
|
+
const [currentBranch, setCurrentBranch] = useState2(null);
|
|
664
|
+
const [selectedRemote, setSelectedRemote] = useState2(null);
|
|
665
|
+
const [loading, setLoading] = useState2(true);
|
|
666
|
+
const [error, setError] = useState2(void 0);
|
|
667
|
+
const { focusCount } = useTerminalFocus();
|
|
668
|
+
const currentRepoSlug = useMemo(() => {
|
|
669
|
+
if (!selectedRemote) return null;
|
|
670
|
+
const remote = remotes.find((r) => r.name === selectedRemote);
|
|
671
|
+
if (!remote) return null;
|
|
672
|
+
return getRepoFromRemote(remote.url);
|
|
673
|
+
}, [selectedRemote, remotes]);
|
|
674
|
+
useEffect2(() => {
|
|
675
|
+
const gitRepoCheck = isGitRepo();
|
|
676
|
+
setIsRepo(gitRepoCheck);
|
|
677
|
+
if (!gitRepoCheck) {
|
|
678
|
+
setLoading(false);
|
|
679
|
+
setError("Not a git repository");
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const rootResult = getRepoRoot();
|
|
683
|
+
if (rootResult.success) {
|
|
684
|
+
setRepoPath(rootResult.data);
|
|
685
|
+
}
|
|
686
|
+
const branchResult = getCurrentBranch();
|
|
687
|
+
if (branchResult.success) {
|
|
688
|
+
setCurrentBranch(branchResult.data);
|
|
689
|
+
}
|
|
690
|
+
const remotesResult = listRemotes();
|
|
691
|
+
if (!remotesResult.success) {
|
|
692
|
+
setError(remotesResult.error);
|
|
693
|
+
setLoading(false);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
setRemotes(remotesResult.data);
|
|
697
|
+
const remoteNames = remotesResult.data.map((r) => r.name);
|
|
698
|
+
const defaultRemote = getSelectedRemote(rootResult.success ? rootResult.data : "", remoteNames);
|
|
699
|
+
setSelectedRemote(defaultRemote);
|
|
700
|
+
setLoading(false);
|
|
701
|
+
}, []);
|
|
702
|
+
useEffect2(() => {
|
|
703
|
+
if (!isRepo || focusCount === 0) return;
|
|
704
|
+
const result = getCurrentBranch();
|
|
705
|
+
if (result.success && result.data !== currentBranch) {
|
|
706
|
+
setCurrentBranch(result.data);
|
|
707
|
+
}
|
|
708
|
+
}, [isRepo, focusCount]);
|
|
709
|
+
const selectRemote = useCallback(
|
|
710
|
+
(remoteName) => {
|
|
711
|
+
setSelectedRemote(remoteName);
|
|
712
|
+
if (repoPath) {
|
|
713
|
+
updateRepoConfig(repoPath, { selectedRemote: remoteName });
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
[repoPath]
|
|
717
|
+
);
|
|
718
|
+
const refreshBranch = useCallback(() => {
|
|
719
|
+
const branchResult = getCurrentBranch();
|
|
720
|
+
if (branchResult.success) {
|
|
721
|
+
setCurrentBranch(branchResult.data);
|
|
722
|
+
return branchResult.data;
|
|
723
|
+
}
|
|
724
|
+
return null;
|
|
725
|
+
}, []);
|
|
726
|
+
return {
|
|
727
|
+
isRepo,
|
|
728
|
+
repoPath,
|
|
729
|
+
remotes,
|
|
730
|
+
currentBranch,
|
|
731
|
+
selectedRemote,
|
|
732
|
+
currentRepoSlug,
|
|
733
|
+
selectRemote,
|
|
734
|
+
refreshBranch,
|
|
735
|
+
loading,
|
|
736
|
+
error
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/hooks/github/usePRPolling.ts
|
|
741
|
+
import { useRef, useEffect as useEffect3, useState as useState3, useCallback as useCallback2 } from "react";
|
|
742
|
+
function usePRPolling() {
|
|
743
|
+
const prNumbersBeforeCreate = useRef(/* @__PURE__ */ new Set());
|
|
744
|
+
const pollingIntervalRef = useRef(null);
|
|
745
|
+
const [isPolling, setIsPolling] = useState3(false);
|
|
746
|
+
const stopPolling = useCallback2(() => {
|
|
747
|
+
if (pollingIntervalRef.current) {
|
|
748
|
+
clearInterval(pollingIntervalRef.current);
|
|
749
|
+
pollingIntervalRef.current = null;
|
|
750
|
+
}
|
|
751
|
+
setIsPolling(false);
|
|
752
|
+
}, []);
|
|
753
|
+
const startPolling = useCallback2(
|
|
754
|
+
(options) => {
|
|
755
|
+
const {
|
|
756
|
+
branch,
|
|
757
|
+
repoSlug,
|
|
758
|
+
existingPRNumbers,
|
|
759
|
+
onNewPR,
|
|
760
|
+
onPRsUpdated,
|
|
761
|
+
maxAttempts = 24,
|
|
762
|
+
pollInterval = 5e3
|
|
763
|
+
} = options;
|
|
764
|
+
stopPolling();
|
|
765
|
+
prNumbersBeforeCreate.current = new Set(existingPRNumbers);
|
|
766
|
+
let attempts = 0;
|
|
767
|
+
setIsPolling(true);
|
|
768
|
+
pollingIntervalRef.current = setInterval(async () => {
|
|
769
|
+
attempts++;
|
|
770
|
+
if (attempts > maxAttempts) {
|
|
771
|
+
stopPolling();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const result = await listPRsForBranch(branch, repoSlug);
|
|
775
|
+
if (result.success) {
|
|
776
|
+
onPRsUpdated(result.data);
|
|
777
|
+
const newPR = result.data.find(
|
|
778
|
+
(pr) => !prNumbersBeforeCreate.current.has(pr.number)
|
|
779
|
+
);
|
|
780
|
+
if (newPR) {
|
|
781
|
+
stopPolling();
|
|
782
|
+
onNewPR(newPR);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}, pollInterval);
|
|
786
|
+
},
|
|
787
|
+
[stopPolling]
|
|
788
|
+
);
|
|
789
|
+
useEffect3(() => {
|
|
790
|
+
return () => {
|
|
791
|
+
if (pollingIntervalRef.current) {
|
|
792
|
+
clearInterval(pollingIntervalRef.current);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}, []);
|
|
796
|
+
return {
|
|
797
|
+
startPolling,
|
|
798
|
+
stopPolling,
|
|
799
|
+
isPolling
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/hooks/github/usePullRequests.ts
|
|
804
|
+
import { useCallback as useCallback3, useState as useState4 } from "react";
|
|
805
|
+
function usePullRequests() {
|
|
806
|
+
const [prs, setPrs] = useState4([]);
|
|
807
|
+
const [selectedPR, setSelectedPR] = useState4(null);
|
|
808
|
+
const [prDetails, setPrDetails] = useState4(null);
|
|
809
|
+
const [loading, setLoading] = useState4({
|
|
810
|
+
prs: false,
|
|
811
|
+
details: false
|
|
812
|
+
});
|
|
813
|
+
const [errors, setErrors] = useState4({});
|
|
814
|
+
const refreshPRs = useCallback3(async (branch, repoSlug) => {
|
|
815
|
+
setLoading((prev) => ({ ...prev, prs: true }));
|
|
816
|
+
setPrs([]);
|
|
817
|
+
setSelectedPR(null);
|
|
818
|
+
setPrDetails(null);
|
|
819
|
+
try {
|
|
820
|
+
const result = await listPRsForBranch(branch, repoSlug);
|
|
821
|
+
if (result.success) {
|
|
822
|
+
setPrs(result.data);
|
|
823
|
+
setErrors((prev) => ({ ...prev, prs: void 0 }));
|
|
824
|
+
return result.data[0] ?? null;
|
|
825
|
+
} else {
|
|
826
|
+
setErrors((prev) => ({ ...prev, prs: result.error }));
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
} catch (err) {
|
|
830
|
+
setErrors((prev) => ({ ...prev, prs: String(err) }));
|
|
831
|
+
return null;
|
|
832
|
+
} finally {
|
|
833
|
+
setLoading((prev) => ({ ...prev, prs: false }));
|
|
834
|
+
}
|
|
835
|
+
}, []);
|
|
836
|
+
const refreshDetails = useCallback3(async (pr, repoSlug) => {
|
|
837
|
+
setLoading((prev) => ({ ...prev, details: true }));
|
|
838
|
+
try {
|
|
839
|
+
const result = await getPRDetails(pr.number, repoSlug);
|
|
840
|
+
if (result.success) {
|
|
841
|
+
setPrDetails(result.data);
|
|
842
|
+
setErrors((prev) => ({ ...prev, details: void 0 }));
|
|
843
|
+
} else {
|
|
844
|
+
setErrors((prev) => ({ ...prev, details: result.error }));
|
|
845
|
+
}
|
|
846
|
+
} catch (err) {
|
|
847
|
+
setErrors((prev) => ({ ...prev, details: String(err) }));
|
|
848
|
+
} finally {
|
|
849
|
+
setLoading((prev) => ({ ...prev, details: false }));
|
|
850
|
+
}
|
|
851
|
+
}, []);
|
|
852
|
+
const fetchPRsAndDetails = useCallback3(async (branch, repoSlug) => {
|
|
853
|
+
const firstPR = await refreshPRs(branch, repoSlug);
|
|
854
|
+
if (firstPR) {
|
|
855
|
+
setSelectedPR(firstPR);
|
|
856
|
+
refreshDetails(firstPR, repoSlug);
|
|
857
|
+
}
|
|
858
|
+
}, [refreshPRs, refreshDetails]);
|
|
859
|
+
const selectPR = useCallback3((pr, repoSlug) => {
|
|
860
|
+
setSelectedPR(pr);
|
|
861
|
+
if (repoSlug) {
|
|
862
|
+
refreshDetails(pr, repoSlug);
|
|
863
|
+
}
|
|
864
|
+
}, [refreshDetails]);
|
|
865
|
+
const setError = useCallback3((key, message) => {
|
|
866
|
+
setErrors((prev) => ({ ...prev, [key]: message }));
|
|
867
|
+
}, []);
|
|
868
|
+
return {
|
|
869
|
+
prs,
|
|
870
|
+
selectedPR,
|
|
871
|
+
prDetails,
|
|
872
|
+
refreshPRs,
|
|
873
|
+
refreshDetails,
|
|
874
|
+
fetchPRsAndDetails,
|
|
875
|
+
selectPR,
|
|
876
|
+
loading,
|
|
877
|
+
errors,
|
|
878
|
+
setError,
|
|
879
|
+
// Expose setters for cases where external code needs to update state directly
|
|
880
|
+
setPrs,
|
|
881
|
+
setSelectedPR
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
623
885
|
// src/components/github/PRDetailsBox.tsx
|
|
624
886
|
import open from "open";
|
|
625
|
-
import { useRef } from "react";
|
|
887
|
+
import { useRef as useRef2 } from "react";
|
|
626
888
|
import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
|
|
627
889
|
import { ScrollView } from "ink-scroll-view";
|
|
628
890
|
|
|
@@ -768,7 +1030,7 @@ function getCheckSortOrder(check) {
|
|
|
768
1030
|
}
|
|
769
1031
|
function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
770
1032
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
771
|
-
const scrollRef =
|
|
1033
|
+
const scrollRef = useRef2(null);
|
|
772
1034
|
const title = "[3] PR Details";
|
|
773
1035
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
774
1036
|
const displayTitle = pr ? `${title} - #${pr.number}` : title;
|
|
@@ -892,7 +1154,7 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
|
892
1154
|
}
|
|
893
1155
|
|
|
894
1156
|
// src/components/github/PullRequestsBox.tsx
|
|
895
|
-
import { useEffect, useState } from "react";
|
|
1157
|
+
import { useEffect as useEffect4, useState as useState5 } from "react";
|
|
896
1158
|
import { TitledBox } from "@mishieck/ink-titled-box";
|
|
897
1159
|
import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
|
|
898
1160
|
|
|
@@ -930,9 +1192,9 @@ function PullRequestsBox({
|
|
|
930
1192
|
repoSlug,
|
|
931
1193
|
isFocused
|
|
932
1194
|
}) {
|
|
933
|
-
const [highlightedIndex, setHighlightedIndex] =
|
|
1195
|
+
const [highlightedIndex, setHighlightedIndex] = useState5(0);
|
|
934
1196
|
const totalItems = prs.length + 1;
|
|
935
|
-
|
|
1197
|
+
useEffect4(() => {
|
|
936
1198
|
const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
|
|
937
1199
|
if (idx >= 0) setHighlightedIndex(idx);
|
|
938
1200
|
}, [selectedPR, prs]);
|
|
@@ -997,13 +1259,13 @@ function PullRequestsBox({
|
|
|
997
1259
|
}
|
|
998
1260
|
|
|
999
1261
|
// src/components/github/RemotesBox.tsx
|
|
1000
|
-
import { useEffect as
|
|
1262
|
+
import { useEffect as useEffect5, useState as useState6 } from "react";
|
|
1001
1263
|
import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
|
|
1002
1264
|
import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
|
|
1003
1265
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1004
1266
|
function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
|
|
1005
|
-
const [highlightedIndex, setHighlightedIndex] =
|
|
1006
|
-
|
|
1267
|
+
const [highlightedIndex, setHighlightedIndex] = useState6(0);
|
|
1268
|
+
useEffect5(() => {
|
|
1007
1269
|
const idx = remotes.findIndex((r) => r.name === selectedRemote);
|
|
1008
1270
|
if (idx >= 0) setHighlightedIndex(idx);
|
|
1009
1271
|
}, [selectedRemote, remotes]);
|
|
@@ -1052,209 +1314,98 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
|
|
|
1052
1314
|
|
|
1053
1315
|
// src/components/github/GitHubView.tsx
|
|
1054
1316
|
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" });
|
|
1317
|
+
function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
1318
|
+
const repo = useGitRepo();
|
|
1319
|
+
const pullRequests = usePullRequests();
|
|
1320
|
+
const polling = usePRPolling();
|
|
1321
|
+
const [focusedBox, setFocusedBox] = useState7("remotes");
|
|
1322
|
+
const lastFetchedRef = useRef3(null);
|
|
1323
|
+
useEffect6(() => {
|
|
1324
|
+
if (repo.loading || !repo.currentBranch || !repo.currentRepoSlug) return;
|
|
1325
|
+
const current = { branch: repo.currentBranch, repoSlug: repo.currentRepoSlug };
|
|
1326
|
+
const last = lastFetchedRef.current;
|
|
1327
|
+
if (last && last.branch === current.branch && last.repoSlug === current.repoSlug) return;
|
|
1328
|
+
lastFetchedRef.current = current;
|
|
1329
|
+
pullRequests.fetchPRsAndDetails(repo.currentBranch, repo.currentRepoSlug);
|
|
1330
|
+
}, [repo.loading, repo.currentBranch, repo.currentRepoSlug, pullRequests.fetchPRsAndDetails]);
|
|
1331
|
+
useEffect6(() => {
|
|
1332
|
+
if (isFocused) {
|
|
1333
|
+
repo.refreshBranch();
|
|
1089
1334
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1335
|
+
}, [isFocused, repo.refreshBranch]);
|
|
1336
|
+
useEffect6(() => {
|
|
1337
|
+
onFocusedBoxChange == null ? void 0 : onFocusedBoxChange(focusedBox);
|
|
1338
|
+
}, [focusedBox, onFocusedBoxChange]);
|
|
1339
|
+
const handleRemoteSelect = useCallback4(
|
|
1340
|
+
(remoteName) => {
|
|
1341
|
+
repo.selectRemote(remoteName);
|
|
1342
|
+
const remote = repo.remotes.find((r) => r.name === remoteName);
|
|
1343
|
+
if (!remote || !repo.currentBranch) return;
|
|
1344
|
+
const repoSlug = getRepoFromRemote(remote.url);
|
|
1345
|
+
if (!repoSlug) return;
|
|
1346
|
+
lastFetchedRef.current = { branch: repo.currentBranch, repoSlug };
|
|
1347
|
+
pullRequests.fetchPRsAndDetails(repo.currentBranch, repoSlug);
|
|
1348
|
+
},
|
|
1349
|
+
[repo.selectRemote, repo.remotes, repo.currentBranch, pullRequests.fetchPRsAndDetails]
|
|
1350
|
+
);
|
|
1351
|
+
const handlePRSelect = useCallback4(
|
|
1352
|
+
(pr) => {
|
|
1353
|
+
pullRequests.selectPR(pr, repo.currentRepoSlug);
|
|
1354
|
+
},
|
|
1355
|
+
[pullRequests.selectPR, repo.currentRepoSlug]
|
|
1356
|
+
);
|
|
1357
|
+
const createPRContext = useRef3({ repo, pullRequests, onLogUpdated });
|
|
1358
|
+
createPRContext.current = { repo, pullRequests, onLogUpdated };
|
|
1359
|
+
const handleCreatePR = useCallback4(() => {
|
|
1360
|
+
const { repo: repo2, pullRequests: pullRequests2 } = createPRContext.current;
|
|
1361
|
+
if (!repo2.currentBranch) {
|
|
1362
|
+
pullRequests2.setError("prs", "No branch detected");
|
|
1098
1363
|
return;
|
|
1099
1364
|
}
|
|
1100
|
-
const
|
|
1101
|
-
if (
|
|
1102
|
-
|
|
1365
|
+
const remoteResult = findRemoteWithBranch(repo2.currentBranch);
|
|
1366
|
+
if (!remoteResult.success) {
|
|
1367
|
+
pullRequests2.setError("prs", "Push your branch to a remote first");
|
|
1368
|
+
return;
|
|
1103
1369
|
}
|
|
1104
|
-
|
|
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();
|
|
1169
|
-
}
|
|
1170
|
-
}, [currentRepoSlug, currentBranch, refreshPRs]);
|
|
1171
|
-
useEffect3(() => {
|
|
1172
|
-
if (!selectedPR || !currentRepoSlug) {
|
|
1173
|
-
setPrDetails(null);
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
refreshDetails();
|
|
1177
|
-
}, [selectedPR, currentRepoSlug, refreshDetails]);
|
|
1178
|
-
const handleRemoteSelect = useCallback(
|
|
1179
|
-
(remoteName) => {
|
|
1180
|
-
setSelectedRemote(remoteName);
|
|
1181
|
-
if (repoPath) {
|
|
1182
|
-
updateRepoConfig(repoPath, { selectedRemote: remoteName });
|
|
1183
|
-
}
|
|
1184
|
-
},
|
|
1185
|
-
[repoPath]
|
|
1186
|
-
);
|
|
1187
|
-
const handlePRSelect = useCallback((pr) => {
|
|
1188
|
-
setSelectedPR(pr);
|
|
1189
|
-
}, []);
|
|
1190
|
-
const prNumbersBeforeCreate = useRef2(/* @__PURE__ */ new Set());
|
|
1191
|
-
const pollingIntervalRef = useRef2(null);
|
|
1192
|
-
const handleCreatePR = useCallback(() => {
|
|
1193
|
-
if (!currentBranch) {
|
|
1194
|
-
setErrors((prev) => ({ ...prev, prs: "No branch detected" }));
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
|
-
const remoteResult = findRemoteWithBranch(currentBranch);
|
|
1198
|
-
if (!remoteResult.success) {
|
|
1199
|
-
setErrors((prev) => ({ ...prev, prs: "Push your branch to a remote first" }));
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
prNumbersBeforeCreate.current = new Set(prs.map((pr) => pr.number));
|
|
1203
|
-
const headFlag = `${remoteResult.data.owner}:${currentBranch}`;
|
|
1204
|
-
exec3(`gh pr create --web --head "${headFlag}"`, (error) => {
|
|
1205
|
-
process.stdout.emit("resize");
|
|
1370
|
+
openPRCreationPage(remoteResult.data.owner, repo2.currentBranch, (error) => {
|
|
1206
1371
|
if (error) {
|
|
1207
|
-
|
|
1372
|
+
pullRequests2.setError("prs", `Failed to create PR: ${error.message}`);
|
|
1208
1373
|
}
|
|
1209
1374
|
});
|
|
1210
|
-
if (!currentRepoSlug) return;
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
if (result.success) {
|
|
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);
|
|
1375
|
+
if (!repo2.currentRepoSlug) return;
|
|
1376
|
+
polling.startPolling({
|
|
1377
|
+
branch: repo2.currentBranch,
|
|
1378
|
+
repoSlug: repo2.currentRepoSlug,
|
|
1379
|
+
existingPRNumbers: pullRequests2.prs.map((pr) => pr.number),
|
|
1380
|
+
onPRsUpdated: (prs) => {
|
|
1381
|
+
pullRequests2.setPrs(prs);
|
|
1382
|
+
},
|
|
1383
|
+
onNewPR: (newPR) => {
|
|
1384
|
+
var _a;
|
|
1385
|
+
const ctx = createPRContext.current;
|
|
1386
|
+
const tickets = ctx.repo.repoPath && ctx.repo.currentBranch ? getLinkedTickets(ctx.repo.repoPath, ctx.repo.currentBranch).map((t) => t.key) : [];
|
|
1387
|
+
logPRCreated(newPR.number, newPR.title, tickets);
|
|
1388
|
+
(_a = ctx.onLogUpdated) == null ? void 0 : _a.call(ctx);
|
|
1389
|
+
ctx.pullRequests.setSelectedPR(newPR);
|
|
1390
|
+
if (ctx.repo.currentRepoSlug) {
|
|
1391
|
+
ctx.pullRequests.refreshDetails(newPR, ctx.repo.currentRepoSlug);
|
|
1239
1392
|
}
|
|
1240
1393
|
}
|
|
1241
|
-
}
|
|
1242
|
-
}, [
|
|
1243
|
-
useEffect3(() => {
|
|
1244
|
-
return () => {
|
|
1245
|
-
if (pollingIntervalRef.current) {
|
|
1246
|
-
clearInterval(pollingIntervalRef.current);
|
|
1247
|
-
}
|
|
1248
|
-
};
|
|
1249
|
-
}, []);
|
|
1394
|
+
});
|
|
1395
|
+
}, [polling.startPolling]);
|
|
1250
1396
|
useInput4(
|
|
1251
1397
|
(input) => {
|
|
1252
1398
|
if (input === "1") setFocusedBox("remotes");
|
|
1253
1399
|
if (input === "2") setFocusedBox("prs");
|
|
1254
1400
|
if (input === "3") setFocusedBox("details");
|
|
1255
1401
|
if (input === "r") {
|
|
1256
|
-
|
|
1257
|
-
if (focusedBox === "
|
|
1402
|
+
const freshBranch = repo.refreshBranch() ?? repo.currentBranch;
|
|
1403
|
+
if (focusedBox === "prs" && freshBranch && repo.currentRepoSlug) {
|
|
1404
|
+
pullRequests.fetchPRsAndDetails(freshBranch, repo.currentRepoSlug);
|
|
1405
|
+
}
|
|
1406
|
+
if (focusedBox === "details" && pullRequests.selectedPR && repo.currentRepoSlug) {
|
|
1407
|
+
pullRequests.refreshDetails(pullRequests.selectedPR, repo.currentRepoSlug);
|
|
1408
|
+
}
|
|
1258
1409
|
}
|
|
1259
1410
|
if (input === "n" && focusedBox === "prs") {
|
|
1260
1411
|
handleCreatePR();
|
|
@@ -1262,41 +1413,41 @@ function GitHubView({ isFocused, onKeybindingsChange, onLogUpdated }) {
|
|
|
1262
1413
|
},
|
|
1263
1414
|
{ isActive: isFocused }
|
|
1264
1415
|
);
|
|
1265
|
-
if (isRepo === false) {
|
|
1416
|
+
if (repo.isRepo === false) {
|
|
1266
1417
|
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
1418
|
}
|
|
1268
1419
|
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, children: [
|
|
1269
1420
|
/* @__PURE__ */ jsx5(
|
|
1270
1421
|
RemotesBox,
|
|
1271
1422
|
{
|
|
1272
|
-
remotes,
|
|
1273
|
-
selectedRemote,
|
|
1423
|
+
remotes: repo.remotes,
|
|
1424
|
+
selectedRemote: repo.selectedRemote,
|
|
1274
1425
|
onSelect: handleRemoteSelect,
|
|
1275
|
-
loading: loading
|
|
1276
|
-
error:
|
|
1426
|
+
loading: repo.loading,
|
|
1427
|
+
error: repo.error,
|
|
1277
1428
|
isFocused: isFocused && focusedBox === "remotes"
|
|
1278
1429
|
}
|
|
1279
1430
|
),
|
|
1280
1431
|
/* @__PURE__ */ jsx5(
|
|
1281
1432
|
PullRequestsBox,
|
|
1282
1433
|
{
|
|
1283
|
-
prs,
|
|
1284
|
-
selectedPR,
|
|
1434
|
+
prs: pullRequests.prs,
|
|
1435
|
+
selectedPR: pullRequests.selectedPR,
|
|
1285
1436
|
onSelect: handlePRSelect,
|
|
1286
1437
|
onCreatePR: handleCreatePR,
|
|
1287
|
-
loading: loading.prs,
|
|
1288
|
-
error: errors.prs,
|
|
1289
|
-
branch: currentBranch,
|
|
1290
|
-
repoSlug: currentRepoSlug,
|
|
1438
|
+
loading: pullRequests.loading.prs,
|
|
1439
|
+
error: pullRequests.errors.prs,
|
|
1440
|
+
branch: repo.currentBranch,
|
|
1441
|
+
repoSlug: repo.currentRepoSlug,
|
|
1291
1442
|
isFocused: isFocused && focusedBox === "prs"
|
|
1292
1443
|
}
|
|
1293
1444
|
),
|
|
1294
1445
|
/* @__PURE__ */ jsx5(
|
|
1295
1446
|
PRDetailsBox,
|
|
1296
1447
|
{
|
|
1297
|
-
pr: prDetails,
|
|
1298
|
-
loading: loading.details,
|
|
1299
|
-
error: errors.details,
|
|
1448
|
+
pr: pullRequests.prDetails,
|
|
1449
|
+
loading: pullRequests.loading.details,
|
|
1450
|
+
error: pullRequests.errors.details,
|
|
1300
1451
|
isFocused: isFocused && focusedBox === "details"
|
|
1301
1452
|
}
|
|
1302
1453
|
)
|
|
@@ -1304,22 +1455,291 @@ function GitHubView({ isFocused, onKeybindingsChange, onLogUpdated }) {
|
|
|
1304
1455
|
}
|
|
1305
1456
|
|
|
1306
1457
|
// src/components/jira/JiraView.tsx
|
|
1307
|
-
import {
|
|
1458
|
+
import { useEffect as useEffect9, useRef as useRef5 } from "react";
|
|
1308
1459
|
import open2 from "open";
|
|
1309
1460
|
import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
|
|
1310
1461
|
import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
|
|
1311
1462
|
|
|
1463
|
+
// src/hooks/jira/useJiraTickets.ts
|
|
1464
|
+
import { useCallback as useCallback5, useState as useState8 } from "react";
|
|
1465
|
+
function useJiraTickets() {
|
|
1466
|
+
const [jiraState, setJiraState] = useState8("not_configured");
|
|
1467
|
+
const [tickets, setTickets] = useState8([]);
|
|
1468
|
+
const [loading, setLoading] = useState8({ configure: false, link: false });
|
|
1469
|
+
const [errors, setErrors] = useState8({});
|
|
1470
|
+
const initializeJiraState = useCallback5(
|
|
1471
|
+
async (repoPath, currentBranch, repoSlug) => {
|
|
1472
|
+
if (!isJiraConfigured(repoPath)) {
|
|
1473
|
+
setJiraState("not_configured");
|
|
1474
|
+
setTickets([]);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
const linkedTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1478
|
+
if (linkedTickets.length > 0) {
|
|
1479
|
+
setTickets(linkedTickets);
|
|
1480
|
+
setJiraState("has_tickets");
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
let ticketKey = extractTicketKey(currentBranch);
|
|
1484
|
+
if (!ticketKey && repoSlug) {
|
|
1485
|
+
const prResult = await listPRsForBranch(currentBranch, repoSlug);
|
|
1486
|
+
if (prResult.success && prResult.data.length > 0) {
|
|
1487
|
+
ticketKey = extractTicketKey(prResult.data[0].title);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (!ticketKey) {
|
|
1491
|
+
setTickets([]);
|
|
1492
|
+
setJiraState("no_tickets");
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1496
|
+
const creds = getJiraCredentials(repoPath);
|
|
1497
|
+
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1498
|
+
setTickets([]);
|
|
1499
|
+
setJiraState("no_tickets");
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
1503
|
+
const result = await getIssue(auth, ticketKey);
|
|
1504
|
+
if (result.success) {
|
|
1505
|
+
const linkedTicket = {
|
|
1506
|
+
key: result.data.key,
|
|
1507
|
+
summary: result.data.fields.summary,
|
|
1508
|
+
status: result.data.fields.status.name,
|
|
1509
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1510
|
+
};
|
|
1511
|
+
addLinkedTicket(repoPath, currentBranch, linkedTicket);
|
|
1512
|
+
setTickets([linkedTicket]);
|
|
1513
|
+
setJiraState("has_tickets");
|
|
1514
|
+
} else {
|
|
1515
|
+
setTickets([]);
|
|
1516
|
+
setJiraState("no_tickets");
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
[]
|
|
1520
|
+
);
|
|
1521
|
+
const refreshTickets = useCallback5((repoPath, currentBranch) => {
|
|
1522
|
+
const linkedTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1523
|
+
setTickets(linkedTickets);
|
|
1524
|
+
setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
|
|
1525
|
+
}, []);
|
|
1526
|
+
const configureJira = useCallback5(
|
|
1527
|
+
async (repoPath, siteUrl, email, apiToken) => {
|
|
1528
|
+
setLoading((prev) => ({ ...prev, configure: true }));
|
|
1529
|
+
setErrors((prev) => ({ ...prev, configure: void 0 }));
|
|
1530
|
+
const auth = { siteUrl, email, apiToken };
|
|
1531
|
+
const result = await validateCredentials(auth);
|
|
1532
|
+
if (!result.success) {
|
|
1533
|
+
setErrors((prev) => ({ ...prev, configure: result.error }));
|
|
1534
|
+
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1535
|
+
return false;
|
|
1536
|
+
}
|
|
1537
|
+
setJiraSiteUrl(repoPath, siteUrl);
|
|
1538
|
+
setJiraCredentials(repoPath, email, apiToken);
|
|
1539
|
+
setJiraState("no_tickets");
|
|
1540
|
+
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1541
|
+
return true;
|
|
1542
|
+
},
|
|
1543
|
+
[]
|
|
1544
|
+
);
|
|
1545
|
+
const linkTicket = useCallback5(
|
|
1546
|
+
async (repoPath, currentBranch, ticketInput) => {
|
|
1547
|
+
setLoading((prev) => ({ ...prev, link: true }));
|
|
1548
|
+
setErrors((prev) => ({ ...prev, link: void 0 }));
|
|
1549
|
+
const ticketKey = parseTicketKey(ticketInput);
|
|
1550
|
+
if (!ticketKey) {
|
|
1551
|
+
setErrors((prev) => ({ ...prev, link: "Invalid ticket format. Use PROJ-123 or a Jira URL." }));
|
|
1552
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1553
|
+
return false;
|
|
1554
|
+
}
|
|
1555
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1556
|
+
const creds = getJiraCredentials(repoPath);
|
|
1557
|
+
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1558
|
+
setErrors((prev) => ({ ...prev, link: "Jira not configured" }));
|
|
1559
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1562
|
+
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
1563
|
+
const result = await getIssue(auth, ticketKey);
|
|
1564
|
+
if (!result.success) {
|
|
1565
|
+
setErrors((prev) => ({ ...prev, link: result.error }));
|
|
1566
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
const linkedTicket = {
|
|
1570
|
+
key: result.data.key,
|
|
1571
|
+
summary: result.data.fields.summary,
|
|
1572
|
+
status: result.data.fields.status.name,
|
|
1573
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1574
|
+
};
|
|
1575
|
+
addLinkedTicket(repoPath, currentBranch, linkedTicket);
|
|
1576
|
+
const newTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1577
|
+
setTickets(newTickets);
|
|
1578
|
+
setJiraState("has_tickets");
|
|
1579
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1580
|
+
return true;
|
|
1581
|
+
},
|
|
1582
|
+
[]
|
|
1583
|
+
);
|
|
1584
|
+
const unlinkTicket = useCallback5((repoPath, currentBranch, ticketKey) => {
|
|
1585
|
+
removeLinkedTicket(repoPath, currentBranch, ticketKey);
|
|
1586
|
+
}, []);
|
|
1587
|
+
const clearError = useCallback5((key) => {
|
|
1588
|
+
setErrors((prev) => ({ ...prev, [key]: void 0 }));
|
|
1589
|
+
}, []);
|
|
1590
|
+
return {
|
|
1591
|
+
jiraState,
|
|
1592
|
+
tickets,
|
|
1593
|
+
loading,
|
|
1594
|
+
errors,
|
|
1595
|
+
initializeJiraState,
|
|
1596
|
+
refreshTickets,
|
|
1597
|
+
configureJira,
|
|
1598
|
+
linkTicket,
|
|
1599
|
+
unlinkTicket,
|
|
1600
|
+
clearError
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// src/hooks/logs/useLogs.ts
|
|
1605
|
+
import { useCallback as useCallback6, useEffect as useEffect7, useRef as useRef4, useState as useState9 } from "react";
|
|
1606
|
+
function useLogs() {
|
|
1607
|
+
const [logFiles, setLogFiles] = useState9([]);
|
|
1608
|
+
const [selectedDate, setSelectedDate] = useState9(null);
|
|
1609
|
+
const [logContent, setLogContent] = useState9(null);
|
|
1610
|
+
const [highlightedIndex, setHighlightedIndex] = useState9(0);
|
|
1611
|
+
const initializedRef = useRef4(false);
|
|
1612
|
+
const loadLogContent = useCallback6((date) => {
|
|
1613
|
+
if (!date) {
|
|
1614
|
+
setLogContent(null);
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
const content = readLog(date);
|
|
1618
|
+
setLogContent(content);
|
|
1619
|
+
return content;
|
|
1620
|
+
}, []);
|
|
1621
|
+
const refreshLogFiles = useCallback6(() => {
|
|
1622
|
+
const files = listLogFiles();
|
|
1623
|
+
setLogFiles(files);
|
|
1624
|
+
return files;
|
|
1625
|
+
}, []);
|
|
1626
|
+
const initialize = useCallback6(() => {
|
|
1627
|
+
const files = listLogFiles();
|
|
1628
|
+
setLogFiles(files);
|
|
1629
|
+
if (files.length === 0) return;
|
|
1630
|
+
const today = getTodayDate();
|
|
1631
|
+
const todayFile = files.find((f) => f.date === today);
|
|
1632
|
+
if (todayFile) {
|
|
1633
|
+
setSelectedDate(todayFile.date);
|
|
1634
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1635
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1636
|
+
loadLogContent(todayFile.date);
|
|
1637
|
+
} else {
|
|
1638
|
+
setSelectedDate(files[0].date);
|
|
1639
|
+
setHighlightedIndex(0);
|
|
1640
|
+
loadLogContent(files[0].date);
|
|
1641
|
+
}
|
|
1642
|
+
}, [loadLogContent]);
|
|
1643
|
+
useEffect7(() => {
|
|
1644
|
+
if (initializedRef.current) return;
|
|
1645
|
+
initializedRef.current = true;
|
|
1646
|
+
initialize();
|
|
1647
|
+
}, [initialize]);
|
|
1648
|
+
const selectDate = useCallback6((date) => {
|
|
1649
|
+
setSelectedDate(date);
|
|
1650
|
+
loadLogContent(date);
|
|
1651
|
+
}, [loadLogContent]);
|
|
1652
|
+
const refresh = useCallback6(() => {
|
|
1653
|
+
refreshLogFiles();
|
|
1654
|
+
if (selectedDate) {
|
|
1655
|
+
loadLogContent(selectedDate);
|
|
1656
|
+
}
|
|
1657
|
+
}, [refreshLogFiles, selectedDate, loadLogContent]);
|
|
1658
|
+
const handleExternalLogUpdate = useCallback6(() => {
|
|
1659
|
+
const files = listLogFiles();
|
|
1660
|
+
setLogFiles(files);
|
|
1661
|
+
const today = getTodayDate();
|
|
1662
|
+
if (selectedDate === today) {
|
|
1663
|
+
loadLogContent(today);
|
|
1664
|
+
} else if (!selectedDate && files.length > 0) {
|
|
1665
|
+
const todayFile = files.find((f) => f.date === today);
|
|
1666
|
+
if (todayFile) {
|
|
1667
|
+
setSelectedDate(today);
|
|
1668
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1669
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1670
|
+
loadLogContent(today);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}, [selectedDate, loadLogContent]);
|
|
1674
|
+
const handleLogCreated = useCallback6(() => {
|
|
1675
|
+
const files = listLogFiles();
|
|
1676
|
+
setLogFiles(files);
|
|
1677
|
+
const today = getTodayDate();
|
|
1678
|
+
setSelectedDate(today);
|
|
1679
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1680
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1681
|
+
loadLogContent(today);
|
|
1682
|
+
}, [loadLogContent]);
|
|
1683
|
+
return {
|
|
1684
|
+
logFiles,
|
|
1685
|
+
selectedDate,
|
|
1686
|
+
logContent,
|
|
1687
|
+
highlightedIndex,
|
|
1688
|
+
setHighlightedIndex,
|
|
1689
|
+
selectDate,
|
|
1690
|
+
refresh,
|
|
1691
|
+
handleExternalLogUpdate,
|
|
1692
|
+
handleLogCreated
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/hooks/useModal.ts
|
|
1697
|
+
import { useCallback as useCallback7, useState as useState10 } from "react";
|
|
1698
|
+
function useModal() {
|
|
1699
|
+
const [modalType, setModalType] = useState10("none");
|
|
1700
|
+
const open3 = useCallback7((type) => setModalType(type), []);
|
|
1701
|
+
const close = useCallback7(() => setModalType("none"), []);
|
|
1702
|
+
const isOpen = modalType !== "none";
|
|
1703
|
+
return {
|
|
1704
|
+
type: modalType,
|
|
1705
|
+
isOpen,
|
|
1706
|
+
open: open3,
|
|
1707
|
+
close
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// src/hooks/useListNavigation.ts
|
|
1712
|
+
import { useCallback as useCallback8, useState as useState11 } from "react";
|
|
1713
|
+
function useListNavigation(length) {
|
|
1714
|
+
const [index, setIndex] = useState11(0);
|
|
1715
|
+
const prev = useCallback8(() => {
|
|
1716
|
+
setIndex((i) => Math.max(0, i - 1));
|
|
1717
|
+
}, []);
|
|
1718
|
+
const next = useCallback8(() => {
|
|
1719
|
+
setIndex((i) => Math.min(length - 1, i + 1));
|
|
1720
|
+
}, [length]);
|
|
1721
|
+
const clampedIndex = Math.min(index, Math.max(0, length - 1));
|
|
1722
|
+
const reset = useCallback8(() => setIndex(0), []);
|
|
1723
|
+
return {
|
|
1724
|
+
index: length === 0 ? 0 : clampedIndex,
|
|
1725
|
+
prev,
|
|
1726
|
+
next,
|
|
1727
|
+
reset,
|
|
1728
|
+
setIndex
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1312
1732
|
// src/components/jira/ChangeStatusModal.tsx
|
|
1313
|
-
import { useEffect as
|
|
1733
|
+
import { useEffect as useEffect8, useState as useState12 } from "react";
|
|
1314
1734
|
import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
|
|
1315
1735
|
import SelectInput from "ink-select-input";
|
|
1316
1736
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1317
1737
|
function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onCancel }) {
|
|
1318
|
-
const [transitions, setTransitions] =
|
|
1319
|
-
const [loading, setLoading] =
|
|
1320
|
-
const [applying, setApplying] =
|
|
1321
|
-
const [error, setError] =
|
|
1322
|
-
|
|
1738
|
+
const [transitions, setTransitions] = useState12([]);
|
|
1739
|
+
const [loading, setLoading] = useState12(true);
|
|
1740
|
+
const [applying, setApplying] = useState12(false);
|
|
1741
|
+
const [error, setError] = useState12(null);
|
|
1742
|
+
useEffect8(() => {
|
|
1323
1743
|
const fetchTransitions = async () => {
|
|
1324
1744
|
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1325
1745
|
const creds = getJiraCredentials(repoPath);
|
|
@@ -1388,7 +1808,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1388
1808
|
}
|
|
1389
1809
|
|
|
1390
1810
|
// src/components/jira/ConfigureJiraSiteModal.tsx
|
|
1391
|
-
import { useState as
|
|
1811
|
+
import { useState as useState13 } from "react";
|
|
1392
1812
|
import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
|
|
1393
1813
|
|
|
1394
1814
|
// src/lib/editor.ts
|
|
@@ -1429,10 +1849,10 @@ function ConfigureJiraSiteModal({
|
|
|
1429
1849
|
loading,
|
|
1430
1850
|
error
|
|
1431
1851
|
}) {
|
|
1432
|
-
const [siteUrl, setSiteUrl] =
|
|
1433
|
-
const [email, setEmail] =
|
|
1434
|
-
const [apiToken, setApiToken] =
|
|
1435
|
-
const [selectedItem, setSelectedItem] =
|
|
1852
|
+
const [siteUrl, setSiteUrl] = useState13(initialSiteUrl ?? "");
|
|
1853
|
+
const [email, setEmail] = useState13(initialEmail ?? "");
|
|
1854
|
+
const [apiToken, setApiToken] = useState13("");
|
|
1855
|
+
const [selectedItem, setSelectedItem] = useState13("siteUrl");
|
|
1436
1856
|
const items = ["siteUrl", "email", "apiToken", "submit"];
|
|
1437
1857
|
const canSubmit = siteUrl.trim() && email.trim() && apiToken.trim();
|
|
1438
1858
|
useInput6(
|
|
@@ -1513,7 +1933,7 @@ function ConfigureJiraSiteModal({
|
|
|
1513
1933
|
}
|
|
1514
1934
|
|
|
1515
1935
|
// src/components/jira/LinkTicketModal.tsx
|
|
1516
|
-
import { useState as
|
|
1936
|
+
import { useState as useState14 } from "react";
|
|
1517
1937
|
import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
|
|
1518
1938
|
|
|
1519
1939
|
// src/components/ui/TextInput.tsx
|
|
@@ -1548,7 +1968,7 @@ function TextInput({ value, onChange, placeholder, isActive, mask }) {
|
|
|
1548
1968
|
// src/components/jira/LinkTicketModal.tsx
|
|
1549
1969
|
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1550
1970
|
function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
|
|
1551
|
-
const [ticketInput, setTicketInput] =
|
|
1971
|
+
const [ticketInput, setTicketInput] = useState14("");
|
|
1552
1972
|
const canSubmit = ticketInput.trim().length > 0;
|
|
1553
1973
|
useInput8(
|
|
1554
1974
|
(_input, key) => {
|
|
@@ -1606,225 +2026,106 @@ function TicketItem({ ticketKey, summary, status, isHighlighted, isSelected }) {
|
|
|
1606
2026
|
|
|
1607
2027
|
// src/components/jira/JiraView.tsx
|
|
1608
2028
|
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;
|
|
2029
|
+
function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated }) {
|
|
2030
|
+
const repo = useGitRepo();
|
|
2031
|
+
const jira = useJiraTickets();
|
|
2032
|
+
const modal = useModal();
|
|
2033
|
+
const nav = useListNavigation(jira.tickets.length);
|
|
2034
|
+
const lastInitRef = useRef5(null);
|
|
2035
|
+
useEffect9(() => {
|
|
2036
|
+
if (repo.loading || !repo.repoPath || !repo.currentBranch) return;
|
|
2037
|
+
const current = { branch: repo.currentBranch };
|
|
2038
|
+
const last = lastInitRef.current;
|
|
2039
|
+
if (last && last.branch === current.branch) return;
|
|
2040
|
+
lastInitRef.current = current;
|
|
2041
|
+
jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
|
|
2042
|
+
}, [repo.loading, repo.repoPath, repo.currentBranch, repo.currentRepoSlug, jira.initializeJiraState]);
|
|
2043
|
+
useEffect9(() => {
|
|
2044
|
+
if (isFocused) {
|
|
2045
|
+
repo.refreshBranch();
|
|
2046
|
+
} else {
|
|
2047
|
+
modal.close();
|
|
1670
2048
|
}
|
|
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];
|
|
2049
|
+
}, [isFocused, repo.refreshBranch, modal.close]);
|
|
2050
|
+
useEffect9(() => {
|
|
2051
|
+
onModalChange == null ? void 0 : onModalChange(modal.isOpen);
|
|
2052
|
+
}, [modal.isOpen, onModalChange]);
|
|
2053
|
+
useEffect9(() => {
|
|
2054
|
+
onJiraStateChange == null ? void 0 : onJiraStateChange(jira.jiraState);
|
|
2055
|
+
}, [jira.jiraState, onJiraStateChange]);
|
|
2056
|
+
const handleConfigureSubmit = async (siteUrl, email, apiToken) => {
|
|
2057
|
+
if (!repo.repoPath) return;
|
|
2058
|
+
const success = await jira.configureJira(repo.repoPath, siteUrl, email, apiToken);
|
|
2059
|
+
if (success) modal.close();
|
|
2060
|
+
};
|
|
2061
|
+
const handleLinkSubmit = async (ticketInput) => {
|
|
2062
|
+
if (!repo.repoPath || !repo.currentBranch) return;
|
|
2063
|
+
const success = await jira.linkTicket(repo.repoPath, repo.currentBranch, ticketInput);
|
|
2064
|
+
if (success) modal.close();
|
|
2065
|
+
};
|
|
2066
|
+
const handleUnlinkTicket = () => {
|
|
2067
|
+
if (!repo.repoPath || !repo.currentBranch || jira.tickets.length === 0) return;
|
|
2068
|
+
const ticket = jira.tickets[nav.index];
|
|
1767
2069
|
if (ticket) {
|
|
1768
|
-
|
|
1769
|
-
refreshTickets();
|
|
1770
|
-
|
|
2070
|
+
jira.unlinkTicket(repo.repoPath, repo.currentBranch, ticket.key);
|
|
2071
|
+
jira.refreshTickets(repo.repoPath, repo.currentBranch);
|
|
2072
|
+
nav.prev();
|
|
1771
2073
|
}
|
|
1772
|
-
}
|
|
1773
|
-
const handleOpenInBrowser =
|
|
1774
|
-
if (!repoPath || tickets.length === 0) return;
|
|
1775
|
-
const ticket = tickets[
|
|
1776
|
-
const siteUrl = getJiraSiteUrl(repoPath);
|
|
2074
|
+
};
|
|
2075
|
+
const handleOpenInBrowser = () => {
|
|
2076
|
+
if (!repo.repoPath || jira.tickets.length === 0) return;
|
|
2077
|
+
const ticket = jira.tickets[nav.index];
|
|
2078
|
+
const siteUrl = getJiraSiteUrl(repo.repoPath);
|
|
1777
2079
|
if (ticket && siteUrl) {
|
|
1778
|
-
|
|
1779
|
-
open2(url).catch(() => {
|
|
2080
|
+
open2(`${siteUrl}/browse/${ticket.key}`).catch(() => {
|
|
1780
2081
|
});
|
|
1781
2082
|
}
|
|
1782
|
-
}
|
|
2083
|
+
};
|
|
2084
|
+
const handleCopyLink = () => {
|
|
2085
|
+
if (!repo.repoPath || jira.tickets.length === 0) return;
|
|
2086
|
+
const ticket = jira.tickets[nav.index];
|
|
2087
|
+
const siteUrl = getJiraSiteUrl(repo.repoPath);
|
|
2088
|
+
if (ticket && siteUrl) {
|
|
2089
|
+
copyToClipboard(`${siteUrl}/browse/${ticket.key}`);
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
const handleStatusComplete = (newStatus) => {
|
|
2093
|
+
if (!repo.repoPath || !repo.currentBranch) return;
|
|
2094
|
+
const ticket = jira.tickets[nav.index];
|
|
2095
|
+
if (!ticket) return;
|
|
2096
|
+
updateTicketStatus(repo.repoPath, repo.currentBranch, ticket.key, newStatus);
|
|
2097
|
+
logJiraStatusChanged(ticket.key, ticket.summary, ticket.status, newStatus);
|
|
2098
|
+
onLogUpdated == null ? void 0 : onLogUpdated();
|
|
2099
|
+
modal.close();
|
|
2100
|
+
jira.refreshTickets(repo.repoPath, repo.currentBranch);
|
|
2101
|
+
};
|
|
1783
2102
|
useInput9(
|
|
1784
2103
|
(input, key) => {
|
|
1785
|
-
if (
|
|
1786
|
-
|
|
1787
|
-
setShowConfigureModal(true);
|
|
2104
|
+
if (input === "c" && jira.jiraState === "not_configured") {
|
|
2105
|
+
modal.open("configure");
|
|
1788
2106
|
return;
|
|
1789
2107
|
}
|
|
1790
|
-
if (input === "l" && jiraState !== "not_configured") {
|
|
1791
|
-
|
|
2108
|
+
if (input === "l" && jira.jiraState !== "not_configured") {
|
|
2109
|
+
modal.open("link");
|
|
1792
2110
|
return;
|
|
1793
2111
|
}
|
|
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
|
-
}
|
|
2112
|
+
if (jira.jiraState === "has_tickets") {
|
|
2113
|
+
if (key.upArrow || input === "k") nav.prev();
|
|
2114
|
+
if (key.downArrow || input === "j") nav.next();
|
|
2115
|
+
if (input === "s") modal.open("status");
|
|
2116
|
+
if (input === "d") handleUnlinkTicket();
|
|
2117
|
+
if (input === "o") handleOpenInBrowser();
|
|
2118
|
+
if (input === "y") handleCopyLink();
|
|
1818
2119
|
}
|
|
1819
2120
|
},
|
|
1820
|
-
{ isActive: isFocused && !
|
|
2121
|
+
{ isActive: isFocused && !modal.isOpen }
|
|
1821
2122
|
);
|
|
1822
|
-
if (isRepo === false) {
|
|
2123
|
+
if (repo.isRepo === false) {
|
|
1823
2124
|
return /* @__PURE__ */ jsx11(TitledBox4, { borderStyle: "round", titles: ["Jira"], flexShrink: 0, children: /* @__PURE__ */ jsx11(Text11, { color: "red", children: "Not a git repository" }) });
|
|
1824
2125
|
}
|
|
1825
|
-
if (
|
|
1826
|
-
const siteUrl = repoPath ? getJiraSiteUrl(repoPath) : void 0;
|
|
1827
|
-
const creds = repoPath ? getJiraCredentials(repoPath) : { email: null, apiToken: null };
|
|
2126
|
+
if (modal.type === "configure") {
|
|
2127
|
+
const siteUrl = repo.repoPath ? getJiraSiteUrl(repo.repoPath) : void 0;
|
|
2128
|
+
const creds = repo.repoPath ? getJiraCredentials(repo.repoPath) : { email: null, apiToken: null };
|
|
1828
2129
|
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
|
|
1829
2130
|
ConfigureJiraSiteModal,
|
|
1830
2131
|
{
|
|
@@ -1832,60 +2133,53 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange, onLogUpdated
|
|
|
1832
2133
|
initialEmail: creds.email ?? void 0,
|
|
1833
2134
|
onSubmit: handleConfigureSubmit,
|
|
1834
2135
|
onCancel: () => {
|
|
1835
|
-
|
|
1836
|
-
|
|
2136
|
+
modal.close();
|
|
2137
|
+
jira.clearError("configure");
|
|
1837
2138
|
},
|
|
1838
|
-
loading: loading.configure,
|
|
1839
|
-
error: errors.configure
|
|
2139
|
+
loading: jira.loading.configure,
|
|
2140
|
+
error: jira.errors.configure
|
|
1840
2141
|
}
|
|
1841
2142
|
) });
|
|
1842
2143
|
}
|
|
1843
|
-
if (
|
|
2144
|
+
if (modal.type === "link") {
|
|
1844
2145
|
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
|
|
1845
2146
|
LinkTicketModal,
|
|
1846
2147
|
{
|
|
1847
2148
|
onSubmit: handleLinkSubmit,
|
|
1848
2149
|
onCancel: () => {
|
|
1849
|
-
|
|
1850
|
-
|
|
2150
|
+
modal.close();
|
|
2151
|
+
jira.clearError("link");
|
|
1851
2152
|
},
|
|
1852
|
-
loading: loading.link,
|
|
1853
|
-
error: errors.link
|
|
2153
|
+
loading: jira.loading.link,
|
|
2154
|
+
error: jira.errors.link
|
|
1854
2155
|
}
|
|
1855
2156
|
) });
|
|
1856
2157
|
}
|
|
1857
|
-
if (
|
|
1858
|
-
const ticket = tickets[
|
|
2158
|
+
if (modal.type === "status" && repo.repoPath && repo.currentBranch && jira.tickets[nav.index]) {
|
|
2159
|
+
const ticket = jira.tickets[nav.index];
|
|
1859
2160
|
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
|
|
1860
2161
|
ChangeStatusModal,
|
|
1861
2162
|
{
|
|
1862
|
-
repoPath,
|
|
2163
|
+
repoPath: repo.repoPath,
|
|
1863
2164
|
ticketKey: ticket.key,
|
|
1864
2165
|
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)
|
|
2166
|
+
onComplete: handleStatusComplete,
|
|
2167
|
+
onCancel: modal.close
|
|
1874
2168
|
}
|
|
1875
2169
|
) });
|
|
1876
2170
|
}
|
|
1877
2171
|
const title = "[4] Jira";
|
|
1878
2172
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
1879
2173
|
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(
|
|
2174
|
+
jira.jiraState === "not_configured" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No Jira site configured" }),
|
|
2175
|
+
jira.jiraState === "no_tickets" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No tickets linked to this branch" }),
|
|
2176
|
+
jira.jiraState === "has_tickets" && jira.tickets.map((ticket, idx) => /* @__PURE__ */ jsx11(
|
|
1883
2177
|
TicketItem,
|
|
1884
2178
|
{
|
|
1885
2179
|
ticketKey: ticket.key,
|
|
1886
2180
|
summary: ticket.summary,
|
|
1887
2181
|
status: ticket.status,
|
|
1888
|
-
isHighlighted: idx ===
|
|
2182
|
+
isHighlighted: idx === nav.index
|
|
1889
2183
|
},
|
|
1890
2184
|
ticket.key
|
|
1891
2185
|
))
|
|
@@ -1893,7 +2187,7 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange, onLogUpdated
|
|
|
1893
2187
|
}
|
|
1894
2188
|
|
|
1895
2189
|
// src/components/logs/LogsView.tsx
|
|
1896
|
-
import {
|
|
2190
|
+
import { useEffect as useEffect10 } from "react";
|
|
1897
2191
|
import { Box as Box14, useInput as useInput12 } from "ink";
|
|
1898
2192
|
|
|
1899
2193
|
// src/components/logs/LogsHistoryBox.tsx
|
|
@@ -1956,35 +2250,133 @@ function LogsHistoryBox({
|
|
|
1956
2250
|
}
|
|
1957
2251
|
|
|
1958
2252
|
// src/components/logs/LogViewerBox.tsx
|
|
1959
|
-
import { useRef as
|
|
2253
|
+
import { useRef as useRef6, useState as useState15 } from "react";
|
|
1960
2254
|
import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
|
|
1961
2255
|
import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
|
|
1962
2256
|
import { ScrollView as ScrollView2 } from "ink-scroll-view";
|
|
1963
2257
|
import TextInput2 from "ink-text-input";
|
|
2258
|
+
|
|
2259
|
+
// src/lib/claude/api.ts
|
|
2260
|
+
import { exec as exec3 } from "child_process";
|
|
2261
|
+
function runClaudePrompt(prompt) {
|
|
2262
|
+
let childProcess = null;
|
|
2263
|
+
let cancelled = false;
|
|
2264
|
+
const promise = new Promise((resolve) => {
|
|
2265
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''").replace(/\n/g, "\\n");
|
|
2266
|
+
const command = `claude -p $'${escapedPrompt}' --output-format json < /dev/null`;
|
|
2267
|
+
childProcess = exec3(command, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
2268
|
+
if (cancelled) {
|
|
2269
|
+
resolve({
|
|
2270
|
+
success: false,
|
|
2271
|
+
error: "Cancelled",
|
|
2272
|
+
errorType: "execution_error"
|
|
2273
|
+
});
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
if (error) {
|
|
2277
|
+
if (error.message.includes("command not found") || error.message.includes("ENOENT") || error.code === "ENOENT") {
|
|
2278
|
+
resolve({
|
|
2279
|
+
success: false,
|
|
2280
|
+
error: "Claude CLI not installed. Run: npm install -g @anthropic-ai/claude-code",
|
|
2281
|
+
errorType: "not_installed"
|
|
2282
|
+
});
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
resolve({
|
|
2286
|
+
success: false,
|
|
2287
|
+
error: (stderr == null ? void 0 : stderr.trim()) || error.message,
|
|
2288
|
+
errorType: "execution_error"
|
|
2289
|
+
});
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
if (!(stdout == null ? void 0 : stdout.trim())) {
|
|
2293
|
+
resolve({
|
|
2294
|
+
success: false,
|
|
2295
|
+
error: (stderr == null ? void 0 : stderr.trim()) || "Claude returned empty response",
|
|
2296
|
+
errorType: "execution_error"
|
|
2297
|
+
});
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
try {
|
|
2301
|
+
const json = JSON.parse(stdout.trim());
|
|
2302
|
+
if (json.is_error) {
|
|
2303
|
+
resolve({
|
|
2304
|
+
success: false,
|
|
2305
|
+
error: json.result || "Claude returned an error",
|
|
2306
|
+
errorType: "execution_error"
|
|
2307
|
+
});
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
resolve({
|
|
2311
|
+
success: true,
|
|
2312
|
+
data: json.result || stdout.trim()
|
|
2313
|
+
});
|
|
2314
|
+
} catch {
|
|
2315
|
+
resolve({
|
|
2316
|
+
success: true,
|
|
2317
|
+
data: stdout.trim()
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
});
|
|
2322
|
+
const cancel = () => {
|
|
2323
|
+
cancelled = true;
|
|
2324
|
+
if (childProcess) {
|
|
2325
|
+
childProcess.kill("SIGTERM");
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
return { promise, cancel };
|
|
2329
|
+
}
|
|
2330
|
+
function generateStandupNotes(logContent) {
|
|
2331
|
+
const prompt = `You are helping a developer prepare standup notes. Based on the following log entries, generate concise standup notes that summarize what was accomplished.
|
|
2332
|
+
|
|
2333
|
+
Format the output as bullet points grouped by category (e.g., Features, Bug Fixes, Refactoring, etc.). Keep it brief and professional.
|
|
2334
|
+
|
|
2335
|
+
Log entries:
|
|
2336
|
+
${logContent}
|
|
2337
|
+
|
|
2338
|
+
Generate the standup notes:`;
|
|
2339
|
+
return runClaudePrompt(prompt);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// src/components/logs/LogViewerBox.tsx
|
|
1964
2343
|
import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1965
2344
|
function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
|
|
1966
|
-
const scrollRef =
|
|
1967
|
-
const [isInputMode, setIsInputMode] =
|
|
1968
|
-
const [inputValue, setInputValue] =
|
|
2345
|
+
const scrollRef = useRef6(null);
|
|
2346
|
+
const [isInputMode, setIsInputMode] = useState15(false);
|
|
2347
|
+
const [inputValue, setInputValue] = useState15("");
|
|
2348
|
+
const [isGeneratingStandup, setIsGeneratingStandup] = useState15(false);
|
|
2349
|
+
const [standupResult, setStandupResult] = useState15(null);
|
|
2350
|
+
const claudeProcessRef = useRef6(null);
|
|
1969
2351
|
const title = "[6] Log Content";
|
|
1970
2352
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
1971
2353
|
const displayTitle = date ? `${title} - ${date}.md` : title;
|
|
1972
2354
|
useInput11(
|
|
1973
2355
|
(input, key) => {
|
|
1974
|
-
var _a, _b;
|
|
2356
|
+
var _a, _b, _c;
|
|
1975
2357
|
if (key.escape && isInputMode) {
|
|
1976
2358
|
setIsInputMode(false);
|
|
1977
2359
|
setInputValue("");
|
|
1978
2360
|
return;
|
|
1979
2361
|
}
|
|
1980
|
-
if (
|
|
2362
|
+
if (key.escape && isGeneratingStandup) {
|
|
2363
|
+
(_a = claudeProcessRef.current) == null ? void 0 : _a.cancel();
|
|
2364
|
+
claudeProcessRef.current = null;
|
|
2365
|
+
setIsGeneratingStandup(false);
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (key.escape && standupResult) {
|
|
2369
|
+
setStandupResult(null);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (isInputMode || standupResult) {
|
|
1981
2373
|
return;
|
|
1982
2374
|
}
|
|
1983
2375
|
if (key.upArrow || input === "k") {
|
|
1984
|
-
(
|
|
2376
|
+
(_b = scrollRef.current) == null ? void 0 : _b.scrollBy(-1);
|
|
1985
2377
|
}
|
|
1986
2378
|
if (key.downArrow || input === "j") {
|
|
1987
|
-
(
|
|
2379
|
+
(_c = scrollRef.current) == null ? void 0 : _c.scrollBy(1);
|
|
1988
2380
|
}
|
|
1989
2381
|
if (input === "e" && date) {
|
|
1990
2382
|
openLogInEditor(date);
|
|
@@ -2003,6 +2395,21 @@ function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
|
|
|
2003
2395
|
if (input === "r") {
|
|
2004
2396
|
onRefresh();
|
|
2005
2397
|
}
|
|
2398
|
+
if (input === "c" && date && content && !isGeneratingStandup) {
|
|
2399
|
+
setIsGeneratingStandup(true);
|
|
2400
|
+
setStandupResult(null);
|
|
2401
|
+
const process2 = generateStandupNotes(content);
|
|
2402
|
+
claudeProcessRef.current = process2;
|
|
2403
|
+
process2.promise.then((result) => {
|
|
2404
|
+
claudeProcessRef.current = null;
|
|
2405
|
+
setIsGeneratingStandup(false);
|
|
2406
|
+
if (result.success) {
|
|
2407
|
+
setStandupResult({ type: "success", message: result.data });
|
|
2408
|
+
} else if (result.error !== "Cancelled") {
|
|
2409
|
+
setStandupResult({ type: "error", message: result.error });
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2006
2413
|
},
|
|
2007
2414
|
{ isActive: isFocused }
|
|
2008
2415
|
);
|
|
@@ -2037,98 +2444,35 @@ ${value.trim()}
|
|
|
2037
2444
|
onChange: (val) => setInputValue(val.replace(/[\r\n]/g, "")),
|
|
2038
2445
|
onSubmit: handleInputSubmit
|
|
2039
2446
|
}
|
|
2040
|
-
) }) })
|
|
2447
|
+
) }) }),
|
|
2448
|
+
isGeneratingStandup && /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: ["Standup Notes"], borderColor: "yellow", children: /* @__PURE__ */ jsxs13(Box13, { paddingX: 1, flexDirection: "column", children: [
|
|
2449
|
+
/* @__PURE__ */ jsx13(Text13, { color: "yellow", children: "Generating standup notes..." }),
|
|
2450
|
+
/* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Press Esc to cancel" })
|
|
2451
|
+
] }) }),
|
|
2452
|
+
standupResult && /* @__PURE__ */ jsx13(
|
|
2453
|
+
TitledBox6,
|
|
2454
|
+
{
|
|
2455
|
+
borderStyle: "round",
|
|
2456
|
+
titles: ["Standup Notes"],
|
|
2457
|
+
borderColor: standupResult.type === "error" ? "red" : "green",
|
|
2458
|
+
children: /* @__PURE__ */ jsxs13(Box13, { paddingX: 1, flexDirection: "column", children: [
|
|
2459
|
+
standupResult.type === "error" ? /* @__PURE__ */ jsx13(Text13, { color: "red", children: standupResult.message }) : /* @__PURE__ */ jsx13(Markdown, { children: standupResult.message }),
|
|
2460
|
+
/* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Press Esc to dismiss" })
|
|
2461
|
+
] })
|
|
2462
|
+
}
|
|
2463
|
+
)
|
|
2041
2464
|
] });
|
|
2042
2465
|
}
|
|
2043
2466
|
|
|
2044
2467
|
// src/components/logs/LogsView.tsx
|
|
2045
2468
|
import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2046
|
-
function LogsView({ isFocused,
|
|
2047
|
-
const
|
|
2048
|
-
|
|
2049
|
-
const [logContent, setLogContent] = useState9(null);
|
|
2050
|
-
const [highlightedIndex, setHighlightedIndex] = useState9(0);
|
|
2051
|
-
useEffect6(() => {
|
|
2052
|
-
if (!isFocused) {
|
|
2053
|
-
onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
|
|
2054
|
-
return;
|
|
2055
|
-
}
|
|
2056
|
-
const bindings = [];
|
|
2057
|
-
if (focusedBox === "history") {
|
|
2058
|
-
bindings.push({ key: "Enter", label: "Select" });
|
|
2059
|
-
} else if (focusedBox === "viewer") {
|
|
2060
|
-
bindings.push({ key: "i", label: "Add Entry" });
|
|
2061
|
-
bindings.push({ key: "e", label: "Edit" });
|
|
2062
|
-
bindings.push({ key: "n", label: "New Log", color: "green" });
|
|
2063
|
-
bindings.push({ key: "r", label: "Refresh" });
|
|
2064
|
-
}
|
|
2065
|
-
onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
|
|
2066
|
-
}, [isFocused, focusedBox, onKeybindingsChange]);
|
|
2067
|
-
const refreshLogFiles = useCallback3(() => {
|
|
2068
|
-
const files = listLogFiles();
|
|
2069
|
-
setLogFiles(files);
|
|
2070
|
-
if (files.length > 0 && !selectedDate) {
|
|
2071
|
-
const today = getTodayDate();
|
|
2072
|
-
const todayFile = files.find((f) => f.date === today);
|
|
2073
|
-
if (todayFile) {
|
|
2074
|
-
setSelectedDate(todayFile.date);
|
|
2075
|
-
const idx = files.findIndex((f) => f.date === today);
|
|
2076
|
-
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
2077
|
-
} else {
|
|
2078
|
-
setSelectedDate(files[0].date);
|
|
2079
|
-
setHighlightedIndex(0);
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
}, [selectedDate]);
|
|
2083
|
-
useEffect6(() => {
|
|
2084
|
-
refreshLogFiles();
|
|
2085
|
-
}, [refreshLogFiles]);
|
|
2086
|
-
useEffect6(() => {
|
|
2087
|
-
if (selectedDate) {
|
|
2088
|
-
const content = readLog(selectedDate);
|
|
2089
|
-
setLogContent(content);
|
|
2090
|
-
} else {
|
|
2091
|
-
setLogContent(null);
|
|
2092
|
-
}
|
|
2093
|
-
}, [selectedDate]);
|
|
2094
|
-
useEffect6(() => {
|
|
2469
|
+
function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
|
|
2470
|
+
const logs = useLogs();
|
|
2471
|
+
useEffect10(() => {
|
|
2095
2472
|
if (refreshKey !== void 0 && refreshKey > 0) {
|
|
2096
|
-
|
|
2097
|
-
setLogFiles(files);
|
|
2098
|
-
const today = getTodayDate();
|
|
2099
|
-
if (selectedDate === today) {
|
|
2100
|
-
const content = readLog(today);
|
|
2101
|
-
setLogContent(content);
|
|
2102
|
-
} else if (!selectedDate && files.length > 0) {
|
|
2103
|
-
const todayFile = files.find((f) => f.date === today);
|
|
2104
|
-
if (todayFile) {
|
|
2105
|
-
setSelectedDate(today);
|
|
2106
|
-
const idx = files.findIndex((f) => f.date === today);
|
|
2107
|
-
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2473
|
+
logs.handleExternalLogUpdate();
|
|
2110
2474
|
}
|
|
2111
|
-
}, [refreshKey,
|
|
2112
|
-
const handleSelectDate = useCallback3((date) => {
|
|
2113
|
-
setSelectedDate(date);
|
|
2114
|
-
}, []);
|
|
2115
|
-
const handleRefresh = useCallback3(() => {
|
|
2116
|
-
refreshLogFiles();
|
|
2117
|
-
if (selectedDate) {
|
|
2118
|
-
const content = readLog(selectedDate);
|
|
2119
|
-
setLogContent(content);
|
|
2120
|
-
}
|
|
2121
|
-
}, [refreshLogFiles, selectedDate]);
|
|
2122
|
-
const handleLogCreated = useCallback3(() => {
|
|
2123
|
-
const files = listLogFiles();
|
|
2124
|
-
setLogFiles(files);
|
|
2125
|
-
const today = getTodayDate();
|
|
2126
|
-
setSelectedDate(today);
|
|
2127
|
-
const idx = files.findIndex((f) => f.date === today);
|
|
2128
|
-
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
2129
|
-
const content = readLog(today);
|
|
2130
|
-
setLogContent(content);
|
|
2131
|
-
}, []);
|
|
2475
|
+
}, [refreshKey, logs.handleExternalLogUpdate]);
|
|
2132
2476
|
useInput12(
|
|
2133
2477
|
(input) => {
|
|
2134
2478
|
if (input === "5") onFocusedBoxChange("history");
|
|
@@ -2140,22 +2484,22 @@ function LogsView({ isFocused, onKeybindingsChange, refreshKey, focusedBox, onFo
|
|
|
2140
2484
|
/* @__PURE__ */ jsx14(
|
|
2141
2485
|
LogsHistoryBox,
|
|
2142
2486
|
{
|
|
2143
|
-
logFiles,
|
|
2144
|
-
selectedDate,
|
|
2145
|
-
highlightedIndex,
|
|
2146
|
-
onHighlight: setHighlightedIndex,
|
|
2147
|
-
onSelect:
|
|
2487
|
+
logFiles: logs.logFiles,
|
|
2488
|
+
selectedDate: logs.selectedDate,
|
|
2489
|
+
highlightedIndex: logs.highlightedIndex,
|
|
2490
|
+
onHighlight: logs.setHighlightedIndex,
|
|
2491
|
+
onSelect: logs.selectDate,
|
|
2148
2492
|
isFocused: isFocused && focusedBox === "history"
|
|
2149
2493
|
}
|
|
2150
2494
|
),
|
|
2151
2495
|
/* @__PURE__ */ jsx14(
|
|
2152
2496
|
LogViewerBox,
|
|
2153
2497
|
{
|
|
2154
|
-
date: selectedDate,
|
|
2155
|
-
content: logContent,
|
|
2498
|
+
date: logs.selectedDate,
|
|
2499
|
+
content: logs.logContent,
|
|
2156
2500
|
isFocused: isFocused && focusedBox === "viewer",
|
|
2157
|
-
onRefresh:
|
|
2158
|
-
onLogCreated: handleLogCreated
|
|
2501
|
+
onRefresh: logs.refresh,
|
|
2502
|
+
onLogCreated: logs.handleLogCreated
|
|
2159
2503
|
}
|
|
2160
2504
|
)
|
|
2161
2505
|
] });
|
|
@@ -2180,16 +2524,85 @@ function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
|
|
|
2180
2524
|
] }, binding.key)) });
|
|
2181
2525
|
}
|
|
2182
2526
|
|
|
2527
|
+
// src/constants/github.ts
|
|
2528
|
+
var GITHUB_KEYBINDINGS = {
|
|
2529
|
+
remotes: [
|
|
2530
|
+
{ key: "Space", label: "Select Remote" }
|
|
2531
|
+
],
|
|
2532
|
+
prs: [
|
|
2533
|
+
{ key: "Space", label: "Select" },
|
|
2534
|
+
{ key: "n", label: "New PR", color: "green" },
|
|
2535
|
+
{ key: "r", label: "Refresh" },
|
|
2536
|
+
{ key: "o", label: "Open", color: "green" },
|
|
2537
|
+
{ key: "y", label: "Copy Link" }
|
|
2538
|
+
],
|
|
2539
|
+
details: [
|
|
2540
|
+
{ key: "r", label: "Refresh" },
|
|
2541
|
+
{ key: "o", label: "Open", color: "green" }
|
|
2542
|
+
]
|
|
2543
|
+
};
|
|
2544
|
+
|
|
2545
|
+
// src/constants/jira.ts
|
|
2546
|
+
var JIRA_KEYBINDINGS = {
|
|
2547
|
+
not_configured: [{ key: "c", label: "Configure Jira" }],
|
|
2548
|
+
no_tickets: [{ key: "l", label: "Link Ticket" }],
|
|
2549
|
+
has_tickets: [
|
|
2550
|
+
{ key: "l", label: "Link" },
|
|
2551
|
+
{ key: "s", label: "Status" },
|
|
2552
|
+
{ key: "d", label: "Unlink", color: "red" },
|
|
2553
|
+
{ key: "o", label: "Open", color: "green" },
|
|
2554
|
+
{ key: "y", label: "Copy Link" }
|
|
2555
|
+
]
|
|
2556
|
+
};
|
|
2557
|
+
|
|
2558
|
+
// src/constants/logs.ts
|
|
2559
|
+
var LOGS_KEYBINDINGS = {
|
|
2560
|
+
history: [
|
|
2561
|
+
{ key: "Enter", label: "Select" }
|
|
2562
|
+
],
|
|
2563
|
+
viewer: [
|
|
2564
|
+
{ key: "i", label: "Add Entry" },
|
|
2565
|
+
{ key: "e", label: "Edit" },
|
|
2566
|
+
{ key: "n", label: "New Log", color: "green" },
|
|
2567
|
+
{ key: "c", label: "Standup" },
|
|
2568
|
+
{ key: "r", label: "Refresh" }
|
|
2569
|
+
]
|
|
2570
|
+
};
|
|
2571
|
+
|
|
2572
|
+
// src/lib/keybindings.ts
|
|
2573
|
+
function computeKeybindings(focusedView, state) {
|
|
2574
|
+
switch (focusedView) {
|
|
2575
|
+
case "github":
|
|
2576
|
+
return GITHUB_KEYBINDINGS[state.github.focusedBox];
|
|
2577
|
+
case "jira":
|
|
2578
|
+
if (state.jira.modalOpen) return [];
|
|
2579
|
+
return JIRA_KEYBINDINGS[state.jira.jiraState];
|
|
2580
|
+
case "logs":
|
|
2581
|
+
return LOGS_KEYBINDINGS[state.logs.focusedBox];
|
|
2582
|
+
default:
|
|
2583
|
+
return [];
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2183
2587
|
// src/app.tsx
|
|
2184
2588
|
import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2185
2589
|
function App() {
|
|
2186
2590
|
const { exit } = useApp();
|
|
2187
|
-
const [focusedView, setFocusedView] =
|
|
2188
|
-
const [modalOpen, setModalOpen] =
|
|
2189
|
-
const [
|
|
2190
|
-
const [
|
|
2191
|
-
const [
|
|
2192
|
-
const
|
|
2591
|
+
const [focusedView, setFocusedView] = useState16("github");
|
|
2592
|
+
const [modalOpen, setModalOpen] = useState16(false);
|
|
2593
|
+
const [logRefreshKey, setLogRefreshKey] = useState16(0);
|
|
2594
|
+
const [githubFocusedBox, setGithubFocusedBox] = useState16("remotes");
|
|
2595
|
+
const [jiraState, setJiraState] = useState16("not_configured");
|
|
2596
|
+
const [logsFocusedBox, setLogsFocusedBox] = useState16("history");
|
|
2597
|
+
const keybindings = useMemo2(
|
|
2598
|
+
() => computeKeybindings(focusedView, {
|
|
2599
|
+
github: { focusedBox: githubFocusedBox },
|
|
2600
|
+
jira: { jiraState, modalOpen },
|
|
2601
|
+
logs: { focusedBox: logsFocusedBox }
|
|
2602
|
+
}),
|
|
2603
|
+
[focusedView, githubFocusedBox, jiraState, modalOpen, logsFocusedBox]
|
|
2604
|
+
);
|
|
2605
|
+
const handleLogUpdated = useCallback9(() => {
|
|
2193
2606
|
setLogRefreshKey((prev) => prev + 1);
|
|
2194
2607
|
}, []);
|
|
2195
2608
|
useInput13(
|
|
@@ -2221,7 +2634,7 @@ function App() {
|
|
|
2221
2634
|
GitHubView,
|
|
2222
2635
|
{
|
|
2223
2636
|
isFocused: focusedView === "github",
|
|
2224
|
-
|
|
2637
|
+
onFocusedBoxChange: setGithubFocusedBox,
|
|
2225
2638
|
onLogUpdated: handleLogUpdated
|
|
2226
2639
|
}
|
|
2227
2640
|
),
|
|
@@ -2230,7 +2643,7 @@ function App() {
|
|
|
2230
2643
|
{
|
|
2231
2644
|
isFocused: focusedView === "jira",
|
|
2232
2645
|
onModalChange: setModalOpen,
|
|
2233
|
-
|
|
2646
|
+
onJiraStateChange: setJiraState,
|
|
2234
2647
|
onLogUpdated: handleLogUpdated
|
|
2235
2648
|
}
|
|
2236
2649
|
)
|
|
@@ -2239,14 +2652,13 @@ function App() {
|
|
|
2239
2652
|
LogsView,
|
|
2240
2653
|
{
|
|
2241
2654
|
isFocused: focusedView === "logs",
|
|
2242
|
-
onKeybindingsChange: focusedView === "logs" ? setContextBindings : void 0,
|
|
2243
2655
|
refreshKey: logRefreshKey,
|
|
2244
2656
|
focusedBox: logsFocusedBox,
|
|
2245
2657
|
onFocusedBoxChange: setLogsFocusedBox
|
|
2246
2658
|
}
|
|
2247
2659
|
) })
|
|
2248
2660
|
] }),
|
|
2249
|
-
/* @__PURE__ */ jsx16(KeybindingsBar, { contextBindings, modalOpen })
|
|
2661
|
+
/* @__PURE__ */ jsx16(KeybindingsBar, { contextBindings: keybindings, modalOpen })
|
|
2250
2662
|
] });
|
|
2251
2663
|
}
|
|
2252
2664
|
|
|
@@ -2255,16 +2667,16 @@ import { render as inkRender } from "ink";
|
|
|
2255
2667
|
|
|
2256
2668
|
// src/lib/Screen.tsx
|
|
2257
2669
|
import { Box as Box17, useStdout as useStdout2 } from "ink";
|
|
2258
|
-
import { useCallback as
|
|
2670
|
+
import { useCallback as useCallback10, useEffect as useEffect11, useState as useState17 } from "react";
|
|
2259
2671
|
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
2260
2672
|
function Screen({ children }) {
|
|
2261
2673
|
const { stdout } = useStdout2();
|
|
2262
|
-
const getSize =
|
|
2674
|
+
const getSize = useCallback10(
|
|
2263
2675
|
() => ({ height: stdout.rows, width: stdout.columns }),
|
|
2264
2676
|
[stdout]
|
|
2265
2677
|
);
|
|
2266
|
-
const [size, setSize] =
|
|
2267
|
-
|
|
2678
|
+
const [size, setSize] = useState17(getSize);
|
|
2679
|
+
useEffect11(() => {
|
|
2268
2680
|
const onResize = () => setSize(getSize());
|
|
2269
2681
|
stdout.on("resize", onResize);
|
|
2270
2682
|
return () => {
|