clairo 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1200 -786
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
import meow from "meow";
|
|
5
5
|
|
|
6
6
|
// src/app.tsx
|
|
7
|
-
import { useState as
|
|
8
|
-
import { Box as
|
|
7
|
+
import { useCallback as useCallback4, useState as useState9 } from "react";
|
|
8
|
+
import { Box as Box16, useApp, useInput as useInput13 } from "ink";
|
|
9
9
|
|
|
10
10
|
// src/components/github/GitHubView.tsx
|
|
11
11
|
import { exec as exec3 } from "child_process";
|
|
12
|
-
import { useCallback, useEffect as useEffect3, useState as useState3 } from "react";
|
|
12
|
+
import { useCallback, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
|
|
13
13
|
import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
|
|
14
14
|
import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
|
|
15
15
|
|
|
@@ -241,803 +241,965 @@ async function getPRDetails(prNumber, repo) {
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
// src/
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
import { Box as Box2, Text as Text2, useInput } from "ink";
|
|
249
|
-
import { ScrollView } from "ink-scroll-view";
|
|
250
|
-
|
|
251
|
-
// src/components/ui/Markdown.tsx
|
|
252
|
-
import { Box, Text } from "ink";
|
|
253
|
-
import Link from "ink-link";
|
|
254
|
-
import { marked } from "marked";
|
|
255
|
-
import Table from "cli-table3";
|
|
256
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
257
|
-
function Markdown({ children }) {
|
|
258
|
-
const tokens = marked.lexer(children);
|
|
259
|
-
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: tokens.map((token, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token }, idx)) });
|
|
244
|
+
// src/lib/jira/parser.ts
|
|
245
|
+
var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
246
|
+
function isValidTicketKeyFormat(key) {
|
|
247
|
+
return TICKET_KEY_PATTERN.test(key.toUpperCase());
|
|
260
248
|
}
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
});
|
|
271
|
-
if (hasLinks) {
|
|
272
|
-
return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
|
|
273
|
-
}
|
|
274
|
-
return /* @__PURE__ */ jsx(Text, { children: renderInline(token.tokens) });
|
|
275
|
-
}
|
|
276
|
-
case "code":
|
|
277
|
-
return /* @__PURE__ */ jsx(Box, { marginY: 1, paddingX: 1, borderStyle: "single", borderColor: "gray", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: token.text }) });
|
|
278
|
-
case "blockquote":
|
|
279
|
-
return /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
|
|
280
|
-
/* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2502 " }),
|
|
281
|
-
/* @__PURE__ */ jsx(Box, { flexDirection: "column", children: (_b = token.tokens) == null ? void 0 : _b.map((t, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, idx)) })
|
|
282
|
-
] });
|
|
283
|
-
case "list":
|
|
284
|
-
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: token.items.map((item, idx) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
285
|
-
/* @__PURE__ */ jsx(Text, { children: token.ordered ? `${idx + 1}. ` : "\u2022 " }),
|
|
286
|
-
/* @__PURE__ */ jsx(Box, { flexDirection: "column", children: item.tokens.map((t, i) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, i)) })
|
|
287
|
-
] }, idx)) });
|
|
288
|
-
case "table":
|
|
289
|
-
return /* @__PURE__ */ jsx(TableRenderer, { token });
|
|
290
|
-
case "hr":
|
|
291
|
-
return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(40) });
|
|
292
|
-
case "space":
|
|
293
|
-
return null;
|
|
294
|
-
default:
|
|
295
|
-
if ("text" in token && typeof token.text === "string") {
|
|
296
|
-
return /* @__PURE__ */ jsx(Text, { children: token.text });
|
|
297
|
-
}
|
|
298
|
-
return null;
|
|
249
|
+
function parseTicketKey(input) {
|
|
250
|
+
const trimmed = input.trim();
|
|
251
|
+
const urlMatch = trimmed.match(/\/browse\/([A-Za-z][A-Za-z0-9]+-\d+)/i);
|
|
252
|
+
if (urlMatch) {
|
|
253
|
+
return urlMatch[1].toUpperCase();
|
|
254
|
+
}
|
|
255
|
+
const upperInput = trimmed.toUpperCase();
|
|
256
|
+
if (isValidTicketKeyFormat(upperInput)) {
|
|
257
|
+
return upperInput;
|
|
299
258
|
}
|
|
259
|
+
return null;
|
|
300
260
|
}
|
|
301
|
-
function
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
261
|
+
function extractTicketKeyFromBranch(branchName) {
|
|
262
|
+
const match = branchName.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
|
|
263
|
+
if (match) {
|
|
264
|
+
const candidate = match[1].toUpperCase();
|
|
265
|
+
if (isValidTicketKeyFormat(candidate)) {
|
|
266
|
+
return candidate;
|
|
267
|
+
}
|
|
308
268
|
}
|
|
309
|
-
return
|
|
269
|
+
return null;
|
|
310
270
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
271
|
+
|
|
272
|
+
// src/lib/jira/config.ts
|
|
273
|
+
function isJiraConfigured(repoPath) {
|
|
274
|
+
const config = getRepoConfig(repoPath);
|
|
275
|
+
return !!(config.jiraSiteUrl && config.jiraEmail && config.jiraApiToken);
|
|
276
|
+
}
|
|
277
|
+
function getJiraSiteUrl(repoPath) {
|
|
278
|
+
const config = getRepoConfig(repoPath);
|
|
279
|
+
return config.jiraSiteUrl ?? null;
|
|
280
|
+
}
|
|
281
|
+
function setJiraSiteUrl(repoPath, siteUrl) {
|
|
282
|
+
updateRepoConfig(repoPath, { jiraSiteUrl: siteUrl });
|
|
283
|
+
}
|
|
284
|
+
function getJiraCredentials(repoPath) {
|
|
285
|
+
const config = getRepoConfig(repoPath);
|
|
286
|
+
return {
|
|
287
|
+
email: config.jiraEmail ?? null,
|
|
288
|
+
apiToken: config.jiraApiToken ?? null
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function setJiraCredentials(repoPath, email, apiToken) {
|
|
292
|
+
updateRepoConfig(repoPath, { jiraEmail: email, jiraApiToken: apiToken });
|
|
293
|
+
}
|
|
294
|
+
function getLinkedTickets(repoPath, branch) {
|
|
295
|
+
var _a;
|
|
296
|
+
const config = getRepoConfig(repoPath);
|
|
297
|
+
return ((_a = config.branchTickets) == null ? void 0 : _a[branch]) ?? [];
|
|
298
|
+
}
|
|
299
|
+
function addLinkedTicket(repoPath, branch, ticket) {
|
|
300
|
+
const config = getRepoConfig(repoPath);
|
|
301
|
+
const branchTickets = config.branchTickets ?? {};
|
|
302
|
+
const tickets = branchTickets[branch] ?? [];
|
|
303
|
+
if (tickets.some((t) => t.key === ticket.key)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
updateRepoConfig(repoPath, {
|
|
307
|
+
branchTickets: {
|
|
308
|
+
...branchTickets,
|
|
309
|
+
[branch]: [...tickets, ticket]
|
|
344
310
|
}
|
|
345
311
|
});
|
|
346
312
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
313
|
+
function removeLinkedTicket(repoPath, branch, ticketKey) {
|
|
314
|
+
const config = getRepoConfig(repoPath);
|
|
315
|
+
const branchTickets = config.branchTickets ?? {};
|
|
316
|
+
const tickets = branchTickets[branch] ?? [];
|
|
317
|
+
updateRepoConfig(repoPath, {
|
|
318
|
+
branchTickets: {
|
|
319
|
+
...branchTickets,
|
|
320
|
+
[branch]: tickets.filter((t) => t.key !== ticketKey)
|
|
352
321
|
}
|
|
353
|
-
|
|
354
|
-
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function updateTicketStatus(repoPath, branch, ticketKey, newStatus) {
|
|
325
|
+
const config = getRepoConfig(repoPath);
|
|
326
|
+
const branchTickets = config.branchTickets ?? {};
|
|
327
|
+
const tickets = branchTickets[branch] ?? [];
|
|
328
|
+
updateRepoConfig(repoPath, {
|
|
329
|
+
branchTickets: {
|
|
330
|
+
...branchTickets,
|
|
331
|
+
[branch]: tickets.map((t) => t.key === ticketKey ? { ...t, status: newStatus } : t)
|
|
355
332
|
}
|
|
356
|
-
|
|
357
|
-
}).join("");
|
|
333
|
+
});
|
|
358
334
|
}
|
|
359
335
|
|
|
360
|
-
// src/
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (conclusion === "SUCCESS") return "green";
|
|
365
|
-
if (conclusion === "FAILURE" || conclusion === "ERROR") return "red";
|
|
366
|
-
if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "gray";
|
|
367
|
-
if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
|
|
368
|
-
return "yellow";
|
|
369
|
-
if (check.status === "COMPLETED") return "green";
|
|
370
|
-
return void 0;
|
|
336
|
+
// src/lib/jira/api.ts
|
|
337
|
+
function createAuthHeader(email, apiToken) {
|
|
338
|
+
const credentials = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
339
|
+
return `Basic ${credentials}`;
|
|
371
340
|
}
|
|
372
|
-
function
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
341
|
+
async function jiraFetch(auth, endpoint, options) {
|
|
342
|
+
const url = `${auth.siteUrl}/rest/api/3${endpoint}`;
|
|
343
|
+
const method = (options == null ? void 0 : options.method) ?? "GET";
|
|
344
|
+
try {
|
|
345
|
+
const headers = {
|
|
346
|
+
Authorization: createAuthHeader(auth.email, auth.apiToken),
|
|
347
|
+
Accept: "application/json"
|
|
348
|
+
};
|
|
349
|
+
const fetchOptions = { method, headers };
|
|
350
|
+
if (options == null ? void 0 : options.body) {
|
|
351
|
+
headers["Content-Type"] = "application/json";
|
|
352
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
353
|
+
}
|
|
354
|
+
const response = await fetch(url, fetchOptions);
|
|
355
|
+
if (!response.ok) {
|
|
356
|
+
const text = await response.text();
|
|
357
|
+
return { ok: false, status: response.status, error: text };
|
|
358
|
+
}
|
|
359
|
+
if (response.status === 204) {
|
|
360
|
+
return { ok: true, status: response.status, data: null };
|
|
361
|
+
}
|
|
362
|
+
const data = await response.json();
|
|
363
|
+
return { ok: true, status: response.status, data };
|
|
364
|
+
} catch (err) {
|
|
365
|
+
const message = err instanceof Error ? err.message : "Network error";
|
|
366
|
+
return { ok: false, status: 0, error: message };
|
|
367
|
+
}
|
|
381
368
|
}
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
369
|
+
async function validateCredentials(auth) {
|
|
370
|
+
const result = await jiraFetch(auth, "/myself");
|
|
371
|
+
if (!result.ok) {
|
|
372
|
+
if (result.status === 401 || result.status === 403) {
|
|
373
|
+
return {
|
|
374
|
+
success: false,
|
|
375
|
+
error: "Invalid credentials. Check your email and API token.",
|
|
376
|
+
errorType: "auth_error"
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
success: false,
|
|
381
|
+
error: result.error ?? "Failed to connect to Jira",
|
|
382
|
+
errorType: "api_error"
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return { success: true, data: result.data };
|
|
386
|
+
}
|
|
387
|
+
async function getIssue(auth, ticketKey) {
|
|
388
|
+
const result = await jiraFetch(auth, `/issue/${ticketKey}?fields=summary,status`);
|
|
389
|
+
if (!result.ok) {
|
|
390
|
+
if (result.status === 401 || result.status === 403) {
|
|
391
|
+
return {
|
|
392
|
+
success: false,
|
|
393
|
+
error: "Authentication failed",
|
|
394
|
+
errorType: "auth_error"
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (result.status === 404) {
|
|
398
|
+
return {
|
|
399
|
+
success: false,
|
|
400
|
+
error: `Ticket ${ticketKey} not found`,
|
|
401
|
+
errorType: "invalid_ticket"
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
success: false,
|
|
406
|
+
error: result.error ?? "Failed to fetch issue",
|
|
407
|
+
errorType: "api_error"
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return { success: true, data: result.data };
|
|
411
|
+
}
|
|
412
|
+
async function getTransitions(auth, ticketKey) {
|
|
413
|
+
const result = await jiraFetch(auth, `/issue/${ticketKey}/transitions`);
|
|
414
|
+
if (!result.ok) {
|
|
415
|
+
if (result.status === 401 || result.status === 403) {
|
|
416
|
+
return {
|
|
417
|
+
success: false,
|
|
418
|
+
error: "Authentication failed",
|
|
419
|
+
errorType: "auth_error"
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (result.status === 404) {
|
|
423
|
+
return {
|
|
424
|
+
success: false,
|
|
425
|
+
error: `Ticket ${ticketKey} not found`,
|
|
426
|
+
errorType: "invalid_ticket"
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
success: false,
|
|
431
|
+
error: result.error ?? "Failed to fetch transitions",
|
|
432
|
+
errorType: "api_error"
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const data = result.data;
|
|
436
|
+
return { success: true, data: data.transitions };
|
|
437
|
+
}
|
|
438
|
+
async function applyTransition(auth, ticketKey, transitionId) {
|
|
439
|
+
const result = await jiraFetch(auth, `/issue/${ticketKey}/transitions`, {
|
|
440
|
+
method: "POST",
|
|
441
|
+
body: { transition: { id: transitionId } }
|
|
442
|
+
});
|
|
443
|
+
if (!result.ok) {
|
|
444
|
+
if (result.status === 401 || result.status === 403) {
|
|
445
|
+
return {
|
|
446
|
+
success: false,
|
|
447
|
+
error: "Authentication failed",
|
|
448
|
+
errorType: "auth_error"
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (result.status === 404) {
|
|
452
|
+
return {
|
|
453
|
+
success: false,
|
|
454
|
+
error: `Ticket ${ticketKey} not found`,
|
|
455
|
+
errorType: "invalid_ticket"
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
success: false,
|
|
460
|
+
error: result.error ?? "Failed to apply transition",
|
|
461
|
+
errorType: "api_error"
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return { success: true, data: null };
|
|
471
465
|
}
|
|
472
466
|
|
|
473
|
-
// src/
|
|
474
|
-
import {
|
|
475
|
-
import {
|
|
476
|
-
import {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
467
|
+
// src/lib/logs/index.ts
|
|
468
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, appendFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
469
|
+
import { homedir as homedir2 } from "os";
|
|
470
|
+
import { join as join2 } from "path";
|
|
471
|
+
import { spawnSync } from "child_process";
|
|
472
|
+
var LOGS_DIRECTORY = join2(homedir2(), ".clairo", "logs");
|
|
473
|
+
function ensureLogsDirectory() {
|
|
474
|
+
if (!existsSync2(LOGS_DIRECTORY)) {
|
|
475
|
+
mkdirSync2(LOGS_DIRECTORY, { recursive: true });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function getTodayDate() {
|
|
479
|
+
const now = /* @__PURE__ */ new Date();
|
|
480
|
+
const year = now.getFullYear();
|
|
481
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
482
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
483
|
+
return `${year}-${month}-${day}`;
|
|
484
|
+
}
|
|
485
|
+
function formatTimestamp() {
|
|
486
|
+
const now = /* @__PURE__ */ new Date();
|
|
487
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
488
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
489
|
+
return `${hours}:${minutes}`;
|
|
490
|
+
}
|
|
491
|
+
function listLogFiles() {
|
|
492
|
+
ensureLogsDirectory();
|
|
483
493
|
try {
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
+
const files = readdirSync(LOGS_DIRECTORY);
|
|
495
|
+
const today = getTodayDate();
|
|
496
|
+
const logFiles = files.filter((file) => /^\d{4}-\d{2}-\d{2}\.md$/.test(file)).map((file) => {
|
|
497
|
+
const date = file.replace(".md", "");
|
|
498
|
+
return {
|
|
499
|
+
date,
|
|
500
|
+
filename: file,
|
|
501
|
+
isToday: date === today
|
|
502
|
+
};
|
|
503
|
+
}).sort((a, b) => b.date.localeCompare(a.date));
|
|
504
|
+
return logFiles;
|
|
494
505
|
} catch {
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function readLog(date) {
|
|
510
|
+
const filePath = join2(LOGS_DIRECTORY, `${date}.md`);
|
|
511
|
+
try {
|
|
512
|
+
if (!existsSync2(filePath)) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
return readFileSync2(filePath, "utf-8");
|
|
516
|
+
} catch {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function getLogFilePath(date) {
|
|
521
|
+
return join2(LOGS_DIRECTORY, `${date}.md`);
|
|
522
|
+
}
|
|
523
|
+
function logExists(date) {
|
|
524
|
+
return existsSync2(getLogFilePath(date));
|
|
525
|
+
}
|
|
526
|
+
function createEmptyLog(date) {
|
|
527
|
+
ensureLogsDirectory();
|
|
528
|
+
const filePath = getLogFilePath(date);
|
|
529
|
+
if (existsSync2(filePath)) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const header = `# Log - ${date}
|
|
533
|
+
`;
|
|
534
|
+
writeFileSync2(filePath, header);
|
|
535
|
+
}
|
|
536
|
+
function appendToLog(date, entry) {
|
|
537
|
+
ensureLogsDirectory();
|
|
538
|
+
const filePath = getLogFilePath(date);
|
|
539
|
+
if (!existsSync2(filePath)) {
|
|
540
|
+
const header = `# Log - ${date}
|
|
541
|
+
|
|
542
|
+
`;
|
|
543
|
+
writeFileSync2(filePath, header);
|
|
544
|
+
}
|
|
545
|
+
appendFileSync(filePath, entry);
|
|
546
|
+
}
|
|
547
|
+
function openLogInEditor(date) {
|
|
548
|
+
const filePath = getLogFilePath(date);
|
|
549
|
+
if (!existsSync2(filePath)) {
|
|
495
550
|
return false;
|
|
496
551
|
}
|
|
552
|
+
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
553
|
+
const result = spawnSync(editor, [filePath], {
|
|
554
|
+
stdio: "inherit"
|
|
555
|
+
});
|
|
556
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
557
|
+
process.stdout.emit("resize");
|
|
558
|
+
return result.status === 0;
|
|
497
559
|
}
|
|
498
560
|
|
|
499
|
-
// src/
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const totalItems = prs.length + 1;
|
|
514
|
-
useEffect(() => {
|
|
515
|
-
const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
|
|
516
|
-
if (idx >= 0) setHighlightedIndex(idx);
|
|
517
|
-
}, [selectedPR, prs]);
|
|
518
|
-
useInput2(
|
|
519
|
-
(input, key) => {
|
|
520
|
-
if (!isFocused) return;
|
|
521
|
-
if (key.upArrow || input === "k") {
|
|
522
|
-
setHighlightedIndex((prev) => Math.max(0, prev - 1));
|
|
523
|
-
}
|
|
524
|
-
if (key.downArrow || input === "j") {
|
|
525
|
-
setHighlightedIndex((prev) => Math.min(totalItems - 1, prev + 1));
|
|
526
|
-
}
|
|
527
|
-
if (key.return) {
|
|
528
|
-
if (highlightedIndex === prs.length) {
|
|
529
|
-
onCreatePR();
|
|
530
|
-
} else if (prs[highlightedIndex]) {
|
|
531
|
-
onSelect(prs[highlightedIndex]);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
if (input === "y" && repoSlug && prs[highlightedIndex]) {
|
|
535
|
-
const pr = prs[highlightedIndex];
|
|
536
|
-
const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
|
|
537
|
-
copyToClipboard(url);
|
|
538
|
-
}
|
|
539
|
-
},
|
|
540
|
-
{ isActive: isFocused }
|
|
541
|
-
);
|
|
542
|
-
const title = "[2] Pull Requests";
|
|
543
|
-
const subtitle = branch ? ` (${branch})` : "";
|
|
544
|
-
const borderColor = isFocused ? "yellow" : void 0;
|
|
545
|
-
return /* @__PURE__ */ jsx3(TitledBox2, { borderStyle: "round", titles: [`${title}${subtitle}`], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
|
|
546
|
-
loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
|
|
547
|
-
error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
|
|
548
|
-
!loading && !error && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
549
|
-
prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }),
|
|
550
|
-
prs.map((pr, idx) => {
|
|
551
|
-
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
552
|
-
const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
|
|
553
|
-
const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
|
|
554
|
-
return /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
|
|
555
|
-
prefix,
|
|
556
|
-
"#",
|
|
557
|
-
pr.number,
|
|
558
|
-
" ",
|
|
559
|
-
pr.isDraft ? "[Draft] " : "",
|
|
560
|
-
pr.title
|
|
561
|
-
] }, pr.number);
|
|
562
|
-
}),
|
|
563
|
-
/* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
|
|
564
|
-
isFocused && highlightedIndex === prs.length ? "> " : " ",
|
|
565
|
-
"+ Create new PR"
|
|
566
|
-
] })
|
|
567
|
-
] })
|
|
568
|
-
] }) });
|
|
561
|
+
// src/lib/logs/logger.ts
|
|
562
|
+
function logPRCreated(prNumber, title, jiraTickets) {
|
|
563
|
+
const timestamp = formatTimestamp();
|
|
564
|
+
const today = getTodayDate();
|
|
565
|
+
let entry = `## ${timestamp} - Created PR #${prNumber}
|
|
566
|
+
|
|
567
|
+
${title}
|
|
568
|
+
`;
|
|
569
|
+
if (jiraTickets.length > 0) {
|
|
570
|
+
entry += `Jira: ${jiraTickets.join(", ")}
|
|
571
|
+
`;
|
|
572
|
+
}
|
|
573
|
+
entry += "\n";
|
|
574
|
+
appendToLog(today, entry);
|
|
569
575
|
}
|
|
576
|
+
function logJiraStatusChanged(ticketKey, oldStatus, newStatus) {
|
|
577
|
+
const timestamp = formatTimestamp();
|
|
578
|
+
const today = getTodayDate();
|
|
579
|
+
const entry = `## ${timestamp} - Updated Jira ticket
|
|
570
580
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
576
|
-
function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
|
|
577
|
-
const [highlightedIndex, setHighlightedIndex] = useState2(0);
|
|
578
|
-
useEffect2(() => {
|
|
579
|
-
const idx = remotes.findIndex((r) => r.name === selectedRemote);
|
|
580
|
-
if (idx >= 0) setHighlightedIndex(idx);
|
|
581
|
-
}, [selectedRemote, remotes]);
|
|
582
|
-
useInput3(
|
|
583
|
-
(input, key) => {
|
|
584
|
-
if (!isFocused || remotes.length === 0) return;
|
|
585
|
-
if (key.upArrow || input === "k") {
|
|
586
|
-
setHighlightedIndex((prev) => Math.max(0, prev - 1));
|
|
587
|
-
}
|
|
588
|
-
if (key.downArrow || input === "j") {
|
|
589
|
-
setHighlightedIndex((prev) => Math.min(remotes.length - 1, prev + 1));
|
|
590
|
-
}
|
|
591
|
-
if (key.return) {
|
|
592
|
-
onSelect(remotes[highlightedIndex].name);
|
|
593
|
-
}
|
|
594
|
-
},
|
|
595
|
-
{ isActive: isFocused }
|
|
596
|
-
);
|
|
597
|
-
const title = "[1] Remotes";
|
|
598
|
-
const borderColor = isFocused ? "yellow" : void 0;
|
|
599
|
-
return /* @__PURE__ */ jsx4(TitledBox3, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
|
|
600
|
-
loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
|
|
601
|
-
error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
|
|
602
|
-
!loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
|
|
603
|
-
!loading && !error && remotes.map((remote, idx) => {
|
|
604
|
-
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
605
|
-
const isSelected = remote.name === selectedRemote;
|
|
606
|
-
const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
|
|
607
|
-
return /* @__PURE__ */ jsxs4(Text4, { color: isSelected ? "green" : void 0, children: [
|
|
608
|
-
prefix,
|
|
609
|
-
remote.name,
|
|
610
|
-
" (",
|
|
611
|
-
remote.url,
|
|
612
|
-
")"
|
|
613
|
-
] }, remote.name);
|
|
614
|
-
})
|
|
615
|
-
] }) });
|
|
581
|
+
${ticketKey}: ${oldStatus} \u2192 ${newStatus}
|
|
582
|
+
|
|
583
|
+
`;
|
|
584
|
+
appendToLog(today, entry);
|
|
616
585
|
}
|
|
617
586
|
|
|
618
|
-
// src/components/github/
|
|
619
|
-
import {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
bindings.push({ key: "r", label: "Refresh" });
|
|
648
|
-
bindings.push({ key: "o", label: "Open", color: "green" });
|
|
649
|
-
bindings.push({ key: "y", label: "Copy Link" });
|
|
650
|
-
} else if (focusedBox === "details") {
|
|
651
|
-
bindings.push({ key: "r", label: "Refresh" });
|
|
652
|
-
bindings.push({ key: "o", label: "Open", color: "green" });
|
|
653
|
-
}
|
|
654
|
-
onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
|
|
655
|
-
}, [isFocused, focusedBox, onKeybindingsChange]);
|
|
656
|
-
useEffect3(() => {
|
|
657
|
-
const gitRepoCheck = isGitRepo();
|
|
658
|
-
setIsRepo(gitRepoCheck);
|
|
659
|
-
if (!gitRepoCheck) {
|
|
660
|
-
setLoading((prev) => ({ ...prev, remotes: false }));
|
|
661
|
-
setErrors((prev) => ({ ...prev, remotes: "Not a git repository" }));
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
const rootResult = getRepoRoot();
|
|
665
|
-
if (rootResult.success) {
|
|
666
|
-
setRepoPath(rootResult.data);
|
|
667
|
-
}
|
|
668
|
-
const branchResult = getCurrentBranch();
|
|
669
|
-
if (branchResult.success) {
|
|
670
|
-
setCurrentBranch(branchResult.data);
|
|
671
|
-
}
|
|
672
|
-
const remotesResult = listRemotes();
|
|
673
|
-
if (remotesResult.success) {
|
|
674
|
-
setRemotes(remotesResult.data);
|
|
675
|
-
const remoteNames = remotesResult.data.map((r) => r.name);
|
|
676
|
-
const defaultRemote = getSelectedRemote(rootResult.success ? rootResult.data : "", remoteNames);
|
|
677
|
-
setSelectedRemote(defaultRemote);
|
|
678
|
-
} else {
|
|
679
|
-
setErrors((prev) => ({ ...prev, remotes: remotesResult.error }));
|
|
680
|
-
}
|
|
681
|
-
setLoading((prev) => ({ ...prev, remotes: false }));
|
|
682
|
-
}, []);
|
|
683
|
-
const refreshPRs = useCallback(async () => {
|
|
684
|
-
if (!currentBranch || !currentRepoSlug) return;
|
|
685
|
-
setLoading((prev) => ({ ...prev, prs: true }));
|
|
686
|
-
try {
|
|
687
|
-
const result = await listPRsForBranch(currentBranch, currentRepoSlug);
|
|
688
|
-
if (result.success) {
|
|
689
|
-
setPrs(result.data);
|
|
690
|
-
if (result.data.length > 0) {
|
|
691
|
-
setSelectedPR((prev) => prev ?? result.data[0]);
|
|
692
|
-
}
|
|
693
|
-
setErrors((prev) => ({ ...prev, prs: void 0 }));
|
|
694
|
-
} else {
|
|
695
|
-
setErrors((prev) => ({ ...prev, prs: result.error }));
|
|
587
|
+
// src/components/github/PRDetailsBox.tsx
|
|
588
|
+
import { useRef } from "react";
|
|
589
|
+
import open from "open";
|
|
590
|
+
import { TitledBox } from "@mishieck/ink-titled-box";
|
|
591
|
+
import { Box as Box2, Text as Text2, useInput } from "ink";
|
|
592
|
+
import { ScrollView } from "ink-scroll-view";
|
|
593
|
+
|
|
594
|
+
// src/components/ui/Markdown.tsx
|
|
595
|
+
import { Box, Text } from "ink";
|
|
596
|
+
import Link from "ink-link";
|
|
597
|
+
import { marked } from "marked";
|
|
598
|
+
import Table from "cli-table3";
|
|
599
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
600
|
+
function Markdown({ children }) {
|
|
601
|
+
const tokens = marked.lexer(children);
|
|
602
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: tokens.map((token, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token }, idx)) });
|
|
603
|
+
}
|
|
604
|
+
function TokenRenderer({ token }) {
|
|
605
|
+
var _a, _b;
|
|
606
|
+
switch (token.type) {
|
|
607
|
+
case "heading":
|
|
608
|
+
return /* @__PURE__ */ jsx(Box, { marginTop: token.depth === 1 ? 0 : 1, children: /* @__PURE__ */ jsx(Text, { bold: true, underline: token.depth === 1, children: renderInline(token.tokens) }) });
|
|
609
|
+
case "paragraph": {
|
|
610
|
+
const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some((t) => {
|
|
611
|
+
var _a2;
|
|
612
|
+
return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
|
|
613
|
+
});
|
|
614
|
+
if (hasLinks) {
|
|
615
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
|
|
696
616
|
}
|
|
697
|
-
|
|
698
|
-
setErrors((prev) => ({ ...prev, prs: String(err) }));
|
|
699
|
-
} finally {
|
|
700
|
-
setLoading((prev) => ({ ...prev, prs: false }));
|
|
617
|
+
return /* @__PURE__ */ jsx(Text, { children: renderInline(token.tokens) });
|
|
701
618
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
619
|
+
case "code":
|
|
620
|
+
return /* @__PURE__ */ jsx(Box, { marginY: 1, paddingX: 1, borderStyle: "single", borderColor: "gray", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: token.text }) });
|
|
621
|
+
case "blockquote":
|
|
622
|
+
return /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
|
|
623
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2502 " }),
|
|
624
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", children: (_b = token.tokens) == null ? void 0 : _b.map((t, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, idx)) })
|
|
625
|
+
] });
|
|
626
|
+
case "list":
|
|
627
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: token.items.map((item, idx) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
628
|
+
/* @__PURE__ */ jsx(Text, { children: token.ordered ? `${idx + 1}. ` : "\u2022 " }),
|
|
629
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", children: item.tokens.map((t, i) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, i)) })
|
|
630
|
+
] }, idx)) });
|
|
631
|
+
case "table":
|
|
632
|
+
return /* @__PURE__ */ jsx(TableRenderer, { token });
|
|
633
|
+
case "hr":
|
|
634
|
+
return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(40) });
|
|
635
|
+
case "space":
|
|
636
|
+
return null;
|
|
637
|
+
default:
|
|
638
|
+
if ("text" in token && typeof token.text === "string") {
|
|
639
|
+
return /* @__PURE__ */ jsx(Text, { children: token.text });
|
|
713
640
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function TableRenderer({ token }) {
|
|
645
|
+
const table = new Table({
|
|
646
|
+
head: token.header.map((cell) => renderInlineToString(cell.tokens)),
|
|
647
|
+
style: { head: ["cyan"], border: ["gray"] }
|
|
648
|
+
});
|
|
649
|
+
for (const row of token.rows) {
|
|
650
|
+
table.push(row.map((cell) => renderInlineToString(cell.tokens)));
|
|
651
|
+
}
|
|
652
|
+
return /* @__PURE__ */ jsx(Text, { children: table.toString() });
|
|
653
|
+
}
|
|
654
|
+
function renderInline(tokens) {
|
|
655
|
+
if (!tokens) return null;
|
|
656
|
+
return tokens.map((token, idx) => {
|
|
657
|
+
switch (token.type) {
|
|
658
|
+
case "text":
|
|
659
|
+
return /* @__PURE__ */ jsx(Text, { children: token.text }, idx);
|
|
660
|
+
case "strong":
|
|
661
|
+
return /* @__PURE__ */ jsx(Text, { bold: true, children: renderInline(token.tokens) }, idx);
|
|
662
|
+
case "em":
|
|
663
|
+
return /* @__PURE__ */ jsx(Text, { italic: true, children: renderInline(token.tokens) }, idx);
|
|
664
|
+
case "codespan":
|
|
665
|
+
return /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
666
|
+
"`",
|
|
667
|
+
token.text,
|
|
668
|
+
"`"
|
|
669
|
+
] }, idx);
|
|
670
|
+
case "link":
|
|
671
|
+
return /* @__PURE__ */ jsx(Link, { url: token.href, children: /* @__PURE__ */ jsx(Text, { color: "blue", children: renderInlineToString(token.tokens) }) }, idx);
|
|
672
|
+
case "image":
|
|
673
|
+
return /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
|
|
674
|
+
"[Image: ",
|
|
675
|
+
token.text || token.href,
|
|
676
|
+
"]"
|
|
677
|
+
] }, idx);
|
|
678
|
+
case "br":
|
|
679
|
+
return /* @__PURE__ */ jsx(Text, { children: "\n" }, idx);
|
|
680
|
+
case "del":
|
|
681
|
+
return /* @__PURE__ */ jsx(Text, { strikethrough: true, children: renderInline(token.tokens) }, idx);
|
|
682
|
+
default:
|
|
683
|
+
if ("text" in token && typeof token.text === "string") {
|
|
684
|
+
return /* @__PURE__ */ jsx(Text, { children: token.text }, idx);
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
718
687
|
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
setCurrentRepoSlug(repo);
|
|
727
|
-
setPrs([]);
|
|
728
|
-
setSelectedPR(null);
|
|
729
|
-
}, [selectedRemote, currentBranch, remotes]);
|
|
730
|
-
useEffect3(() => {
|
|
731
|
-
if (currentRepoSlug && currentBranch) {
|
|
732
|
-
refreshPRs();
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
function renderInlineToString(tokens) {
|
|
691
|
+
if (!tokens) return "";
|
|
692
|
+
return tokens.map((token) => {
|
|
693
|
+
if ("text" in token && typeof token.text === "string") {
|
|
694
|
+
return token.text;
|
|
733
695
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if (!selectedPR || !currentRepoSlug) {
|
|
737
|
-
setPrDetails(null);
|
|
738
|
-
return;
|
|
696
|
+
if ("tokens" in token && Array.isArray(token.tokens)) {
|
|
697
|
+
return renderInlineToString(token.tokens);
|
|
739
698
|
}
|
|
740
|
-
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
699
|
+
return "";
|
|
700
|
+
}).join("");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/components/github/PRDetailsBox.tsx
|
|
704
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
705
|
+
function getCheckColor(check) {
|
|
706
|
+
const conclusion = check.conclusion ?? check.state;
|
|
707
|
+
if (conclusion === "SUCCESS") return "green";
|
|
708
|
+
if (conclusion === "FAILURE" || conclusion === "ERROR") return "red";
|
|
709
|
+
if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "gray";
|
|
710
|
+
if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
|
|
711
|
+
return "yellow";
|
|
712
|
+
if (check.status === "COMPLETED") return "green";
|
|
713
|
+
return void 0;
|
|
714
|
+
}
|
|
715
|
+
function getCheckIcon(check) {
|
|
716
|
+
const conclusion = check.conclusion ?? check.state;
|
|
717
|
+
if (conclusion === "SUCCESS") return "\u2713";
|
|
718
|
+
if (conclusion === "FAILURE" || conclusion === "ERROR") return "\u2717";
|
|
719
|
+
if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "\u25CB";
|
|
720
|
+
if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
|
|
721
|
+
return "\u25CF";
|
|
722
|
+
if (check.status === "COMPLETED") return "\u2713";
|
|
723
|
+
return "?";
|
|
724
|
+
}
|
|
725
|
+
function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
726
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
727
|
+
const scrollRef = useRef(null);
|
|
728
|
+
const title = "[3] PR Details";
|
|
729
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
730
|
+
const displayTitle = pr ? `${title} - #${pr.number}` : title;
|
|
731
|
+
const reviewStatus = (pr == null ? void 0 : pr.reviewDecision) ?? "PENDING";
|
|
732
|
+
const reviewColor = reviewStatus === "APPROVED" ? "green" : reviewStatus === "CHANGES_REQUESTED" ? "red" : "yellow";
|
|
733
|
+
const getMergeDisplay = () => {
|
|
734
|
+
if (!pr) return { text: "UNKNOWN", color: "yellow" };
|
|
735
|
+
if (pr.state === "MERGED") return { text: "MERGED", color: "magenta" };
|
|
736
|
+
if (pr.state === "CLOSED") return { text: "CLOSED", color: "red" };
|
|
737
|
+
if (pr.mergeable === "MERGEABLE") return { text: "MERGEABLE", color: "green" };
|
|
738
|
+
if (pr.mergeable === "CONFLICTING") return { text: "CONFLICTING", color: "red" };
|
|
739
|
+
return { text: pr.mergeable ?? "UNKNOWN", color: "yellow" };
|
|
740
|
+
};
|
|
741
|
+
const mergeDisplay = getMergeDisplay();
|
|
742
|
+
useInput(
|
|
743
|
+
(input, key) => {
|
|
744
|
+
var _a2, _b2;
|
|
745
|
+
if (key.upArrow || input === "k") {
|
|
746
|
+
(_a2 = scrollRef.current) == null ? void 0 : _a2.scrollBy(-1);
|
|
747
747
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const handleCreatePR = useCallback(() => {
|
|
755
|
-
exec3("gh pr create --web", () => {
|
|
756
|
-
process.stdout.emit("resize");
|
|
757
|
-
});
|
|
758
|
-
}, []);
|
|
759
|
-
useInput4(
|
|
760
|
-
(input) => {
|
|
761
|
-
if (input === "1") setFocusedBox("remotes");
|
|
762
|
-
if (input === "2") setFocusedBox("prs");
|
|
763
|
-
if (input === "3") setFocusedBox("details");
|
|
764
|
-
if (input === "r") {
|
|
765
|
-
if (focusedBox === "prs") refreshPRs();
|
|
766
|
-
if (focusedBox === "details") refreshDetails();
|
|
748
|
+
if (key.downArrow || input === "j") {
|
|
749
|
+
(_b2 = scrollRef.current) == null ? void 0 : _b2.scrollBy(1);
|
|
750
|
+
}
|
|
751
|
+
if (input === "o" && (pr == null ? void 0 : pr.url)) {
|
|
752
|
+
open(pr.url).catch(() => {
|
|
753
|
+
});
|
|
767
754
|
}
|
|
768
755
|
},
|
|
769
756
|
{ isActive: isFocused }
|
|
770
757
|
);
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
/* @__PURE__ */
|
|
776
|
-
|
|
777
|
-
{
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
|
|
758
|
+
return /* @__PURE__ */ jsx2(TitledBox, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx2(ScrollView, { ref: scrollRef, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
|
|
759
|
+
loading && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Loading details..." }),
|
|
760
|
+
error && /* @__PURE__ */ jsx2(Text2, { color: "red", children: error }),
|
|
761
|
+
!loading && !error && !pr && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Select a PR to view details" }),
|
|
762
|
+
!loading && !error && pr && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
763
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: pr.title }),
|
|
764
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
765
|
+
"by ",
|
|
766
|
+
((_a = pr.author) == null ? void 0 : _a.login) ?? "unknown",
|
|
767
|
+
" | ",
|
|
768
|
+
((_b = pr.commits) == null ? void 0 : _b.length) ?? 0,
|
|
769
|
+
" commits"
|
|
770
|
+
] }),
|
|
771
|
+
/* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
|
|
772
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Review: " }),
|
|
773
|
+
/* @__PURE__ */ jsx2(Text2, { color: reviewColor, children: reviewStatus }),
|
|
774
|
+
/* @__PURE__ */ jsx2(Text2, { children: " | " }),
|
|
775
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Status: " }),
|
|
776
|
+
/* @__PURE__ */ jsx2(Text2, { color: mergeDisplay.color, children: mergeDisplay.text })
|
|
777
|
+
] }),
|
|
778
|
+
(((_c = pr.assignees) == null ? void 0 : _c.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
|
|
779
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Assignees: " }),
|
|
780
|
+
/* @__PURE__ */ jsx2(Text2, { children: pr.assignees.map((a) => a.login).join(", ") })
|
|
781
|
+
] }),
|
|
782
|
+
(((_d = pr.reviews) == null ? void 0 : _d.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
783
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Reviews:" }),
|
|
784
|
+
pr.reviews.map((review, idx) => {
|
|
785
|
+
const color = review.state === "APPROVED" ? "green" : review.state === "CHANGES_REQUESTED" ? "red" : review.state === "COMMENTED" ? "blue" : "yellow";
|
|
786
|
+
const icon = review.state === "APPROVED" ? "\u2713" : review.state === "CHANGES_REQUESTED" ? "\u2717" : review.state === "COMMENTED" ? "\u{1F4AC}" : "\u25CB";
|
|
787
|
+
return /* @__PURE__ */ jsxs2(Text2, { color, children: [
|
|
788
|
+
" ",
|
|
789
|
+
icon,
|
|
790
|
+
" ",
|
|
791
|
+
review.author.login
|
|
792
|
+
] }, idx);
|
|
793
|
+
})
|
|
794
|
+
] }),
|
|
795
|
+
(((_e = pr.reviewRequests) == null ? void 0 : _e.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
796
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Pending: " }),
|
|
797
|
+
/* @__PURE__ */ jsx2(Text2, { color: "yellow", children: pr.reviewRequests.map((r) => r.login ?? r.name ?? r.slug ?? "Team").join(", ") })
|
|
798
|
+
] }),
|
|
799
|
+
(((_f = pr.statusCheckRollup) == null ? void 0 : _f.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
|
|
800
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Checks:" }),
|
|
801
|
+
(_g = pr.statusCheckRollup) == null ? void 0 : _g.map((check, idx) => /* @__PURE__ */ jsxs2(Text2, { color: getCheckColor(check), children: [
|
|
802
|
+
" ",
|
|
803
|
+
getCheckIcon(check),
|
|
804
|
+
" ",
|
|
805
|
+
check.name ?? check.context
|
|
806
|
+
] }, idx))
|
|
807
|
+
] }),
|
|
808
|
+
pr.body && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
|
|
809
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Description:" }),
|
|
810
|
+
/* @__PURE__ */ jsx2(Markdown, { children: pr.body })
|
|
811
|
+
] })
|
|
812
|
+
] })
|
|
813
|
+
] }) }) }) });
|
|
810
814
|
}
|
|
811
815
|
|
|
812
|
-
// src/components/
|
|
813
|
-
import {
|
|
814
|
-
import
|
|
815
|
-
import {
|
|
816
|
-
import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
|
|
816
|
+
// src/components/github/PullRequestsBox.tsx
|
|
817
|
+
import { useEffect, useState } from "react";
|
|
818
|
+
import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
|
|
819
|
+
import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
|
|
817
820
|
|
|
818
|
-
// src/lib/
|
|
819
|
-
|
|
820
|
-
function
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
const match = branchName.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
|
|
837
|
-
if (match) {
|
|
838
|
-
const candidate = match[1].toUpperCase();
|
|
839
|
-
if (isValidTicketKeyFormat(candidate)) {
|
|
840
|
-
return candidate;
|
|
841
|
-
}
|
|
821
|
+
// src/lib/clipboard.ts
|
|
822
|
+
import { exec as exec2 } from "child_process";
|
|
823
|
+
async function copyToClipboard(text) {
|
|
824
|
+
var _a, _b;
|
|
825
|
+
const command = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
|
|
826
|
+
try {
|
|
827
|
+
const child = exec2(command);
|
|
828
|
+
(_a = child.stdin) == null ? void 0 : _a.write(text);
|
|
829
|
+
(_b = child.stdin) == null ? void 0 : _b.end();
|
|
830
|
+
await new Promise((resolve, reject) => {
|
|
831
|
+
child.on("close", (code) => {
|
|
832
|
+
if (code === 0) resolve();
|
|
833
|
+
else reject(new Error(`Clipboard command exited with code ${code}`));
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
return true;
|
|
837
|
+
} catch {
|
|
838
|
+
return false;
|
|
842
839
|
}
|
|
843
|
-
return null;
|
|
844
840
|
}
|
|
845
841
|
|
|
846
|
-
// src/
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
842
|
+
// src/components/github/PullRequestsBox.tsx
|
|
843
|
+
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
844
|
+
function PullRequestsBox({
|
|
845
|
+
prs,
|
|
846
|
+
selectedPR,
|
|
847
|
+
onSelect,
|
|
848
|
+
onCreatePR,
|
|
849
|
+
loading,
|
|
850
|
+
error,
|
|
851
|
+
branch,
|
|
852
|
+
repoSlug,
|
|
853
|
+
isFocused
|
|
854
|
+
}) {
|
|
855
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
856
|
+
const totalItems = prs.length + 1;
|
|
857
|
+
useEffect(() => {
|
|
858
|
+
const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
|
|
859
|
+
if (idx >= 0) setHighlightedIndex(idx);
|
|
860
|
+
}, [selectedPR, prs]);
|
|
861
|
+
useInput2(
|
|
862
|
+
(input, key) => {
|
|
863
|
+
if (!isFocused) return;
|
|
864
|
+
if (key.upArrow || input === "k") {
|
|
865
|
+
setHighlightedIndex((prev) => Math.max(0, prev - 1));
|
|
866
|
+
}
|
|
867
|
+
if (key.downArrow || input === "j") {
|
|
868
|
+
setHighlightedIndex((prev) => Math.min(totalItems - 1, prev + 1));
|
|
869
|
+
}
|
|
870
|
+
if (key.return) {
|
|
871
|
+
if (highlightedIndex === prs.length) {
|
|
872
|
+
onCreatePR();
|
|
873
|
+
} else if (prs[highlightedIndex]) {
|
|
874
|
+
onSelect(prs[highlightedIndex]);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (input === "y" && repoSlug && prs[highlightedIndex]) {
|
|
878
|
+
const pr = prs[highlightedIndex];
|
|
879
|
+
const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
|
|
880
|
+
copyToClipboard(url);
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
{ isActive: isFocused }
|
|
884
|
+
);
|
|
885
|
+
const title = "[2] Pull Requests";
|
|
886
|
+
const subtitle = branch ? ` (${branch})` : "";
|
|
887
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
888
|
+
return /* @__PURE__ */ jsx3(TitledBox2, { borderStyle: "round", titles: [`${title}${subtitle}`], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
|
|
889
|
+
loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
|
|
890
|
+
error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
|
|
891
|
+
!loading && !error && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
892
|
+
prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }),
|
|
893
|
+
prs.map((pr, idx) => {
|
|
894
|
+
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
895
|
+
const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
|
|
896
|
+
const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
|
|
897
|
+
return /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
|
|
898
|
+
prefix,
|
|
899
|
+
"#",
|
|
900
|
+
pr.number,
|
|
901
|
+
" ",
|
|
902
|
+
pr.isDraft ? "[Draft] " : "",
|
|
903
|
+
pr.title
|
|
904
|
+
] }, pr.number);
|
|
905
|
+
}),
|
|
906
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
|
|
907
|
+
isFocused && highlightedIndex === prs.length ? "> " : " ",
|
|
908
|
+
"+ Create new PR"
|
|
909
|
+
] })
|
|
910
|
+
] })
|
|
911
|
+
] }) });
|
|
872
912
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
913
|
+
|
|
914
|
+
// src/components/github/RemotesBox.tsx
|
|
915
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
916
|
+
import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
|
|
917
|
+
import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
|
|
918
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
919
|
+
function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
|
|
920
|
+
const [highlightedIndex, setHighlightedIndex] = useState2(0);
|
|
921
|
+
useEffect2(() => {
|
|
922
|
+
const idx = remotes.findIndex((r) => r.name === selectedRemote);
|
|
923
|
+
if (idx >= 0) setHighlightedIndex(idx);
|
|
924
|
+
}, [selectedRemote, remotes]);
|
|
925
|
+
useInput3(
|
|
926
|
+
(input, key) => {
|
|
927
|
+
if (!isFocused || remotes.length === 0) return;
|
|
928
|
+
if (key.upArrow || input === "k") {
|
|
929
|
+
setHighlightedIndex((prev) => Math.max(0, prev - 1));
|
|
930
|
+
}
|
|
931
|
+
if (key.downArrow || input === "j") {
|
|
932
|
+
setHighlightedIndex((prev) => Math.min(remotes.length - 1, prev + 1));
|
|
933
|
+
}
|
|
934
|
+
if (key.return) {
|
|
935
|
+
onSelect(remotes[highlightedIndex].name);
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
{ isActive: isFocused }
|
|
939
|
+
);
|
|
940
|
+
const title = "[1] Remotes";
|
|
941
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
942
|
+
return /* @__PURE__ */ jsx4(TitledBox3, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
|
|
943
|
+
loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
|
|
944
|
+
error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
|
|
945
|
+
!loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
|
|
946
|
+
!loading && !error && remotes.map((remote, idx) => {
|
|
947
|
+
const isHighlighted = isFocused && idx === highlightedIndex;
|
|
948
|
+
const isSelected = remote.name === selectedRemote;
|
|
949
|
+
const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
|
|
950
|
+
return /* @__PURE__ */ jsxs4(Text4, { color: isSelected ? "green" : void 0, children: [
|
|
951
|
+
prefix,
|
|
952
|
+
remote.name,
|
|
953
|
+
" (",
|
|
954
|
+
remote.url,
|
|
955
|
+
")"
|
|
956
|
+
] }, remote.name);
|
|
957
|
+
})
|
|
958
|
+
] }) });
|
|
886
959
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
960
|
+
|
|
961
|
+
// src/components/github/GitHubView.tsx
|
|
962
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
963
|
+
function GitHubView({ isFocused, onKeybindingsChange, onLogUpdated }) {
|
|
964
|
+
const [isRepo, setIsRepo] = useState3(null);
|
|
965
|
+
const [repoPath, setRepoPath] = useState3(null);
|
|
966
|
+
const [remotes, setRemotes] = useState3([]);
|
|
967
|
+
const [currentBranch, setCurrentBranch] = useState3(null);
|
|
968
|
+
const [currentRepoSlug, setCurrentRepoSlug] = useState3(null);
|
|
969
|
+
const [selectedRemote, setSelectedRemote] = useState3(null);
|
|
970
|
+
const [selectedPR, setSelectedPR] = useState3(null);
|
|
971
|
+
const [prs, setPrs] = useState3([]);
|
|
972
|
+
const [prDetails, setPrDetails] = useState3(null);
|
|
973
|
+
const [loading, setLoading] = useState3({
|
|
974
|
+
remotes: true,
|
|
975
|
+
prs: false,
|
|
976
|
+
details: false
|
|
896
977
|
});
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
branchTickets: {
|
|
904
|
-
...branchTickets,
|
|
905
|
-
[branch]: tickets.map((t) => t.key === ticketKey ? { ...t, status: newStatus } : t)
|
|
978
|
+
const [errors, setErrors] = useState3({});
|
|
979
|
+
const [focusedBox, setFocusedBox] = useState3("remotes");
|
|
980
|
+
useEffect3(() => {
|
|
981
|
+
if (!isFocused) {
|
|
982
|
+
onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
|
|
983
|
+
return;
|
|
906
984
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
try {
|
|
919
|
-
const headers = {
|
|
920
|
-
Authorization: createAuthHeader(auth.email, auth.apiToken),
|
|
921
|
-
Accept: "application/json"
|
|
922
|
-
};
|
|
923
|
-
const fetchOptions = { method, headers };
|
|
924
|
-
if (options == null ? void 0 : options.body) {
|
|
925
|
-
headers["Content-Type"] = "application/json";
|
|
926
|
-
fetchOptions.body = JSON.stringify(options.body);
|
|
985
|
+
const bindings = [];
|
|
986
|
+
if (focusedBox === "remotes") {
|
|
987
|
+
bindings.push({ key: "Enter", label: "Select Remote" });
|
|
988
|
+
} else if (focusedBox === "prs") {
|
|
989
|
+
bindings.push({ key: "n", label: "New PR", color: "green" });
|
|
990
|
+
bindings.push({ key: "r", label: "Refresh" });
|
|
991
|
+
bindings.push({ key: "o", label: "Open", color: "green" });
|
|
992
|
+
bindings.push({ key: "y", label: "Copy Link" });
|
|
993
|
+
} else if (focusedBox === "details") {
|
|
994
|
+
bindings.push({ key: "r", label: "Refresh" });
|
|
995
|
+
bindings.push({ key: "o", label: "Open", color: "green" });
|
|
927
996
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
997
|
+
onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
|
|
998
|
+
}, [isFocused, focusedBox, onKeybindingsChange]);
|
|
999
|
+
useEffect3(() => {
|
|
1000
|
+
const gitRepoCheck = isGitRepo();
|
|
1001
|
+
setIsRepo(gitRepoCheck);
|
|
1002
|
+
if (!gitRepoCheck) {
|
|
1003
|
+
setLoading((prev) => ({ ...prev, remotes: false }));
|
|
1004
|
+
setErrors((prev) => ({ ...prev, remotes: "Not a git repository" }));
|
|
1005
|
+
return;
|
|
932
1006
|
}
|
|
933
|
-
|
|
934
|
-
|
|
1007
|
+
const rootResult = getRepoRoot();
|
|
1008
|
+
if (rootResult.success) {
|
|
1009
|
+
setRepoPath(rootResult.data);
|
|
935
1010
|
}
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const message = err instanceof Error ? err.message : "Network error";
|
|
940
|
-
return { ok: false, status: 0, error: message };
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
async function validateCredentials(auth) {
|
|
944
|
-
const result = await jiraFetch(auth, "/myself");
|
|
945
|
-
if (!result.ok) {
|
|
946
|
-
if (result.status === 401 || result.status === 403) {
|
|
947
|
-
return {
|
|
948
|
-
success: false,
|
|
949
|
-
error: "Invalid credentials. Check your email and API token.",
|
|
950
|
-
errorType: "auth_error"
|
|
951
|
-
};
|
|
1011
|
+
const branchResult = getCurrentBranch();
|
|
1012
|
+
if (branchResult.success) {
|
|
1013
|
+
setCurrentBranch(branchResult.data);
|
|
952
1014
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
async function getIssue(auth, ticketKey) {
|
|
962
|
-
const result = await jiraFetch(auth, `/issue/${ticketKey}?fields=summary,status`);
|
|
963
|
-
if (!result.ok) {
|
|
964
|
-
if (result.status === 401 || result.status === 403) {
|
|
965
|
-
return {
|
|
966
|
-
success: false,
|
|
967
|
-
error: "Authentication failed",
|
|
968
|
-
errorType: "auth_error"
|
|
969
|
-
};
|
|
1015
|
+
const remotesResult = listRemotes();
|
|
1016
|
+
if (remotesResult.success) {
|
|
1017
|
+
setRemotes(remotesResult.data);
|
|
1018
|
+
const remoteNames = remotesResult.data.map((r) => r.name);
|
|
1019
|
+
const defaultRemote = getSelectedRemote(rootResult.success ? rootResult.data : "", remoteNames);
|
|
1020
|
+
setSelectedRemote(defaultRemote);
|
|
1021
|
+
} else {
|
|
1022
|
+
setErrors((prev) => ({ ...prev, remotes: remotesResult.error }));
|
|
970
1023
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1024
|
+
setLoading((prev) => ({ ...prev, remotes: false }));
|
|
1025
|
+
}, []);
|
|
1026
|
+
const refreshPRs = useCallback(async () => {
|
|
1027
|
+
if (!currentBranch || !currentRepoSlug) return;
|
|
1028
|
+
setLoading((prev) => ({ ...prev, prs: true }));
|
|
1029
|
+
try {
|
|
1030
|
+
const result = await listPRsForBranch(currentBranch, currentRepoSlug);
|
|
1031
|
+
if (result.success) {
|
|
1032
|
+
setPrs(result.data);
|
|
1033
|
+
if (result.data.length > 0) {
|
|
1034
|
+
setSelectedPR((prev) => prev ?? result.data[0]);
|
|
1035
|
+
}
|
|
1036
|
+
setErrors((prev) => ({ ...prev, prs: void 0 }));
|
|
1037
|
+
} else {
|
|
1038
|
+
setErrors((prev) => ({ ...prev, prs: result.error }));
|
|
1039
|
+
}
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
setErrors((prev) => ({ ...prev, prs: String(err) }));
|
|
1042
|
+
} finally {
|
|
1043
|
+
setLoading((prev) => ({ ...prev, prs: false }));
|
|
977
1044
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
};
|
|
1045
|
+
}, [currentBranch, currentRepoSlug]);
|
|
1046
|
+
const refreshDetails = useCallback(async () => {
|
|
1047
|
+
if (!selectedPR || !currentRepoSlug) return;
|
|
1048
|
+
setLoading((prev) => ({ ...prev, details: true }));
|
|
1049
|
+
try {
|
|
1050
|
+
const result = await getPRDetails(selectedPR.number, currentRepoSlug);
|
|
1051
|
+
if (result.success) {
|
|
1052
|
+
setPrDetails(result.data);
|
|
1053
|
+
setErrors((prev) => ({ ...prev, details: void 0 }));
|
|
1054
|
+
} else {
|
|
1055
|
+
setErrors((prev) => ({ ...prev, details: result.error }));
|
|
1056
|
+
}
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
setErrors((prev) => ({ ...prev, details: String(err) }));
|
|
1059
|
+
} finally {
|
|
1060
|
+
setLoading((prev) => ({ ...prev, details: false }));
|
|
995
1061
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1062
|
+
}, [selectedPR, currentRepoSlug]);
|
|
1063
|
+
useEffect3(() => {
|
|
1064
|
+
if (!selectedRemote || !currentBranch) return;
|
|
1065
|
+
const remote = remotes.find((r) => r.name === selectedRemote);
|
|
1066
|
+
if (!remote) return;
|
|
1067
|
+
const repo = getRepoFromRemote(remote.url);
|
|
1068
|
+
if (!repo) return;
|
|
1069
|
+
setCurrentRepoSlug(repo);
|
|
1070
|
+
setPrs([]);
|
|
1071
|
+
setSelectedPR(null);
|
|
1072
|
+
}, [selectedRemote, currentBranch, remotes]);
|
|
1073
|
+
useEffect3(() => {
|
|
1074
|
+
if (currentRepoSlug && currentBranch) {
|
|
1075
|
+
refreshPRs();
|
|
1002
1076
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
}
|
|
1009
|
-
const data = result.data;
|
|
1010
|
-
return { success: true, data: data.transitions };
|
|
1011
|
-
}
|
|
1012
|
-
async function applyTransition(auth, ticketKey, transitionId) {
|
|
1013
|
-
const result = await jiraFetch(auth, `/issue/${ticketKey}/transitions`, {
|
|
1014
|
-
method: "POST",
|
|
1015
|
-
body: { transition: { id: transitionId } }
|
|
1016
|
-
});
|
|
1017
|
-
if (!result.ok) {
|
|
1018
|
-
if (result.status === 401 || result.status === 403) {
|
|
1019
|
-
return {
|
|
1020
|
-
success: false,
|
|
1021
|
-
error: "Authentication failed",
|
|
1022
|
-
errorType: "auth_error"
|
|
1023
|
-
};
|
|
1077
|
+
}, [currentRepoSlug, currentBranch, refreshPRs]);
|
|
1078
|
+
useEffect3(() => {
|
|
1079
|
+
if (!selectedPR || !currentRepoSlug) {
|
|
1080
|
+
setPrDetails(null);
|
|
1081
|
+
return;
|
|
1024
1082
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1083
|
+
refreshDetails();
|
|
1084
|
+
}, [selectedPR, currentRepoSlug, refreshDetails]);
|
|
1085
|
+
const handleRemoteSelect = useCallback(
|
|
1086
|
+
(remoteName) => {
|
|
1087
|
+
setSelectedRemote(remoteName);
|
|
1088
|
+
if (repoPath) {
|
|
1089
|
+
updateRepoConfig(repoPath, { selectedRemote: remoteName });
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
[repoPath]
|
|
1093
|
+
);
|
|
1094
|
+
const handlePRSelect = useCallback((pr) => {
|
|
1095
|
+
setSelectedPR(pr);
|
|
1096
|
+
}, []);
|
|
1097
|
+
const prNumbersBeforeCreate = useRef2(/* @__PURE__ */ new Set());
|
|
1098
|
+
const pollingIntervalRef = useRef2(null);
|
|
1099
|
+
const handleCreatePR = useCallback(() => {
|
|
1100
|
+
prNumbersBeforeCreate.current = new Set(prs.map((pr) => pr.number));
|
|
1101
|
+
exec3("gh pr create --web", () => {
|
|
1102
|
+
process.stdout.emit("resize");
|
|
1103
|
+
});
|
|
1104
|
+
if (!currentBranch || !currentRepoSlug) return;
|
|
1105
|
+
let attempts = 0;
|
|
1106
|
+
const maxAttempts = 24;
|
|
1107
|
+
const pollInterval = 5e3;
|
|
1108
|
+
if (pollingIntervalRef.current) {
|
|
1109
|
+
clearInterval(pollingIntervalRef.current);
|
|
1031
1110
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1111
|
+
pollingIntervalRef.current = setInterval(async () => {
|
|
1112
|
+
attempts++;
|
|
1113
|
+
if (attempts > maxAttempts) {
|
|
1114
|
+
if (pollingIntervalRef.current) {
|
|
1115
|
+
clearInterval(pollingIntervalRef.current);
|
|
1116
|
+
pollingIntervalRef.current = null;
|
|
1117
|
+
}
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const result = await listPRsForBranch(currentBranch, currentRepoSlug);
|
|
1121
|
+
if (result.success) {
|
|
1122
|
+
setPrs(result.data);
|
|
1123
|
+
const newPR = result.data.find((pr) => !prNumbersBeforeCreate.current.has(pr.number));
|
|
1124
|
+
if (newPR) {
|
|
1125
|
+
if (pollingIntervalRef.current) {
|
|
1126
|
+
clearInterval(pollingIntervalRef.current);
|
|
1127
|
+
pollingIntervalRef.current = null;
|
|
1128
|
+
}
|
|
1129
|
+
const tickets = repoPath && currentBranch ? getLinkedTickets(repoPath, currentBranch).map((t) => t.key) : [];
|
|
1130
|
+
logPRCreated(newPR.number, newPR.title, tickets);
|
|
1131
|
+
onLogUpdated == null ? void 0 : onLogUpdated();
|
|
1132
|
+
setSelectedPR(newPR);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}, pollInterval);
|
|
1136
|
+
}, [prs, currentBranch, currentRepoSlug, repoPath, onLogUpdated]);
|
|
1137
|
+
useEffect3(() => {
|
|
1138
|
+
return () => {
|
|
1139
|
+
if (pollingIntervalRef.current) {
|
|
1140
|
+
clearInterval(pollingIntervalRef.current);
|
|
1141
|
+
}
|
|
1036
1142
|
};
|
|
1143
|
+
}, []);
|
|
1144
|
+
useInput4(
|
|
1145
|
+
(input) => {
|
|
1146
|
+
if (input === "1") setFocusedBox("remotes");
|
|
1147
|
+
if (input === "2") setFocusedBox("prs");
|
|
1148
|
+
if (input === "3") setFocusedBox("details");
|
|
1149
|
+
if (input === "r") {
|
|
1150
|
+
if (focusedBox === "prs") refreshPRs();
|
|
1151
|
+
if (focusedBox === "details") refreshDetails();
|
|
1152
|
+
}
|
|
1153
|
+
},
|
|
1154
|
+
{ isActive: isFocused }
|
|
1155
|
+
);
|
|
1156
|
+
if (isRepo === false) {
|
|
1157
|
+
return /* @__PURE__ */ jsx5(TitledBox4, { borderStyle: "round", titles: ["Error"], flexGrow: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: "Current directory is not a git repository" }) });
|
|
1037
1158
|
}
|
|
1038
|
-
return {
|
|
1159
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, children: [
|
|
1160
|
+
/* @__PURE__ */ jsx5(
|
|
1161
|
+
RemotesBox,
|
|
1162
|
+
{
|
|
1163
|
+
remotes,
|
|
1164
|
+
selectedRemote,
|
|
1165
|
+
onSelect: handleRemoteSelect,
|
|
1166
|
+
loading: loading.remotes,
|
|
1167
|
+
error: errors.remotes,
|
|
1168
|
+
isFocused: isFocused && focusedBox === "remotes"
|
|
1169
|
+
}
|
|
1170
|
+
),
|
|
1171
|
+
/* @__PURE__ */ jsx5(
|
|
1172
|
+
PullRequestsBox,
|
|
1173
|
+
{
|
|
1174
|
+
prs,
|
|
1175
|
+
selectedPR,
|
|
1176
|
+
onSelect: handlePRSelect,
|
|
1177
|
+
onCreatePR: handleCreatePR,
|
|
1178
|
+
loading: loading.prs,
|
|
1179
|
+
error: errors.prs,
|
|
1180
|
+
branch: currentBranch,
|
|
1181
|
+
repoSlug: currentRepoSlug,
|
|
1182
|
+
isFocused: isFocused && focusedBox === "prs"
|
|
1183
|
+
}
|
|
1184
|
+
),
|
|
1185
|
+
/* @__PURE__ */ jsx5(
|
|
1186
|
+
PRDetailsBox,
|
|
1187
|
+
{
|
|
1188
|
+
pr: prDetails,
|
|
1189
|
+
loading: loading.details,
|
|
1190
|
+
error: errors.details,
|
|
1191
|
+
isFocused: isFocused && focusedBox === "details"
|
|
1192
|
+
}
|
|
1193
|
+
)
|
|
1194
|
+
] });
|
|
1039
1195
|
}
|
|
1040
1196
|
|
|
1197
|
+
// src/components/jira/JiraView.tsx
|
|
1198
|
+
import { useCallback as useCallback2, useEffect as useEffect5, useState as useState7 } from "react";
|
|
1199
|
+
import open2 from "open";
|
|
1200
|
+
import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
|
|
1201
|
+
import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
|
|
1202
|
+
|
|
1041
1203
|
// src/components/jira/ChangeStatusModal.tsx
|
|
1042
1204
|
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
1043
1205
|
import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
|
|
@@ -1121,17 +1283,17 @@ import { useState as useState5 } from "react";
|
|
|
1121
1283
|
import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
|
|
1122
1284
|
|
|
1123
1285
|
// src/lib/editor.ts
|
|
1124
|
-
import { spawnSync } from "child_process";
|
|
1125
|
-
import { mkdtempSync, readFileSync as
|
|
1286
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1287
|
+
import { mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1126
1288
|
import { tmpdir } from "os";
|
|
1127
|
-
import { join as
|
|
1289
|
+
import { join as join3 } from "path";
|
|
1128
1290
|
function openInEditor(content, filename) {
|
|
1129
1291
|
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
1130
|
-
const tempDir = mkdtempSync(
|
|
1131
|
-
const tempFile =
|
|
1292
|
+
const tempDir = mkdtempSync(join3(tmpdir(), "clairo-"));
|
|
1293
|
+
const tempFile = join3(tempDir, filename);
|
|
1132
1294
|
try {
|
|
1133
|
-
|
|
1134
|
-
const result =
|
|
1295
|
+
writeFileSync3(tempFile, content);
|
|
1296
|
+
const result = spawnSync2(editor, [tempFile], {
|
|
1135
1297
|
stdio: "inherit"
|
|
1136
1298
|
});
|
|
1137
1299
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
@@ -1139,7 +1301,7 @@ function openInEditor(content, filename) {
|
|
|
1139
1301
|
if (result.status !== 0) {
|
|
1140
1302
|
return null;
|
|
1141
1303
|
}
|
|
1142
|
-
return
|
|
1304
|
+
return readFileSync3(tempFile, "utf-8");
|
|
1143
1305
|
} finally {
|
|
1144
1306
|
try {
|
|
1145
1307
|
rmSync(tempDir, { recursive: true });
|
|
@@ -1335,7 +1497,7 @@ function TicketItem({ ticketKey, summary, status, isHighlighted, isSelected }) {
|
|
|
1335
1497
|
|
|
1336
1498
|
// src/components/jira/JiraView.tsx
|
|
1337
1499
|
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1338
|
-
function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
|
|
1500
|
+
function JiraView({ isFocused, onModalChange, onKeybindingsChange, onLogUpdated }) {
|
|
1339
1501
|
const [repoPath, setRepoPath] = useState7(null);
|
|
1340
1502
|
const [currentBranch, setCurrentBranch] = useState7(null);
|
|
1341
1503
|
const [isRepo, setIsRepo] = useState7(null);
|
|
@@ -1592,7 +1754,10 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
|
|
|
1592
1754
|
ticketKey: ticket.key,
|
|
1593
1755
|
currentStatus: ticket.status,
|
|
1594
1756
|
onComplete: (newStatus) => {
|
|
1757
|
+
const oldStatus = ticket.status;
|
|
1595
1758
|
updateTicketStatus(repoPath, currentBranch, ticket.key, newStatus);
|
|
1759
|
+
logJiraStatusChanged(ticket.key, oldStatus, newStatus);
|
|
1760
|
+
onLogUpdated == null ? void 0 : onLogUpdated();
|
|
1596
1761
|
setShowStatusModal(false);
|
|
1597
1762
|
refreshTickets();
|
|
1598
1763
|
},
|
|
@@ -1618,9 +1783,237 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
|
|
|
1618
1783
|
] }) });
|
|
1619
1784
|
}
|
|
1620
1785
|
|
|
1621
|
-
// src/components/
|
|
1622
|
-
import {
|
|
1786
|
+
// src/components/logs/LogsView.tsx
|
|
1787
|
+
import { useCallback as useCallback3, useEffect as useEffect6, useState as useState8 } from "react";
|
|
1788
|
+
import { Box as Box14, useInput as useInput12 } from "ink";
|
|
1789
|
+
|
|
1790
|
+
// src/components/logs/LogsHistoryBox.tsx
|
|
1791
|
+
import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
|
|
1792
|
+
import { Box as Box12, Text as Text12, useInput as useInput10 } from "ink";
|
|
1623
1793
|
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1794
|
+
function LogsHistoryBox({
|
|
1795
|
+
logFiles,
|
|
1796
|
+
selectedDate,
|
|
1797
|
+
highlightedIndex,
|
|
1798
|
+
onHighlight,
|
|
1799
|
+
onSelect,
|
|
1800
|
+
isFocused
|
|
1801
|
+
}) {
|
|
1802
|
+
const title = "[5] Logs";
|
|
1803
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
1804
|
+
useInput10(
|
|
1805
|
+
(input, key) => {
|
|
1806
|
+
if (logFiles.length === 0) return;
|
|
1807
|
+
if (key.upArrow || input === "k") {
|
|
1808
|
+
onHighlight(Math.max(0, highlightedIndex - 1));
|
|
1809
|
+
}
|
|
1810
|
+
if (key.downArrow || input === "j") {
|
|
1811
|
+
onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
|
|
1812
|
+
}
|
|
1813
|
+
if (key.return) {
|
|
1814
|
+
const file = logFiles[highlightedIndex];
|
|
1815
|
+
if (file) {
|
|
1816
|
+
onSelect(file.date);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
},
|
|
1820
|
+
{ isActive: isFocused }
|
|
1821
|
+
);
|
|
1822
|
+
return /* @__PURE__ */ jsx12(TitledBox6, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
|
|
1823
|
+
logFiles.length === 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "No logs yet" }),
|
|
1824
|
+
logFiles.map((file, idx) => {
|
|
1825
|
+
const isHighlighted = idx === highlightedIndex;
|
|
1826
|
+
const isSelected = file.date === selectedDate;
|
|
1827
|
+
const cursor = isHighlighted ? ">" : " ";
|
|
1828
|
+
const indicator = isSelected ? " *" : "";
|
|
1829
|
+
return /* @__PURE__ */ jsxs12(Box12, { children: [
|
|
1830
|
+
/* @__PURE__ */ jsxs12(Text12, { color: isHighlighted ? "yellow" : void 0, children: [
|
|
1831
|
+
cursor,
|
|
1832
|
+
" "
|
|
1833
|
+
] }),
|
|
1834
|
+
/* @__PURE__ */ jsx12(
|
|
1835
|
+
Text12,
|
|
1836
|
+
{
|
|
1837
|
+
color: file.isToday ? "green" : void 0,
|
|
1838
|
+
bold: file.isToday,
|
|
1839
|
+
children: file.date
|
|
1840
|
+
}
|
|
1841
|
+
),
|
|
1842
|
+
file.isToday && /* @__PURE__ */ jsx12(Text12, { color: "green", children: " (today)" }),
|
|
1843
|
+
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: indicator })
|
|
1844
|
+
] }, file.date);
|
|
1845
|
+
})
|
|
1846
|
+
] }) });
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// src/components/logs/LogViewerBox.tsx
|
|
1850
|
+
import { useRef as useRef3 } from "react";
|
|
1851
|
+
import { TitledBox as TitledBox7 } from "@mishieck/ink-titled-box";
|
|
1852
|
+
import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
|
|
1853
|
+
import { ScrollView as ScrollView2 } from "ink-scroll-view";
|
|
1854
|
+
import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1855
|
+
function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
|
|
1856
|
+
const scrollRef = useRef3(null);
|
|
1857
|
+
const title = "[6] Log Content";
|
|
1858
|
+
const borderColor = isFocused ? "yellow" : void 0;
|
|
1859
|
+
const displayTitle = date ? `${title} - ${date}.md` : title;
|
|
1860
|
+
useInput11(
|
|
1861
|
+
(input, key) => {
|
|
1862
|
+
var _a, _b;
|
|
1863
|
+
if (key.upArrow || input === "k") {
|
|
1864
|
+
(_a = scrollRef.current) == null ? void 0 : _a.scrollBy(-1);
|
|
1865
|
+
}
|
|
1866
|
+
if (key.downArrow || input === "j") {
|
|
1867
|
+
(_b = scrollRef.current) == null ? void 0 : _b.scrollBy(1);
|
|
1868
|
+
}
|
|
1869
|
+
if (input === "e" && date) {
|
|
1870
|
+
openLogInEditor(date);
|
|
1871
|
+
onRefresh();
|
|
1872
|
+
}
|
|
1873
|
+
if (input === "n") {
|
|
1874
|
+
const today = getTodayDate();
|
|
1875
|
+
if (!logExists(today)) {
|
|
1876
|
+
createEmptyLog(today);
|
|
1877
|
+
onLogCreated();
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
if (input === "r") {
|
|
1881
|
+
onRefresh();
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1884
|
+
{ isActive: isFocused }
|
|
1885
|
+
);
|
|
1886
|
+
return /* @__PURE__ */ jsx13(TitledBox7, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx13(Box13, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx13(ScrollView2, { ref: scrollRef, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
|
|
1887
|
+
!date && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Select a log file to view" }),
|
|
1888
|
+
date && content === null && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Log file not found" }),
|
|
1889
|
+
date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Empty log file" }),
|
|
1890
|
+
date && content && content.trim() !== "" && /* @__PURE__ */ jsx13(Markdown, { children: content })
|
|
1891
|
+
] }) }) }) });
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// src/components/logs/LogsView.tsx
|
|
1895
|
+
import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1896
|
+
function LogsView({ isFocused, onKeybindingsChange, refreshKey }) {
|
|
1897
|
+
const [logFiles, setLogFiles] = useState8([]);
|
|
1898
|
+
const [selectedDate, setSelectedDate] = useState8(null);
|
|
1899
|
+
const [logContent, setLogContent] = useState8(null);
|
|
1900
|
+
const [highlightedIndex, setHighlightedIndex] = useState8(0);
|
|
1901
|
+
const [focusedBox, setFocusedBox] = useState8("history");
|
|
1902
|
+
useEffect6(() => {
|
|
1903
|
+
if (!isFocused) {
|
|
1904
|
+
onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
const bindings = [];
|
|
1908
|
+
if (focusedBox === "history") {
|
|
1909
|
+
bindings.push({ key: "Enter", label: "Select" });
|
|
1910
|
+
} else if (focusedBox === "viewer") {
|
|
1911
|
+
bindings.push({ key: "e", label: "Edit" });
|
|
1912
|
+
bindings.push({ key: "n", label: "New Log", color: "green" });
|
|
1913
|
+
bindings.push({ key: "r", label: "Refresh" });
|
|
1914
|
+
}
|
|
1915
|
+
onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
|
|
1916
|
+
}, [isFocused, focusedBox, onKeybindingsChange]);
|
|
1917
|
+
const refreshLogFiles = useCallback3(() => {
|
|
1918
|
+
const files = listLogFiles();
|
|
1919
|
+
setLogFiles(files);
|
|
1920
|
+
if (files.length > 0 && !selectedDate) {
|
|
1921
|
+
const today = getTodayDate();
|
|
1922
|
+
const todayFile = files.find((f) => f.date === today);
|
|
1923
|
+
if (todayFile) {
|
|
1924
|
+
setSelectedDate(todayFile.date);
|
|
1925
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1926
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1927
|
+
} else {
|
|
1928
|
+
setSelectedDate(files[0].date);
|
|
1929
|
+
setHighlightedIndex(0);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}, [selectedDate]);
|
|
1933
|
+
useEffect6(() => {
|
|
1934
|
+
refreshLogFiles();
|
|
1935
|
+
}, [refreshLogFiles]);
|
|
1936
|
+
useEffect6(() => {
|
|
1937
|
+
if (selectedDate) {
|
|
1938
|
+
const content = readLog(selectedDate);
|
|
1939
|
+
setLogContent(content);
|
|
1940
|
+
} else {
|
|
1941
|
+
setLogContent(null);
|
|
1942
|
+
}
|
|
1943
|
+
}, [selectedDate]);
|
|
1944
|
+
useEffect6(() => {
|
|
1945
|
+
if (refreshKey !== void 0 && refreshKey > 0) {
|
|
1946
|
+
const files = listLogFiles();
|
|
1947
|
+
setLogFiles(files);
|
|
1948
|
+
const today = getTodayDate();
|
|
1949
|
+
if (selectedDate === today) {
|
|
1950
|
+
const content = readLog(today);
|
|
1951
|
+
setLogContent(content);
|
|
1952
|
+
} else if (!selectedDate && files.length > 0) {
|
|
1953
|
+
const todayFile = files.find((f) => f.date === today);
|
|
1954
|
+
if (todayFile) {
|
|
1955
|
+
setSelectedDate(today);
|
|
1956
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1957
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}, [refreshKey, selectedDate]);
|
|
1962
|
+
const handleSelectDate = useCallback3((date) => {
|
|
1963
|
+
setSelectedDate(date);
|
|
1964
|
+
}, []);
|
|
1965
|
+
const handleRefresh = useCallback3(() => {
|
|
1966
|
+
refreshLogFiles();
|
|
1967
|
+
if (selectedDate) {
|
|
1968
|
+
const content = readLog(selectedDate);
|
|
1969
|
+
setLogContent(content);
|
|
1970
|
+
}
|
|
1971
|
+
}, [refreshLogFiles, selectedDate]);
|
|
1972
|
+
const handleLogCreated = useCallback3(() => {
|
|
1973
|
+
const files = listLogFiles();
|
|
1974
|
+
setLogFiles(files);
|
|
1975
|
+
const today = getTodayDate();
|
|
1976
|
+
setSelectedDate(today);
|
|
1977
|
+
const idx = files.findIndex((f) => f.date === today);
|
|
1978
|
+
setHighlightedIndex(idx >= 0 ? idx : 0);
|
|
1979
|
+
const content = readLog(today);
|
|
1980
|
+
setLogContent(content);
|
|
1981
|
+
}, []);
|
|
1982
|
+
useInput12(
|
|
1983
|
+
(input) => {
|
|
1984
|
+
if (input === "5") setFocusedBox("history");
|
|
1985
|
+
if (input === "6") setFocusedBox("viewer");
|
|
1986
|
+
},
|
|
1987
|
+
{ isActive: isFocused }
|
|
1988
|
+
);
|
|
1989
|
+
return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", flexGrow: 1, children: [
|
|
1990
|
+
/* @__PURE__ */ jsx14(
|
|
1991
|
+
LogsHistoryBox,
|
|
1992
|
+
{
|
|
1993
|
+
logFiles,
|
|
1994
|
+
selectedDate,
|
|
1995
|
+
highlightedIndex,
|
|
1996
|
+
onHighlight: setHighlightedIndex,
|
|
1997
|
+
onSelect: handleSelectDate,
|
|
1998
|
+
isFocused: isFocused && focusedBox === "history"
|
|
1999
|
+
}
|
|
2000
|
+
),
|
|
2001
|
+
/* @__PURE__ */ jsx14(
|
|
2002
|
+
LogViewerBox,
|
|
2003
|
+
{
|
|
2004
|
+
date: selectedDate,
|
|
2005
|
+
content: logContent,
|
|
2006
|
+
isFocused: isFocused && focusedBox === "viewer",
|
|
2007
|
+
onRefresh: handleRefresh,
|
|
2008
|
+
onLogCreated: handleLogCreated
|
|
2009
|
+
}
|
|
2010
|
+
)
|
|
2011
|
+
] });
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// src/components/ui/KeybindingsBar.tsx
|
|
2015
|
+
import { Box as Box15, Text as Text14 } from "ink";
|
|
2016
|
+
import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1624
2017
|
var globalBindings = [
|
|
1625
2018
|
{ key: "1-4", label: "Focus" },
|
|
1626
2019
|
{ key: "j/k", label: "Navigate" },
|
|
@@ -1631,20 +2024,24 @@ var modalBindings = [
|
|
|
1631
2024
|
];
|
|
1632
2025
|
function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
|
|
1633
2026
|
const allBindings = modalOpen ? [...contextBindings, ...modalBindings] : [...contextBindings, ...globalBindings];
|
|
1634
|
-
return /* @__PURE__ */
|
|
1635
|
-
/* @__PURE__ */
|
|
1636
|
-
/* @__PURE__ */
|
|
2027
|
+
return /* @__PURE__ */ jsx15(Box15, { flexShrink: 0, paddingX: 1, gap: 2, children: allBindings.map((binding) => /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
|
|
2028
|
+
/* @__PURE__ */ jsx15(Text14, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
|
|
2029
|
+
/* @__PURE__ */ jsx15(Text14, { dimColor: true, children: binding.label })
|
|
1637
2030
|
] }, binding.key)) });
|
|
1638
2031
|
}
|
|
1639
2032
|
|
|
1640
2033
|
// src/app.tsx
|
|
1641
|
-
import { jsx as
|
|
2034
|
+
import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1642
2035
|
function App() {
|
|
1643
2036
|
const { exit } = useApp();
|
|
1644
|
-
const [focusedView, setFocusedView] =
|
|
1645
|
-
const [modalOpen, setModalOpen] =
|
|
1646
|
-
const [contextBindings, setContextBindings] =
|
|
1647
|
-
|
|
2037
|
+
const [focusedView, setFocusedView] = useState9("github");
|
|
2038
|
+
const [modalOpen, setModalOpen] = useState9(false);
|
|
2039
|
+
const [contextBindings, setContextBindings] = useState9([]);
|
|
2040
|
+
const [logRefreshKey, setLogRefreshKey] = useState9(0);
|
|
2041
|
+
const handleLogUpdated = useCallback4(() => {
|
|
2042
|
+
setLogRefreshKey((prev) => prev + 1);
|
|
2043
|
+
}, []);
|
|
2044
|
+
useInput13(
|
|
1648
2045
|
(input, key) => {
|
|
1649
2046
|
if (key.ctrl && input === "c") {
|
|
1650
2047
|
exit();
|
|
@@ -1655,26 +2052,43 @@ function App() {
|
|
|
1655
2052
|
if (input === "4") {
|
|
1656
2053
|
setFocusedView("jira");
|
|
1657
2054
|
}
|
|
2055
|
+
if (input === "5" || input === "6") {
|
|
2056
|
+
setFocusedView("logs");
|
|
2057
|
+
}
|
|
1658
2058
|
},
|
|
1659
2059
|
{ isActive: !modalOpen }
|
|
1660
2060
|
);
|
|
1661
|
-
return /* @__PURE__ */
|
|
1662
|
-
/* @__PURE__ */
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2061
|
+
return /* @__PURE__ */ jsxs16(Box16, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children: [
|
|
2062
|
+
/* @__PURE__ */ jsxs16(Box16, { flexGrow: 1, flexDirection: "row", columnGap: 1, children: [
|
|
2063
|
+
/* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
|
|
2064
|
+
/* @__PURE__ */ jsx16(
|
|
2065
|
+
GitHubView,
|
|
2066
|
+
{
|
|
2067
|
+
isFocused: focusedView === "github",
|
|
2068
|
+
onKeybindingsChange: focusedView === "github" ? setContextBindings : void 0,
|
|
2069
|
+
onLogUpdated: handleLogUpdated
|
|
2070
|
+
}
|
|
2071
|
+
),
|
|
2072
|
+
/* @__PURE__ */ jsx16(
|
|
2073
|
+
JiraView,
|
|
2074
|
+
{
|
|
2075
|
+
isFocused: focusedView === "jira",
|
|
2076
|
+
onModalChange: setModalOpen,
|
|
2077
|
+
onKeybindingsChange: focusedView === "jira" ? setContextBindings : void 0,
|
|
2078
|
+
onLogUpdated: handleLogUpdated
|
|
2079
|
+
}
|
|
2080
|
+
)
|
|
2081
|
+
] }),
|
|
2082
|
+
/* @__PURE__ */ jsx16(Box16, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: /* @__PURE__ */ jsx16(
|
|
2083
|
+
LogsView,
|
|
2084
|
+
{
|
|
2085
|
+
isFocused: focusedView === "logs",
|
|
2086
|
+
onKeybindingsChange: focusedView === "logs" ? setContextBindings : void 0,
|
|
2087
|
+
refreshKey: logRefreshKey
|
|
2088
|
+
}
|
|
2089
|
+
) })
|
|
2090
|
+
] }),
|
|
2091
|
+
/* @__PURE__ */ jsx16(KeybindingsBar, { contextBindings, modalOpen })
|
|
1678
2092
|
] });
|
|
1679
2093
|
}
|
|
1680
2094
|
|
|
@@ -1682,34 +2096,34 @@ function App() {
|
|
|
1682
2096
|
import { render as inkRender } from "ink";
|
|
1683
2097
|
|
|
1684
2098
|
// src/lib/Screen.tsx
|
|
1685
|
-
import { Box as
|
|
1686
|
-
import { useCallback as
|
|
1687
|
-
import { jsx as
|
|
2099
|
+
import { Box as Box17, useStdout } from "ink";
|
|
2100
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useState as useState10 } from "react";
|
|
2101
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
1688
2102
|
function Screen({ children }) {
|
|
1689
2103
|
const { stdout } = useStdout();
|
|
1690
|
-
const getSize =
|
|
2104
|
+
const getSize = useCallback5(
|
|
1691
2105
|
() => ({ height: stdout.rows, width: stdout.columns }),
|
|
1692
2106
|
[stdout]
|
|
1693
2107
|
);
|
|
1694
|
-
const [size, setSize] =
|
|
1695
|
-
|
|
2108
|
+
const [size, setSize] = useState10(getSize);
|
|
2109
|
+
useEffect7(() => {
|
|
1696
2110
|
const onResize = () => setSize(getSize());
|
|
1697
2111
|
stdout.on("resize", onResize);
|
|
1698
2112
|
return () => {
|
|
1699
2113
|
stdout.off("resize", onResize);
|
|
1700
2114
|
};
|
|
1701
2115
|
}, [stdout, getSize]);
|
|
1702
|
-
return /* @__PURE__ */
|
|
2116
|
+
return /* @__PURE__ */ jsx17(Box17, { height: size.height, width: size.width, children });
|
|
1703
2117
|
}
|
|
1704
2118
|
|
|
1705
2119
|
// src/lib/render.tsx
|
|
1706
|
-
import { jsx as
|
|
2120
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
1707
2121
|
var ENTER_ALT_BUFFER = "\x1B[?1049h";
|
|
1708
2122
|
var EXIT_ALT_BUFFER = "\x1B[?1049l";
|
|
1709
2123
|
var CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
1710
2124
|
function render(node, options) {
|
|
1711
2125
|
process.stdout.write(ENTER_ALT_BUFFER + CLEAR_SCREEN);
|
|
1712
|
-
const element = /* @__PURE__ */
|
|
2126
|
+
const element = /* @__PURE__ */ jsx18(Screen, { children: node });
|
|
1713
2127
|
const instance = inkRender(element, options);
|
|
1714
2128
|
setImmediate(() => instance.rerender(element));
|
|
1715
2129
|
const cleanup = () => process.stdout.write(EXIT_ALT_BUFFER);
|
|
@@ -1730,7 +2144,7 @@ function render(node, options) {
|
|
|
1730
2144
|
}
|
|
1731
2145
|
|
|
1732
2146
|
// src/cli.tsx
|
|
1733
|
-
import { jsx as
|
|
2147
|
+
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
1734
2148
|
meow(
|
|
1735
2149
|
`
|
|
1736
2150
|
Usage
|
|
@@ -1752,4 +2166,4 @@ meow(
|
|
|
1752
2166
|
}
|
|
1753
2167
|
}
|
|
1754
2168
|
);
|
|
1755
|
-
render(/* @__PURE__ */
|
|
2169
|
+
render(/* @__PURE__ */ jsx19(App, {}));
|