@kud/gh-cockpit 0.0.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/index.js ADDED
@@ -0,0 +1,624 @@
1
+ import { __export, withRetry, toGHItem, signalPath, computeHealth, buildInboxQuery, lib_exports, AiLauncher, seedPromptFor, CopyPromptNotice, DrillView, __reExport } from './chunk-A3NXVFEI.js';
2
+ export { buildInboxQuery, computeHealth, signalPath, toGHItem, withRetry } from './chunk-A3NXVFEI.js';
3
+ import { parsePatterns, HealthPanel, CommentsPanel, inboxConfig } from '@kud/gh-ink';
4
+ import { $ } from 'zx';
5
+ import { useState, useEffect } from 'react';
6
+ import { useInput, Box, Text } from 'ink';
7
+ import { useTabs, Tabs, useListCursor, colors, ScrollView } from '@kud/ink-ui';
8
+ import { fetchHealth, fetchPrComments } from '@kud/gh';
9
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
10
+ import { readFileSync, mkdirSync, writeFileSync } from 'fs';
11
+ import { homedir } from 'os';
12
+ import { join } from 'path';
13
+
14
+ // src/index.ts
15
+ var index_exports = {};
16
+ __export(index_exports, {
17
+ IssueView: () => IssueView,
18
+ PrView: () => PrView,
19
+ buildInboxQuery: () => buildInboxQuery,
20
+ checkDrillFor: () => checkDrillFor,
21
+ computeHealth: () => computeHealth,
22
+ defineCockpit: () => defineCockpit,
23
+ detailFor: () => detailFor,
24
+ hasPatterns: () => hasPatterns,
25
+ parseArgs: () => parseArgs,
26
+ registerCheckDrills: () => registerCheckDrills,
27
+ signalPath: () => signalPath,
28
+ toGHItem: () => toGHItem,
29
+ withRetry: () => withRetry
30
+ });
31
+
32
+ // src/config.ts
33
+ var defineCockpit = (config) => config;
34
+ var parseArgs = (argv) => {
35
+ const include = [];
36
+ const exclude = [];
37
+ let here = false;
38
+ let named;
39
+ for (let i = 0; i < argv.length; i++) {
40
+ const arg = argv[i];
41
+ if (arg === "--here") {
42
+ here = true;
43
+ continue;
44
+ }
45
+ const eq = arg.match(/^--(include|exclude)=(.*)$/);
46
+ if (eq) {
47
+ (eq[1] === "include" ? include : exclude).push(...parsePatterns(eq[2]));
48
+ continue;
49
+ }
50
+ if (arg === "--include" || arg === "--exclude") {
51
+ const next = argv[i + 1];
52
+ if (next === void 0 || next.startsWith("-")) continue;
53
+ i++;
54
+ (arg === "--include" ? include : exclude).push(...parsePatterns(next));
55
+ continue;
56
+ }
57
+ if (!arg.startsWith("-") && !named) named = arg;
58
+ }
59
+ return {
60
+ here,
61
+ named,
62
+ filter: { include, exclude }
63
+ };
64
+ };
65
+ var hasPatterns = (f) => (f.include?.length ?? 0) > 0 || (f.exclude?.length ?? 0) > 0;
66
+
67
+ // src/views/check-drill.tsx
68
+ var registered = [];
69
+ var registerCheckDrills = (drills) => {
70
+ registered = drills;
71
+ };
72
+ var checkDrillFor = (url) => registered.find((d) => d.match(url)) ?? null;
73
+ var fetchComments = async (repo, number, kind = "pr") => {
74
+ if (kind === "issue") return fetchIssueConversation(repo, number);
75
+ const [owner, name] = repo.split("/");
76
+ return fetchPrComments(owner, name, number);
77
+ };
78
+ var ISSUE_QUERY = `query($owner: String!, $name: String!, $number: Int!) {
79
+ repository(owner: $owner, name: $name) {
80
+ issue(number: $number) {
81
+ comments(first: 50) { nodes { author { login } body createdAt url } }
82
+ }
83
+ }
84
+ }`;
85
+ var fetchIssueConversation = async (repo, number) => {
86
+ const [owner, name] = repo.split("/");
87
+ const [body, rest] = await Promise.all([
88
+ fetchBody("issue", repo, number),
89
+ $`gh api graphql -f query=${ISSUE_QUERY} -f owner=${owner} -f name=${name} -F number=${number}`.quiet()
90
+ ]);
91
+ const nodes = JSON.parse(rest.stdout).data.repository.issue.comments.nodes;
92
+ const comments = nodes.map((c) => ({
93
+ author: c.author?.login ?? "ghost",
94
+ body: (c.body ?? "").trim(),
95
+ createdAt: c.createdAt,
96
+ url: c.url
97
+ }));
98
+ return {
99
+ headRef: "",
100
+ conversation: body ? [body, ...comments] : comments,
101
+ threads: []
102
+ };
103
+ };
104
+ var fetchBody = async (kind, repo, number) => {
105
+ try {
106
+ const { stdout } = await $`gh ${kind} view ${number} --repo ${repo} --json body,author,createdAt`.quiet();
107
+ const node = JSON.parse(stdout);
108
+ const text = (node.body ?? "").trim();
109
+ return text ? {
110
+ author: node.author?.login ?? "",
111
+ body: text,
112
+ createdAt: node.createdAt
113
+ } : null;
114
+ } catch {
115
+ return null;
116
+ }
117
+ };
118
+ var jobIdOf = (url) => url?.match(/\/job\/(\d+)/)?.[1] ?? null;
119
+ var stripTimestamp = (line) => line.replace(/^\d{4}-\d\d-\d\dT[\d:.]+Z\s?/, "").replace(/\s+$/, "");
120
+ var colorFor = (clean) => {
121
+ const lower = clean.toLowerCase();
122
+ if (/(^|\W)(error|fail(ed|ure)?)\b/.test(lower)) return colors.error;
123
+ if (/(^|\W)warn(ing)?\b/.test(lower)) return colors.warning;
124
+ return void 0;
125
+ };
126
+ var processLog = (raw) => {
127
+ const lines = [];
128
+ let firstError = -1;
129
+ for (const l of raw) {
130
+ const clean = stripTimestamp(l);
131
+ const cmd = clean.match(/^##\[(\w+)\](.*)$/);
132
+ if (cmd) {
133
+ const [, kind, rest] = cmd;
134
+ if (kind === "endgroup") continue;
135
+ if (kind === "group") {
136
+ lines.push({ text: "\u25B8 " + rest, dim: true, bold: true });
137
+ } else if (kind === "error") {
138
+ if (firstError < 0) firstError = lines.length;
139
+ lines.push({ text: rest, color: colors.error });
140
+ } else if (kind === "warning") {
141
+ lines.push({ text: rest, color: colors.warning });
142
+ } else {
143
+ lines.push({ text: rest, dim: true });
144
+ }
145
+ continue;
146
+ }
147
+ lines.push({ text: clean, color: colorFor(clean) });
148
+ }
149
+ const jumpTo = firstError >= 0 ? Math.max(0, firstError - 3) : Math.max(0, lines.length - 1);
150
+ return { lines, jumpTo };
151
+ };
152
+ var CheckLogView = ({
153
+ repo,
154
+ jobId,
155
+ name,
156
+ url,
157
+ onBack
158
+ }) => {
159
+ const [state, setState] = useState({ phase: "loading" });
160
+ useEffect(() => {
161
+ let live = true;
162
+ $`gh api repos/${repo}/actions/jobs/${jobId}/logs`.quiet().then(
163
+ (r) => live && setState({ phase: "ready", lines: r.stdout.split("\n") })
164
+ ).catch(
165
+ (e) => live && setState({ phase: "error", message: e.message })
166
+ );
167
+ return () => {
168
+ live = false;
169
+ };
170
+ }, [repo, jobId]);
171
+ useInput((input, key) => {
172
+ if (key.escape || input === "q") return onBack();
173
+ if (input === "o" && url) $`open ${url}`.catch(() => {
174
+ });
175
+ });
176
+ let lines = [];
177
+ let jumpTo = 0;
178
+ if (state.phase === "loading")
179
+ lines = [{ text: "Fetching log\u2026", color: colors.info }];
180
+ else if (state.phase === "error")
181
+ lines = [{ text: `Error: ${state.message}`, color: colors.error }];
182
+ else ({ lines, jumpTo } = processLog(state.lines));
183
+ return /* @__PURE__ */ jsx(
184
+ DrillView,
185
+ {
186
+ title: `${name} \u2014 log`,
187
+ subtitle: state.phase === "ready" ? `${state.lines.length} lines` : void 0,
188
+ hints: [
189
+ ["\u2191\u2193/space", "scroll"],
190
+ ["g/G", "top/tail"],
191
+ ["o", "open"],
192
+ ["q/esc", "back"]
193
+ ],
194
+ children: /* @__PURE__ */ jsx(ScrollView, { lines, initialStart: jumpTo })
195
+ }
196
+ );
197
+ };
198
+ var QUERY = `query($owner: String!, $name: String!, $number: Int!) {
199
+ repository(owner: $owner, name: $name) {
200
+ pullRequest(number: $number) {
201
+ reviewThreads(first: 100) { nodes { path line } }
202
+ }
203
+ }
204
+ }`;
205
+ var fetchFiles = async (repo, number) => {
206
+ const [owner, name] = repo.split("/");
207
+ const out = await $`gh api graphql -f query=${QUERY} -f owner=${owner} -f name=${name} -F number=${number}`.quiet();
208
+ const nodes = JSON.parse(out.stdout).data.repository.pullRequest.reviewThreads.nodes ?? [];
209
+ const seen = /* @__PURE__ */ new Map();
210
+ for (const n of nodes)
211
+ if (n.path && !seen.has(n.path))
212
+ seen.set(n.path, { path: n.path, line: n.line });
213
+ return [...seen.values()];
214
+ };
215
+ var openInEditor = async (repoPath, file) => {
216
+ const target = file.line ? `${file.path}:${file.line}` : file.path;
217
+ for (const ed of ["cursor", "code"]) {
218
+ if ((await $({ nothrow: true, quiet: true })`command -v ${ed}`).exitCode === 0) {
219
+ void $({
220
+ nothrow: true,
221
+ quiet: true
222
+ })`${ed} -g ${repoPath}/${target}`.catch(() => {
223
+ });
224
+ return ed;
225
+ }
226
+ }
227
+ const editor = process.env.EDITOR || "vi";
228
+ await (0, lib_exports.openInTab)(
229
+ `cd ${repoPath} && ${editor} ${file.line ? `+${file.line} ` : ""}${file.path}`
230
+ );
231
+ return editor;
232
+ };
233
+ var FilePicker = ({
234
+ repo,
235
+ number,
236
+ onBack
237
+ }) => {
238
+ const [files, setFiles] = useState(null);
239
+ const [note, setNote] = useState(null);
240
+ useEffect(() => {
241
+ let live = true;
242
+ fetchFiles(repo, number).then((f) => live && setFiles(f)).catch((e) => live && setNote(e.message));
243
+ return () => {
244
+ live = false;
245
+ };
246
+ }, [repo, number]);
247
+ const list = files ?? [];
248
+ const { cursor } = useListCursor(list.length);
249
+ const safeCursor = Math.min(cursor, Math.max(0, list.length - 1));
250
+ const open = async (f) => {
251
+ setNote(`\u22EF resolving ${repo}\u2026`);
252
+ const repoPath = await (0, lib_exports.resolveRepoPath)(repo);
253
+ if (!repoPath) {
254
+ setNote(`\u2717 ${repo} isn't checked out locally`);
255
+ return;
256
+ }
257
+ const ed = await openInEditor(repoPath, f);
258
+ setNote(`\u2197 ${f.path}${f.line ? `:${f.line}` : ""} \u2014 ${ed}`);
259
+ };
260
+ useInput((input, key) => {
261
+ if (key.escape || input === "q") return onBack();
262
+ if (key.return && list[safeCursor]) void open(list[safeCursor]);
263
+ });
264
+ return /* @__PURE__ */ jsxs(
265
+ DrillView,
266
+ {
267
+ title: `Files \xB7 #${number} \xB7 ${repo}`,
268
+ subtitle: "files touched by review comments",
269
+ hints: [
270
+ ["\u2191\u2193", "nav"],
271
+ ["\u21B5", "open in editor"],
272
+ ["q/esc", "back"]
273
+ ],
274
+ children: [
275
+ !files ? /* @__PURE__ */ jsx(Text, { color: colors.info, children: "Fetching files\u2026" }) : list.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No files referenced in review comments." }) : /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: list.map((f, i) => /* @__PURE__ */ jsxs(Box, { children: [
276
+ /* @__PURE__ */ jsx(Text, { color: colors.info, children: i === safeCursor ? " \u276F " : " " }),
277
+ /* @__PURE__ */ jsx(Text, { bold: i === safeCursor, children: f.path }),
278
+ f.line ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: `:${f.line}` }) : null
279
+ ] }, f.path)) }),
280
+ note ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: colors.success, children: " " + note }) }) : null
281
+ ]
282
+ }
283
+ );
284
+ };
285
+ var cacheDir = () => join(
286
+ process.env.XDG_CACHE_HOME || join(homedir(), ".cache"),
287
+ inboxConfig().cacheNamespace
288
+ );
289
+ var cacheFile = (key) => join(cacheDir(), `${key.replace(/[^a-z0-9._-]/gi, "-")}.json`);
290
+ var readJson = (key) => {
291
+ try {
292
+ return JSON.parse(readFileSync(cacheFile(key), "utf8")).v;
293
+ } catch {
294
+ return null;
295
+ }
296
+ };
297
+ var writeJson = (key, v) => {
298
+ try {
299
+ mkdirSync(cacheDir(), { recursive: true });
300
+ writeFileSync(cacheFile(key), JSON.stringify({ v, at: Date.now() }));
301
+ } catch {
302
+ }
303
+ };
304
+ var useCachedResource = (key, fetcher) => {
305
+ const [data, setData] = useState(() => readJson(key));
306
+ const [error, setError] = useState(null);
307
+ const [tick, setTick] = useState(0);
308
+ useEffect(() => {
309
+ let live = true;
310
+ fetcher().then((fresh) => {
311
+ if (!live) return;
312
+ writeJson(key, fresh);
313
+ setData(fresh);
314
+ setError(null);
315
+ }).catch((e) => live && setError(e.message));
316
+ return () => {
317
+ live = false;
318
+ };
319
+ }, [key, tick]);
320
+ return {
321
+ data,
322
+ error,
323
+ loading: data === null && error === null,
324
+ reload: () => setTick((t) => t + 1)
325
+ };
326
+ };
327
+ var PrView = ({
328
+ item,
329
+ login,
330
+ onBack,
331
+ defaultTab,
332
+ onRefresh,
333
+ onRemove,
334
+ onMerged
335
+ }) => {
336
+ const [log, setLog] = useState(null);
337
+ const [ai, setAi] = useState(false);
338
+ const [files, setFiles] = useState(false);
339
+ const [copy, setCopy] = useState(false);
340
+ const [replying, setReplying] = useState(false);
341
+ const menu = (0, lib_exports.useActionMenu)();
342
+ const comments = useCachedResource(
343
+ `pr-comments-${item.repo}-${item.number}`,
344
+ () => fetchComments(item.repo, item.number, "pr")
345
+ );
346
+ const unresolvedCount = (comments.data?.threads ?? []).filter(
347
+ (t) => !t.isResolved
348
+ ).length;
349
+ const conversationLabel = unresolvedCount ? `Conversation (${unresolvedCount} unresolved)` : "Conversation";
350
+ const tabItems = [
351
+ { value: "health", label: "Health" },
352
+ { value: "conversation", label: conversationLabel }
353
+ ];
354
+ const inputActive = log === null && !ai && !files && !replying && !copy;
355
+ const menuOpen = menu.actions !== null;
356
+ const { active, setActive } = useTabs(tabItems, {
357
+ initial: defaultTab ?? "health",
358
+ isActive: inputActive && !menuOpen
359
+ });
360
+ const tab = active ?? "health";
361
+ const health = useCachedResource(
362
+ `pr-health-${item.repo}-${item.number}`,
363
+ () => fetchHealth(item.repo, item.number)
364
+ );
365
+ const checkLabel = (c) => c.workflowName ? `${c.workflowName} / ${c.context ?? c.name}` : c.context ?? c.name ?? "";
366
+ const onOpenCheck = (c) => {
367
+ const url = c.detailsUrl ?? c.targetUrl ?? "";
368
+ const jobId = jobIdOf(url);
369
+ if (jobId || checkDrillFor(url))
370
+ setLog({ repo: item.repo, jobId: jobId ?? "", name: checkLabel(c), url });
371
+ else if (url) $`open ${url}`.catch(() => {
372
+ });
373
+ };
374
+ useInput(
375
+ (input, key) => {
376
+ if (menu.handleKey(key)) return;
377
+ if (key.escape || input === "q") return onBack();
378
+ if (input === "M") {
379
+ menu.open([
380
+ ...(0, lib_exports.buildActions)(
381
+ item,
382
+ login,
383
+ () => {
384
+ },
385
+ void 0,
386
+ void 0,
387
+ void 0,
388
+ onRefresh,
389
+ // onRemove navigates back itself, having stripped the row. Calling
390
+ // onBack() as well would re-set state from a closure captured
391
+ // before the removal, putting the row straight back.
392
+ (removed) => onRemove ? onRemove(removed) : onBack()
393
+ ),
394
+ // Appended here rather than via gh-ink's extension seam: on this screen
395
+ // the launcher is mounted directly by `a` below, not opened as an overlay
396
+ // through onOpenExt, so there is no extension for buildActions to list.
397
+ // The browse screen gets the same entry the other way, from delegate's
398
+ // scope: "item" — same action, two hosts, two routes to it.
399
+ {
400
+ label: "Delegate to an agent",
401
+ hint: "a",
402
+ run: () => setAi(true)
403
+ },
404
+ {
405
+ label: "Copy prompt to clipboard",
406
+ hint: "y",
407
+ run: () => setCopy(true)
408
+ }
409
+ ]);
410
+ return;
411
+ }
412
+ if (input === "o") {
413
+ $`open ${item.url}`.catch(() => {
414
+ });
415
+ return;
416
+ }
417
+ if (input === "a") {
418
+ setAi(true);
419
+ return;
420
+ }
421
+ if (input === "y") {
422
+ setCopy(true);
423
+ return;
424
+ }
425
+ if (input === "e") {
426
+ setFiles(true);
427
+ return;
428
+ }
429
+ if (key.leftArrow || key.rightArrow)
430
+ setActive((t) => t === "health" ? "conversation" : "health");
431
+ },
432
+ { isActive: inputActive }
433
+ );
434
+ if (files)
435
+ return /* @__PURE__ */ jsx(
436
+ FilePicker,
437
+ {
438
+ repo: item.repo,
439
+ number: item.number,
440
+ onBack: () => setFiles(false)
441
+ }
442
+ );
443
+ if (ai)
444
+ return /* @__PURE__ */ jsx(
445
+ AiLauncher,
446
+ {
447
+ item,
448
+ login,
449
+ prompt: seedPromptFor(item),
450
+ onBack: () => setAi(false)
451
+ }
452
+ );
453
+ if (copy)
454
+ return /* @__PURE__ */ jsx(CopyPromptNotice, { item, onBack: () => setCopy(false) });
455
+ if (log) {
456
+ const drill = checkDrillFor(log.url);
457
+ return drill ? /* @__PURE__ */ jsx(Fragment, { children: drill.render({
458
+ repo: log.repo,
459
+ url: log.url,
460
+ name: log.name,
461
+ onBack: () => setLog(null)
462
+ }) }) : /* @__PURE__ */ jsx(
463
+ CheckLogView,
464
+ {
465
+ repo: log.repo,
466
+ jobId: log.jobId,
467
+ name: log.name,
468
+ url: log.url,
469
+ onBack: () => setLog(null)
470
+ }
471
+ );
472
+ }
473
+ const hints = tab === "health" ? [
474
+ ["\u2191\u2193", "nav"],
475
+ ["\u21B5/l", "log"],
476
+ ["r", "retrigger"],
477
+ ["m", "merge"],
478
+ ["a", "AI"],
479
+ ["y", "copy prompt"],
480
+ ["e", "files"],
481
+ ["M", "actions"],
482
+ ["\u2190\u2192", "tab"],
483
+ ["o", "open PR"],
484
+ ["q", "back"]
485
+ ] : [
486
+ ["\u2191\u2193", "thread"],
487
+ ["x", "resolve"],
488
+ ["r", "reply"],
489
+ ["R", "show resolved"],
490
+ ["M", "actions"],
491
+ ["a", "AI"],
492
+ ["y", "copy prompt"],
493
+ ["\u2190\u2192", "tab"],
494
+ ["q", "back"]
495
+ ];
496
+ return /* @__PURE__ */ jsxs(
497
+ DrillView,
498
+ {
499
+ title: `#${item.number} \xB7 ${item.repo}`,
500
+ subtitle: item.title,
501
+ hints,
502
+ children: [
503
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Tabs, { active: tab, items: tabItems }) }),
504
+ menu.actions ? /* @__PURE__ */ jsx(lib_exports.ActionMenu, { item, actions: menu.actions, cursor: menu.cursor }) : tab === "health" ? /* @__PURE__ */ jsx(
505
+ HealthPanel,
506
+ {
507
+ repo: item.repo,
508
+ number: item.number,
509
+ data: health.data,
510
+ error: health.error,
511
+ reload: health.reload,
512
+ onOpenCheck,
513
+ onMerged: onMerged ? () => onMerged(item) : void 0
514
+ }
515
+ ) : /* @__PURE__ */ jsx(
516
+ CommentsPanel,
517
+ {
518
+ repo: item.repo,
519
+ number: item.number,
520
+ data: comments.data,
521
+ error: comments.error,
522
+ reload: comments.reload,
523
+ onReplyingChange: setReplying,
524
+ showConversationHeading: false
525
+ }
526
+ )
527
+ ]
528
+ }
529
+ );
530
+ };
531
+ var IssueView = ({
532
+ item,
533
+ login,
534
+ onBack
535
+ }) => {
536
+ const [replying, setReplying] = useState(false);
537
+ const [ai, setAi] = useState(false);
538
+ const [copy, setCopy] = useState(false);
539
+ const comments = useCachedResource(
540
+ `issue-comments-${item.repo}-${item.number}`,
541
+ () => fetchComments(item.repo, item.number, "issue")
542
+ );
543
+ const tabItems = [
544
+ { value: "conversation", label: "Conversation" }
545
+ ];
546
+ useInput(
547
+ (input, key) => {
548
+ if (key.escape || input === "q") return onBack();
549
+ if (input === "o") $`open ${item.url}`.catch(() => {
550
+ });
551
+ if (input === "a") setAi(true);
552
+ if (input === "y") setCopy(true);
553
+ },
554
+ { isActive: !replying && !ai && !copy }
555
+ );
556
+ if (ai)
557
+ return /* @__PURE__ */ jsx(
558
+ AiLauncher,
559
+ {
560
+ item,
561
+ login,
562
+ prompt: seedPromptFor({ ...item, kind: "issue" }),
563
+ onBack: () => setAi(false)
564
+ }
565
+ );
566
+ if (copy)
567
+ return /* @__PURE__ */ jsx(
568
+ CopyPromptNotice,
569
+ {
570
+ item: { ...item, kind: "issue" },
571
+ onBack: () => setCopy(false)
572
+ }
573
+ );
574
+ return /* @__PURE__ */ jsxs(
575
+ DrillView,
576
+ {
577
+ title: `#${item.number} \xB7 ${item.repo}`,
578
+ subtitle: item.title,
579
+ hints: [
580
+ ["\u2191\u2193", "scroll"],
581
+ ["a", "AI"],
582
+ ["y", "copy prompt"],
583
+ ["o", "open in browser"],
584
+ ["q/esc", "back"]
585
+ ],
586
+ children: [
587
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Tabs, { active: "conversation", items: tabItems }) }),
588
+ /* @__PURE__ */ jsx(
589
+ CommentsPanel,
590
+ {
591
+ repo: item.repo,
592
+ number: item.number,
593
+ data: comments.data,
594
+ error: comments.error,
595
+ reload: comments.reload,
596
+ onReplyingChange: setReplying,
597
+ showConversationHeading: false
598
+ }
599
+ )
600
+ ]
601
+ }
602
+ );
603
+ };
604
+ var detailFor = (ctx) => ctx.kind === "pr" ? /* @__PURE__ */ jsx(
605
+ PrView,
606
+ {
607
+ item: ctx.item,
608
+ login: ctx.login,
609
+ onBack: ctx.onBack,
610
+ onRefresh: ctx.onRefresh,
611
+ onRemove: ctx.onRemove,
612
+ onMerged: ctx.onMerged
613
+ }
614
+ ) : (
615
+ // No refresh/remove/merged: IssueView has no action menu to hang them off,
616
+ // so passing them would type-check into a handler nothing ever calls. They
617
+ // belong here the day it grows the `M` menu PrView has.
618
+ /* @__PURE__ */ jsx(IssueView, { item: ctx.item, login: ctx.login, onBack: ctx.onBack })
619
+ );
620
+
621
+ // src/index.ts
622
+ __reExport(index_exports, lib_exports);
623
+
624
+ export { IssueView, PrView, checkDrillFor, defineCockpit, detailFor, hasPatterns, parseArgs, registerCheckDrills };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@kud/gh-cockpit",
3
+ "version": "0.0.0",
4
+ "description": "A configurable GitHub cockpit for the terminal \u2014 your PRs, reviews and issues in one Ink TUI, grouped by whose move it is.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./extensions": {
14
+ "types": "./dist/extensions/index.d.ts",
15
+ "import": "./dist/extensions/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "vitest run"
29
+ },
30
+ "keywords": [
31
+ "github",
32
+ "pull-request",
33
+ "cli",
34
+ "ink",
35
+ "react",
36
+ "terminal",
37
+ "tui",
38
+ "cockpit",
39
+ "inbox"
40
+ ],
41
+ "author": "Erwann Mest <m@kud.io>",
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/kud/gh",
46
+ "directory": "packages/gh-cockpit"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "peerDependencies": {
52
+ "ink": ">=7",
53
+ "react": ">=19"
54
+ },
55
+ "dependencies": {
56
+ "@kud/gh": "0.5.1",
57
+ "@kud/gh-ink": "0.21.0",
58
+ "@kud/ink-ui": "0.14.0",
59
+ "zx": "8.8.5"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "22.20.1",
63
+ "@types/react": "19.2.17",
64
+ "ink": "7.1.0",
65
+ "ink-testing-library": "4.0.0",
66
+ "react": "19.2.7",
67
+ "tsup": "8.5.1",
68
+ "typescript": "5.9.3",
69
+ "vitest": "4.1.10"
70
+ }
71
+ }