clairo 1.0.7 → 1.0.9
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 +1231 -959
- 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,327 @@ 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 getExistingJiraConfigs(excludeRepoPath) {
|
|
577
|
+
const config = loadConfig();
|
|
578
|
+
const repos = config.repositories ?? {};
|
|
579
|
+
const configs = [];
|
|
580
|
+
const seen = /* @__PURE__ */ new Set();
|
|
581
|
+
for (const [repoPath, repoConfig] of Object.entries(repos)) {
|
|
582
|
+
if (repoPath === excludeRepoPath) continue;
|
|
583
|
+
if (!repoConfig.jiraSiteUrl || !repoConfig.jiraEmail || !repoConfig.jiraApiToken) continue;
|
|
584
|
+
const key = `${repoConfig.jiraSiteUrl}|${repoConfig.jiraEmail}`;
|
|
585
|
+
if (seen.has(key)) continue;
|
|
586
|
+
seen.add(key);
|
|
587
|
+
configs.push({
|
|
588
|
+
repoPath,
|
|
589
|
+
siteUrl: repoConfig.jiraSiteUrl,
|
|
590
|
+
email: repoConfig.jiraEmail,
|
|
591
|
+
apiToken: repoConfig.jiraApiToken
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return configs;
|
|
595
|
+
}
|
|
596
|
+
function isJiraConfigured(repoPath) {
|
|
597
|
+
const config = getRepoConfig(repoPath);
|
|
598
|
+
return !!(config.jiraSiteUrl && config.jiraEmail && config.jiraApiToken);
|
|
599
|
+
}
|
|
600
|
+
function getJiraSiteUrl(repoPath) {
|
|
601
|
+
const config = getRepoConfig(repoPath);
|
|
602
|
+
return config.jiraSiteUrl ?? null;
|
|
603
|
+
}
|
|
604
|
+
function setJiraSiteUrl(repoPath, siteUrl) {
|
|
605
|
+
updateRepoConfig(repoPath, { jiraSiteUrl: siteUrl });
|
|
606
|
+
}
|
|
317
607
|
function getJiraCredentials(repoPath) {
|
|
318
608
|
const config = getRepoConfig(repoPath);
|
|
319
609
|
return {
|
|
@@ -324,6 +614,13 @@ function getJiraCredentials(repoPath) {
|
|
|
324
614
|
function setJiraCredentials(repoPath, email, apiToken) {
|
|
325
615
|
updateRepoConfig(repoPath, { jiraEmail: email, jiraApiToken: apiToken });
|
|
326
616
|
}
|
|
617
|
+
function clearJiraConfig(repoPath) {
|
|
618
|
+
updateRepoConfig(repoPath, {
|
|
619
|
+
jiraSiteUrl: void 0,
|
|
620
|
+
jiraEmail: void 0,
|
|
621
|
+
jiraApiToken: void 0
|
|
622
|
+
});
|
|
623
|
+
}
|
|
327
624
|
function getLinkedTickets(repoPath, branch) {
|
|
328
625
|
var _a;
|
|
329
626
|
const config = getRepoConfig(repoPath);
|
|
@@ -498,10 +795,10 @@ async function applyTransition(auth, ticketKey, transitionId) {
|
|
|
498
795
|
}
|
|
499
796
|
|
|
500
797
|
// src/lib/logs/index.ts
|
|
501
|
-
import {
|
|
798
|
+
import { spawnSync } from "child_process";
|
|
799
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
502
800
|
import { homedir as homedir2 } from "os";
|
|
503
801
|
import { join as join2 } from "path";
|
|
504
|
-
import { spawnSync } from "child_process";
|
|
505
802
|
var LOGS_DIRECTORY = join2(homedir2(), ".clairo", "logs");
|
|
506
803
|
function ensureLogsDirectory() {
|
|
507
804
|
if (!existsSync2(LOGS_DIRECTORY)) {
|
|
@@ -581,305 +878,49 @@ function appendToLog(date, entry) {
|
|
|
581
878
|
appendFileSync(filePath, entry);
|
|
582
879
|
}
|
|
583
880
|
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
|
-
};
|
|
881
|
+
const filePath = getLogFilePath(date);
|
|
882
|
+
if (!existsSync2(filePath)) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
const timestamp = formatTimestamp();
|
|
886
|
+
appendFileSync(filePath, `
|
|
887
|
+
## ${timestamp}
|
|
888
|
+
|
|
889
|
+
`);
|
|
890
|
+
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
891
|
+
const result = spawnSync(editor, [filePath], {
|
|
892
|
+
stdio: "inherit"
|
|
893
|
+
});
|
|
894
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
895
|
+
process.stdout.emit("resize");
|
|
896
|
+
return result.status === 0;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/lib/logs/logger.ts
|
|
900
|
+
function logPRCreated(prNumber, title, jiraTickets) {
|
|
901
|
+
const timestamp = formatTimestamp();
|
|
902
|
+
const today = getTodayDate();
|
|
903
|
+
let entry = `## ${timestamp} - Created PR #${prNumber}
|
|
904
|
+
|
|
905
|
+
${title}
|
|
906
|
+
`;
|
|
907
|
+
if (jiraTickets.length > 0) {
|
|
908
|
+
entry += `Jira: ${jiraTickets.join(", ")}
|
|
909
|
+
`;
|
|
910
|
+
}
|
|
911
|
+
entry += "\n";
|
|
912
|
+
appendToLog(today, entry);
|
|
913
|
+
}
|
|
914
|
+
function logJiraStatusChanged(ticketKey, ticketName, oldStatus, newStatus) {
|
|
915
|
+
const timestamp = formatTimestamp();
|
|
916
|
+
const today = getTodayDate();
|
|
917
|
+
const entry = `## ${timestamp} - Updated Jira ticket
|
|
918
|
+
|
|
919
|
+
${ticketKey}: ${ticketName}
|
|
920
|
+
${oldStatus} \u2192 ${newStatus}
|
|
921
|
+
|
|
922
|
+
`;
|
|
923
|
+
appendToLog(today, entry);
|
|
883
924
|
}
|
|
884
925
|
|
|
885
926
|
// src/components/github/PRDetailsBox.tsx
|
|
@@ -889,10 +930,10 @@ import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
|
|
|
889
930
|
import { ScrollView } from "ink-scroll-view";
|
|
890
931
|
|
|
891
932
|
// src/components/ui/Markdown.tsx
|
|
933
|
+
import Table from "cli-table3";
|
|
934
|
+
import { marked } from "marked";
|
|
892
935
|
import { Box, Text } from "ink";
|
|
893
936
|
import Link from "ink-link";
|
|
894
|
-
import { marked } from "marked";
|
|
895
|
-
import Table from "cli-table3";
|
|
896
937
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
897
938
|
function Markdown({ children }) {
|
|
898
939
|
const tokens = marked.lexer(children);
|
|
@@ -904,10 +945,12 @@ function TokenRenderer({ token }) {
|
|
|
904
945
|
case "heading":
|
|
905
946
|
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
947
|
case "paragraph": {
|
|
907
|
-
const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some(
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
948
|
+
const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some(
|
|
949
|
+
(t) => {
|
|
950
|
+
var _a2;
|
|
951
|
+
return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
|
|
952
|
+
}
|
|
953
|
+
);
|
|
911
954
|
if (hasLinks) {
|
|
912
955
|
return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
|
|
913
956
|
}
|
|
@@ -1153,10 +1196,387 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
|
1153
1196
|
] });
|
|
1154
1197
|
}
|
|
1155
1198
|
|
|
1156
|
-
// src/components/github/PullRequestsBox.tsx
|
|
1157
|
-
import
|
|
1158
|
-
import {
|
|
1159
|
-
import {
|
|
1199
|
+
// src/components/github/PullRequestsBox.tsx
|
|
1200
|
+
import open2 from "open";
|
|
1201
|
+
import { useEffect as useEffect7, useState as useState10 } from "react";
|
|
1202
|
+
import { TitledBox } from "@mishieck/ink-titled-box";
|
|
1203
|
+
import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
|
|
1204
|
+
import { ScrollView as ScrollView2 } from "ink-scroll-view";
|
|
1205
|
+
|
|
1206
|
+
// src/hooks/jira/useJiraTickets.ts
|
|
1207
|
+
import { useCallback as useCallback4, useState as useState5 } from "react";
|
|
1208
|
+
function useJiraTickets() {
|
|
1209
|
+
const [jiraState, setJiraState] = useState5("not_configured");
|
|
1210
|
+
const [tickets, setTickets] = useState5([]);
|
|
1211
|
+
const [loading, setLoading] = useState5({ configure: false, link: false });
|
|
1212
|
+
const [errors, setErrors] = useState5({});
|
|
1213
|
+
const initializeJiraState = useCallback4(async (repoPath, currentBranch, repoSlug) => {
|
|
1214
|
+
if (!isJiraConfigured(repoPath)) {
|
|
1215
|
+
setJiraState("not_configured");
|
|
1216
|
+
setTickets([]);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const linkedTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1220
|
+
if (linkedTickets.length > 0) {
|
|
1221
|
+
setTickets(linkedTickets);
|
|
1222
|
+
setJiraState("has_tickets");
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
let ticketKey = extractTicketKey(currentBranch);
|
|
1226
|
+
if (!ticketKey && repoSlug) {
|
|
1227
|
+
const prResult = await listPRsForBranch(currentBranch, repoSlug);
|
|
1228
|
+
if (prResult.success && prResult.data.length > 0) {
|
|
1229
|
+
ticketKey = extractTicketKey(prResult.data[0].title);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (!ticketKey) {
|
|
1233
|
+
setTickets([]);
|
|
1234
|
+
setJiraState("no_tickets");
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1238
|
+
const creds = getJiraCredentials(repoPath);
|
|
1239
|
+
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1240
|
+
setTickets([]);
|
|
1241
|
+
setJiraState("no_tickets");
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
1245
|
+
const result = await getIssue(auth, ticketKey);
|
|
1246
|
+
if (result.success) {
|
|
1247
|
+
const linkedTicket = {
|
|
1248
|
+
key: result.data.key,
|
|
1249
|
+
summary: result.data.fields.summary,
|
|
1250
|
+
status: result.data.fields.status.name,
|
|
1251
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1252
|
+
};
|
|
1253
|
+
addLinkedTicket(repoPath, currentBranch, linkedTicket);
|
|
1254
|
+
setTickets([linkedTicket]);
|
|
1255
|
+
setJiraState("has_tickets");
|
|
1256
|
+
} else {
|
|
1257
|
+
setTickets([]);
|
|
1258
|
+
setJiraState("no_tickets");
|
|
1259
|
+
}
|
|
1260
|
+
}, []);
|
|
1261
|
+
const refreshTickets = useCallback4((repoPath, currentBranch) => {
|
|
1262
|
+
const linkedTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1263
|
+
setTickets(linkedTickets);
|
|
1264
|
+
setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
|
|
1265
|
+
}, []);
|
|
1266
|
+
const configureJira = useCallback4(
|
|
1267
|
+
async (repoPath, siteUrl, email, apiToken) => {
|
|
1268
|
+
setLoading((prev) => ({ ...prev, configure: true }));
|
|
1269
|
+
setErrors((prev) => ({ ...prev, configure: void 0 }));
|
|
1270
|
+
const auth = { siteUrl, email, apiToken };
|
|
1271
|
+
const result = await validateCredentials(auth);
|
|
1272
|
+
if (!result.success) {
|
|
1273
|
+
setErrors((prev) => ({ ...prev, configure: result.error }));
|
|
1274
|
+
duckEvents.emit("error");
|
|
1275
|
+
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1278
|
+
setJiraSiteUrl(repoPath, siteUrl);
|
|
1279
|
+
setJiraCredentials(repoPath, email, apiToken);
|
|
1280
|
+
setJiraState("no_tickets");
|
|
1281
|
+
duckEvents.emit("jira:configured");
|
|
1282
|
+
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1283
|
+
return true;
|
|
1284
|
+
},
|
|
1285
|
+
[]
|
|
1286
|
+
);
|
|
1287
|
+
const linkTicket = useCallback4(
|
|
1288
|
+
async (repoPath, currentBranch, ticketInput) => {
|
|
1289
|
+
setLoading((prev) => ({ ...prev, link: true }));
|
|
1290
|
+
setErrors((prev) => ({ ...prev, link: void 0 }));
|
|
1291
|
+
const ticketKey = parseTicketKey(ticketInput);
|
|
1292
|
+
if (!ticketKey) {
|
|
1293
|
+
setErrors((prev) => ({ ...prev, link: "Invalid ticket format. Use PROJ-123 or a Jira URL." }));
|
|
1294
|
+
duckEvents.emit("error");
|
|
1295
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1299
|
+
const creds = getJiraCredentials(repoPath);
|
|
1300
|
+
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1301
|
+
setErrors((prev) => ({ ...prev, link: "Jira not configured" }));
|
|
1302
|
+
duckEvents.emit("error");
|
|
1303
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
1307
|
+
const result = await getIssue(auth, ticketKey);
|
|
1308
|
+
if (!result.success) {
|
|
1309
|
+
setErrors((prev) => ({ ...prev, link: result.error }));
|
|
1310
|
+
duckEvents.emit("error");
|
|
1311
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
const linkedTicket = {
|
|
1315
|
+
key: result.data.key,
|
|
1316
|
+
summary: result.data.fields.summary,
|
|
1317
|
+
status: result.data.fields.status.name,
|
|
1318
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1319
|
+
};
|
|
1320
|
+
addLinkedTicket(repoPath, currentBranch, linkedTicket);
|
|
1321
|
+
const newTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1322
|
+
setTickets(newTickets);
|
|
1323
|
+
setJiraState("has_tickets");
|
|
1324
|
+
duckEvents.emit("jira:linked");
|
|
1325
|
+
setLoading((prev) => ({ ...prev, link: false }));
|
|
1326
|
+
return true;
|
|
1327
|
+
},
|
|
1328
|
+
[]
|
|
1329
|
+
);
|
|
1330
|
+
const unlinkTicket = useCallback4((repoPath, currentBranch, ticketKey) => {
|
|
1331
|
+
removeLinkedTicket(repoPath, currentBranch, ticketKey);
|
|
1332
|
+
}, []);
|
|
1333
|
+
const clearError = useCallback4((key) => {
|
|
1334
|
+
setErrors((prev) => ({ ...prev, [key]: void 0 }));
|
|
1335
|
+
}, []);
|
|
1336
|
+
return {
|
|
1337
|
+
jiraState,
|
|
1338
|
+
tickets,
|
|
1339
|
+
loading,
|
|
1340
|
+
errors,
|
|
1341
|
+
initializeJiraState,
|
|
1342
|
+
refreshTickets,
|
|
1343
|
+
configureJira,
|
|
1344
|
+
linkTicket,
|
|
1345
|
+
unlinkTicket,
|
|
1346
|
+
clearError
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/hooks/logs/useLogs.ts
|
|
1351
|
+
import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef3, useState as useState6 } from "react";
|
|
1352
|
+
function useLogs() {
|
|
1353
|
+
const [logFiles, setLogFiles] = useState6([]);
|
|
1354
|
+
const [selectedDate, setSelectedDate] = useState6(null);
|
|
1355
|
+
const [logContent, setLogContent] = useState6(null);
|
|
1356
|
+
const [highlightedIndex, setHighlightedIndex] = useState6(0);
|
|
1357
|
+
const initializedRef = useRef3(false);
|
|
1358
|
+
const loadLogContent = useCallback5((date) => {
|
|
1359
|
+
if (!date) {
|
|
1360
|
+
setLogContent(null);
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
const content = readLog(date);
|
|
1364
|
+
setLogContent(content);
|
|
1365
|
+
return content;
|
|
1366
|
+
}, []);
|
|
1367
|
+
const refreshLogFiles = useCallback5(() => {
|
|
1368
|
+
const files = listLogFiles();
|
|
1369
|
+
setLogFiles(files);
|
|
1370
|
+
return files;
|
|
1371
|
+
}, []);
|
|
1372
|
+
const initialize = useCallback5(() => {
|
|
1373
|
+
const files = listLogFiles();
|
|
1374
|
+
setLogFiles(files);
|
|
1375
|
+
if (files.length === 0) return;
|
|
1376
|
+
const today = getTodayDate();
|
|
1377
|
+
const todayFile = files.find((f) => f.date === today);
|
|
1378
|
+
if (todayFile) {
|
|
1379
|
+
setSelectedDate(todayFile.date);
|
|
1380
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1381
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1382
|
+
loadLogContent(todayFile.date);
|
|
1383
|
+
} else {
|
|
1384
|
+
setSelectedDate(files[0].date);
|
|
1385
|
+
setHighlightedIndex(0);
|
|
1386
|
+
loadLogContent(files[0].date);
|
|
1387
|
+
}
|
|
1388
|
+
}, [loadLogContent]);
|
|
1389
|
+
useEffect4(() => {
|
|
1390
|
+
if (initializedRef.current) return;
|
|
1391
|
+
initializedRef.current = true;
|
|
1392
|
+
initialize();
|
|
1393
|
+
}, [initialize]);
|
|
1394
|
+
const selectDate = useCallback5(
|
|
1395
|
+
(date) => {
|
|
1396
|
+
setSelectedDate(date);
|
|
1397
|
+
loadLogContent(date);
|
|
1398
|
+
},
|
|
1399
|
+
[loadLogContent]
|
|
1400
|
+
);
|
|
1401
|
+
const refresh = useCallback5(() => {
|
|
1402
|
+
refreshLogFiles();
|
|
1403
|
+
if (selectedDate) {
|
|
1404
|
+
loadLogContent(selectedDate);
|
|
1405
|
+
}
|
|
1406
|
+
}, [refreshLogFiles, selectedDate, loadLogContent]);
|
|
1407
|
+
const handleExternalLogUpdate = useCallback5(() => {
|
|
1408
|
+
const files = listLogFiles();
|
|
1409
|
+
setLogFiles(files);
|
|
1410
|
+
const today = getTodayDate();
|
|
1411
|
+
if (selectedDate === today) {
|
|
1412
|
+
loadLogContent(today);
|
|
1413
|
+
} else if (!selectedDate && files.length > 0) {
|
|
1414
|
+
const todayFile = files.find((f) => f.date === today);
|
|
1415
|
+
if (todayFile) {
|
|
1416
|
+
setSelectedDate(today);
|
|
1417
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1418
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1419
|
+
loadLogContent(today);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}, [selectedDate, loadLogContent]);
|
|
1423
|
+
const handleLogCreated = useCallback5(() => {
|
|
1424
|
+
const files = listLogFiles();
|
|
1425
|
+
setLogFiles(files);
|
|
1426
|
+
const today = getTodayDate();
|
|
1427
|
+
setSelectedDate(today);
|
|
1428
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1429
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1430
|
+
loadLogContent(today);
|
|
1431
|
+
}, [loadLogContent]);
|
|
1432
|
+
return {
|
|
1433
|
+
logFiles,
|
|
1434
|
+
selectedDate,
|
|
1435
|
+
logContent,
|
|
1436
|
+
highlightedIndex,
|
|
1437
|
+
setHighlightedIndex,
|
|
1438
|
+
selectDate,
|
|
1439
|
+
refresh,
|
|
1440
|
+
handleExternalLogUpdate,
|
|
1441
|
+
handleLogCreated
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// src/hooks/useModal.ts
|
|
1446
|
+
import { useCallback as useCallback6, useState as useState7 } from "react";
|
|
1447
|
+
function useModal() {
|
|
1448
|
+
const [modalType, setModalType] = useState7("none");
|
|
1449
|
+
const open4 = useCallback6((type) => setModalType(type), []);
|
|
1450
|
+
const close = useCallback6(() => setModalType("none"), []);
|
|
1451
|
+
const isOpen = modalType !== "none";
|
|
1452
|
+
return {
|
|
1453
|
+
type: modalType,
|
|
1454
|
+
isOpen,
|
|
1455
|
+
open: open4,
|
|
1456
|
+
close
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// src/hooks/useListNavigation.ts
|
|
1461
|
+
import { useCallback as useCallback7, useState as useState8 } from "react";
|
|
1462
|
+
function useListNavigation(length) {
|
|
1463
|
+
const [index, setIndex] = useState8(0);
|
|
1464
|
+
const prev = useCallback7(() => {
|
|
1465
|
+
setIndex((i) => Math.max(0, i - 1));
|
|
1466
|
+
}, []);
|
|
1467
|
+
const next = useCallback7(() => {
|
|
1468
|
+
setIndex((i) => Math.min(length - 1, i + 1));
|
|
1469
|
+
}, [length]);
|
|
1470
|
+
const clampedIndex = Math.min(index, Math.max(0, length - 1));
|
|
1471
|
+
const reset = useCallback7(() => setIndex(0), []);
|
|
1472
|
+
return {
|
|
1473
|
+
index: length === 0 ? 0 : clampedIndex,
|
|
1474
|
+
prev,
|
|
1475
|
+
next,
|
|
1476
|
+
reset,
|
|
1477
|
+
setIndex
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// src/hooks/useScrollToIndex.ts
|
|
1482
|
+
import { useEffect as useEffect5, useRef as useRef4 } from "react";
|
|
1483
|
+
function useScrollToIndex(index) {
|
|
1484
|
+
const scrollRef = useRef4(null);
|
|
1485
|
+
useEffect5(() => {
|
|
1486
|
+
const ref = scrollRef.current;
|
|
1487
|
+
if (!ref) return;
|
|
1488
|
+
const pos = ref.getItemPosition(index);
|
|
1489
|
+
const viewportHeight = ref.getViewportHeight();
|
|
1490
|
+
const scrollOffset = ref.getScrollOffset();
|
|
1491
|
+
if (!pos) return;
|
|
1492
|
+
if (pos.top < scrollOffset) {
|
|
1493
|
+
ref.scrollTo(pos.top);
|
|
1494
|
+
} else if (pos.top + pos.height > scrollOffset + viewportHeight) {
|
|
1495
|
+
ref.scrollTo(pos.top + pos.height - viewportHeight);
|
|
1496
|
+
}
|
|
1497
|
+
}, [index]);
|
|
1498
|
+
return scrollRef;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// src/hooks/useRubberDuck.ts
|
|
1502
|
+
import { useCallback as useCallback8, useEffect as useEffect6, useState as useState9 } from "react";
|
|
1503
|
+
var DUCK_MESSAGES = [
|
|
1504
|
+
"Quack.",
|
|
1505
|
+
"Quack quack quack.",
|
|
1506
|
+
"Have you tried explaining it out loud?",
|
|
1507
|
+
"It's always DNS.",
|
|
1508
|
+
"Did you check the logs?",
|
|
1509
|
+
"Maybe add a console.log?",
|
|
1510
|
+
"Is it plugged in?",
|
|
1511
|
+
"Works on my machine.",
|
|
1512
|
+
"Have you tried reading the error message?",
|
|
1513
|
+
"I believe in you!",
|
|
1514
|
+
"It's probably a race condition.",
|
|
1515
|
+
"Have you tried turning it off and on again?",
|
|
1516
|
+
"Are you sure it compiled?",
|
|
1517
|
+
"It's not a bug, it's a feature.",
|
|
1518
|
+
"Did you clear the cache?",
|
|
1519
|
+
"Try deleting node_modules.",
|
|
1520
|
+
"That's quackers!",
|
|
1521
|
+
"Rubber duck debugging, activate!",
|
|
1522
|
+
"*supportive quacking*"
|
|
1523
|
+
];
|
|
1524
|
+
var REACTION_MESSAGES = {
|
|
1525
|
+
"pr:merged": ["Quack! It shipped!", "Merged!", "To production we go!"],
|
|
1526
|
+
"pr:opened": ["A new PR! Exciting!", "Time for review!", "Fresh code incoming!"],
|
|
1527
|
+
"pr:reviewed": ["Feedback time!", "Reviews are in!", "*attentive quacking*"],
|
|
1528
|
+
"pr:approved": ["Approved!", "LGTM!", "Ship it!"],
|
|
1529
|
+
"pr:changes-requested": ["Some changes needed...", "Back to the drawing board!", "Iterate iterate!"],
|
|
1530
|
+
error: ["Uh oh...", "There there...", "*concerned quacking*", "Quack... not good."],
|
|
1531
|
+
"jira:transition": ["Ticket moving!", "Progress!", "Workflow in motion!"],
|
|
1532
|
+
"jira:linked": ["Ticket linked!", "Jira connection made!", "Tracking enabled!"],
|
|
1533
|
+
"jira:configured": ["Jira ready!", "Integration complete!", "Connected to Jira!"]
|
|
1534
|
+
};
|
|
1535
|
+
function useRubberDuck() {
|
|
1536
|
+
const [state, setState] = useState9({
|
|
1537
|
+
visible: false,
|
|
1538
|
+
message: DUCK_MESSAGES[0]
|
|
1539
|
+
});
|
|
1540
|
+
const getRandomMessage = useCallback8(() => {
|
|
1541
|
+
const index = Math.floor(Math.random() * DUCK_MESSAGES.length);
|
|
1542
|
+
return DUCK_MESSAGES[index];
|
|
1543
|
+
}, []);
|
|
1544
|
+
const toggleDuck = useCallback8(() => {
|
|
1545
|
+
setState((prev) => ({
|
|
1546
|
+
...prev,
|
|
1547
|
+
visible: !prev.visible,
|
|
1548
|
+
message: !prev.visible ? getRandomMessage() : prev.message
|
|
1549
|
+
}));
|
|
1550
|
+
}, [getRandomMessage]);
|
|
1551
|
+
const quack = useCallback8(() => {
|
|
1552
|
+
if (state.visible) {
|
|
1553
|
+
setState((prev) => ({
|
|
1554
|
+
...prev,
|
|
1555
|
+
message: getRandomMessage()
|
|
1556
|
+
}));
|
|
1557
|
+
}
|
|
1558
|
+
}, [state.visible, getRandomMessage]);
|
|
1559
|
+
const getReactionMessage = useCallback8((event) => {
|
|
1560
|
+
const messages = REACTION_MESSAGES[event];
|
|
1561
|
+
return messages[Math.floor(Math.random() * messages.length)];
|
|
1562
|
+
}, []);
|
|
1563
|
+
useEffect6(() => {
|
|
1564
|
+
const unsubscribe = duckEvents.subscribe((event) => {
|
|
1565
|
+
setState((prev) => ({
|
|
1566
|
+
...prev,
|
|
1567
|
+
visible: true,
|
|
1568
|
+
message: getReactionMessage(event)
|
|
1569
|
+
}));
|
|
1570
|
+
});
|
|
1571
|
+
return unsubscribe;
|
|
1572
|
+
}, [getReactionMessage]);
|
|
1573
|
+
return {
|
|
1574
|
+
visible: state.visible,
|
|
1575
|
+
message: state.message,
|
|
1576
|
+
toggleDuck,
|
|
1577
|
+
quack
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1160
1580
|
|
|
1161
1581
|
// src/lib/clipboard.ts
|
|
1162
1582
|
import { exec as exec2 } from "child_process";
|
|
@@ -1180,7 +1600,7 @@ async function copyToClipboard(text) {
|
|
|
1180
1600
|
}
|
|
1181
1601
|
|
|
1182
1602
|
// src/components/github/PullRequestsBox.tsx
|
|
1183
|
-
import {
|
|
1603
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1184
1604
|
function PullRequestsBox({
|
|
1185
1605
|
prs,
|
|
1186
1606
|
selectedPR,
|
|
@@ -1192,9 +1612,11 @@ function PullRequestsBox({
|
|
|
1192
1612
|
repoSlug,
|
|
1193
1613
|
isFocused
|
|
1194
1614
|
}) {
|
|
1195
|
-
const [highlightedIndex, setHighlightedIndex] =
|
|
1615
|
+
const [highlightedIndex, setHighlightedIndex] = useState10(0);
|
|
1616
|
+
const [copied, setCopied] = useState10(false);
|
|
1617
|
+
const scrollRef = useScrollToIndex(highlightedIndex);
|
|
1196
1618
|
const totalItems = prs.length + 1;
|
|
1197
|
-
|
|
1619
|
+
useEffect7(() => {
|
|
1198
1620
|
const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
|
|
1199
1621
|
if (idx >= 0) setHighlightedIndex(idx);
|
|
1200
1622
|
}, [selectedPR, prs]);
|
|
@@ -1218,54 +1640,74 @@ function PullRequestsBox({
|
|
|
1218
1640
|
const pr = prs[highlightedIndex];
|
|
1219
1641
|
const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
|
|
1220
1642
|
copyToClipboard(url);
|
|
1643
|
+
setCopied(true);
|
|
1644
|
+
setTimeout(() => setCopied(false), 1500);
|
|
1645
|
+
}
|
|
1646
|
+
if (input === "o" && repoSlug && prs[highlightedIndex]) {
|
|
1647
|
+
const pr = prs[highlightedIndex];
|
|
1648
|
+
const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
|
|
1649
|
+
open2(url).catch(() => {
|
|
1650
|
+
});
|
|
1221
1651
|
}
|
|
1222
1652
|
},
|
|
1223
1653
|
{ isActive: isFocused }
|
|
1224
1654
|
);
|
|
1225
1655
|
const title = "[2] Pull Requests";
|
|
1226
1656
|
const subtitle = branch ? ` (${branch})` : "";
|
|
1657
|
+
const copiedIndicator = copied ? " [Copied!]" : "";
|
|
1227
1658
|
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
|
-
|
|
1659
|
+
return /* @__PURE__ */ jsx3(
|
|
1660
|
+
TitledBox,
|
|
1661
|
+
{
|
|
1662
|
+
borderStyle: "round",
|
|
1663
|
+
titles: [`${title}${subtitle}${copiedIndicator}`],
|
|
1664
|
+
borderColor,
|
|
1665
|
+
height: 5,
|
|
1666
|
+
children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
|
|
1667
|
+
loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
|
|
1668
|
+
error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
|
|
1669
|
+
!loading && !error && /* @__PURE__ */ jsxs3(ScrollView2, { ref: scrollRef, children: [
|
|
1670
|
+
prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }, "empty"),
|
|
1671
|
+
prs.map((pr, idx) => {
|
|
1672
|
+
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
1673
|
+
const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
|
|
1674
|
+
const cursor = isHighlighted ? ">" : " ";
|
|
1675
|
+
const indicator = isSelected ? " *" : "";
|
|
1676
|
+
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1677
|
+
/* @__PURE__ */ jsxs3(Text3, { color: isHighlighted ? "yellow" : void 0, children: [
|
|
1678
|
+
cursor,
|
|
1679
|
+
" "
|
|
1680
|
+
] }),
|
|
1681
|
+
/* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
|
|
1682
|
+
"#",
|
|
1683
|
+
pr.number,
|
|
1684
|
+
" ",
|
|
1685
|
+
pr.isDraft ? "[Draft] " : "",
|
|
1686
|
+
pr.title
|
|
1687
|
+
] }),
|
|
1688
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: indicator })
|
|
1689
|
+
] }, pr.number);
|
|
1690
|
+
}),
|
|
1691
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
|
|
1692
|
+
isFocused && highlightedIndex === prs.length ? "> " : " ",
|
|
1693
|
+
"+ Create new PR"
|
|
1694
|
+
] }, "create")
|
|
1695
|
+
] })
|
|
1696
|
+
] })
|
|
1697
|
+
}
|
|
1698
|
+
);
|
|
1259
1699
|
}
|
|
1260
1700
|
|
|
1261
1701
|
// src/components/github/RemotesBox.tsx
|
|
1262
|
-
import { useEffect as
|
|
1702
|
+
import { useEffect as useEffect8, useState as useState11 } from "react";
|
|
1263
1703
|
import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
|
|
1264
1704
|
import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
|
|
1705
|
+
import { ScrollView as ScrollView3 } from "ink-scroll-view";
|
|
1265
1706
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1266
1707
|
function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
|
|
1267
|
-
const [highlightedIndex, setHighlightedIndex] =
|
|
1268
|
-
|
|
1708
|
+
const [highlightedIndex, setHighlightedIndex] = useState11(0);
|
|
1709
|
+
const scrollRef = useScrollToIndex(highlightedIndex);
|
|
1710
|
+
useEffect8(() => {
|
|
1269
1711
|
const idx = remotes.findIndex((r) => r.name === selectedRemote);
|
|
1270
1712
|
if (idx >= 0) setHighlightedIndex(idx);
|
|
1271
1713
|
}, [selectedRemote, remotes]);
|
|
@@ -1286,11 +1728,11 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
|
|
|
1286
1728
|
);
|
|
1287
1729
|
const title = "[1] Remotes";
|
|
1288
1730
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
1289
|
-
return /* @__PURE__ */ jsx4(TitledBox2, { borderStyle: "round", titles: [title], borderColor,
|
|
1731
|
+
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
1732
|
loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
|
|
1291
1733
|
error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
|
|
1292
1734
|
!loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
|
|
1293
|
-
!loading && !error && remotes.map((remote, idx) => {
|
|
1735
|
+
!loading && !error && remotes.length > 0 && /* @__PURE__ */ jsx4(ScrollView3, { ref: scrollRef, children: remotes.map((remote, idx) => {
|
|
1294
1736
|
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
1295
1737
|
const isSelected = remote.name === selectedRemote;
|
|
1296
1738
|
const cursor = isHighlighted ? ">" : " ";
|
|
@@ -1308,7 +1750,7 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
|
|
|
1308
1750
|
] }),
|
|
1309
1751
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: indicator })
|
|
1310
1752
|
] }, remote.name);
|
|
1311
|
-
})
|
|
1753
|
+
}) })
|
|
1312
1754
|
] }) });
|
|
1313
1755
|
}
|
|
1314
1756
|
|
|
@@ -1318,9 +1760,9 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1318
1760
|
const repo = useGitRepo();
|
|
1319
1761
|
const pullRequests = usePullRequests();
|
|
1320
1762
|
const polling = usePRPolling();
|
|
1321
|
-
const [focusedBox, setFocusedBox] =
|
|
1322
|
-
const lastFetchedRef =
|
|
1323
|
-
|
|
1763
|
+
const [focusedBox, setFocusedBox] = useState12("remotes");
|
|
1764
|
+
const lastFetchedRef = useRef5(null);
|
|
1765
|
+
useEffect9(() => {
|
|
1324
1766
|
if (repo.loading || !repo.currentBranch || !repo.currentRepoSlug) return;
|
|
1325
1767
|
const current = { branch: repo.currentBranch, repoSlug: repo.currentRepoSlug };
|
|
1326
1768
|
const last = lastFetchedRef.current;
|
|
@@ -1328,15 +1770,15 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1328
1770
|
lastFetchedRef.current = current;
|
|
1329
1771
|
pullRequests.fetchPRsAndDetails(repo.currentBranch, repo.currentRepoSlug);
|
|
1330
1772
|
}, [repo.loading, repo.currentBranch, repo.currentRepoSlug, pullRequests.fetchPRsAndDetails]);
|
|
1331
|
-
|
|
1773
|
+
useEffect9(() => {
|
|
1332
1774
|
if (isFocused) {
|
|
1333
1775
|
repo.refreshBranch();
|
|
1334
1776
|
}
|
|
1335
1777
|
}, [isFocused, repo.refreshBranch]);
|
|
1336
|
-
|
|
1778
|
+
useEffect9(() => {
|
|
1337
1779
|
onFocusedBoxChange == null ? void 0 : onFocusedBoxChange(focusedBox);
|
|
1338
1780
|
}, [focusedBox, onFocusedBoxChange]);
|
|
1339
|
-
const handleRemoteSelect =
|
|
1781
|
+
const handleRemoteSelect = useCallback9(
|
|
1340
1782
|
(remoteName) => {
|
|
1341
1783
|
repo.selectRemote(remoteName);
|
|
1342
1784
|
const remote = repo.remotes.find((r) => r.name === remoteName);
|
|
@@ -1348,28 +1790,31 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1348
1790
|
},
|
|
1349
1791
|
[repo.selectRemote, repo.remotes, repo.currentBranch, pullRequests.fetchPRsAndDetails]
|
|
1350
1792
|
);
|
|
1351
|
-
const handlePRSelect =
|
|
1793
|
+
const handlePRSelect = useCallback9(
|
|
1352
1794
|
(pr) => {
|
|
1353
1795
|
pullRequests.selectPR(pr, repo.currentRepoSlug);
|
|
1354
1796
|
},
|
|
1355
1797
|
[pullRequests.selectPR, repo.currentRepoSlug]
|
|
1356
1798
|
);
|
|
1357
|
-
const createPRContext =
|
|
1799
|
+
const createPRContext = useRef5({ repo, pullRequests, onLogUpdated });
|
|
1358
1800
|
createPRContext.current = { repo, pullRequests, onLogUpdated };
|
|
1359
|
-
const handleCreatePR =
|
|
1801
|
+
const handleCreatePR = useCallback9(() => {
|
|
1360
1802
|
const { repo: repo2, pullRequests: pullRequests2 } = createPRContext.current;
|
|
1361
1803
|
if (!repo2.currentBranch) {
|
|
1362
1804
|
pullRequests2.setError("prs", "No branch detected");
|
|
1805
|
+
duckEvents.emit("error");
|
|
1363
1806
|
return;
|
|
1364
1807
|
}
|
|
1365
1808
|
const remoteResult = findRemoteWithBranch(repo2.currentBranch);
|
|
1366
1809
|
if (!remoteResult.success) {
|
|
1367
1810
|
pullRequests2.setError("prs", "Push your branch to a remote first");
|
|
1811
|
+
duckEvents.emit("error");
|
|
1368
1812
|
return;
|
|
1369
1813
|
}
|
|
1370
1814
|
openPRCreationPage(remoteResult.data.owner, repo2.currentBranch, (error) => {
|
|
1371
1815
|
if (error) {
|
|
1372
1816
|
pullRequests2.setError("prs", `Failed to create PR: ${error.message}`);
|
|
1817
|
+
duckEvents.emit("error");
|
|
1373
1818
|
}
|
|
1374
1819
|
});
|
|
1375
1820
|
if (!repo2.currentRepoSlug) return;
|
|
@@ -1385,6 +1830,7 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1385
1830
|
const ctx = createPRContext.current;
|
|
1386
1831
|
const tickets = ctx.repo.repoPath && ctx.repo.currentBranch ? getLinkedTickets(ctx.repo.repoPath, ctx.repo.currentBranch).map((t) => t.key) : [];
|
|
1387
1832
|
logPRCreated(newPR.number, newPR.title, tickets);
|
|
1833
|
+
duckEvents.emit("pr:opened");
|
|
1388
1834
|
(_a = ctx.onLogUpdated) == null ? void 0 : _a.call(ctx);
|
|
1389
1835
|
ctx.pullRequests.setSelectedPR(newPR);
|
|
1390
1836
|
if (ctx.repo.currentRepoSlug) {
|
|
@@ -1455,296 +1901,95 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1455
1901
|
}
|
|
1456
1902
|
|
|
1457
1903
|
// 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";
|
|
1904
|
+
import open3 from "open";
|
|
1905
|
+
import { useEffect as useEffect11, useRef as useRef6 } from "react";
|
|
1462
1906
|
|
|
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);
|
|
1907
|
+
// src/components/jira/LinkTicketModal.tsx
|
|
1908
|
+
import { useState as useState13 } from "react";
|
|
1909
|
+
import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
|
|
1910
|
+
|
|
1911
|
+
// src/components/ui/TextInput.tsx
|
|
1912
|
+
import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
|
|
1913
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1914
|
+
function TextInput({ value, onChange, placeholder, isActive, mask }) {
|
|
1915
|
+
useInput5(
|
|
1916
|
+
(input, key) => {
|
|
1917
|
+
if (key.backspace || key.delete) {
|
|
1918
|
+
if (value.length > 0) {
|
|
1919
|
+
onChange(value.slice(0, -1));
|
|
1488
1920
|
}
|
|
1489
|
-
}
|
|
1490
|
-
if (!ticketKey) {
|
|
1491
|
-
setTickets([]);
|
|
1492
|
-
setJiraState("no_tickets");
|
|
1493
1921
|
return;
|
|
1494
1922
|
}
|
|
1495
|
-
|
|
1496
|
-
const creds = getJiraCredentials(repoPath);
|
|
1497
|
-
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1498
|
-
setTickets([]);
|
|
1499
|
-
setJiraState("no_tickets");
|
|
1923
|
+
if (key.return || key.escape || key.upArrow || key.downArrow || key.tab) {
|
|
1500
1924
|
return;
|
|
1501
1925
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
if (result.success) {
|
|
1505
|
-
const linkedTicket = {
|
|
1506
|
-
key: result.data.key,
|
|
1507
|
-
summary: result.data.fields.summary,
|
|
1508
|
-
status: result.data.fields.status.name,
|
|
1509
|
-
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1510
|
-
};
|
|
1511
|
-
addLinkedTicket(repoPath, currentBranch, linkedTicket);
|
|
1512
|
-
setTickets([linkedTicket]);
|
|
1513
|
-
setJiraState("has_tickets");
|
|
1514
|
-
} else {
|
|
1515
|
-
setTickets([]);
|
|
1516
|
-
setJiraState("no_tickets");
|
|
1517
|
-
}
|
|
1518
|
-
},
|
|
1519
|
-
[]
|
|
1520
|
-
);
|
|
1521
|
-
const refreshTickets = useCallback5((repoPath, currentBranch) => {
|
|
1522
|
-
const linkedTickets = getLinkedTickets(repoPath, currentBranch);
|
|
1523
|
-
setTickets(linkedTickets);
|
|
1524
|
-
setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
|
|
1525
|
-
}, []);
|
|
1526
|
-
const configureJira = useCallback5(
|
|
1527
|
-
async (repoPath, siteUrl, email, apiToken) => {
|
|
1528
|
-
setLoading((prev) => ({ ...prev, configure: true }));
|
|
1529
|
-
setErrors((prev) => ({ ...prev, configure: void 0 }));
|
|
1530
|
-
const auth = { siteUrl, email, apiToken };
|
|
1531
|
-
const result = await validateCredentials(auth);
|
|
1532
|
-
if (!result.success) {
|
|
1533
|
-
setErrors((prev) => ({ ...prev, configure: result.error }));
|
|
1534
|
-
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1535
|
-
return false;
|
|
1536
|
-
}
|
|
1537
|
-
setJiraSiteUrl(repoPath, siteUrl);
|
|
1538
|
-
setJiraCredentials(repoPath, email, apiToken);
|
|
1539
|
-
setJiraState("no_tickets");
|
|
1540
|
-
setLoading((prev) => ({ ...prev, configure: false }));
|
|
1541
|
-
return true;
|
|
1542
|
-
},
|
|
1543
|
-
[]
|
|
1544
|
-
);
|
|
1545
|
-
const linkTicket = useCallback5(
|
|
1546
|
-
async (repoPath, currentBranch, ticketInput) => {
|
|
1547
|
-
setLoading((prev) => ({ ...prev, link: true }));
|
|
1548
|
-
setErrors((prev) => ({ ...prev, link: void 0 }));
|
|
1549
|
-
const ticketKey = parseTicketKey(ticketInput);
|
|
1550
|
-
if (!ticketKey) {
|
|
1551
|
-
setErrors((prev) => ({ ...prev, link: "Invalid ticket format. Use PROJ-123 or a Jira URL." }));
|
|
1552
|
-
setLoading((prev) => ({ ...prev, link: false }));
|
|
1553
|
-
return false;
|
|
1554
|
-
}
|
|
1555
|
-
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1556
|
-
const creds = getJiraCredentials(repoPath);
|
|
1557
|
-
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1558
|
-
setErrors((prev) => ({ ...prev, link: "Jira not configured" }));
|
|
1559
|
-
setLoading((prev) => ({ ...prev, link: false }));
|
|
1560
|
-
return false;
|
|
1561
|
-
}
|
|
1562
|
-
const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
|
|
1563
|
-
const result = await getIssue(auth, ticketKey);
|
|
1564
|
-
if (!result.success) {
|
|
1565
|
-
setErrors((prev) => ({ ...prev, link: result.error }));
|
|
1566
|
-
setLoading((prev) => ({ ...prev, link: false }));
|
|
1567
|
-
return false;
|
|
1926
|
+
if (input && input.length === 1 && input.charCodeAt(0) >= 32) {
|
|
1927
|
+
onChange(value + input);
|
|
1568
1928
|
}
|
|
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
1929
|
},
|
|
1582
|
-
|
|
1930
|
+
{ isActive }
|
|
1583
1931
|
);
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
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
|
-
};
|
|
1932
|
+
const displayValue = mask ? "*".repeat(value.length) : value;
|
|
1933
|
+
const showPlaceholder = value.length === 0 && placeholder;
|
|
1934
|
+
return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { children: [
|
|
1935
|
+
showPlaceholder ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx6(Text6, { children: displayValue }),
|
|
1936
|
+
isActive && /* @__PURE__ */ jsx6(Text6, { backgroundColor: "yellow", children: " " })
|
|
1937
|
+
] }) });
|
|
1694
1938
|
}
|
|
1695
1939
|
|
|
1696
|
-
// src/
|
|
1697
|
-
import {
|
|
1698
|
-
function
|
|
1699
|
-
const [
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1940
|
+
// src/components/jira/LinkTicketModal.tsx
|
|
1941
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1942
|
+
function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
|
|
1943
|
+
const [ticketInput, setTicketInput] = useState13("");
|
|
1944
|
+
const canSubmit = ticketInput.trim().length > 0;
|
|
1945
|
+
useInput6(
|
|
1946
|
+
(_input, key) => {
|
|
1947
|
+
if (loading) return;
|
|
1948
|
+
if (key.escape) {
|
|
1949
|
+
onCancel();
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
if (key.return && canSubmit) {
|
|
1953
|
+
onSubmit(ticketInput.trim());
|
|
1954
|
+
}
|
|
1955
|
+
},
|
|
1956
|
+
{ isActive: !loading }
|
|
1957
|
+
);
|
|
1958
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
|
|
1959
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "yellow", children: "Link Jira Ticket" }),
|
|
1960
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Type ticket ID, Enter to submit, Esc to cancel" }),
|
|
1961
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
|
|
1962
|
+
error && /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: error }) }),
|
|
1963
|
+
/* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1964
|
+
/* @__PURE__ */ jsx7(Text7, { color: "blue", children: "Ticket: " }),
|
|
1965
|
+
/* @__PURE__ */ jsx7(TextInput, { value: ticketInput, onChange: setTicketInput, placeholder: "PROJ-123", isActive: !loading })
|
|
1966
|
+
] }),
|
|
1967
|
+
loading && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "Fetching ticket..." }) }),
|
|
1968
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Examples: PROJ-123 or https://company.atlassian.net/browse/PROJ-123" }) })
|
|
1969
|
+
] });
|
|
1709
1970
|
}
|
|
1710
1971
|
|
|
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
|
-
}
|
|
1972
|
+
// src/components/jira/JiraView.tsx
|
|
1973
|
+
import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
|
|
1974
|
+
import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
|
|
1731
1975
|
|
|
1732
1976
|
// src/components/jira/ChangeStatusModal.tsx
|
|
1733
|
-
import { useEffect as
|
|
1734
|
-
import { Box as
|
|
1977
|
+
import { useEffect as useEffect10, useState as useState14 } from "react";
|
|
1978
|
+
import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
|
|
1735
1979
|
import SelectInput from "ink-select-input";
|
|
1736
|
-
import { jsx as
|
|
1980
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1737
1981
|
function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onCancel }) {
|
|
1738
|
-
const [transitions, setTransitions] =
|
|
1739
|
-
const [loading, setLoading] =
|
|
1740
|
-
const [applying, setApplying] =
|
|
1741
|
-
const [error, setError] =
|
|
1742
|
-
|
|
1982
|
+
const [transitions, setTransitions] = useState14([]);
|
|
1983
|
+
const [loading, setLoading] = useState14(true);
|
|
1984
|
+
const [applying, setApplying] = useState14(false);
|
|
1985
|
+
const [error, setError] = useState14(null);
|
|
1986
|
+
useEffect10(() => {
|
|
1743
1987
|
const fetchTransitions = async () => {
|
|
1744
1988
|
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1745
1989
|
const creds = getJiraCredentials(repoPath);
|
|
1746
1990
|
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1747
1991
|
setError("Jira not configured");
|
|
1992
|
+
duckEvents.emit("error");
|
|
1748
1993
|
setLoading(false);
|
|
1749
1994
|
return;
|
|
1750
1995
|
}
|
|
@@ -1754,6 +1999,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1754
1999
|
setTransitions(result.data);
|
|
1755
2000
|
} else {
|
|
1756
2001
|
setError(result.error);
|
|
2002
|
+
duckEvents.emit("error");
|
|
1757
2003
|
}
|
|
1758
2004
|
setLoading(false);
|
|
1759
2005
|
};
|
|
@@ -1766,6 +2012,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1766
2012
|
const creds = getJiraCredentials(repoPath);
|
|
1767
2013
|
if (!siteUrl || !creds.email || !creds.apiToken) {
|
|
1768
2014
|
setError("Jira not configured");
|
|
2015
|
+
duckEvents.emit("error");
|
|
1769
2016
|
setApplying(false);
|
|
1770
2017
|
return;
|
|
1771
2018
|
}
|
|
@@ -1774,13 +2021,15 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1774
2021
|
if (result.success) {
|
|
1775
2022
|
const transition = transitions.find((t) => t.id === item.value);
|
|
1776
2023
|
const newStatus = (transition == null ? void 0 : transition.to.name) ?? item.label;
|
|
2024
|
+
duckEvents.emit("jira:transition");
|
|
1777
2025
|
onComplete(newStatus);
|
|
1778
2026
|
} else {
|
|
1779
2027
|
setError(result.error);
|
|
2028
|
+
duckEvents.emit("error");
|
|
1780
2029
|
setApplying(false);
|
|
1781
2030
|
}
|
|
1782
2031
|
};
|
|
1783
|
-
|
|
2032
|
+
useInput7(
|
|
1784
2033
|
(_input, key) => {
|
|
1785
2034
|
if (key.escape && !applying) {
|
|
1786
2035
|
onCancel();
|
|
@@ -1792,24 +2041,28 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
1792
2041
|
label: t.name,
|
|
1793
2042
|
value: t.id
|
|
1794
2043
|
}));
|
|
1795
|
-
const initialIndex = Math.max(
|
|
1796
|
-
|
|
1797
|
-
|
|
2044
|
+
const initialIndex = Math.max(
|
|
2045
|
+
0,
|
|
2046
|
+
transitions.findIndex((t) => t.to.name === currentStatus)
|
|
2047
|
+
);
|
|
2048
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
|
|
2049
|
+
/* @__PURE__ */ jsxs8(Text8, { bold: true, color: "yellow", children: [
|
|
1798
2050
|
"Change Status: ",
|
|
1799
2051
|
ticketKey
|
|
1800
2052
|
] }),
|
|
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__ */
|
|
2053
|
+
loading && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Loading transitions..." }),
|
|
2054
|
+
error && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "red", children: error }) }),
|
|
2055
|
+
!loading && !error && transitions.length === 0 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "No available transitions" }),
|
|
2056
|
+
!loading && !error && transitions.length > 0 && !applying && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx8(SelectInput, { items, initialIndex, onSelect: handleSelect }) }),
|
|
2057
|
+
applying && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Updating status..." }) }),
|
|
2058
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Esc to cancel" }) })
|
|
1807
2059
|
] });
|
|
1808
2060
|
}
|
|
1809
2061
|
|
|
1810
2062
|
// src/components/jira/ConfigureJiraSiteModal.tsx
|
|
1811
|
-
import { useState as
|
|
1812
|
-
import { Box as
|
|
2063
|
+
import { useState as useState15 } from "react";
|
|
2064
|
+
import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
|
|
2065
|
+
import { ScrollView as ScrollView4 } from "ink-scroll-view";
|
|
1813
2066
|
|
|
1814
2067
|
// src/lib/editor.ts
|
|
1815
2068
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -1840,28 +2093,58 @@ function openInEditor(content, filename) {
|
|
|
1840
2093
|
}
|
|
1841
2094
|
|
|
1842
2095
|
// src/components/jira/ConfigureJiraSiteModal.tsx
|
|
1843
|
-
import { jsx as
|
|
2096
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2097
|
+
var MAX_VISIBLE_ITEMS = 4;
|
|
1844
2098
|
function ConfigureJiraSiteModal({
|
|
1845
2099
|
initialSiteUrl,
|
|
1846
2100
|
initialEmail,
|
|
2101
|
+
existingConfigs = [],
|
|
1847
2102
|
onSubmit,
|
|
1848
2103
|
onCancel,
|
|
1849
2104
|
loading,
|
|
1850
2105
|
error
|
|
1851
2106
|
}) {
|
|
1852
|
-
const
|
|
1853
|
-
const [
|
|
1854
|
-
const [
|
|
1855
|
-
const
|
|
2107
|
+
const hasExisting = existingConfigs.length > 0;
|
|
2108
|
+
const [mode, setMode] = useState15(hasExisting ? "choose" : "manual");
|
|
2109
|
+
const [selectedExisting, setSelectedExisting] = useState15(0);
|
|
2110
|
+
const scrollRef = useScrollToIndex(selectedExisting);
|
|
2111
|
+
const [siteUrl, setSiteUrl] = useState15(initialSiteUrl ?? "");
|
|
2112
|
+
const [email, setEmail] = useState15(initialEmail ?? "");
|
|
2113
|
+
const [apiToken, setApiToken] = useState15("");
|
|
2114
|
+
const [selectedItem, setSelectedItem] = useState15("siteUrl");
|
|
1856
2115
|
const items = ["siteUrl", "email", "apiToken", "submit"];
|
|
1857
2116
|
const canSubmit = siteUrl.trim() && email.trim() && apiToken.trim();
|
|
1858
|
-
|
|
2117
|
+
const chooseItems = existingConfigs.length + 1;
|
|
2118
|
+
useInput8(
|
|
1859
2119
|
(input, key) => {
|
|
1860
2120
|
if (loading) return;
|
|
1861
2121
|
if (key.escape) {
|
|
2122
|
+
if (mode === "manual" && hasExisting) {
|
|
2123
|
+
setMode("choose");
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
1862
2126
|
onCancel();
|
|
1863
2127
|
return;
|
|
1864
2128
|
}
|
|
2129
|
+
if (mode === "choose") {
|
|
2130
|
+
if (key.upArrow || input === "k") {
|
|
2131
|
+
setSelectedExisting((prev) => Math.max(0, prev - 1));
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
if (key.downArrow || input === "j") {
|
|
2135
|
+
setSelectedExisting((prev) => Math.min(chooseItems - 1, prev + 1));
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
if (key.return) {
|
|
2139
|
+
if (selectedExisting < existingConfigs.length) {
|
|
2140
|
+
const config = existingConfigs[selectedExisting];
|
|
2141
|
+
onSubmit(config.siteUrl, config.email, config.apiToken);
|
|
2142
|
+
} else {
|
|
2143
|
+
setMode("manual");
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
1865
2148
|
if (key.upArrow || input === "k") {
|
|
1866
2149
|
setSelectedItem((prev) => {
|
|
1867
2150
|
const idx = items.indexOf(prev);
|
|
@@ -1904,104 +2187,71 @@ function ConfigureJiraSiteModal({
|
|
|
1904
2187
|
const prefix = isSelected ? "> " : " ";
|
|
1905
2188
|
const color = isSelected ? "yellow" : void 0;
|
|
1906
2189
|
const displayValue = isSensitive && value ? "*".repeat(Math.min(value.length, 20)) : value;
|
|
1907
|
-
return /* @__PURE__ */
|
|
1908
|
-
/* @__PURE__ */
|
|
2190
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
2191
|
+
/* @__PURE__ */ jsxs9(Text9, { color, bold: isSelected, children: [
|
|
1909
2192
|
prefix,
|
|
1910
2193
|
label
|
|
1911
2194
|
] }),
|
|
1912
|
-
value !== void 0 && /* @__PURE__ */
|
|
2195
|
+
value !== void 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: displayValue || "(empty - press Enter to edit)" }) })
|
|
1913
2196
|
] });
|
|
1914
2197
|
};
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
/* @__PURE__ */
|
|
1919
|
-
|
|
2198
|
+
if (mode === "choose") {
|
|
2199
|
+
const totalItems = existingConfigs.length + 1;
|
|
2200
|
+
const listHeight = Math.min(totalItems * 2, MAX_VISIBLE_ITEMS * 2);
|
|
2201
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
|
|
2202
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Configure Jira Site" }),
|
|
2203
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Select an existing configuration or enter new credentials" }),
|
|
2204
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
2205
|
+
error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
|
|
2206
|
+
/* @__PURE__ */ jsx9(Box9, { height: listHeight, overflow: "hidden", children: /* @__PURE__ */ jsxs9(ScrollView4, { ref: scrollRef, children: [
|
|
2207
|
+
existingConfigs.map((config, idx) => {
|
|
2208
|
+
const isSelected = selectedExisting === idx;
|
|
2209
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
2210
|
+
/* @__PURE__ */ jsxs9(Text9, { color: isSelected ? "yellow" : void 0, bold: isSelected, children: [
|
|
2211
|
+
isSelected ? "> " : " ",
|
|
2212
|
+
config.siteUrl
|
|
2213
|
+
] }),
|
|
2214
|
+
/* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
2215
|
+
" ",
|
|
2216
|
+
config.email
|
|
2217
|
+
] })
|
|
2218
|
+
] }, config.siteUrl + config.email);
|
|
2219
|
+
}),
|
|
2220
|
+
/* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(
|
|
2221
|
+
Text9,
|
|
2222
|
+
{
|
|
2223
|
+
color: selectedExisting === existingConfigs.length ? "yellow" : void 0,
|
|
2224
|
+
bold: selectedExisting === existingConfigs.length,
|
|
2225
|
+
children: [
|
|
2226
|
+
selectedExisting === existingConfigs.length ? "> " : " ",
|
|
2227
|
+
"Enter new credentials..."
|
|
2228
|
+
]
|
|
2229
|
+
}
|
|
2230
|
+
) })
|
|
2231
|
+
] }) }),
|
|
2232
|
+
loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Validating credentials..." }) })
|
|
2233
|
+
] });
|
|
2234
|
+
}
|
|
2235
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
|
|
2236
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Configure Jira Site" }),
|
|
2237
|
+
/* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
2238
|
+
"Up/Down to select, Enter to edit, Esc to ",
|
|
2239
|
+
hasExisting ? "go back" : "cancel"
|
|
2240
|
+
] }),
|
|
2241
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
2242
|
+
error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
|
|
1920
2243
|
renderItem("siteUrl", "Site URL (e.g., https://company.atlassian.net)", siteUrl),
|
|
1921
|
-
/* @__PURE__ */
|
|
2244
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
1922
2245
|
renderItem("email", "Email", email),
|
|
1923
|
-
/* @__PURE__ */
|
|
2246
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
1924
2247
|
renderItem("apiToken", "API Token", apiToken, true),
|
|
1925
|
-
/* @__PURE__ */
|
|
1926
|
-
/* @__PURE__ */
|
|
2248
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
2249
|
+
/* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(Text9, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
|
|
1927
2250
|
selectedItem === "submit" ? "> " : " ",
|
|
1928
2251
|
canSubmit ? "[Save Configuration]" : "[Fill all fields first]"
|
|
1929
2252
|
] }) }),
|
|
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" }) })
|
|
2253
|
+
loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Validating credentials..." }) }),
|
|
2254
|
+
/* @__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
2255
|
] });
|
|
2006
2256
|
}
|
|
2007
2257
|
|
|
@@ -2031,8 +2281,8 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2031
2281
|
const jira = useJiraTickets();
|
|
2032
2282
|
const modal = useModal();
|
|
2033
2283
|
const nav = useListNavigation(jira.tickets.length);
|
|
2034
|
-
const lastInitRef =
|
|
2035
|
-
|
|
2284
|
+
const lastInitRef = useRef6(null);
|
|
2285
|
+
useEffect11(() => {
|
|
2036
2286
|
if (repo.loading || !repo.repoPath || !repo.currentBranch) return;
|
|
2037
2287
|
const current = { branch: repo.currentBranch };
|
|
2038
2288
|
const last = lastInitRef.current;
|
|
@@ -2040,17 +2290,17 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2040
2290
|
lastInitRef.current = current;
|
|
2041
2291
|
jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
|
|
2042
2292
|
}, [repo.loading, repo.repoPath, repo.currentBranch, repo.currentRepoSlug, jira.initializeJiraState]);
|
|
2043
|
-
|
|
2293
|
+
useEffect11(() => {
|
|
2044
2294
|
if (isFocused) {
|
|
2045
2295
|
repo.refreshBranch();
|
|
2046
2296
|
} else {
|
|
2047
2297
|
modal.close();
|
|
2048
2298
|
}
|
|
2049
2299
|
}, [isFocused, repo.refreshBranch, modal.close]);
|
|
2050
|
-
|
|
2300
|
+
useEffect11(() => {
|
|
2051
2301
|
onModalChange == null ? void 0 : onModalChange(modal.isOpen);
|
|
2052
2302
|
}, [modal.isOpen, onModalChange]);
|
|
2053
|
-
|
|
2303
|
+
useEffect11(() => {
|
|
2054
2304
|
onJiraStateChange == null ? void 0 : onJiraStateChange(jira.jiraState);
|
|
2055
2305
|
}, [jira.jiraState, onJiraStateChange]);
|
|
2056
2306
|
const handleConfigureSubmit = async (siteUrl, email, apiToken) => {
|
|
@@ -2077,7 +2327,7 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2077
2327
|
const ticket = jira.tickets[nav.index];
|
|
2078
2328
|
const siteUrl = getJiraSiteUrl(repo.repoPath);
|
|
2079
2329
|
if (ticket && siteUrl) {
|
|
2080
|
-
|
|
2330
|
+
open3(`${siteUrl}/browse/${ticket.key}`).catch(() => {
|
|
2081
2331
|
});
|
|
2082
2332
|
}
|
|
2083
2333
|
};
|
|
@@ -2099,6 +2349,11 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2099
2349
|
modal.close();
|
|
2100
2350
|
jira.refreshTickets(repo.repoPath, repo.currentBranch);
|
|
2101
2351
|
};
|
|
2352
|
+
const handleRemoveConfig = () => {
|
|
2353
|
+
if (!repo.repoPath || !repo.currentBranch) return;
|
|
2354
|
+
clearJiraConfig(repo.repoPath);
|
|
2355
|
+
jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
|
|
2356
|
+
};
|
|
2102
2357
|
useInput9(
|
|
2103
2358
|
(input, key) => {
|
|
2104
2359
|
if (input === "c" && jira.jiraState === "not_configured") {
|
|
@@ -2109,6 +2364,10 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2109
2364
|
modal.open("link");
|
|
2110
2365
|
return;
|
|
2111
2366
|
}
|
|
2367
|
+
if (input === "r" && jira.jiraState !== "not_configured") {
|
|
2368
|
+
handleRemoveConfig();
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2112
2371
|
if (jira.jiraState === "has_tickets") {
|
|
2113
2372
|
if (key.upArrow || input === "k") nav.prev();
|
|
2114
2373
|
if (key.downArrow || input === "j") nav.next();
|
|
@@ -2126,11 +2385,13 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2126
2385
|
if (modal.type === "configure") {
|
|
2127
2386
|
const siteUrl = repo.repoPath ? getJiraSiteUrl(repo.repoPath) : void 0;
|
|
2128
2387
|
const creds = repo.repoPath ? getJiraCredentials(repo.repoPath) : { email: null, apiToken: null };
|
|
2388
|
+
const existingConfigs = getExistingJiraConfigs(repo.repoPath ?? void 0);
|
|
2129
2389
|
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
|
|
2130
2390
|
ConfigureJiraSiteModal,
|
|
2131
2391
|
{
|
|
2132
2392
|
initialSiteUrl: siteUrl ?? void 0,
|
|
2133
2393
|
initialEmail: creds.email ?? void 0,
|
|
2394
|
+
existingConfigs,
|
|
2134
2395
|
onSubmit: handleConfigureSubmit,
|
|
2135
2396
|
onCancel: () => {
|
|
2136
2397
|
modal.close();
|
|
@@ -2187,73 +2448,14 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2187
2448
|
}
|
|
2188
2449
|
|
|
2189
2450
|
// src/components/logs/LogsView.tsx
|
|
2190
|
-
import { useEffect as
|
|
2451
|
+
import { useEffect as useEffect12 } from "react";
|
|
2191
2452
|
import { Box as Box14, useInput as useInput12 } from "ink";
|
|
2192
2453
|
|
|
2193
|
-
// src/components/logs/
|
|
2454
|
+
// src/components/logs/LogViewerBox.tsx
|
|
2455
|
+
import { useRef as useRef7, useState as useState16 } from "react";
|
|
2194
2456
|
import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
|
|
2195
2457
|
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";
|
|
2458
|
+
import { ScrollView as ScrollView5 } from "ink-scroll-view";
|
|
2257
2459
|
import TextInput2 from "ink-text-input";
|
|
2258
2460
|
|
|
2259
2461
|
// src/lib/claude/api.ts
|
|
@@ -2340,18 +2542,18 @@ Generate the standup notes:`;
|
|
|
2340
2542
|
}
|
|
2341
2543
|
|
|
2342
2544
|
// src/components/logs/LogViewerBox.tsx
|
|
2343
|
-
import { jsx as
|
|
2545
|
+
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2344
2546
|
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 =
|
|
2547
|
+
const scrollRef = useRef7(null);
|
|
2548
|
+
const [isInputMode, setIsInputMode] = useState16(false);
|
|
2549
|
+
const [inputValue, setInputValue] = useState16("");
|
|
2550
|
+
const [isGeneratingStandup, setIsGeneratingStandup] = useState16(false);
|
|
2551
|
+
const [standupResult, setStandupResult] = useState16(null);
|
|
2552
|
+
const claudeProcessRef = useRef7(null);
|
|
2351
2553
|
const title = "[6] Log Content";
|
|
2352
2554
|
const borderColor = isFocused ? "yellow" : void 0;
|
|
2353
2555
|
const displayTitle = date ? `${title} - ${date}.md` : title;
|
|
2354
|
-
|
|
2556
|
+
useInput10(
|
|
2355
2557
|
(input, key) => {
|
|
2356
2558
|
var _a, _b, _c;
|
|
2357
2559
|
if (key.escape && isInputMode) {
|
|
@@ -2430,14 +2632,14 @@ ${value.trim()}
|
|
|
2430
2632
|
setIsInputMode(false);
|
|
2431
2633
|
onRefresh();
|
|
2432
2634
|
};
|
|
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__ */
|
|
2635
|
+
return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", flexGrow: 1, children: [
|
|
2636
|
+
/* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx12(ScrollView5, { ref: scrollRef, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
|
|
2637
|
+
!date && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Select a log file to view" }),
|
|
2638
|
+
date && content === null && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Log file not found" }),
|
|
2639
|
+
date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Empty log file" }),
|
|
2640
|
+
date && content && content.trim() !== "" && /* @__PURE__ */ jsx12(Markdown, { children: content })
|
|
2439
2641
|
] }) }) }) }),
|
|
2440
|
-
isInputMode && /* @__PURE__ */
|
|
2642
|
+
isInputMode && /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: ["Add Entry"], borderColor: "yellow", children: /* @__PURE__ */ jsx12(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx12(
|
|
2441
2643
|
TextInput2,
|
|
2442
2644
|
{
|
|
2443
2645
|
value: inputValue,
|
|
@@ -2445,30 +2647,84 @@ ${value.trim()}
|
|
|
2445
2647
|
onSubmit: handleInputSubmit
|
|
2446
2648
|
}
|
|
2447
2649
|
) }) }),
|
|
2448
|
-
isGeneratingStandup && /* @__PURE__ */
|
|
2449
|
-
/* @__PURE__ */
|
|
2450
|
-
/* @__PURE__ */
|
|
2650
|
+
isGeneratingStandup && /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: ["Standup Notes"], borderColor: "yellow", children: /* @__PURE__ */ jsxs12(Box12, { paddingX: 1, flexDirection: "column", children: [
|
|
2651
|
+
/* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "Generating standup notes..." }),
|
|
2652
|
+
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Press Esc to cancel" })
|
|
2451
2653
|
] }) }),
|
|
2452
|
-
standupResult && /* @__PURE__ */
|
|
2453
|
-
|
|
2654
|
+
standupResult && /* @__PURE__ */ jsx12(
|
|
2655
|
+
TitledBox5,
|
|
2454
2656
|
{
|
|
2455
2657
|
borderStyle: "round",
|
|
2456
2658
|
titles: ["Standup Notes"],
|
|
2457
2659
|
borderColor: standupResult.type === "error" ? "red" : "green",
|
|
2458
|
-
children: /* @__PURE__ */
|
|
2459
|
-
standupResult.type === "error" ? /* @__PURE__ */
|
|
2460
|
-
/* @__PURE__ */
|
|
2660
|
+
children: /* @__PURE__ */ jsxs12(Box12, { paddingX: 1, flexDirection: "column", children: [
|
|
2661
|
+
standupResult.type === "error" ? /* @__PURE__ */ jsx12(Text12, { color: "red", children: standupResult.message }) : /* @__PURE__ */ jsx12(Markdown, { children: standupResult.message }),
|
|
2662
|
+
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Press Esc to dismiss" })
|
|
2461
2663
|
] })
|
|
2462
2664
|
}
|
|
2463
2665
|
)
|
|
2464
2666
|
] });
|
|
2465
2667
|
}
|
|
2466
2668
|
|
|
2669
|
+
// src/components/logs/LogsHistoryBox.tsx
|
|
2670
|
+
import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
|
|
2671
|
+
import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
|
|
2672
|
+
import { ScrollView as ScrollView6 } from "ink-scroll-view";
|
|
2673
|
+
import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2674
|
+
function LogsHistoryBox({
|
|
2675
|
+
logFiles,
|
|
2676
|
+
selectedDate,
|
|
2677
|
+
highlightedIndex,
|
|
2678
|
+
onHighlight,
|
|
2679
|
+
onSelect,
|
|
2680
|
+
isFocused
|
|
2681
|
+
}) {
|
|
2682
|
+
const scrollRef = useScrollToIndex(highlightedIndex);
|
|
2683
|
+
const title = "[5] Logs";
|
|
2684
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
2685
|
+
useInput11(
|
|
2686
|
+
(input, key) => {
|
|
2687
|
+
if (logFiles.length === 0) return;
|
|
2688
|
+
if (key.upArrow || input === "k") {
|
|
2689
|
+
onHighlight(Math.max(0, highlightedIndex - 1));
|
|
2690
|
+
}
|
|
2691
|
+
if (key.downArrow || input === "j") {
|
|
2692
|
+
onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
|
|
2693
|
+
}
|
|
2694
|
+
if (key.return) {
|
|
2695
|
+
const file = logFiles[highlightedIndex];
|
|
2696
|
+
if (file) {
|
|
2697
|
+
onSelect(file.date);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
},
|
|
2701
|
+
{ isActive: isFocused }
|
|
2702
|
+
);
|
|
2703
|
+
return /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
|
|
2704
|
+
logFiles.length === 0 && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "No logs yet" }),
|
|
2705
|
+
logFiles.length > 0 && /* @__PURE__ */ jsx13(ScrollView6, { ref: scrollRef, children: logFiles.map((file, idx) => {
|
|
2706
|
+
const isHighlighted = idx === highlightedIndex;
|
|
2707
|
+
const isSelected = file.date === selectedDate;
|
|
2708
|
+
const cursor = isHighlighted ? ">" : " ";
|
|
2709
|
+
const indicator = isSelected ? " *" : "";
|
|
2710
|
+
return /* @__PURE__ */ jsxs13(Box13, { children: [
|
|
2711
|
+
/* @__PURE__ */ jsxs13(Text13, { color: isHighlighted ? "yellow" : void 0, children: [
|
|
2712
|
+
cursor,
|
|
2713
|
+
" "
|
|
2714
|
+
] }),
|
|
2715
|
+
/* @__PURE__ */ jsx13(Text13, { color: file.isToday ? "green" : void 0, bold: file.isToday, children: file.date }),
|
|
2716
|
+
file.isToday && /* @__PURE__ */ jsx13(Text13, { color: "green", children: " (today)" }),
|
|
2717
|
+
/* @__PURE__ */ jsx13(Text13, { dimColor: true, children: indicator })
|
|
2718
|
+
] }, file.date);
|
|
2719
|
+
}) })
|
|
2720
|
+
] }) });
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2467
2723
|
// src/components/logs/LogsView.tsx
|
|
2468
2724
|
import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2469
2725
|
function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
|
|
2470
2726
|
const logs = useLogs();
|
|
2471
|
-
|
|
2727
|
+
useEffect12(() => {
|
|
2472
2728
|
if (refreshKey !== void 0 && refreshKey > 0) {
|
|
2473
2729
|
logs.handleExternalLogUpdate();
|
|
2474
2730
|
}
|
|
@@ -2513,22 +2769,25 @@ var globalBindings = [
|
|
|
2513
2769
|
{ key: "j/k", label: "Navigate" },
|
|
2514
2770
|
{ key: "Ctrl+C", label: "Quit" }
|
|
2515
2771
|
];
|
|
2516
|
-
var modalBindings = [
|
|
2517
|
-
|
|
2518
|
-
]
|
|
2519
|
-
function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
|
|
2772
|
+
var modalBindings = [{ key: "Esc", label: "Cancel" }];
|
|
2773
|
+
var DUCK_ASCII = "<(')___";
|
|
2774
|
+
function KeybindingsBar({ contextBindings = [], modalOpen = false, duck }) {
|
|
2520
2775
|
const allBindings = modalOpen ? [...contextBindings, ...modalBindings] : [...contextBindings, ...globalBindings];
|
|
2521
|
-
return /* @__PURE__ */
|
|
2522
|
-
/* @__PURE__ */
|
|
2523
|
-
|
|
2524
|
-
|
|
2776
|
+
return /* @__PURE__ */ jsxs15(Box15, { flexShrink: 0, paddingX: 1, gap: 2, children: [
|
|
2777
|
+
allBindings.map((binding) => /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
|
|
2778
|
+
/* @__PURE__ */ jsx15(Text14, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
|
|
2779
|
+
/* @__PURE__ */ jsx15(Text14, { dimColor: true, children: binding.label })
|
|
2780
|
+
] }, binding.key)),
|
|
2781
|
+
(duck == null ? void 0 : duck.visible) && /* @__PURE__ */ jsxs15(Box15, { flexGrow: 1, justifyContent: "flex-end", gap: 1, children: [
|
|
2782
|
+
/* @__PURE__ */ jsx15(Text14, { children: DUCK_ASCII }),
|
|
2783
|
+
/* @__PURE__ */ jsx15(Text14, { dimColor: true, children: duck.message })
|
|
2784
|
+
] })
|
|
2785
|
+
] });
|
|
2525
2786
|
}
|
|
2526
2787
|
|
|
2527
2788
|
// src/constants/github.ts
|
|
2528
2789
|
var GITHUB_KEYBINDINGS = {
|
|
2529
|
-
remotes: [
|
|
2530
|
-
{ key: "Space", label: "Select Remote" }
|
|
2531
|
-
],
|
|
2790
|
+
remotes: [{ key: "Space", label: "Select Remote" }],
|
|
2532
2791
|
prs: [
|
|
2533
2792
|
{ key: "Space", label: "Select" },
|
|
2534
2793
|
{ key: "n", label: "New PR", color: "green" },
|
|
@@ -2545,21 +2804,23 @@ var GITHUB_KEYBINDINGS = {
|
|
|
2545
2804
|
// src/constants/jira.ts
|
|
2546
2805
|
var JIRA_KEYBINDINGS = {
|
|
2547
2806
|
not_configured: [{ key: "c", label: "Configure Jira" }],
|
|
2548
|
-
no_tickets: [
|
|
2807
|
+
no_tickets: [
|
|
2808
|
+
{ key: "l", label: "Link Ticket" },
|
|
2809
|
+
{ key: "r", label: "Remove Config", color: "red" }
|
|
2810
|
+
],
|
|
2549
2811
|
has_tickets: [
|
|
2550
2812
|
{ key: "l", label: "Link" },
|
|
2551
2813
|
{ key: "s", label: "Status" },
|
|
2552
2814
|
{ key: "d", label: "Unlink", color: "red" },
|
|
2553
2815
|
{ key: "o", label: "Open", color: "green" },
|
|
2554
|
-
{ key: "y", label: "Copy Link" }
|
|
2816
|
+
{ key: "y", label: "Copy Link" },
|
|
2817
|
+
{ key: "r", label: "Remove Config", color: "red" }
|
|
2555
2818
|
]
|
|
2556
2819
|
};
|
|
2557
2820
|
|
|
2558
2821
|
// src/constants/logs.ts
|
|
2559
2822
|
var LOGS_KEYBINDINGS = {
|
|
2560
|
-
history: [
|
|
2561
|
-
{ key: "Enter", label: "Select" }
|
|
2562
|
-
],
|
|
2823
|
+
history: [{ key: "Enter", label: "Select" }],
|
|
2563
2824
|
viewer: [
|
|
2564
2825
|
{ key: "i", label: "Add Entry" },
|
|
2565
2826
|
{ key: "e", label: "Edit" },
|
|
@@ -2588,12 +2849,13 @@ function computeKeybindings(focusedView, state) {
|
|
|
2588
2849
|
import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2589
2850
|
function App() {
|
|
2590
2851
|
const { exit } = useApp();
|
|
2591
|
-
const [focusedView, setFocusedView] =
|
|
2592
|
-
const [modalOpen, setModalOpen] =
|
|
2593
|
-
const [logRefreshKey, setLogRefreshKey] =
|
|
2594
|
-
const
|
|
2595
|
-
const [
|
|
2596
|
-
const [
|
|
2852
|
+
const [focusedView, setFocusedView] = useState17("github");
|
|
2853
|
+
const [modalOpen, setModalOpen] = useState17(false);
|
|
2854
|
+
const [logRefreshKey, setLogRefreshKey] = useState17(0);
|
|
2855
|
+
const duck = useRubberDuck();
|
|
2856
|
+
const [githubFocusedBox, setGithubFocusedBox] = useState17("remotes");
|
|
2857
|
+
const [jiraState, setJiraState] = useState17("not_configured");
|
|
2858
|
+
const [logsFocusedBox, setLogsFocusedBox] = useState17("history");
|
|
2597
2859
|
const keybindings = useMemo2(
|
|
2598
2860
|
() => computeKeybindings(focusedView, {
|
|
2599
2861
|
github: { focusedBox: githubFocusedBox },
|
|
@@ -2602,7 +2864,7 @@ function App() {
|
|
|
2602
2864
|
}),
|
|
2603
2865
|
[focusedView, githubFocusedBox, jiraState, modalOpen, logsFocusedBox]
|
|
2604
2866
|
);
|
|
2605
|
-
const handleLogUpdated =
|
|
2867
|
+
const handleLogUpdated = useCallback10(() => {
|
|
2606
2868
|
setLogRefreshKey((prev) => prev + 1);
|
|
2607
2869
|
}, []);
|
|
2608
2870
|
useInput13(
|
|
@@ -2624,6 +2886,12 @@ function App() {
|
|
|
2624
2886
|
setFocusedView("logs");
|
|
2625
2887
|
setLogsFocusedBox("viewer");
|
|
2626
2888
|
}
|
|
2889
|
+
if (input === "d") {
|
|
2890
|
+
duck.toggleDuck();
|
|
2891
|
+
}
|
|
2892
|
+
if (input === "q" && duck.visible) {
|
|
2893
|
+
duck.quack();
|
|
2894
|
+
}
|
|
2627
2895
|
},
|
|
2628
2896
|
{ isActive: !modalOpen }
|
|
2629
2897
|
);
|
|
@@ -2658,7 +2926,14 @@ function App() {
|
|
|
2658
2926
|
}
|
|
2659
2927
|
) })
|
|
2660
2928
|
] }),
|
|
2661
|
-
/* @__PURE__ */ jsx16(
|
|
2929
|
+
/* @__PURE__ */ jsx16(
|
|
2930
|
+
KeybindingsBar,
|
|
2931
|
+
{
|
|
2932
|
+
contextBindings: keybindings,
|
|
2933
|
+
modalOpen,
|
|
2934
|
+
duck: { visible: duck.visible, message: duck.message }
|
|
2935
|
+
}
|
|
2936
|
+
)
|
|
2662
2937
|
] });
|
|
2663
2938
|
}
|
|
2664
2939
|
|
|
@@ -2666,17 +2941,14 @@ function App() {
|
|
|
2666
2941
|
import { render as inkRender } from "ink";
|
|
2667
2942
|
|
|
2668
2943
|
// src/lib/Screen.tsx
|
|
2944
|
+
import { useCallback as useCallback11, useEffect as useEffect13, useState as useState18 } from "react";
|
|
2669
2945
|
import { Box as Box17, useStdout as useStdout2 } from "ink";
|
|
2670
|
-
import { useCallback as useCallback10, useEffect as useEffect11, useState as useState17 } from "react";
|
|
2671
2946
|
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
2672
2947
|
function Screen({ children }) {
|
|
2673
2948
|
const { stdout } = useStdout2();
|
|
2674
|
-
const getSize =
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
);
|
|
2678
|
-
const [size, setSize] = useState17(getSize);
|
|
2679
|
-
useEffect11(() => {
|
|
2949
|
+
const getSize = useCallback11(() => ({ height: stdout.rows, width: stdout.columns }), [stdout]);
|
|
2950
|
+
const [size, setSize] = useState18(getSize);
|
|
2951
|
+
useEffect13(() => {
|
|
2680
2952
|
const onResize = () => setSize(getSize());
|
|
2681
2953
|
stdout.on("resize", onResize);
|
|
2682
2954
|
return () => {
|