clairo 1.0.7 → 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 +1135 -976
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,14 +4,84 @@
|
|
|
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 { useCallback as
|
|
11
|
+
import { useCallback as useCallback9, useEffect as useEffect9, useRef as useRef5, useState as useState12 } from "react";
|
|
12
12
|
import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
|
|
13
13
|
import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
|
|
14
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
|
+
|
|
32
|
+
// src/lib/config/index.ts
|
|
33
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
34
|
+
import { homedir } from "os";
|
|
35
|
+
import { dirname, join } from "path";
|
|
36
|
+
var CONFIG_PATH = join(homedir(), ".clairo", "config.json");
|
|
37
|
+
var DEFAULT_CONFIG = {};
|
|
38
|
+
function loadConfig() {
|
|
39
|
+
try {
|
|
40
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
41
|
+
return DEFAULT_CONFIG;
|
|
42
|
+
}
|
|
43
|
+
const content = readFileSync(CONFIG_PATH, "utf-8");
|
|
44
|
+
return JSON.parse(content);
|
|
45
|
+
} catch {
|
|
46
|
+
return DEFAULT_CONFIG;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function saveConfig(config) {
|
|
50
|
+
const dir = dirname(CONFIG_PATH);
|
|
51
|
+
if (!existsSync(dir)) {
|
|
52
|
+
mkdirSync(dir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/lib/github/config.ts
|
|
58
|
+
function getRepoConfig(repoPath) {
|
|
59
|
+
const config = loadConfig();
|
|
60
|
+
const repos = config.repositories ?? {};
|
|
61
|
+
return repos[repoPath] ?? {};
|
|
62
|
+
}
|
|
63
|
+
function updateRepoConfig(repoPath, updates) {
|
|
64
|
+
const config = loadConfig();
|
|
65
|
+
if (!config.repositories) {
|
|
66
|
+
config.repositories = {};
|
|
67
|
+
}
|
|
68
|
+
config.repositories[repoPath] = {
|
|
69
|
+
...config.repositories[repoPath],
|
|
70
|
+
...updates
|
|
71
|
+
};
|
|
72
|
+
saveConfig(config);
|
|
73
|
+
}
|
|
74
|
+
function getSelectedRemote(repoPath, availableRemotes) {
|
|
75
|
+
const repoConfig = getRepoConfig(repoPath);
|
|
76
|
+
if (repoConfig.selectedRemote && availableRemotes.includes(repoConfig.selectedRemote)) {
|
|
77
|
+
return repoConfig.selectedRemote;
|
|
78
|
+
}
|
|
79
|
+
if (availableRemotes.includes("origin")) {
|
|
80
|
+
return "origin";
|
|
81
|
+
}
|
|
82
|
+
return availableRemotes[0] ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
15
85
|
// src/lib/github/git.ts
|
|
16
86
|
import { execSync } from "child_process";
|
|
17
87
|
function isGitRepo() {
|
|
@@ -122,9 +192,7 @@ async function isGhAuthenticated() {
|
|
|
122
192
|
function getRepoFromRemote(remoteUrl) {
|
|
123
193
|
const sshMatch = remoteUrl.match(/git@github\.com:(.+?)(?:\.git)?$/);
|
|
124
194
|
if (sshMatch) return sshMatch[1].replace(/\.git$/, "");
|
|
125
|
-
const httpsMatch = remoteUrl.match(
|
|
126
|
-
/https:\/\/github\.com\/(.+?)(?:\.git)?$/
|
|
127
|
-
);
|
|
195
|
+
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/(.+?)(?:\.git)?$/);
|
|
128
196
|
if (httpsMatch) return httpsMatch[1].replace(/\.git$/, "");
|
|
129
197
|
return null;
|
|
130
198
|
}
|
|
@@ -145,9 +213,7 @@ async function listPRsForBranch(branch, repo) {
|
|
|
145
213
|
}
|
|
146
214
|
const fields = "number,title,state,author,createdAt,isDraft";
|
|
147
215
|
try {
|
|
148
|
-
const { stdout } = await execAsync(
|
|
149
|
-
`gh pr view --json ${fields} 2>/dev/null`
|
|
150
|
-
);
|
|
216
|
+
const { stdout } = await execAsync(`gh pr view --json ${fields} 2>/dev/null`);
|
|
151
217
|
const pr = JSON.parse(stdout);
|
|
152
218
|
return { success: true, data: [pr] };
|
|
153
219
|
} catch {
|
|
@@ -157,10 +223,8 @@ async function listPRsForBranch(branch, repo) {
|
|
|
157
223
|
`gh pr list --state open --json ${fields},headRefName --repo "${repo}" 2>/dev/null`
|
|
158
224
|
);
|
|
159
225
|
const allPrs = JSON.parse(stdout);
|
|
160
|
-
const prs = allPrs.filter(
|
|
161
|
-
|
|
162
|
-
);
|
|
163
|
-
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);
|
|
164
228
|
return { success: true, data: result };
|
|
165
229
|
} catch {
|
|
166
230
|
return { success: false, error: "Failed to fetch PRs", errorType: "api_error" };
|
|
@@ -200,9 +264,7 @@ async function getPRDetails(prNumber, repo) {
|
|
|
200
264
|
"reviews",
|
|
201
265
|
"statusCheckRollup"
|
|
202
266
|
].join(",");
|
|
203
|
-
const { stdout } = await execAsync(
|
|
204
|
-
`gh pr view ${prNumber} --json ${fields} --repo "${repo}"`
|
|
205
|
-
);
|
|
267
|
+
const { stdout } = await execAsync(`gh pr view ${prNumber} --json ${fields} --repo "${repo}"`);
|
|
206
268
|
const pr = JSON.parse(stdout);
|
|
207
269
|
return { success: true, data: pr };
|
|
208
270
|
} catch {
|
|
@@ -221,99 +283,307 @@ function openPRCreationPage(owner, branch, onComplete) {
|
|
|
221
283
|
});
|
|
222
284
|
}
|
|
223
285
|
|
|
224
|
-
// src/
|
|
225
|
-
|
|
226
|
-
function
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
return null;
|
|
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 };
|
|
250
310
|
}
|
|
251
311
|
|
|
252
|
-
// src/
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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;
|
|
262
336
|
}
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
291
393
|
};
|
|
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
394
|
}
|
|
304
395
|
|
|
305
|
-
// src/
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/lib/jira/parser.ts
|
|
548
|
+
var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
549
|
+
function isValidTicketKeyFormat(key) {
|
|
550
|
+
return TICKET_KEY_PATTERN.test(key.toUpperCase());
|
|
551
|
+
}
|
|
552
|
+
function parseTicketKey(input) {
|
|
553
|
+
const trimmed = input.trim();
|
|
554
|
+
const urlMatch = trimmed.match(/\/browse\/([A-Za-z][A-Za-z0-9]+-\d+)/i);
|
|
555
|
+
if (urlMatch) {
|
|
556
|
+
return urlMatch[1].toUpperCase();
|
|
557
|
+
}
|
|
558
|
+
const upperInput = trimmed.toUpperCase();
|
|
559
|
+
if (isValidTicketKeyFormat(upperInput)) {
|
|
560
|
+
return upperInput;
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
function extractTicketKey(text) {
|
|
565
|
+
const match = text.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
|
|
566
|
+
if (match) {
|
|
567
|
+
const candidate = match[1].toUpperCase();
|
|
568
|
+
if (isValidTicketKeyFormat(candidate)) {
|
|
569
|
+
return candidate;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/lib/jira/config.ts
|
|
576
|
+
function isJiraConfigured(repoPath) {
|
|
577
|
+
const config = getRepoConfig(repoPath);
|
|
578
|
+
return !!(config.jiraSiteUrl && config.jiraEmail && config.jiraApiToken);
|
|
579
|
+
}
|
|
580
|
+
function getJiraSiteUrl(repoPath) {
|
|
581
|
+
const config = getRepoConfig(repoPath);
|
|
582
|
+
return config.jiraSiteUrl ?? null;
|
|
583
|
+
}
|
|
584
|
+
function setJiraSiteUrl(repoPath, siteUrl) {
|
|
585
|
+
updateRepoConfig(repoPath, { jiraSiteUrl: siteUrl });
|
|
586
|
+
}
|
|
317
587
|
function getJiraCredentials(repoPath) {
|
|
318
588
|
const config = getRepoConfig(repoPath);
|
|
319
589
|
return {
|
|
@@ -498,10 +768,10 @@ async function applyTransition(auth, ticketKey, transitionId) {
|
|
|
498
768
|
}
|
|
499
769
|
|
|
500
770
|
// src/lib/logs/index.ts
|
|
501
|
-
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";
|
|
502
773
|
import { homedir as homedir2 } from "os";
|
|
503
774
|
import { join as join2 } from "path";
|
|
504
|
-
import { spawnSync } from "child_process";
|
|
505
775
|
var LOGS_DIRECTORY = join2(homedir2(), ".clairo", "logs");
|
|
506
776
|
function ensureLogsDirectory() {
|
|
507
777
|
if (!existsSync2(LOGS_DIRECTORY)) {
|
|
@@ -567,319 +837,63 @@ function createEmptyLog(date) {
|
|
|
567
837
|
}
|
|
568
838
|
const header = `# Log - ${date}
|
|
569
839
|
`;
|
|
570
|
-
writeFileSync2(filePath, header);
|
|
571
|
-
}
|
|
572
|
-
function appendToLog(date, entry) {
|
|
573
|
-
ensureLogsDirectory();
|
|
574
|
-
const filePath = getLogFilePath(date);
|
|
575
|
-
if (!existsSync2(filePath)) {
|
|
576
|
-
const header = `# Log - ${date}
|
|
577
|
-
|
|
578
|
-
`;
|
|
579
|
-
writeFileSync2(filePath, header);
|
|
580
|
-
}
|
|
581
|
-
appendFileSync(filePath, entry);
|
|
582
|
-
}
|
|
583
|
-
function openLogInEditor(date) {
|
|
584
|
-
const filePath = getLogFilePath(date);
|
|
585
|
-
if (!existsSync2(filePath)) {
|
|
586
|
-
return false;
|
|
587
|
-
}
|
|
588
|
-
const timestamp = formatTimestamp();
|
|
589
|
-
appendFileSync(filePath, `
|
|
590
|
-
## ${timestamp}
|
|
591
|
-
|
|
592
|
-
`);
|
|
593
|
-
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
594
|
-
const result = spawnSync(editor, [filePath], {
|
|
595
|
-
stdio: "inherit"
|
|
596
|
-
});
|
|
597
|
-
process.stdout.write("\x1B[2J\x1B[H");
|
|
598
|
-
process.stdout.emit("resize");
|
|
599
|
-
return result.status === 0;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// src/lib/logs/logger.ts
|
|
603
|
-
function logPRCreated(prNumber, title, jiraTickets) {
|
|
604
|
-
const timestamp = formatTimestamp();
|
|
605
|
-
const today = getTodayDate();
|
|
606
|
-
let entry = `## ${timestamp} - Created PR #${prNumber}
|
|
607
|
-
|
|
608
|
-
${title}
|
|
609
|
-
`;
|
|
610
|
-
if (jiraTickets.length > 0) {
|
|
611
|
-
entry += `Jira: ${jiraTickets.join(", ")}
|
|
612
|
-
`;
|
|
613
|
-
}
|
|
614
|
-
entry += "\n";
|
|
615
|
-
appendToLog(today, entry);
|
|
616
|
-
}
|
|
617
|
-
function logJiraStatusChanged(ticketKey, ticketName, oldStatus, newStatus) {
|
|
618
|
-
const timestamp = formatTimestamp();
|
|
619
|
-
const today = getTodayDate();
|
|
620
|
-
const entry = `## ${timestamp} - Updated Jira ticket
|
|
621
|
-
|
|
622
|
-
${ticketKey}: ${ticketName}
|
|
623
|
-
${oldStatus} \u2192 ${newStatus}
|
|
624
|
-
|
|
625
|
-
`;
|
|
626
|
-
appendToLog(today, entry);
|
|
627
|
-
}
|
|
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
|
-
};
|
|
840
|
+
writeFileSync2(filePath, header);
|
|
841
|
+
}
|
|
842
|
+
function appendToLog(date, entry) {
|
|
843
|
+
ensureLogsDirectory();
|
|
844
|
+
const filePath = getLogFilePath(date);
|
|
845
|
+
if (!existsSync2(filePath)) {
|
|
846
|
+
const header = `# Log - ${date}
|
|
847
|
+
|
|
848
|
+
`;
|
|
849
|
+
writeFileSync2(filePath, header);
|
|
850
|
+
}
|
|
851
|
+
appendFileSync(filePath, entry);
|
|
852
|
+
}
|
|
853
|
+
function openLogInEditor(date) {
|
|
854
|
+
const filePath = getLogFilePath(date);
|
|
855
|
+
if (!existsSync2(filePath)) {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
const timestamp = formatTimestamp();
|
|
859
|
+
appendFileSync(filePath, `
|
|
860
|
+
## ${timestamp}
|
|
861
|
+
|
|
862
|
+
`);
|
|
863
|
+
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
864
|
+
const result = spawnSync(editor, [filePath], {
|
|
865
|
+
stdio: "inherit"
|
|
866
|
+
});
|
|
867
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
868
|
+
process.stdout.emit("resize");
|
|
869
|
+
return result.status === 0;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/lib/logs/logger.ts
|
|
873
|
+
function logPRCreated(prNumber, title, jiraTickets) {
|
|
874
|
+
const timestamp = formatTimestamp();
|
|
875
|
+
const today = getTodayDate();
|
|
876
|
+
let entry = `## ${timestamp} - Created PR #${prNumber}
|
|
877
|
+
|
|
878
|
+
${title}
|
|
879
|
+
`;
|
|
880
|
+
if (jiraTickets.length > 0) {
|
|
881
|
+
entry += `Jira: ${jiraTickets.join(", ")}
|
|
882
|
+
`;
|
|
883
|
+
}
|
|
884
|
+
entry += "\n";
|
|
885
|
+
appendToLog(today, entry);
|
|
886
|
+
}
|
|
887
|
+
function logJiraStatusChanged(ticketKey, ticketName, oldStatus, newStatus) {
|
|
888
|
+
const timestamp = formatTimestamp();
|
|
889
|
+
const today = getTodayDate();
|
|
890
|
+
const entry = `## ${timestamp} - Updated Jira ticket
|
|
891
|
+
|
|
892
|
+
${ticketKey}: ${ticketName}
|
|
893
|
+
${oldStatus} \u2192 ${newStatus}
|
|
894
|
+
|
|
895
|
+
`;
|
|
896
|
+
appendToLog(today, entry);
|
|
883
897
|
}
|
|
884
898
|
|
|
885
899
|
// src/components/github/PRDetailsBox.tsx
|
|
@@ -889,10 +903,10 @@ import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
|
|
|
889
903
|
import { ScrollView } from "ink-scroll-view";
|
|
890
904
|
|
|
891
905
|
// src/components/ui/Markdown.tsx
|
|
906
|
+
import Table from "cli-table3";
|
|
907
|
+
import { marked } from "marked";
|
|
892
908
|
import { Box, Text } from "ink";
|
|
893
909
|
import Link from "ink-link";
|
|
894
|
-
import { marked } from "marked";
|
|
895
|
-
import Table from "cli-table3";
|
|
896
910
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
897
911
|
function Markdown({ children }) {
|
|
898
912
|
const tokens = marked.lexer(children);
|
|
@@ -904,10 +918,12 @@ function TokenRenderer({ token }) {
|
|
|
904
918
|
case "heading":
|
|
905
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) }) });
|
|
906
920
|
case "paragraph": {
|
|
907
|
-
const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some(
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
+
);
|
|
911
927
|
if (hasLinks) {
|
|
912
928
|
return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
|
|
913
929
|
}
|
|
@@ -1153,10 +1169,387 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
|
1153
1169
|
] });
|
|
1154
1170
|
}
|
|
1155
1171
|
|
|
1156
|
-
// src/components/github/PullRequestsBox.tsx
|
|
1157
|
-
import
|
|
1158
|
-
import {
|
|
1159
|
-
import {
|
|
1172
|
+
// src/components/github/PullRequestsBox.tsx
|
|
1173
|
+
import open2 from "open";
|
|
1174
|
+
import { useEffect as useEffect7, useState as useState10 } from "react";
|
|
1175
|
+
import { TitledBox } from "@mishieck/ink-titled-box";
|
|
1176
|
+
import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
|
|
1177
|
+
import { ScrollView as ScrollView2 } from "ink-scroll-view";
|
|
1178
|
+
|
|
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);
|
|
1203
|
+
}
|
|
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;
|
|
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;
|
|
1257
|
+
},
|
|
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
|
+
}
|
|
1160
1553
|
|
|
1161
1554
|
// src/lib/clipboard.ts
|
|
1162
1555
|
import { exec as exec2 } from "child_process";
|
|
@@ -1180,7 +1573,7 @@ async function copyToClipboard(text) {
|
|
|
1180
1573
|
}
|
|
1181
1574
|
|
|
1182
1575
|
// src/components/github/PullRequestsBox.tsx
|
|
1183
|
-
import {
|
|
1576
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1184
1577
|
function PullRequestsBox({
|
|
1185
1578
|
prs,
|
|
1186
1579
|
selectedPR,
|
|
@@ -1192,9 +1585,11 @@ function PullRequestsBox({
|
|
|
1192
1585
|
repoSlug,
|
|
1193
1586
|
isFocused
|
|
1194
1587
|
}) {
|
|
1195
|
-
const [highlightedIndex, setHighlightedIndex] =
|
|
1588
|
+
const [highlightedIndex, setHighlightedIndex] = useState10(0);
|
|
1589
|
+
const [copied, setCopied] = useState10(false);
|
|
1590
|
+
const scrollRef = useScrollToIndex(highlightedIndex);
|
|
1196
1591
|
const totalItems = prs.length + 1;
|
|
1197
|
-
|
|
1592
|
+
useEffect7(() => {
|
|
1198
1593
|
const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
|
|
1199
1594
|
if (idx >= 0) setHighlightedIndex(idx);
|
|
1200
1595
|
}, [selectedPR, prs]);
|
|
@@ -1218,54 +1613,74 @@ function PullRequestsBox({
|
|
|
1218
1613
|
const pr = prs[highlightedIndex];
|
|
1219
1614
|
const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
|
|
1220
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
|
+
});
|
|
1221
1624
|
}
|
|
1222
1625
|
},
|
|
1223
1626
|
{ isActive: isFocused }
|
|
1224
1627
|
);
|
|
1225
1628
|
const title = "[2] Pull Requests";
|
|
1226
1629
|
const subtitle = branch ? ` (${branch})` : "";
|
|
1630
|
+
const copiedIndicator = copied ? " [Copied!]" : "";
|
|
1227
1631
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
1228
|
-
return /* @__PURE__ */ jsx3(
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
/* @__PURE__ */
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
"
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
+
}
|
|
1671
|
+
);
|
|
1259
1672
|
}
|
|
1260
1673
|
|
|
1261
1674
|
// src/components/github/RemotesBox.tsx
|
|
1262
|
-
import { useEffect as
|
|
1675
|
+
import { useEffect as useEffect8, useState as useState11 } from "react";
|
|
1263
1676
|
import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
|
|
1264
1677
|
import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
|
|
1678
|
+
import { ScrollView as ScrollView3 } from "ink-scroll-view";
|
|
1265
1679
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1266
1680
|
function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
|
|
1267
|
-
const [highlightedIndex, setHighlightedIndex] =
|
|
1268
|
-
|
|
1681
|
+
const [highlightedIndex, setHighlightedIndex] = useState11(0);
|
|
1682
|
+
const scrollRef = useScrollToIndex(highlightedIndex);
|
|
1683
|
+
useEffect8(() => {
|
|
1269
1684
|
const idx = remotes.findIndex((r) => r.name === selectedRemote);
|
|
1270
1685
|
if (idx >= 0) setHighlightedIndex(idx);
|
|
1271
1686
|
}, [selectedRemote, remotes]);
|
|
@@ -1286,11 +1701,11 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
|
|
|
1286
1701
|
);
|
|
1287
1702
|
const title = "[1] Remotes";
|
|
1288
1703
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
1289
|
-
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: [
|
|
1290
1705
|
loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
|
|
1291
1706
|
error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
|
|
1292
1707
|
!loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
|
|
1293
|
-
!loading && !error && remotes.map((remote, idx) => {
|
|
1708
|
+
!loading && !error && remotes.length > 0 && /* @__PURE__ */ jsx4(ScrollView3, { ref: scrollRef, children: remotes.map((remote, idx) => {
|
|
1294
1709
|
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
1295
1710
|
const isSelected = remote.name === selectedRemote;
|
|
1296
1711
|
const cursor = isHighlighted ? ">" : " ";
|
|
@@ -1308,7 +1723,7 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
|
|
|
1308
1723
|
] }),
|
|
1309
1724
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: indicator })
|
|
1310
1725
|
] }, remote.name);
|
|
1311
|
-
})
|
|
1726
|
+
}) })
|
|
1312
1727
|
] }) });
|
|
1313
1728
|
}
|
|
1314
1729
|
|
|
@@ -1318,9 +1733,9 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1318
1733
|
const repo = useGitRepo();
|
|
1319
1734
|
const pullRequests = usePullRequests();
|
|
1320
1735
|
const polling = usePRPolling();
|
|
1321
|
-
const [focusedBox, setFocusedBox] =
|
|
1322
|
-
const lastFetchedRef =
|
|
1323
|
-
|
|
1736
|
+
const [focusedBox, setFocusedBox] = useState12("remotes");
|
|
1737
|
+
const lastFetchedRef = useRef5(null);
|
|
1738
|
+
useEffect9(() => {
|
|
1324
1739
|
if (repo.loading || !repo.currentBranch || !repo.currentRepoSlug) return;
|
|
1325
1740
|
const current = { branch: repo.currentBranch, repoSlug: repo.currentRepoSlug };
|
|
1326
1741
|
const last = lastFetchedRef.current;
|
|
@@ -1328,15 +1743,15 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1328
1743
|
lastFetchedRef.current = current;
|
|
1329
1744
|
pullRequests.fetchPRsAndDetails(repo.currentBranch, repo.currentRepoSlug);
|
|
1330
1745
|
}, [repo.loading, repo.currentBranch, repo.currentRepoSlug, pullRequests.fetchPRsAndDetails]);
|
|
1331
|
-
|
|
1746
|
+
useEffect9(() => {
|
|
1332
1747
|
if (isFocused) {
|
|
1333
1748
|
repo.refreshBranch();
|
|
1334
1749
|
}
|
|
1335
1750
|
}, [isFocused, repo.refreshBranch]);
|
|
1336
|
-
|
|
1751
|
+
useEffect9(() => {
|
|
1337
1752
|
onFocusedBoxChange == null ? void 0 : onFocusedBoxChange(focusedBox);
|
|
1338
1753
|
}, [focusedBox, onFocusedBoxChange]);
|
|
1339
|
-
const handleRemoteSelect =
|
|
1754
|
+
const handleRemoteSelect = useCallback9(
|
|
1340
1755
|
(remoteName) => {
|
|
1341
1756
|
repo.selectRemote(remoteName);
|
|
1342
1757
|
const remote = repo.remotes.find((r) => r.name === remoteName);
|
|
@@ -1348,28 +1763,31 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1348
1763
|
},
|
|
1349
1764
|
[repo.selectRemote, repo.remotes, repo.currentBranch, pullRequests.fetchPRsAndDetails]
|
|
1350
1765
|
);
|
|
1351
|
-
const handlePRSelect =
|
|
1766
|
+
const handlePRSelect = useCallback9(
|
|
1352
1767
|
(pr) => {
|
|
1353
1768
|
pullRequests.selectPR(pr, repo.currentRepoSlug);
|
|
1354
1769
|
},
|
|
1355
1770
|
[pullRequests.selectPR, repo.currentRepoSlug]
|
|
1356
1771
|
);
|
|
1357
|
-
const createPRContext =
|
|
1772
|
+
const createPRContext = useRef5({ repo, pullRequests, onLogUpdated });
|
|
1358
1773
|
createPRContext.current = { repo, pullRequests, onLogUpdated };
|
|
1359
|
-
const handleCreatePR =
|
|
1774
|
+
const handleCreatePR = useCallback9(() => {
|
|
1360
1775
|
const { repo: repo2, pullRequests: pullRequests2 } = createPRContext.current;
|
|
1361
1776
|
if (!repo2.currentBranch) {
|
|
1362
1777
|
pullRequests2.setError("prs", "No branch detected");
|
|
1778
|
+
duckEvents.emit("error");
|
|
1363
1779
|
return;
|
|
1364
1780
|
}
|
|
1365
1781
|
const remoteResult = findRemoteWithBranch(repo2.currentBranch);
|
|
1366
1782
|
if (!remoteResult.success) {
|
|
1367
1783
|
pullRequests2.setError("prs", "Push your branch to a remote first");
|
|
1784
|
+
duckEvents.emit("error");
|
|
1368
1785
|
return;
|
|
1369
1786
|
}
|
|
1370
1787
|
openPRCreationPage(remoteResult.data.owner, repo2.currentBranch, (error) => {
|
|
1371
1788
|
if (error) {
|
|
1372
1789
|
pullRequests2.setError("prs", `Failed to create PR: ${error.message}`);
|
|
1790
|
+
duckEvents.emit("error");
|
|
1373
1791
|
}
|
|
1374
1792
|
});
|
|
1375
1793
|
if (!repo2.currentRepoSlug) return;
|
|
@@ -1385,6 +1803,7 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1385
1803
|
const ctx = createPRContext.current;
|
|
1386
1804
|
const tickets = ctx.repo.repoPath && ctx.repo.currentBranch ? getLinkedTickets(ctx.repo.repoPath, ctx.repo.currentBranch).map((t) => t.key) : [];
|
|
1387
1805
|
logPRCreated(newPR.number, newPR.title, tickets);
|
|
1806
|
+
duckEvents.emit("pr:opened");
|
|
1388
1807
|
(_a = ctx.onLogUpdated) == null ? void 0 : _a.call(ctx);
|
|
1389
1808
|
ctx.pullRequests.setSelectedPR(newPR);
|
|
1390
1809
|
if (ctx.repo.currentRepoSlug) {
|
|
@@ -1455,296 +1874,95 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1455
1874
|
}
|
|
1456
1875
|
|
|
1457
1876
|
// src/components/jira/JiraView.tsx
|
|
1458
|
-
import
|
|
1459
|
-
import
|
|
1460
|
-
import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
|
|
1461
|
-
import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
|
|
1877
|
+
import open3 from "open";
|
|
1878
|
+
import { useEffect as useEffect11, useRef as useRef6 } from "react";
|
|
1462
1879
|
|
|
1463
|
-
// src/
|
|
1464
|
-
import {
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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);
|
|
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));
|
|
1488
1893
|
}
|
|
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
1894
|
return;
|
|
1501
1895
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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
|
-
};
|
|
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
|
+
] }) });
|
|
1694
1911
|
}
|
|
1695
1912
|
|
|
1696
|
-
// src/
|
|
1697
|
-
import {
|
|
1698
|
-
function
|
|
1699
|
-
const [
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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
|
+
] });
|
|
1709
1943
|
}
|
|
1710
1944
|
|
|
1711
|
-
// src/
|
|
1712
|
-
import {
|
|
1713
|
-
|
|
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
|
-
}
|
|
1945
|
+
// src/components/jira/JiraView.tsx
|
|
1946
|
+
import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
|
|
1947
|
+
import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
|
|
1731
1948
|
|
|
1732
1949
|
// src/components/jira/ChangeStatusModal.tsx
|
|
1733
|
-
import { useEffect as
|
|
1734
|
-
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";
|
|
1735
1952
|
import SelectInput from "ink-select-input";
|
|
1736
|
-
import { jsx as
|
|
1953
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1737
1954
|
function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onCancel }) {
|
|
1738
|
-
const [transitions, setTransitions] =
|
|
1739
|
-
const [loading, setLoading] =
|
|
1740
|
-
const [applying, setApplying] =
|
|
1741
|
-
const [error, setError] =
|
|
1742
|
-
|
|
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(() => {
|
|
1743
1960
|
const fetchTransitions = async () => {
|
|
1744
1961
|
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1745
1962
|
const creds = getJiraCredentials(repoPath);
|
|
1746
1963
|
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1747
1964
|
setError("Jira not configured");
|
|
1965
|
+
duckEvents.emit("error");
|
|
1748
1966
|
setLoading(false);
|
|
1749
1967
|
return;
|
|
1750
1968
|
}
|
|
@@ -1754,6 +1972,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1754
1972
|
setTransitions(result.data);
|
|
1755
1973
|
} else {
|
|
1756
1974
|
setError(result.error);
|
|
1975
|
+
duckEvents.emit("error");
|
|
1757
1976
|
}
|
|
1758
1977
|
setLoading(false);
|
|
1759
1978
|
};
|
|
@@ -1766,6 +1985,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1766
1985
|
const creds = getJiraCredentials(repoPath);
|
|
1767
1986
|
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1768
1987
|
setError("Jira not configured");
|
|
1988
|
+
duckEvents.emit("error");
|
|
1769
1989
|
setApplying(false);
|
|
1770
1990
|
return;
|
|
1771
1991
|
}
|
|
@@ -1774,13 +1994,15 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1774
1994
|
if (result.success) {
|
|
1775
1995
|
const transition = transitions.find((t) => t.id === item.value);
|
|
1776
1996
|
const newStatus = (transition == null ? void 0 : transition.to.name) ?? item.label;
|
|
1997
|
+
duckEvents.emit("jira:transition");
|
|
1777
1998
|
onComplete(newStatus);
|
|
1778
1999
|
} else {
|
|
1779
2000
|
setError(result.error);
|
|
2001
|
+
duckEvents.emit("error");
|
|
1780
2002
|
setApplying(false);
|
|
1781
2003
|
}
|
|
1782
2004
|
};
|
|
1783
|
-
|
|
2005
|
+
useInput7(
|
|
1784
2006
|
(_input, key) => {
|
|
1785
2007
|
if (key.escape && !applying) {
|
|
1786
2008
|
onCancel();
|
|
@@ -1792,24 +2014,27 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1792
2014
|
label: t.name,
|
|
1793
2015
|
value: t.id
|
|
1794
2016
|
}));
|
|
1795
|
-
const initialIndex = Math.max(
|
|
1796
|
-
|
|
1797
|
-
|
|
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: [
|
|
1798
2023
|
"Change Status: ",
|
|
1799
2024
|
ticketKey
|
|
1800
2025
|
] }),
|
|
1801
|
-
loading && /* @__PURE__ */
|
|
1802
|
-
error && /* @__PURE__ */
|
|
1803
|
-
!loading && !error && transitions.length === 0 && /* @__PURE__ */
|
|
1804
|
-
!loading && !error && transitions.length > 0 && !applying && /* @__PURE__ */
|
|
1805
|
-
applying && /* @__PURE__ */
|
|
1806
|
-
/* @__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" }) })
|
|
1807
2032
|
] });
|
|
1808
2033
|
}
|
|
1809
2034
|
|
|
1810
2035
|
// src/components/jira/ConfigureJiraSiteModal.tsx
|
|
1811
|
-
import { useState as
|
|
1812
|
-
import { Box as
|
|
2036
|
+
import { useState as useState15 } from "react";
|
|
2037
|
+
import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
|
|
1813
2038
|
|
|
1814
2039
|
// src/lib/editor.ts
|
|
1815
2040
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -1840,7 +2065,7 @@ function openInEditor(content, filename) {
|
|
|
1840
2065
|
}
|
|
1841
2066
|
|
|
1842
2067
|
// src/components/jira/ConfigureJiraSiteModal.tsx
|
|
1843
|
-
import { jsx as
|
|
2068
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1844
2069
|
function ConfigureJiraSiteModal({
|
|
1845
2070
|
initialSiteUrl,
|
|
1846
2071
|
initialEmail,
|
|
@@ -1849,13 +2074,13 @@ function ConfigureJiraSiteModal({
|
|
|
1849
2074
|
loading,
|
|
1850
2075
|
error
|
|
1851
2076
|
}) {
|
|
1852
|
-
const [siteUrl, setSiteUrl] =
|
|
1853
|
-
const [email, setEmail] =
|
|
1854
|
-
const [apiToken, setApiToken] =
|
|
1855
|
-
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");
|
|
1856
2081
|
const items = ["siteUrl", "email", "apiToken", "submit"];
|
|
1857
2082
|
const canSubmit = siteUrl.trim() && email.trim() && apiToken.trim();
|
|
1858
|
-
|
|
2083
|
+
useInput8(
|
|
1859
2084
|
(input, key) => {
|
|
1860
2085
|
if (loading) return;
|
|
1861
2086
|
if (key.escape) {
|
|
@@ -1904,104 +2129,31 @@ function ConfigureJiraSiteModal({
|
|
|
1904
2129
|
const prefix = isSelected ? "> " : " ";
|
|
1905
2130
|
const color = isSelected ? "yellow" : void 0;
|
|
1906
2131
|
const displayValue = isSensitive && value ? "*".repeat(Math.min(value.length, 20)) : value;
|
|
1907
|
-
return /* @__PURE__ */
|
|
1908
|
-
/* @__PURE__ */
|
|
2132
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
2133
|
+
/* @__PURE__ */ jsxs9(Text9, { color, bold: isSelected, children: [
|
|
1909
2134
|
prefix,
|
|
1910
2135
|
label
|
|
1911
2136
|
] }),
|
|
1912
|
-
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)" }) })
|
|
1913
2138
|
] });
|
|
1914
2139
|
};
|
|
1915
|
-
return /* @__PURE__ */
|
|
1916
|
-
/* @__PURE__ */
|
|
1917
|
-
/* @__PURE__ */
|
|
1918
|
-
/* @__PURE__ */
|
|
1919
|
-
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 }) }),
|
|
1920
2145
|
renderItem("siteUrl", "Site URL (e.g., https://company.atlassian.net)", siteUrl),
|
|
1921
|
-
/* @__PURE__ */
|
|
2146
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
1922
2147
|
renderItem("email", "Email", email),
|
|
1923
|
-
/* @__PURE__ */
|
|
2148
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
1924
2149
|
renderItem("apiToken", "API Token", apiToken, true),
|
|
1925
|
-
/* @__PURE__ */
|
|
1926
|
-
/* @__PURE__ */
|
|
2150
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
2151
|
+
/* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(Text9, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
|
|
1927
2152
|
selectedItem === "submit" ? "> " : " ",
|
|
1928
2153
|
canSubmit ? "[Save Configuration]" : "[Fill all fields first]"
|
|
1929
2154
|
] }) }),
|
|
1930
|
-
loading && /* @__PURE__ */
|
|
1931
|
-
/* @__PURE__ */
|
|
1932
|
-
] });
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
// src/components/jira/LinkTicketModal.tsx
|
|
1936
|
-
import { useState as useState14 } from "react";
|
|
1937
|
-
import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
|
|
1938
|
-
|
|
1939
|
-
// src/components/ui/TextInput.tsx
|
|
1940
|
-
import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
|
|
1941
|
-
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1942
|
-
function TextInput({ value, onChange, placeholder, isActive, mask }) {
|
|
1943
|
-
useInput7(
|
|
1944
|
-
(input, key) => {
|
|
1945
|
-
if (key.backspace || key.delete) {
|
|
1946
|
-
if (value.length > 0) {
|
|
1947
|
-
onChange(value.slice(0, -1));
|
|
1948
|
-
}
|
|
1949
|
-
return;
|
|
1950
|
-
}
|
|
1951
|
-
if (key.return || key.escape || key.upArrow || key.downArrow || key.tab) {
|
|
1952
|
-
return;
|
|
1953
|
-
}
|
|
1954
|
-
if (input && input.length === 1 && input.charCodeAt(0) >= 32) {
|
|
1955
|
-
onChange(value + input);
|
|
1956
|
-
}
|
|
1957
|
-
},
|
|
1958
|
-
{ isActive }
|
|
1959
|
-
);
|
|
1960
|
-
const displayValue = mask ? "*".repeat(value.length) : value;
|
|
1961
|
-
const showPlaceholder = value.length === 0 && placeholder;
|
|
1962
|
-
return /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
1963
|
-
showPlaceholder ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx8(Text8, { children: displayValue }),
|
|
1964
|
-
isActive && /* @__PURE__ */ jsx8(Text8, { backgroundColor: "yellow", children: " " })
|
|
1965
|
-
] }) });
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
// src/components/jira/LinkTicketModal.tsx
|
|
1969
|
-
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1970
|
-
function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
|
|
1971
|
-
const [ticketInput, setTicketInput] = useState14("");
|
|
1972
|
-
const canSubmit = ticketInput.trim().length > 0;
|
|
1973
|
-
useInput8(
|
|
1974
|
-
(_input, key) => {
|
|
1975
|
-
if (loading) return;
|
|
1976
|
-
if (key.escape) {
|
|
1977
|
-
onCancel();
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
if (key.return && canSubmit) {
|
|
1981
|
-
onSubmit(ticketInput.trim());
|
|
1982
|
-
}
|
|
1983
|
-
},
|
|
1984
|
-
{ isActive: !loading }
|
|
1985
|
-
);
|
|
1986
|
-
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
|
|
1987
|
-
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "yellow", children: "Link Jira Ticket" }),
|
|
1988
|
-
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Type ticket ID, Enter to submit, Esc to cancel" }),
|
|
1989
|
-
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
1990
|
-
error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
|
|
1991
|
-
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
1992
|
-
/* @__PURE__ */ jsx9(Text9, { color: "blue", children: "Ticket: " }),
|
|
1993
|
-
/* @__PURE__ */ jsx9(
|
|
1994
|
-
TextInput,
|
|
1995
|
-
{
|
|
1996
|
-
value: ticketInput,
|
|
1997
|
-
onChange: setTicketInput,
|
|
1998
|
-
placeholder: "PROJ-123",
|
|
1999
|
-
isActive: !loading
|
|
2000
|
-
}
|
|
2001
|
-
)
|
|
2002
|
-
] }),
|
|
2003
|
-
loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Fetching ticket..." }) }),
|
|
2004
|
-
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Examples: PROJ-123 or https://company.atlassian.net/browse/PROJ-123" }) })
|
|
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" }) })
|
|
2005
2157
|
] });
|
|
2006
2158
|
}
|
|
2007
2159
|
|
|
@@ -2031,8 +2183,8 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2031
2183
|
const jira = useJiraTickets();
|
|
2032
2184
|
const modal = useModal();
|
|
2033
2185
|
const nav = useListNavigation(jira.tickets.length);
|
|
2034
|
-
const lastInitRef =
|
|
2035
|
-
|
|
2186
|
+
const lastInitRef = useRef6(null);
|
|
2187
|
+
useEffect11(() => {
|
|
2036
2188
|
if (repo.loading || !repo.repoPath || !repo.currentBranch) return;
|
|
2037
2189
|
const current = { branch: repo.currentBranch };
|
|
2038
2190
|
const last = lastInitRef.current;
|
|
@@ -2040,17 +2192,17 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2040
2192
|
lastInitRef.current = current;
|
|
2041
2193
|
jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
|
|
2042
2194
|
}, [repo.loading, repo.repoPath, repo.currentBranch, repo.currentRepoSlug, jira.initializeJiraState]);
|
|
2043
|
-
|
|
2195
|
+
useEffect11(() => {
|
|
2044
2196
|
if (isFocused) {
|
|
2045
2197
|
repo.refreshBranch();
|
|
2046
2198
|
} else {
|
|
2047
2199
|
modal.close();
|
|
2048
2200
|
}
|
|
2049
2201
|
}, [isFocused, repo.refreshBranch, modal.close]);
|
|
2050
|
-
|
|
2202
|
+
useEffect11(() => {
|
|
2051
2203
|
onModalChange == null ? void 0 : onModalChange(modal.isOpen);
|
|
2052
2204
|
}, [modal.isOpen, onModalChange]);
|
|
2053
|
-
|
|
2205
|
+
useEffect11(() => {
|
|
2054
2206
|
onJiraStateChange == null ? void 0 : onJiraStateChange(jira.jiraState);
|
|
2055
2207
|
}, [jira.jiraState, onJiraStateChange]);
|
|
2056
2208
|
const handleConfigureSubmit = async (siteUrl, email, apiToken) => {
|
|
@@ -2077,7 +2229,7 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2077
2229
|
const ticket = jira.tickets[nav.index];
|
|
2078
2230
|
const siteUrl = getJiraSiteUrl(repo.repoPath);
|
|
2079
2231
|
if (ticket && siteUrl) {
|
|
2080
|
-
|
|
2232
|
+
open3(`${siteUrl}/browse/${ticket.key}`).catch(() => {
|
|
2081
2233
|
});
|
|
2082
2234
|
}
|
|
2083
2235
|
};
|
|
@@ -2187,73 +2339,14 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2187
2339
|
}
|
|
2188
2340
|
|
|
2189
2341
|
// src/components/logs/LogsView.tsx
|
|
2190
|
-
import { useEffect as
|
|
2342
|
+
import { useEffect as useEffect12 } from "react";
|
|
2191
2343
|
import { Box as Box14, useInput as useInput12 } from "ink";
|
|
2192
2344
|
|
|
2193
|
-
// src/components/logs/
|
|
2345
|
+
// src/components/logs/LogViewerBox.tsx
|
|
2346
|
+
import { useRef as useRef7, useState as useState16 } from "react";
|
|
2194
2347
|
import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
|
|
2195
2348
|
import { Box as Box12, Text as Text12, useInput as useInput10 } from "ink";
|
|
2196
|
-
import {
|
|
2197
|
-
function LogsHistoryBox({
|
|
2198
|
-
logFiles,
|
|
2199
|
-
selectedDate,
|
|
2200
|
-
highlightedIndex,
|
|
2201
|
-
onHighlight,
|
|
2202
|
-
onSelect,
|
|
2203
|
-
isFocused
|
|
2204
|
-
}) {
|
|
2205
|
-
const title = "[5] Logs";
|
|
2206
|
-
const borderColor = isFocused ? "yellow" : void 0;
|
|
2207
|
-
useInput10(
|
|
2208
|
-
(input, key) => {
|
|
2209
|
-
if (logFiles.length === 0) return;
|
|
2210
|
-
if (key.upArrow || input === "k") {
|
|
2211
|
-
onHighlight(Math.max(0, highlightedIndex - 1));
|
|
2212
|
-
}
|
|
2213
|
-
if (key.downArrow || input === "j") {
|
|
2214
|
-
onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
|
|
2215
|
-
}
|
|
2216
|
-
if (key.return) {
|
|
2217
|
-
const file = logFiles[highlightedIndex];
|
|
2218
|
-
if (file) {
|
|
2219
|
-
onSelect(file.date);
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
},
|
|
2223
|
-
{ isActive: isFocused }
|
|
2224
|
-
);
|
|
2225
|
-
return /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
|
|
2226
|
-
logFiles.length === 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "No logs yet" }),
|
|
2227
|
-
logFiles.map((file, idx) => {
|
|
2228
|
-
const isHighlighted = idx === highlightedIndex;
|
|
2229
|
-
const isSelected = file.date === selectedDate;
|
|
2230
|
-
const cursor = isHighlighted ? ">" : " ";
|
|
2231
|
-
const indicator = isSelected ? " *" : "";
|
|
2232
|
-
return /* @__PURE__ */ jsxs12(Box12, { children: [
|
|
2233
|
-
/* @__PURE__ */ jsxs12(Text12, { color: isHighlighted ? "yellow" : void 0, children: [
|
|
2234
|
-
cursor,
|
|
2235
|
-
" "
|
|
2236
|
-
] }),
|
|
2237
|
-
/* @__PURE__ */ jsx12(
|
|
2238
|
-
Text12,
|
|
2239
|
-
{
|
|
2240
|
-
color: file.isToday ? "green" : void 0,
|
|
2241
|
-
bold: file.isToday,
|
|
2242
|
-
children: file.date
|
|
2243
|
-
}
|
|
2244
|
-
),
|
|
2245
|
-
file.isToday && /* @__PURE__ */ jsx12(Text12, { color: "green", children: " (today)" }),
|
|
2246
|
-
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: indicator })
|
|
2247
|
-
] }, file.date);
|
|
2248
|
-
})
|
|
2249
|
-
] }) });
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
// src/components/logs/LogViewerBox.tsx
|
|
2253
|
-
import { useRef as useRef6, useState as useState15 } from "react";
|
|
2254
|
-
import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
|
|
2255
|
-
import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
|
|
2256
|
-
import { ScrollView as ScrollView2 } from "ink-scroll-view";
|
|
2349
|
+
import { ScrollView as ScrollView4 } from "ink-scroll-view";
|
|
2257
2350
|
import TextInput2 from "ink-text-input";
|
|
2258
2351
|
|
|
2259
2352
|
// src/lib/claude/api.ts
|
|
@@ -2340,18 +2433,18 @@ Generate the standup notes:`;
|
|
|
2340
2433
|
}
|
|
2341
2434
|
|
|
2342
2435
|
// src/components/logs/LogViewerBox.tsx
|
|
2343
|
-
import { jsx as
|
|
2436
|
+
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2344
2437
|
function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
|
|
2345
|
-
const scrollRef =
|
|
2346
|
-
const [isInputMode, setIsInputMode] =
|
|
2347
|
-
const [inputValue, setInputValue] =
|
|
2348
|
-
const [isGeneratingStandup, setIsGeneratingStandup] =
|
|
2349
|
-
const [standupResult, setStandupResult] =
|
|
2350
|
-
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);
|
|
2351
2444
|
const title = "[6] Log Content";
|
|
2352
2445
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
2353
2446
|
const displayTitle = date ? `${title} - ${date}.md` : title;
|
|
2354
|
-
|
|
2447
|
+
useInput10(
|
|
2355
2448
|
(input, key) => {
|
|
2356
2449
|
var _a, _b, _c;
|
|
2357
2450
|
if (key.escape && isInputMode) {
|
|
@@ -2430,14 +2523,14 @@ ${value.trim()}
|
|
|
2430
2523
|
setIsInputMode(false);
|
|
2431
2524
|
onRefresh();
|
|
2432
2525
|
};
|
|
2433
|
-
return /* @__PURE__ */
|
|
2434
|
-
/* @__PURE__ */
|
|
2435
|
-
!date && /* @__PURE__ */
|
|
2436
|
-
date && content === null && /* @__PURE__ */
|
|
2437
|
-
date && content !== null && content.trim() === "" && /* @__PURE__ */
|
|
2438
|
-
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 })
|
|
2439
2532
|
] }) }) }) }),
|
|
2440
|
-
isInputMode && /* @__PURE__ */
|
|
2533
|
+
isInputMode && /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: ["Add Entry"], borderColor: "yellow", children: /* @__PURE__ */ jsx12(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx12(
|
|
2441
2534
|
TextInput2,
|
|
2442
2535
|
{
|
|
2443
2536
|
value: inputValue,
|
|
@@ -2445,30 +2538,84 @@ ${value.trim()}
|
|
|
2445
2538
|
onSubmit: handleInputSubmit
|
|
2446
2539
|
}
|
|
2447
2540
|
) }) }),
|
|
2448
|
-
isGeneratingStandup && /* @__PURE__ */
|
|
2449
|
-
/* @__PURE__ */
|
|
2450
|
-
/* @__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" })
|
|
2451
2544
|
] }) }),
|
|
2452
|
-
standupResult && /* @__PURE__ */
|
|
2453
|
-
|
|
2545
|
+
standupResult && /* @__PURE__ */ jsx12(
|
|
2546
|
+
TitledBox5,
|
|
2454
2547
|
{
|
|
2455
2548
|
borderStyle: "round",
|
|
2456
2549
|
titles: ["Standup Notes"],
|
|
2457
2550
|
borderColor: standupResult.type === "error" ? "red" : "green",
|
|
2458
|
-
children: /* @__PURE__ */
|
|
2459
|
-
standupResult.type === "error" ? /* @__PURE__ */
|
|
2460
|
-
/* @__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" })
|
|
2461
2554
|
] })
|
|
2462
2555
|
}
|
|
2463
2556
|
)
|
|
2464
2557
|
] });
|
|
2465
2558
|
}
|
|
2466
2559
|
|
|
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));
|
|
2581
|
+
}
|
|
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);
|
|
2589
|
+
}
|
|
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
|
+
|
|
2467
2614
|
// src/components/logs/LogsView.tsx
|
|
2468
2615
|
import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2469
2616
|
function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
|
|
2470
2617
|
const logs = useLogs();
|
|
2471
|
-
|
|
2618
|
+
useEffect12(() => {
|
|
2472
2619
|
if (refreshKey !== void 0 && refreshKey > 0) {
|
|
2473
2620
|
logs.handleExternalLogUpdate();
|
|
2474
2621
|
}
|
|
@@ -2513,22 +2660,25 @@ var globalBindings = [
|
|
|
2513
2660
|
{ key: "j/k", label: "Navigate" },
|
|
2514
2661
|
{ key: "Ctrl+C", label: "Quit" }
|
|
2515
2662
|
];
|
|
2516
|
-
var modalBindings = [
|
|
2517
|
-
|
|
2518
|
-
]
|
|
2519
|
-
function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
|
|
2663
|
+
var modalBindings = [{ key: "Esc", label: "Cancel" }];
|
|
2664
|
+
var DUCK_ASCII = "<(')___";
|
|
2665
|
+
function KeybindingsBar({ contextBindings = [], modalOpen = false, duck }) {
|
|
2520
2666
|
const allBindings = modalOpen ? [...contextBindings, ...modalBindings] : [...contextBindings, ...globalBindings];
|
|
2521
|
-
return /* @__PURE__ */
|
|
2522
|
-
/* @__PURE__ */
|
|
2523
|
-
|
|
2524
|
-
|
|
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
|
+
] });
|
|
2525
2677
|
}
|
|
2526
2678
|
|
|
2527
2679
|
// src/constants/github.ts
|
|
2528
2680
|
var GITHUB_KEYBINDINGS = {
|
|
2529
|
-
remotes: [
|
|
2530
|
-
{ key: "Space", label: "Select Remote" }
|
|
2531
|
-
],
|
|
2681
|
+
remotes: [{ key: "Space", label: "Select Remote" }],
|
|
2532
2682
|
prs: [
|
|
2533
2683
|
{ key: "Space", label: "Select" },
|
|
2534
2684
|
{ key: "n", label: "New PR", color: "green" },
|
|
@@ -2557,9 +2707,7 @@ var JIRA_KEYBINDINGS = {
|
|
|
2557
2707
|
|
|
2558
2708
|
// src/constants/logs.ts
|
|
2559
2709
|
var LOGS_KEYBINDINGS = {
|
|
2560
|
-
history: [
|
|
2561
|
-
{ key: "Enter", label: "Select" }
|
|
2562
|
-
],
|
|
2710
|
+
history: [{ key: "Enter", label: "Select" }],
|
|
2563
2711
|
viewer: [
|
|
2564
2712
|
{ key: "i", label: "Add Entry" },
|
|
2565
2713
|
{ key: "e", label: "Edit" },
|
|
@@ -2588,12 +2736,13 @@ function computeKeybindings(focusedView, state) {
|
|
|
2588
2736
|
import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2589
2737
|
function App() {
|
|
2590
2738
|
const { exit } = useApp();
|
|
2591
|
-
const [focusedView, setFocusedView] =
|
|
2592
|
-
const [modalOpen, setModalOpen] =
|
|
2593
|
-
const [logRefreshKey, setLogRefreshKey] =
|
|
2594
|
-
const
|
|
2595
|
-
const [
|
|
2596
|
-
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");
|
|
2597
2746
|
const keybindings = useMemo2(
|
|
2598
2747
|
() => computeKeybindings(focusedView, {
|
|
2599
2748
|
github: { focusedBox: githubFocusedBox },
|
|
@@ -2602,7 +2751,7 @@ function App() {
|
|
|
2602
2751
|
}),
|
|
2603
2752
|
[focusedView, githubFocusedBox, jiraState, modalOpen, logsFocusedBox]
|
|
2604
2753
|
);
|
|
2605
|
-
const handleLogUpdated =
|
|
2754
|
+
const handleLogUpdated = useCallback10(() => {
|
|
2606
2755
|
setLogRefreshKey((prev) => prev + 1);
|
|
2607
2756
|
}, []);
|
|
2608
2757
|
useInput13(
|
|
@@ -2624,6 +2773,12 @@ function App() {
|
|
|
2624
2773
|
setFocusedView("logs");
|
|
2625
2774
|
setLogsFocusedBox("viewer");
|
|
2626
2775
|
}
|
|
2776
|
+
if (input === "d") {
|
|
2777
|
+
duck.toggleDuck();
|
|
2778
|
+
}
|
|
2779
|
+
if (input === "q" && duck.visible) {
|
|
2780
|
+
duck.quack();
|
|
2781
|
+
}
|
|
2627
2782
|
},
|
|
2628
2783
|
{ isActive: !modalOpen }
|
|
2629
2784
|
);
|
|
@@ -2658,7 +2813,14 @@ function App() {
|
|
|
2658
2813
|
}
|
|
2659
2814
|
) })
|
|
2660
2815
|
] }),
|
|
2661
|
-
/* @__PURE__ */ jsx16(
|
|
2816
|
+
/* @__PURE__ */ jsx16(
|
|
2817
|
+
KeybindingsBar,
|
|
2818
|
+
{
|
|
2819
|
+
contextBindings: keybindings,
|
|
2820
|
+
modalOpen,
|
|
2821
|
+
duck: { visible: duck.visible, message: duck.message }
|
|
2822
|
+
}
|
|
2823
|
+
)
|
|
2662
2824
|
] });
|
|
2663
2825
|
}
|
|
2664
2826
|
|
|
@@ -2666,17 +2828,14 @@ function App() {
|
|
|
2666
2828
|
import { render as inkRender } from "ink";
|
|
2667
2829
|
|
|
2668
2830
|
// src/lib/Screen.tsx
|
|
2831
|
+
import { useCallback as useCallback11, useEffect as useEffect13, useState as useState18 } from "react";
|
|
2669
2832
|
import { Box as Box17, useStdout as useStdout2 } from "ink";
|
|
2670
|
-
import { useCallback as useCallback10, useEffect as useEffect11, useState as useState17 } from "react";
|
|
2671
2833
|
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
2672
2834
|
function Screen({ children }) {
|
|
2673
2835
|
const { stdout } = useStdout2();
|
|
2674
|
-
const getSize =
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
);
|
|
2678
|
-
const [size, setSize] = useState17(getSize);
|
|
2679
|
-
useEffect11(() => {
|
|
2836
|
+
const getSize = useCallback11(() => ({ height: stdout.rows, width: stdout.columns }), [stdout]);
|
|
2837
|
+
const [size, setSize] = useState18(getSize);
|
|
2838
|
+
useEffect13(() => {
|
|
2680
2839
|
const onResize = () => setSize(getSize());
|
|
2681
2840
|
stdout.on("resize", onResize);
|
|
2682
2841
|
return () => {
|